From c582087c0fe3c4a91459991d915911c917766d6b Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Mon, 13 Oct 2025 01:33:03 -0700 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E5=BC=95=E7=94=A8=E8=B7=AF=E5=BE=84=E4=BB=8Eperfect-p?= =?UTF-8?q?anel/ppanel-server=E5=88=B0perfect-panel/server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 添加版本和构建时间变量 fix: 修正短信队列类型注释错误 style: 清理未使用的代码和测试文件 docs: 更新安装文档中的下载链接 chore: 迁移数据库脚本添加日志和订阅配置 --- 1.conf | 67 + Dockerfile | 10 +- Makefile | 84 ++ adapter/adapter.go | 140 ++ adapter/adapter_test.go | 34 + adapter/client.go | 146 ++ adapter/client_test.go | 153 ++ adapter/utils.go | 1 + adapter/utils_test.go | 46 + apis/admin/application.api | 96 ++ apis/admin/auth.api | 6 +- apis/admin/console.api | 2 +- apis/admin/coupon.api | 8 +- apis/admin/device.api | 15 +- apis/admin/log.api | 237 ++- apis/admin/marketing.api | 167 +++ apis/admin/server.api | 294 ++-- apis/admin/subscribe.api | 101 +- apis/admin/system.api | 92 +- apis/admin/ticket.api | 4 +- apis/admin/tool.api | 45 +- apis/admin/user.api | 63 +- apis/app/announcement.api | 24 - apis/app/auth.api | 105 -- apis/app/document.api | 27 - apis/app/node.api | 49 - apis/app/order.api | 58 - apis/app/payment.api | 23 - apis/app/subscribe.api | 75 - apis/app/user.api | 90 -- apis/app/ws.api | 23 - apis/auth/auth.api | 72 +- apis/common.api | 37 +- apis/node/node.api | 43 +- apis/public/announcement.api | 3 +- apis/public/document.api | 35 +- apis/public/order.api | 2 +- apis/public/payment.api | 3 +- apis/public/portal.api | 9 +- apis/public/subscribe.api | 20 +- apis/public/ticket.api | 15 +- apis/public/user.api | 56 +- apis/swagger_admin.api | 48 +- apis/swagger_app.api | 21 - apis/swagger_common.api | 19 +- apis/swagger_node.api | 14 +- apis/swagger_user.api | 32 +- apis/types.api | 305 ++-- cmd/run.go | 27 +- cmd/version.go | 5 +- doc/config-zh.md | 227 ++- doc/config.md | 242 ++-- doc/image/architecture-en.png | Bin 0 -> 566266 bytes doc/image/architecture-zh.png | Bin 0 -> 561032 bytes doc/install-zh.md | 4 +- doc/install.md | 4 +- generate/gopure-amd64.exe | Bin generate/gopure-arm64.exe | Bin generate/gopure-darwin-amd64 | Bin generate/gopure-darwin-arm64 | Bin generate/gopure-linux-amd64 | Bin generate/gopure-linux-arm64 | Bin go.mod | 40 +- go.sum | 115 +- initialize/config.go | 54 +- initialize/device.go | 26 + initialize/email.go | 17 +- initialize/init.go | 10 +- initialize/invite.go | 8 +- .../database/00001_init_schema.down.sql | 36 + .../migrate/database/00001_init_schema.up.sql | 555 +++++++ .../database/00002_init_basic_data.down.sql | 21 + .../database/00002_init_basic_data.up.sql | 127 ++ initialize/migrate/database/01200-patch.sql | 54 - initialize/migrate/database/01201-patch.sql | 118 -- initialize/migrate/database/01202-patch.sql | 44 - .../database/02003_update_payment.down.sql | 72 + .../database/02003_update_payment.up.sql | 72 + .../database/02004_rebuild_rule.down.sql | 4 + .../database/02004_rebuild_rule.up.sql | 22 + .../02005_device_online_record.down.sql | 52 + .../02005_device_online_record.up.sql | 69 + .../02006_reset_subscribe_record.down.sql | 5 + .../02006_reset_subscribe_record.up.sql | 17 + .../database/02007_adapte_rule.down.sql | 3 + .../migrate/database/02007_adapte_rule.up.sql | 3 + .../migrate/database/02100_task.down.sql | 1 + initialize/migrate/database/02100_task.up.sql | 23 + .../02101_subscribe_application.down.sql | 1 + .../02101_subscribe_application.up.sql | 27 + .../database/02102_subscribe_config.down.sql | 0 .../database/02102_subscribe_config.up.sql | 4 + .../02103_delete_application.down.sql | 0 .../database/02103_delete_application.up.sql | 3 + .../database/02104_system_log.down.sql | 106 ++ .../migrate/database/02104_system_log.up.sql | 19 + .../migrate/database/02105_node.down.sql | 2 + initialize/migrate/database/02105_node.up.sql | 28 + .../migrate/database/02106_subscribe.down.sql | 5 + .../migrate/database/02106_subscribe.up.sql | 7 + .../database/02107_log_setting.down.sql | 0 .../migrate/database/02107_log_setting.up.sql | 4 + .../database/02108_user_referral.down.sql | 3 + .../database/02108_user_referral.up.sql | 7 + .../migrate/database/02109_node_sort.down.sql | 2 + .../migrate/database/02109_node_sort.up.sql | 3 + .../database/02110_traffic_log_index.down.sql | 1 + .../database/02110_traffic_log_index.up.sql | 1 + .../database/02111_clear_table.down.sql | 2 + .../migrate/database/02111_clear_table.up.sql | 2 + .../migrate/database/02112_subscribe.down.sql | 0 .../migrate/database/02112_subscribe.up.sql | 7 + .../migrate/database/02113_task.down.sql | 0 initialize/migrate/database/02113_task.up.sql | 14 + .../database/02114_node_config.down.sql | 0 .../migrate/database/02114_node_config.up.sql | 8 + initialize/migrate/database/ppanel.sql | 562 -------- initialize/migrate/init.go | 610 +------- initialize/migrate/init_test.go | 36 - initialize/migrate/migrate.go | 39 +- initialize/migrate/migrate_test.go | 49 + initialize/migrate/patch/01703.go | 456 ------ initialize/migrate/patch/02000.go | 252 ---- initialize/migrate/patch/03001.go | 30 - initialize/mobile.go | 15 +- initialize/mysql.go | 9 - initialize/node.go | 49 +- initialize/oauth.go | 4 +- initialize/register.go | 8 +- initialize/site.go | 8 +- initialize/statistics.go | 57 - initialize/subscribe.go | 8 +- initialize/telegram.go | 12 +- initialize/verify.go | 8 +- initialize/version.go | 142 +- internal/config/cacheKey.go | 19 +- internal/config/config.go | 109 +- internal/config/constant.go | 5 - .../handler/admin/ads/createAdsHandler.go | 8 +- .../handler/admin/ads/deleteAdsHandler.go | 8 +- .../handler/admin/ads/getAdsDetailHandler.go | 8 +- .../handler/admin/ads/getAdsListHandler.go | 8 +- .../handler/admin/ads/updateAdsHandler.go | 8 +- .../announcement/createAnnouncementHandler.go | 8 +- .../announcement/deleteAnnouncementHandler.go | 8 +- .../announcement/getAnnouncementHandler.go | 8 +- .../getAnnouncementListHandler.go | 8 +- .../announcement/updateAnnouncementHandler.go | 8 +- .../createSubscribeApplicationHandler.go | 26 + .../deleteSubscribeApplicationHandler.go | 26 + .../getSubscribeApplicationListHandler.go | 26 + .../previewSubscribeTemplateHandler.go | 27 + .../updateSubscribeApplicationHandler.go | 26 + .../authMethod/getAuthMethodConfigHandler.go | 8 +- .../authMethod/getAuthMethodListHandler.go | 6 +- .../authMethod/getEmailPlatformHandler.go | 6 +- .../admin/authMethod/getSmsPlatformHandler.go | 6 +- .../admin/authMethod/testEmailSendHandler.go | 8 +- .../admin/authMethod/testSmsSendHandler.go | 8 +- .../updateAuthMethodConfigHandler.go | 8 +- .../console/queryRevenueStatisticsHandler.go | 6 +- .../console/queryServerTotalDataHandler.go | 6 +- .../console/queryTicketWaitReplyHandler.go | 6 +- .../console/queryUserStatisticsHandler.go | 6 +- .../admin/coupon/batchDeleteCouponHandler.go | 8 +- .../admin/coupon/createCouponHandler.go | 8 +- .../admin/coupon/deleteCouponHandler.go | 8 +- .../admin/coupon/getCouponListHandler.go | 8 +- .../admin/coupon/updateCouponHandler.go | 8 +- .../document/batchDeleteDocumentHandler.go | 8 +- .../admin/document/createDocumentHandler.go | 8 +- .../admin/document/deleteDocumentHandler.go | 8 +- .../document/getDocumentDetailHandler.go | 8 +- .../admin/document/getDocumentListHandler.go | 8 +- .../admin/document/updateDocumentHandler.go | 8 +- .../admin/log/filterBalanceLogHandler.go | 26 + .../admin/log/filterCommissionLogHandler.go | 26 + .../admin/log/filterEmailLogHandler.go | 26 + .../handler/admin/log/filterGiftLogHandler.go | 26 + .../admin/log/filterLoginLogHandler.go | 26 + .../admin/log/filterMobileLogHandler.go | 26 + .../admin/log/filterRegisterLogHandler.go | 26 + .../log/filterResetSubscribeLogHandler.go | 26 + .../log/filterServerTrafficLogHandler.go | 26 + .../admin/log/filterSubscribeLogHandler.go | 26 + .../log/filterTrafficLogDetailsHandler.go | 26 + .../filterUserSubscribeTrafficLogHandler.go | 26 + .../handler/admin/log/getLogSettingHandler.go | 18 + .../admin/log/getMessageLogListHandler.go | 8 +- .../admin/log/updateLogSettingHandler.go | 26 + .../createBatchSendEmailTaskHandler.go | 26 + .../admin/marketing/createQuotaTaskHandler.go | 26 + .../getBatchSendEmailTaskListHandler.go | 26 + .../getBatchSendEmailTaskStatusHandler.go | 26 + .../marketing/getPreSendEmailCountHandler.go | 26 + .../marketing/queryQuotaTaskListHandler.go | 26 + .../queryQuotaTaskPreCountHandler.go | 26 + .../marketing/queryQuotaTaskStatusHandler.go | 26 + .../stopBatchSendEmailTaskHandler.go | 26 + .../handler/admin/order/createOrderHandler.go | 8 +- .../admin/order/getOrderListHandler.go | 8 +- .../admin/order/updateOrderStatusHandler.go | 8 +- .../payment/createPaymentMethodHandler.go | 8 +- .../payment/deletePaymentMethodHandler.go | 8 +- .../payment/getPaymentMethodListHandler.go | 8 +- .../payment/getPaymentPlatformHandler.go | 6 +- .../payment/updatePaymentMethodHandler.go | 8 +- .../server/batchDeleteNodeGroupHandler.go | 26 - .../admin/server/batchDeleteNodeHandler.go | 26 - .../admin/server/createNodeGroupHandler.go | 26 - .../handler/admin/server/createNodeHandler.go | 10 +- .../admin/server/createRuleGroupHandler.go | 26 - .../admin/server/createServerHandler.go | 26 + .../admin/server/deleteNodeGroupHandler.go | 26 - .../handler/admin/server/deleteNodeHandler.go | 10 +- .../admin/server/deleteRuleGroupHandler.go | 26 - .../admin/server/deleteServerHandler.go | 26 + .../admin/server/filterNodeListHandler.go | 26 + .../admin/server/filterServerListHandler.go | 26 + .../admin/server/getNodeDetailHandler.go | 26 - .../admin/server/getNodeGroupListHandler.go | 18 - .../admin/server/getNodeListHandler.go | 26 - .../admin/server/getNodeTagListHandler.go | 18 - .../admin/server/getRuleGroupListHandler.go | 18 - .../admin/server/getServerProtocolsHandler.go | 26 + .../server/hasMigrateSeverNodeHandler.go | 18 + .../admin/server/migrateServerNodeHandler.go | 18 + .../handler/admin/server/nodeSortHandler.go | 26 - .../admin/server/queryNodeTagHandler.go | 18 + .../admin/server/resetSortWithNodeHandler.go | 26 + .../server/resetSortWithServerHandler.go | 26 + .../admin/server/toggleNodeStatusHandler.go | 26 + .../admin/server/updateNodeGroupHandler.go | 26 - .../handler/admin/server/updateNodeHandler.go | 10 +- .../admin/server/updateRuleGroupHandler.go | 26 - .../admin/server/updateServerHandler.go | 26 + .../batchDeleteSubscribeGroupHandler.go | 8 +- .../subscribe/batchDeleteSubscribeHandler.go | 8 +- .../subscribe/createSubscribeGroupHandler.go | 8 +- .../admin/subscribe/createSubscribeHandler.go | 8 +- .../subscribe/deleteSubscribeGroupHandler.go | 8 +- .../admin/subscribe/deleteSubscribeHandler.go | 8 +- .../subscribe/getSubscribeDetailsHandler.go | 8 +- .../subscribe/getSubscribeGroupListHandler.go | 6 +- .../subscribe/getSubscribeListHandler.go | 8 +- .../admin/subscribe/subscribeSortHandler.go | 8 +- .../subscribe/updateSubscribeGroupHandler.go | 8 +- .../admin/subscribe/updateSubscribeHandler.go | 8 +- .../admin/system/createApplicationHandler.go | 26 - .../system/createApplicationVersionHandler.go | 26 - .../admin/system/deleteApplicationHandler.go | 26 - .../system/deleteApplicationVersionHandler.go | 26 - .../system/getApplicationConfigHandler.go | 18 - .../admin/system/getApplicationHandler.go | 18 - .../admin/system/getCurrencyConfigHandler.go | 6 +- .../admin/system/getInviteConfigHandler.go | 6 +- .../admin/system/getNodeConfigHandler.go | 6 +- .../admin/system/getNodeMultiplierHandler.go | 6 +- .../system/getPrivacyPolicyConfigHandler.go | 6 +- .../admin/system/getRegisterConfigHandler.go | 6 +- .../admin/system/getSiteConfigHandler.go | 6 +- .../admin/system/getSubscribeConfigHandler.go | 6 +- .../admin/system/getSubscribeTypeHandler.go | 18 - .../admin/system/getTosConfigHandler.go | 6 +- .../system/getVerifyCodeConfigHandler.go | 6 +- .../admin/system/getVerifyConfigHandler.go | 6 +- .../system/preViewNodeMultiplierHandler.go | 18 + .../admin/system/setNodeMultiplierHandler.go | 8 +- .../admin/system/settingTelegramBotHandler.go | 6 +- .../system/updateApplicationConfigHandler.go | 26 - .../admin/system/updateApplicationHandler.go | 26 - .../system/updateApplicationVersionHandler.go | 26 - .../system/updateCurrencyConfigHandler.go | 8 +- .../admin/system/updateInviteConfigHandler.go | 8 +- .../admin/system/updateNodeConfigHandler.go | 8 +- .../updatePrivacyPolicyConfigHandler.go | 8 +- .../system/updateRegisterConfigHandler.go | 8 +- .../admin/system/updateSiteConfigHandler.go | 8 +- .../system/updateSubscribeConfigHandler.go | 8 +- .../admin/system/updateTosConfigHandler.go | 8 +- .../system/updateVerifyCodeConfigHandler.go | 8 +- .../admin/system/updateVerifyConfigHandler.go | 8 +- .../admin/ticket/createTicketFollowHandler.go | 8 +- .../handler/admin/ticket/getTicketHandler.go | 8 +- .../admin/ticket/getTicketListHandler.go | 8 +- .../admin/ticket/updateTicketStatusHandler.go | 8 +- .../handler/admin/tool/getSystemLogHandler.go | 6 +- .../handler/admin/tool/getVersionHandler.go | 18 + .../admin/tool/restartSystemHandler.go | 6 +- .../admin/user/batchDeleteUserHandler.go | 8 +- .../admin/user/createUserAuthMethodHandler.go | 8 +- .../handler/admin/user/createUserHandler.go | 8 +- .../admin/user/createUserSubscribeHandler.go | 8 +- .../handler/admin/user/currentUserHandler.go | 6 +- .../admin/user/deleteUserAuthMethodHandler.go | 8 +- .../admin/user/deleteUserDeviceHandler.go | 8 +- .../handler/admin/user/deleteUserHandler.go | 8 +- .../admin/user/deleteUserSubscribeHandler.go | 8 +- .../admin/user/getUserAuthMethodHandler.go | 8 +- .../admin/user/getUserDetailHandler.go | 8 +- .../handler/admin/user/getUserListHandler.go | 8 +- .../admin/user/getUserLoginLogsHandler.go | 8 +- .../admin/user/getUserSubscribeByIdHandler.go | 8 +- .../user/getUserSubscribeDevicesHandler.go | 8 +- .../admin/user/getUserSubscribeHandler.go | 8 +- .../admin/user/getUserSubscribeLogsHandler.go | 8 +- ...getUserSubscribeResetTrafficLogsHandler.go | 26 + .../getUserSubscribeTrafficLogsHandler.go | 8 +- .../user/kickOfflineByUserDeviceHandler.go | 8 +- .../admin/user/updateUserAuthMethodHandler.go | 8 +- .../admin/user/updateUserBasicInfoHandler.go | 8 +- .../admin/user/updateUserDeviceHandler.go | 8 +- .../user/updateUserNotifySettingHandler.go | 8 +- .../admin/user/updateUserSubscribeHandler.go | 8 +- .../announcement/queryannouncementhandler.go | 26 - internal/handler/app/auth/checkHandler.go | 26 - .../handler/app/auth/getAppConfigHandler.go | 26 - internal/handler/app/auth/loginHandler.go | 42 - internal/handler/app/auth/registerHandler.go | 43 - .../handler/app/auth/resetPasswordHandler.go | 41 - .../document/querydocumentdetailhandler.go | 26 - .../app/document/querydocumentlisthandler.go | 18 - .../handler/app/node/getNodeListHandler.go | 26 - .../app/node/getRuleGroupListHandler.go | 18 - .../handler/app/order/checkoutorderhandler.go | 26 - .../handler/app/order/closeorderhandler.go | 26 - .../app/order/precreateorderhandler.go | 26 - internal/handler/app/order/purchasehandler.go | 26 - .../app/order/queryorderdetailhandler.go | 26 - .../app/order/queryorderlisthandler.go | 26 - internal/handler/app/order/rechargehandler.go | 26 - internal/handler/app/order/renewalhandler.go | 26 - .../handler/app/order/resettraffichandler.go | 26 - .../getavailablepaymentmethodshandler.go | 18 - .../queryApplicationConfigHandler.go | 18 - .../querySubscribeGroupListHandler.go | 18 - .../subscribe/querySubscribeListHandler.go | 18 - .../queryUserAlreadySubscribeHandler.go | 18 - .../queryUserAvailableUserSubscribeHandler.go | 26 - .../resetUserSubscribePeriodHandler.go | 26 - .../handler/app/user/deleteAccountHandler.go | 26 - .../getuseronlinetimestatisticshandler.go | 18 - .../getusersubscribetrafficlogshandler.go | 26 - .../handler/app/user/queryUserInfoHandler.go | 18 - .../app/user/queryuseraffiliatehandler.go | 18 - .../app/user/queryuseraffiliatelisthandler.go | 26 - .../handler/app/user/updatePasswordHandler.go | 26 - internal/handler/app/ws/appWsHandler.go | 20 - internal/handler/auth/checkUserHandler.go | 8 +- .../handler/auth/checkUserTelephoneHandler.go | 8 +- internal/handler/auth/deviceLoginHandler.go | 26 + .../auth/oauth/appleLoginCallbackHandler.go | 8 +- .../auth/oauth/oAuthLoginGetTokenHandler.go | 8 +- .../handler/auth/oauth/oAuthLoginHandler.go | 8 +- internal/handler/auth/resetPasswordHandler.go | 12 +- .../handler/auth/telephoneLoginHandler.go | 12 +- .../auth/telephoneResetPasswordHandler.go | 12 +- .../auth/telephoneUserRegisterHandler.go | 13 +- internal/handler/auth/userLoginHandler.go | 12 +- internal/handler/auth/userRegisterHandler.go | 13 +- .../common/checkverificationcodehandler.go | 8 +- internal/handler/common/getAdsHandler.go | 8 +- .../handler/common/getApplicationHandler.go | 18 - internal/handler/common/getClientHandler.go | 18 + .../handler/common/getGlobalConfigHandler.go | 6 +- .../handler/common/getPrivacyPolicyHandler.go | 6 +- internal/handler/common/getStatHandler.go | 6 +- .../handler/common/getSubscriptionHandler.go | 18 - internal/handler/common/getTosHandler.go | 6 +- .../handler/common/sendEmailCodeHandler.go | 8 +- internal/handler/common/sendSmsCodeHandler.go | 8 +- internal/handler/notify.go | 6 +- .../handler/notify/paymentNotifyHandler.go | 16 +- .../announcement/queryAnnouncementHandler.go | 8 +- .../document/queryDocumentDetailHandler.go | 8 +- .../document/queryDocumentListHandler.go | 6 +- .../handler/public/order/closeOrderHandler.go | 8 +- .../public/order/preCreateOrderHandler.go | 8 +- .../handler/public/order/purchaseHandler.go | 8 +- .../public/order/queryOrderDetailHandler.go | 8 +- .../public/order/queryOrderListHandler.go | 8 +- .../handler/public/order/rechargeHandler.go | 8 +- .../handler/public/order/renewalHandler.go | 8 +- .../public/order/resetTrafficHandler.go | 8 +- .../getAvailablePaymentMethodsHandler.go | 6 +- .../getAvailablePaymentMethodsHandler.go | 6 +- .../public/portal/getSubscriptionHandler.go | 17 +- .../public/portal/prePurchaseOrderHandler.go | 8 +- .../public/portal/purchaseCheckoutHandler.go | 10 +- .../handler/public/portal/purchaseHandler.go | 8 +- .../portal/queryPurchaseOrderHandler.go | 8 +- .../queryApplicationConfigHandler.go | 18 - .../querySubscribeGroupListHandler.go | 6 +- .../subscribe/querySubscribeListHandler.go | 18 +- .../ticket/createUserTicketFollowHandler.go | 8 +- .../public/ticket/createUserTicketHandler.go | 8 +- .../ticket/getUserTicketDetailsHandler.go | 8 +- .../public/ticket/getUserTicketListHandler.go | 8 +- .../ticket/updateUserTicketStatusHandler.go | 8 +- .../public/user/bindOAuthCallbackHandler.go | 8 +- .../handler/public/user/bindOAuthHandler.go | 8 +- .../public/user/bindTelegramHandler.go | 6 +- .../public/user/getDeviceListHandler.go | 18 + .../handler/public/user/getLoginLogHandler.go | 8 +- .../public/user/getOAuthMethodsHandler.go | 6 +- .../public/user/getSubscribeLogHandler.go | 8 +- .../public/user/preUnsubscribeHandler.go | 8 +- .../public/user/queryUserAffiliateHandler.go | 6 +- .../user/queryUserAffiliateListHandler.go | 8 +- .../public/user/queryUserBalanceLogHandler.go | 6 +- .../user/queryUserCommissionLogHandler.go | 8 +- .../public/user/queryUserInfoHandler.go | 6 +- .../public/user/queryUserSubscribeHandler.go | 6 +- .../user/resetUserSubscribeTokenHandler.go | 8 +- .../public/user/unbindDeviceHandler.go | 26 + .../handler/public/user/unbindOAuthHandler.go | 8 +- .../public/user/unbindTelegramHandler.go | 6 +- .../handler/public/user/unsubscribeHandler.go | 8 +- .../public/user/updateBindEmailHandler.go | 8 +- .../public/user/updateBindMobileHandler.go | 8 +- .../public/user/updateUserNotifyHandler.go | 8 +- .../public/user/updateUserPasswordHandler.go | 8 +- .../handler/public/user/verifyEmailHandler.go | 8 +- internal/handler/routes.go | 457 +++--- .../handler/server/getServerConfigHandler.go | 10 +- .../server/getServerUserListHandler.go | 10 +- .../handler/server/pushOnlineUsersHandler.go | 8 +- .../queryServerProtocolConfigHandler.go | 43 + .../handler/server/serverPushStatusHandler.go | 8 +- .../server/serverPushUserTrafficHandler.go | 8 +- internal/handler/subscribe.go | 54 +- internal/handler/telegram.go | 10 +- internal/logic/admin/ads/createAdsLogic.go | 10 +- internal/logic/admin/ads/deleteAdsLogic.go | 8 +- internal/logic/admin/ads/getAdsDetailLogic.go | 10 +- internal/logic/admin/ads/getAdsListLogic.go | 12 +- internal/logic/admin/ads/updateAdsLogic.go | 10 +- .../announcement/createAnnouncementLogic.go | 10 +- .../announcement/deleteAnnouncementLogic.go | 8 +- .../announcement/getAnnouncementListLogic.go | 12 +- .../announcement/getAnnouncementLogic.go | 10 +- .../announcement/updateAnnouncementLogic.go | 8 +- .../createSubscribeApplicationLogic.go | 61 + .../deleteSubscribeApplicationLogic.go | 35 + .../getSubscribeApplicationListLogic.go | 61 + .../previewSubscribeTemplateLogic.go | 76 + .../updateSubscribeApplicationLogic.go | 62 + .../authMethod/getAuthMethodConfigLogic.go | 12 +- .../authMethod/getAuthMethodListLogic.go | 10 +- .../admin/authMethod/getEmailPlatformLogic.go | 8 +- .../admin/authMethod/getSmsPlatformLogic.go | 8 +- .../admin/authMethod/testEmailSendLogic.go | 10 +- .../admin/authMethod/testSmsSendLogic.go | 10 +- .../authMethod/updateAuthMethodConfigLogic.go | 86 +- .../logic/admin/authMethod/validate_test.go | 2 +- .../console/queryRevenueStatisticsLogic.go | 107 +- .../console/queryServerTotalDataLogic.go | 351 +++-- .../console/queryTicketWaitReplyLogic.go | 6 +- .../admin/console/queryUserStatisticsLogic.go | 109 +- .../admin/coupon/batchDeleteCouponLogic.go | 8 +- .../logic/admin/coupon/createCouponLogic.go | 16 +- .../logic/admin/coupon/deleteCouponLogic.go | 8 +- .../logic/admin/coupon/getCouponListLogic.go | 10 +- .../logic/admin/coupon/updateCouponLogic.go | 12 +- .../document/batchDeleteDocumentLogic.go | 8 +- .../admin/document/createDocumentLogic.go | 10 +- .../admin/document/deleteDocumentLogic.go | 8 +- .../admin/document/getDocumentDetailLogic.go | 10 +- .../admin/document/getDocumentListLogic.go | 10 +- .../admin/document/updateDocumentLogic.go | 10 +- .../logic/admin/log/filterBalanceLogLogic.go | 64 + .../admin/log/filterCommissionLogLogic.go | 61 + .../logic/admin/log/filterEmailLogLogic.go | 68 + .../logic/admin/log/filterGiftLogLogic.go | 68 + .../logic/admin/log/filterLoginLogLogic.go | 65 + .../logic/admin/log/filterMobileLogLogic.go | 68 + .../logic/admin/log/filterRegisterLogLogic.go | 66 + .../admin/log/filterResetSubscribeLogLogic.go | 66 + .../admin/log/filterServerTrafficLogLogic.go | 166 +++ .../admin/log/filterSubscribeLogLogic.go | 71 + .../admin/log/filterTrafficLogDetailsLogic.go | 84 ++ .../log/filterUserSubscribeTrafficLogLogic.go | 160 +++ .../logic/admin/log/getLogSettingLogic.go | 37 + .../logic/admin/log/getMessageLogListLogic.go | 50 +- .../logic/admin/log/updateLogSettingLogic.go | 63 + .../createBatchSendEmailTaskLogic.go | 170 +++ .../admin/marketing/createQuotaTaskLogic.go | 104 ++ .../getBatchSendEmailTaskListLogic.go | 89 ++ .../getBatchSendEmailTaskStatusLogic.go | 44 + .../marketing/getPreSendEmailCountLogic.go | 93 ++ .../marketing/queryQuotaTaskListLogic.go | 83 ++ .../marketing/queryQuotaTaskPreCountLogic.go | 55 + .../marketing/queryQuotaTaskStatusLogic.go | 42 + .../marketing/stopBatchSendEmailTaskLogic.go | 42 + .../logic/admin/order/createOrderLogic.go | 12 +- .../logic/admin/order/getOrderListLogic.go | 10 +- .../admin/order/updateOrderStatusLogic.go | 10 +- .../admin/payment/createPaymentMethodLogic.go | 104 +- .../admin/payment/deletePaymentMethodLogic.go | 8 +- .../payment/getPaymentMethodListLogic.go | 35 +- .../admin/payment/getPaymentPlatformLogic.go | 8 +- .../admin/payment/updatePaymentMethodLogic.go | 16 +- .../admin/server/batchDeleteNodeGroupLogic.go | 44 - .../admin/server/batchDeleteNodeLogic.go | 43 - internal/logic/admin/server/constant.go | 11 + .../admin/server/createNodeGroupLogic.go | 40 - .../logic/admin/server/createNodeLogic.go | 92 +- .../admin/server/createRuleGroupLogic.go | 69 - .../logic/admin/server/createServerLogic.go | 110 ++ .../admin/server/deleteNodeGroupLogic.go | 44 - .../logic/admin/server/deleteNodeLogic.go | 49 +- .../admin/server/deleteRuleGroupLogic.go | 35 - .../logic/admin/server/deleteServerLogic.go | 41 + .../logic/admin/server/filterNodeListLogic.go | 64 + .../admin/server/filterServerListLogic.go | 164 +++ .../logic/admin/server/getNodeDetailLogic.go | 43 - .../admin/server/getNodeGroupListLogic.go | 39 - .../logic/admin/server/getNodeListLogic.go | 100 -- .../logic/admin/server/getNodeTagListLogic.go | 53 - .../admin/server/getRuleGroupListLogic.go | 52 - .../admin/server/getServerProtocolsLogic.go | 49 + .../admin/server/hasMigrateSeverNodeLogic.go | 52 + .../admin/server/migrateServerNodeLogic.go | 338 +++++ .../logic/admin/server/queryNodeTagLogic.go | 46 + ...SortLogic.go => resetSortWithNodeLogic.go} | 38 +- .../admin/server/resetSortWithServerLogic.go | 86 ++ .../admin/server/toggleNodeStatusLogic.go | 51 + .../admin/server/updateNodeGroupLogic.go | 40 - .../logic/admin/server/updateNodeLogic.go | 104 +- .../admin/server/updateRuleGroupLogic.go | 50 - .../logic/admin/server/updateServerLogic.go | 119 ++ .../batchDeleteSubscribeGroupLogic.go | 10 +- .../subscribe/batchDeleteSubscribeLogic.go | 10 +- .../subscribe/createSubscribeGroupLogic.go | 10 +- .../admin/subscribe/createSubscribeLogic.go | 18 +- .../subscribe/deleteSubscribeGroupLogic.go | 10 +- .../admin/subscribe/deleteSubscribeLogic.go | 10 +- .../subscribe/getSubscribeDetailsLogic.go | 15 +- .../subscribe/getSubscribeGroupListLogic.go | 12 +- .../admin/subscribe/getSubscribeListLogic.go | 27 +- .../admin/subscribe/subscribeSortLogic.go | 15 +- .../subscribe/updateSubscribeGroupLogic.go | 10 +- .../admin/subscribe/updateSubscribeLogic.go | 20 +- .../admin/system/createApplicationLogic.go | 125 -- .../system/createApplicationVersionLogic.go | 44 - .../admin/system/deleteApplicationLogic.go | 35 - .../system/deleteApplicationVersionLogic.go | 36 - .../admin/system/getApplicationConfigLogic.go | 49 - .../logic/admin/system/getApplicationLogic.go | 113 -- .../admin/system/getCurrencyConfigLogic.go | 10 +- .../admin/system/getInviteConfigLogic.go | 10 +- .../logic/admin/system/getNodeConfigLogic.go | 53 +- .../admin/system/getNodeMultiplierLogic.go | 8 +- .../system/getPrivacyPolicyConfigLogic.go | 10 +- .../admin/system/getRegisterConfigLogic.go | 10 +- .../logic/admin/system/getSiteConfigLogic.go | 10 +- .../admin/system/getSubscribeConfigLogic.go | 10 +- .../admin/system/getSubscribeTypeLogic.go | 42 - .../logic/admin/system/getTosConfigLogic.go | 10 +- .../admin/system/getVerifyCodeConfigLogic.go | 10 +- .../admin/system/getVerifyConfigLogic.go | 12 +- .../system/preViewNodeMultiplierLogic.go | 33 + .../admin/system/setNodeMultiplierLogic.go | 8 +- .../admin/system/settingTelegramBotLogic.go | 6 +- .../system/updateApplicationConfigLogic.go | 45 - .../admin/system/updateApplicationLogic.go | 149 -- .../system/updateApplicationVersionLogic.go | 45 - .../admin/system/updateCurrencyConfigLogic.go | 14 +- .../admin/system/updateInviteConfigLogic.go | 16 +- .../admin/system/updateNodeConfigLogic.go | 20 +- .../system/updatePrivacyPolicyConfigLogic.go | 14 +- .../admin/system/updateRegisterConfigLogic.go | 16 +- .../admin/system/updateSiteConfigLogic.go | 14 +- .../system/updateSubscribeConfigLogic.go | 16 +- .../admin/system/updateTosConfigLogic.go | 14 +- .../system/updateVerifyCodeConfigLogic.go | 14 +- .../admin/system/updateVerifyConfigLogic.go | 16 +- .../admin/ticket/createTicketFollowLogic.go | 10 +- .../logic/admin/ticket/getTicketListLogic.go | 10 +- internal/logic/admin/ticket/getTicketLogic.go | 10 +- .../admin/ticket/updateTicketStatusLogic.go | 8 +- .../logic/admin/tool/getSystemLogLogic.go | 43 +- internal/logic/admin/tool/getVersionLogic.go | 51 + .../logic/admin/tool/restartSystemLogic.go | 4 +- .../logic/admin/user/batchDeleteUserLogic.go | 17 +- .../admin/user/createUserAuthMethodLogic.go | 10 +- internal/logic/admin/user/createUserLogic.go | 24 +- .../admin/user/createUserSubscribeLogic.go | 16 +- internal/logic/admin/user/currentUserLogic.go | 14 +- .../admin/user/deleteUserAuthMethodLogic.go | 8 +- .../logic/admin/user/deleteUserDeviceLogic.go | 8 +- internal/logic/admin/user/deleteUserLogic.go | 15 +- .../admin/user/deleteUserSubscribeLogic.go | 27 +- .../admin/user/getUserAuthMethodLogic.go | 10 +- .../logic/admin/user/getUserDetailLogic.go | 10 +- internal/logic/admin/user/getUserListLogic.go | 27 +- .../logic/admin/user/getUserLoginLogsLogic.go | 36 +- .../admin/user/getUserSubscribeByIdLogic.go | 10 +- .../user/getUserSubscribeDevicesLogic.go | 10 +- .../logic/admin/user/getUserSubscribeLogic.go | 10 +- .../admin/user/getUserSubscribeLogsLogic.go | 17 +- .../getUserSubscribeResetTrafficLogsLogic.go | 62 + .../user/getUserSubscribeTrafficLogsLogic.go | 10 +- .../user/kickOfflineByUserDeviceLogic.go | 8 +- .../admin/user/updateUserAuthMethodLogic.go | 18 +- .../admin/user/updateUserBasicInfoLogic.go | 103 +- .../logic/admin/user/updateUserDeviceLogic.go | 8 +- .../user/updateUserNotifySettingLogic.go | 10 +- .../admin/user/updateUserSubscribeLogic.go | 32 +- .../announcement/queryAnnouncementLogic.go | 47 - internal/logic/app/auth/checkLogic.go | 41 - internal/logic/app/auth/findUserByMethod.go | 59 - internal/logic/app/auth/getAppConfigLogic.go | 136 -- internal/logic/app/auth/loginLogic.go | 194 --- internal/logic/app/auth/registerLogic.go | 249 ---- internal/logic/app/auth/resetPasswordLogic.go | 161 --- .../app/document/queryDocumentDetailLogic.go | 39 - .../app/document/queryDocumentListLogic.go | 48 - internal/logic/app/node/getNodeListLogic.go | 91 -- .../logic/app/node/getRuleGroupListLogic.go | 41 - internal/logic/app/order/calculateCoupon.go | 13 - internal/logic/app/order/calculateFee.go | 20 - .../logic/app/order/checkoutOrderLogic.go | 373 ----- internal/logic/app/order/closeOrderLogic.go | 205 --- internal/logic/app/order/getDiscount.go | 14 - .../logic/app/order/preCreateOrderLogic.go | 104 -- internal/logic/app/order/purchaseLogic.go | 204 --- .../logic/app/order/queryOrderDetailLogic.go | 40 - .../logic/app/order/queryOrderListLogic.go | 56 - internal/logic/app/order/rechargeLogic.go | 92 -- internal/logic/app/order/renewalLogic.go | 178 --- internal/logic/app/order/resetTrafficLogic.go | 146 -- .../getAvailablePaymentMethodsLogic.go | 40 - .../subscribe/queryApplicationConfigLogic.go | 115 -- .../subscribe/querySubscribeGroupListLogic.go | 44 - .../app/subscribe/querySubscribeListLogic.go | 55 - .../queryUserAlreadySubscribeLogic.go | 67 - .../queryUserAvailableUserSubscribeLogic.go | 118 -- .../resetUserSubscribePeriodLogic.go | 60 - internal/logic/app/user/deleteAccountLogic.go | 103 -- .../user/getuseronlinetimestatisticslogic.go | 115 -- .../user/getusersubscribetrafficlogslogic.go | 85 -- .../app/user/queryUserAffiliateListLogic.go | 62 - internal/logic/app/user/queryUserInfoLogic.go | 63 - .../logic/app/user/queryuseraffiliatelogic.go | 60 - .../logic/app/user/updatePasswordLogic.go | 46 - internal/logic/app/ws/appWsLogic.go | 81 -- internal/logic/auth/bindDeviceLogic.go | 234 +++ internal/logic/auth/checkUserLogic.go | 8 +- .../logic/auth/checkUserTelephoneLogic.go | 10 +- internal/logic/auth/deviceLoginLogic.go | 295 ++++ .../auth/oauth/appleLoginCallbackLogic.go | 6 +- .../auth/oauth/oAuthLoginGetTokenLogic.go | 1032 +++++++++---- internal/logic/auth/oauth/oAuthLoginLogic.go | 16 +- internal/logic/auth/resetPasswordLogic.go | 59 +- internal/logic/auth/telephoneLoginLogic.go | 64 +- .../logic/auth/telephoneResetPasswordLogic.go | 62 +- .../logic/auth/telephoneUserRegisterLogic.go | 90 +- internal/logic/auth/userLoginLogic.go | 61 +- internal/logic/auth/userRegisterLogic.go | 84 +- .../common/checkverificationcodelogic.go | 16 +- internal/logic/common/getAdsLogic.go | 10 +- internal/logic/common/getApplicationLogic.go | 136 -- internal/logic/common/getClientLogic.go | 56 + internal/logic/common/getGlobalConfigLogic.go | 15 +- .../logic/common/getPrivacyPolicyLogic.go | 10 +- internal/logic/common/getStatLogic.go | 14 +- internal/logic/common/getSubscriptionLogic.go | 41 - internal/logic/common/getTosLogic.go | 10 +- internal/logic/common/sendEmailCodeLogic.go | 52 +- internal/logic/common/sendSmsCodeLogic.go | 20 +- internal/logic/notify/alipayNotifyLogic.go | 14 +- internal/logic/notify/ePayNotifyLogic.go | 16 +- internal/logic/notify/stripeNotifyLogic.go | 14 +- .../announcement/queryAnnouncementLogic.go | 12 +- .../document/queryDocumentDetailLogic.go | 10 +- .../public/document/queryDocumentListLogic.go | 10 +- .../logic/public/order/calculateCoupon.go | 2 +- internal/logic/public/order/calculateFee.go | 2 +- .../logic/public/order/closeOrderLogic.go | 74 +- internal/logic/public/order/constant.go | 1 - internal/logic/public/order/getDiscount.go | 3 +- .../logic/public/order/preCreateOrderLogic.go | 60 +- internal/logic/public/order/purchaseLogic.go | 92 +- .../public/order/queryOrderDetailLogic.go | 10 +- .../logic/public/order/queryOrderListLogic.go | 14 +- internal/logic/public/order/rechargeLogic.go | 18 +- internal/logic/public/order/renewalLogic.go | 80 +- .../logic/public/order/resetTrafficLogic.go | 45 +- .../getAvailablePaymentMethodsLogic.go | 10 +- .../portal/getAvailablePaymentMethodsLogic.go | 10 +- .../public/portal/getSubscriptionLogic.go | 21 +- .../public/portal/prePurchaseOrderLogic.go | 20 +- .../public/portal/purchaseCheckoutLogic.go | 346 +++-- internal/logic/public/portal/purchaseLogic.go | 44 +- .../public/portal/queryPurchaseOrderLogic.go | 22 +- internal/logic/public/portal/tool.go | 6 +- .../subscribe/queryApplicationConfigLogic.go | 116 -- .../subscribe/querySubscribeGroupListLogic.go | 12 +- .../subscribe/querySubscribeListLogic.go | 24 +- .../ticket/createUserTicketFollowLogic.go | 14 +- .../public/ticket/createUserTicketLogic.go | 14 +- .../ticket/getUserTicketDetailsLogic.go | 14 +- .../public/ticket/getUserTicketListLogic.go | 14 +- .../ticket/updateUserTicketStatusLogic.go | 12 +- .../public/user/bindOAuthCallbackLogic.go | 28 +- internal/logic/public/user/bindOAuthLogic.go | 14 +- .../logic/public/user/bindTelegramLogic.go | 6 +- .../public/user/calculateRemainingAmount.go | 14 +- .../logic/public/user/getDeviceListLogic.go | 39 + .../logic/public/user/getLoginLogLogic.go | 39 +- .../logic/public/user/getOAuthMethodsLogic.go | 14 +- .../logic/public/user/getSubscribeLogLogic.go | 41 +- .../logic/public/user/preUnsubscribeLogic.go | 6 +- .../user/queryUserAffiliateListLogic.go | 12 +- .../public/user/queryUserAffiliateLogic.go | 33 +- .../public/user/queryUserBalanceLogLogic.go | 58 +- .../user/queryUserCommissionLogLogic.go | 44 +- .../logic/public/user/queryUserInfoLogic.go | 38 +- .../public/user/queryUserSubscribeLogic.go | 38 +- .../user/resetUserSubscribeTokenLogic.go | 45 +- .../logic/public/user/unbindDeviceLogic.go | 42 + .../logic/public/user/unbindOAuthLogic.go | 13 +- .../logic/public/user/unbindTelegramLogic.go | 14 +- .../logic/public/user/unsubscribeLogic.go | 142 +- .../logic/public/user/updateBindEmailLogic.go | 14 +- .../public/user/updateBindMobileLogic.go | 16 +- .../public/user/updateUserNotifyLogic.go | 12 +- .../public/user/updateUserPasswordLogic.go | 14 +- .../logic/public/user/verifyEmailLogic.go | 14 +- internal/logic/server/constant.go | 85 +- internal/logic/server/getServerConfigLogic.go | 192 ++- .../logic/server/getServerUserListLogic.go | 89 +- internal/logic/server/pushOnlineUsersLogic.go | 28 +- .../server/queryServerProtocolConfigLogic.go | 93 ++ .../logic/server/serverPushStatusLogic.go | 24 +- .../server/serverPushUserTrafficLogic.go | 39 +- internal/logic/subscribe/subscribeLogic.go | 351 +++-- internal/logic/telegram/bot.go | 14 +- internal/logic/telegram/telegramLogic.go | 12 +- internal/middleware/authMiddleware.go | 22 +- .../{appMiddleware.go => deviceMiddleware.go} | 45 +- internal/middleware/loggerMiddleware.go | 15 +- internal/middleware/notifyMiddleware.go | 4 +- internal/middleware/panDomainMiddleware.go | 53 +- internal/middleware/serverMiddleware.go | 2 +- internal/middleware/traceMiddleware.go | 22 +- internal/model/ads/default.go | 2 +- internal/model/announcement/default.go | 2 +- internal/model/application/application.go | 54 - internal/model/application/default.go | 245 ---- internal/model/application/model.go | 16 - internal/model/auth/auth.go | 69 +- internal/model/auth/default.go | 2 +- internal/model/cache/constant.go | 52 - internal/model/cache/node.go | 584 -------- internal/model/cache/node_test.go | 575 -------- internal/model/cache/types.go | 34 - internal/model/client/application.go | 75 + internal/model/client/default.go | 81 ++ internal/model/coupon/default.go | 2 +- internal/model/document/default.go | 2 +- internal/model/log/default.go | 74 +- internal/model/log/log.go | 445 +++++- internal/model/log/model.go | 58 +- internal/model/node/cache.go | 163 +++ internal/model/node/default.go | 131 ++ internal/model/node/model.go | 185 +++ internal/model/node/node.go | 82 ++ internal/model/node/server.go | 188 +++ internal/model/order/default.go | 2 +- internal/model/order/model.go | 156 +- internal/model/payment/default.go | 29 +- internal/model/payment/payment.go | 73 +- internal/model/server/default.go | 41 +- internal/model/server/model.go | 75 +- internal/model/server/server.go | 61 +- internal/model/subscribe/default.go | 36 +- internal/model/subscribe/model.go | 207 +-- internal/model/subscribe/subscribe.go | 28 +- internal/model/subscribeType/default.go | 117 -- internal/model/subscribeType/model.go | 16 - internal/model/subscribeType/subscribeType.go | 15 - internal/model/system/default.go | 2 +- internal/model/system/model.go | 12 +- internal/model/task/task.go | 151 ++ internal/model/ticket/default.go | 2 +- internal/model/user/authMethod.go | 31 +- internal/model/user/cache.go | 285 ++++ internal/model/user/default.go | 98 +- internal/model/user/device.go | 31 +- internal/model/user/log.go | 81 -- internal/model/user/model.go | 270 ++-- internal/model/user/subscribe.go | 80 +- internal/model/user/user.go | 180 +-- internal/server.go | 14 +- internal/svc/asynq.go | 2 +- internal/svc/devce.go | 8 +- internal/svc/logger.go | 4 +- internal/svc/serviceContext.go | 114 +- internal/types/types.go | 1273 +++++++++++------ pkg/adapter/adapter.go | 71 - pkg/adapter/adapter_test.go | 138 -- pkg/adapter/clash/clash.go | 68 - pkg/adapter/clash/clash_test.go | 41 - pkg/adapter/clash/default.go | 35 - pkg/adapter/clash/model.go | 131 -- pkg/adapter/clash/parse.go | 165 --- pkg/adapter/clash/tool.go | 33 - pkg/adapter/general/uri.go | 245 ---- pkg/adapter/general/uri_test.go | 26 - pkg/adapter/loon/build.go | 27 - pkg/adapter/loon/hysteria2.go | 34 - pkg/adapter/loon/loon_test.go | 29 - pkg/adapter/loon/shadowsocks.go | 49 - pkg/adapter/loon/trojan.go | 44 - pkg/adapter/loon/vless.go | 62 - pkg/adapter/loon/vmess.go | 53 - pkg/adapter/proxy/proxy.go | 114 -- pkg/adapter/quantumultx/build.go | 22 - pkg/adapter/quantumultx/quantumux_test.go | 94 -- pkg/adapter/quantumultx/shadowsocks.go | 23 - pkg/adapter/quantumultx/trojan.go | 39 - pkg/adapter/quantumultx/vmess.go | 45 - pkg/adapter/shadowrocket/build.go | 48 - pkg/adapter/shadowrocket/shadowrocket_test.go | 76 - pkg/adapter/shadowrocket/vmess.go | 57 - pkg/adapter/singbox/build.go | 201 --- pkg/adapter/singbox/default.go | 100 -- pkg/adapter/singbox/hysteria2.go | 76 - pkg/adapter/singbox/multiplex.go | 17 - pkg/adapter/singbox/rule.go | 130 -- pkg/adapter/singbox/rule_test.go | 15 - pkg/adapter/singbox/shadowsocks.go | 34 - pkg/adapter/singbox/singbox.go | 98 -- pkg/adapter/singbox/singbox_test.go | 80 -- pkg/adapter/singbox/tls.go | 87 -- pkg/adapter/singbox/tool.go | 11 - pkg/adapter/singbox/trojan.go | 39 - pkg/adapter/singbox/tuic.go | 40 - pkg/adapter/singbox/v2rayTransport.go | 114 -- pkg/adapter/singbox/vless.go | 44 - pkg/adapter/singbox/vmess.go | 43 - pkg/adapter/surfboard/build.go | 111 -- pkg/adapter/surfboard/build_test.go | 24 - pkg/adapter/surfboard/default.tpl | 29 - pkg/adapter/surfboard/model.go | 12 - pkg/adapter/surfboard/shadowsocks.go | 24 - pkg/adapter/surfboard/shadowsocks_test.go | 28 - pkg/adapter/surfboard/trojan.go | 41 - pkg/adapter/surfboard/trojan_test.go | 36 - pkg/adapter/surfboard/vmess.go | 45 - pkg/adapter/surfboard/vmess_test.go | 33 - pkg/adapter/surge/default.tpl | 61 - pkg/adapter/surge/hysteria2.go | 43 - pkg/adapter/surge/hysteria2_test.go | 70 - pkg/adapter/surge/shadowsocks.go | 24 - pkg/adapter/surge/surge.go | 117 -- pkg/adapter/surge/surge_test.go | 97 -- pkg/adapter/surge/trojan.go | 32 - pkg/adapter/surge/vmess.go | 44 - pkg/adapter/uilts.go | 197 --- pkg/cache/cacheopt.go | 49 + pkg/cache/cacheopt_test.go | 28 + pkg/cache/gorm.go | 23 +- pkg/cache/gorm_test.go | 2 +- pkg/constant/context.go | 1 + pkg/constant/types.go | 21 +- pkg/constant/version.go | 5 +- pkg/countryCenter/county_center.go | 1175 --------------- pkg/countryCenter/county_center_test.go | 14 - pkg/deduction/deduction.go | 362 ++++- pkg/deduction/deduction_test.go | 665 +++++++++ pkg/email/manager.go | 134 ++ pkg/email/platform.go | 2 +- pkg/email/sender.go | 4 +- pkg/email/template.go | 1 - pkg/email/worker.go | 192 +++ pkg/fs/temps.go | 2 +- pkg/hash/consistenthash.go | 2 +- pkg/limit/tokenlimit.go | 4 +- pkg/logger/color.go | 2 +- pkg/logger/color_test.go | 2 +- pkg/logger/config.go | 4 +- pkg/logger/limitedexecutor.go | 4 +- pkg/logger/limitedexecutor_test.go | 2 +- pkg/logger/logtest/logtest.go | 2 +- pkg/logger/logtest/logtest_test.go | 2 +- pkg/logger/read.go | 98 ++ pkg/logger/read_test.go | 16 + pkg/logger/richlogger.go | 4 +- pkg/logger/rotatelogger.go | 4 +- pkg/logger/rotatelogger_test.go | 4 +- pkg/logger/vars.go | 2 +- pkg/logger/writer.go | 4 +- pkg/nodeMultiplier/manage_test.go | 9 +- pkg/nodeMultiplier/manager.go | 4 +- pkg/oauth/apple/apple.html | 2 +- pkg/oauth/apple/apple_test.go | 2 +- pkg/oauth/google/google.go | 2 +- pkg/orm/mysql.go | 2 +- pkg/orm/tool_test.go | 24 +- pkg/payment/alipay/alipay.go | 4 +- pkg/payment/epay/epay.go | 4 +- pkg/payment/payssion/payssion.go | 127 -- pkg/payment/payssion/payssion_test.go | 32 - pkg/payment/platform.go | 21 +- pkg/payment/stripe/stripe.go | 17 +- pkg/payment/stripe/stripe_test.go | 4 +- pkg/proc/shutdown.go | 2 +- pkg/random/RandomKey_test.go | 2 +- pkg/rescue/recover.go | 2 +- pkg/result/httpResult.go | 2 +- pkg/service/service.go | 4 +- pkg/service/servicegroup_test.go | 2 +- pkg/sms/abosend/abosend.go | 6 +- pkg/sms/alibabacloud/alibabacloud.go | 2 +- pkg/sms/platform.go | 2 +- pkg/sms/sender.go | 8 +- pkg/sms/smsbao/smsbao.go | 4 +- pkg/sms/twilio/twilio.go | 4 +- pkg/syncx/cond.go | 4 +- pkg/syncx/donechan.go | 2 +- pkg/syncx/immutableresource.go | 2 +- pkg/syncx/limit.go | 2 +- pkg/syncx/pool.go | 2 +- pkg/syncx/pool_test.go | 2 +- pkg/syncx/resourcemanager.go | 2 +- pkg/syncx/spinlock_test.go | 2 +- pkg/threading/routines.go | 2 +- pkg/timex/ticker.go | 2 +- pkg/tool/cipher_test.go | 3 +- pkg/tool/convert.go | 11 + pkg/tool/copy.go | 42 +- pkg/tool/encryption_test.go | 2 +- pkg/tool/slice.go | 14 +- pkg/tool/sliceReflectToStruct.go | 2 +- pkg/tool/time.go | 15 +- pkg/tool/version_test.go | 2 +- pkg/trace/agent.go | 4 +- pkg/trace/agent_test.go | 2 +- pkg/trace/utils.go | 2 +- pkg/uuidx/uuid.go | 2 +- pkg/uuidx/uuid_test.go | 4 +- pkg/xerr/errCode.go | 7 +- pkg/xerr/errMsg.go | 7 +- ppanel.api | 11 +- ppanel.go | 2 +- queue/handler/routes.go | 32 +- queue/logic/country/getCountryLogic.go | 40 +- queue/logic/email/batchEmailLogic.go | 78 + queue/logic/email/sendEmailLogic.go | 117 +- queue/logic/order/activateOrderLogic.go | 1135 ++++++++------- queue/logic/order/activateOrderLogic.go_bak | 675 +++++++++ queue/logic/order/checkOrderLogic.go | 161 --- queue/logic/order/deferCloseOrderLogic.go | 10 +- queue/logic/sms/sendSmsLogic.go | 31 +- .../subscription/checkSubscriptionLogic.go | 76 +- queue/logic/task/quotaLogic.go | 407 ++++++ queue/logic/traffic/resetTrafficLogic.go | 625 ++++++++ queue/logic/traffic/serverDataLogic.go | 12 +- queue/logic/traffic/trafficStatLogic.go | 176 +++ queue/logic/traffic/trafficStatisticsLogic.go | 78 +- queue/queue.go | 6 +- queue/types/email.go | 15 +- queue/types/order.go | 3 - queue/types/scheduler.go | 3 +- queue/types/server.go | 1 + queue/types/sms.go | 2 +- queue/types/task.go | 9 + readme.md | 332 ++++- readme_zh.md | 288 ++++ scheduler/scheduler.go | 27 +- script/generate.sh | 12 + 974 files changed, 23609 insertions(+), 23398 deletions(-) create mode 100644 1.conf create mode 100644 Makefile create mode 100644 adapter/adapter.go create mode 100644 adapter/adapter_test.go create mode 100644 adapter/client.go create mode 100644 adapter/client_test.go create mode 100644 adapter/utils.go create mode 100644 adapter/utils_test.go create mode 100644 apis/admin/application.api create mode 100644 apis/admin/marketing.api delete mode 100644 apis/app/announcement.api delete mode 100644 apis/app/auth.api delete mode 100644 apis/app/document.api delete mode 100644 apis/app/node.api delete mode 100644 apis/app/order.api delete mode 100644 apis/app/payment.api delete mode 100644 apis/app/subscribe.api delete mode 100644 apis/app/user.api delete mode 100644 apis/app/ws.api delete mode 100644 apis/swagger_app.api create mode 100644 doc/image/architecture-en.png create mode 100644 doc/image/architecture-zh.png mode change 100644 => 100755 generate/gopure-amd64.exe mode change 100644 => 100755 generate/gopure-arm64.exe mode change 100644 => 100755 generate/gopure-darwin-amd64 mode change 100644 => 100755 generate/gopure-darwin-arm64 mode change 100644 => 100755 generate/gopure-linux-amd64 mode change 100644 => 100755 generate/gopure-linux-arm64 create mode 100644 initialize/device.go create mode 100644 initialize/migrate/database/00001_init_schema.down.sql create mode 100644 initialize/migrate/database/00001_init_schema.up.sql create mode 100644 initialize/migrate/database/00002_init_basic_data.down.sql create mode 100644 initialize/migrate/database/00002_init_basic_data.up.sql delete mode 100644 initialize/migrate/database/01200-patch.sql delete mode 100644 initialize/migrate/database/01201-patch.sql delete mode 100644 initialize/migrate/database/01202-patch.sql create mode 100644 initialize/migrate/database/02003_update_payment.down.sql create mode 100644 initialize/migrate/database/02003_update_payment.up.sql create mode 100644 initialize/migrate/database/02004_rebuild_rule.down.sql create mode 100644 initialize/migrate/database/02004_rebuild_rule.up.sql create mode 100644 initialize/migrate/database/02005_device_online_record.down.sql create mode 100644 initialize/migrate/database/02005_device_online_record.up.sql create mode 100644 initialize/migrate/database/02006_reset_subscribe_record.down.sql create mode 100644 initialize/migrate/database/02006_reset_subscribe_record.up.sql create mode 100644 initialize/migrate/database/02007_adapte_rule.down.sql create mode 100644 initialize/migrate/database/02007_adapte_rule.up.sql create mode 100644 initialize/migrate/database/02100_task.down.sql create mode 100644 initialize/migrate/database/02100_task.up.sql create mode 100644 initialize/migrate/database/02101_subscribe_application.down.sql create mode 100644 initialize/migrate/database/02101_subscribe_application.up.sql create mode 100644 initialize/migrate/database/02102_subscribe_config.down.sql create mode 100644 initialize/migrate/database/02102_subscribe_config.up.sql create mode 100644 initialize/migrate/database/02103_delete_application.down.sql create mode 100644 initialize/migrate/database/02103_delete_application.up.sql create mode 100644 initialize/migrate/database/02104_system_log.down.sql create mode 100644 initialize/migrate/database/02104_system_log.up.sql create mode 100644 initialize/migrate/database/02105_node.down.sql create mode 100644 initialize/migrate/database/02105_node.up.sql create mode 100644 initialize/migrate/database/02106_subscribe.down.sql create mode 100644 initialize/migrate/database/02106_subscribe.up.sql create mode 100644 initialize/migrate/database/02107_log_setting.down.sql create mode 100644 initialize/migrate/database/02107_log_setting.up.sql create mode 100644 initialize/migrate/database/02108_user_referral.down.sql create mode 100644 initialize/migrate/database/02108_user_referral.up.sql create mode 100644 initialize/migrate/database/02109_node_sort.down.sql create mode 100644 initialize/migrate/database/02109_node_sort.up.sql create mode 100644 initialize/migrate/database/02110_traffic_log_index.down.sql create mode 100644 initialize/migrate/database/02110_traffic_log_index.up.sql create mode 100644 initialize/migrate/database/02111_clear_table.down.sql create mode 100644 initialize/migrate/database/02111_clear_table.up.sql create mode 100644 initialize/migrate/database/02112_subscribe.down.sql create mode 100644 initialize/migrate/database/02112_subscribe.up.sql create mode 100644 initialize/migrate/database/02113_task.down.sql create mode 100644 initialize/migrate/database/02113_task.up.sql create mode 100644 initialize/migrate/database/02114_node_config.down.sql create mode 100644 initialize/migrate/database/02114_node_config.up.sql delete mode 100644 initialize/migrate/database/ppanel.sql create mode 100644 initialize/migrate/migrate_test.go delete mode 100644 initialize/migrate/patch/01703.go delete mode 100644 initialize/migrate/patch/02000.go delete mode 100644 initialize/migrate/patch/03001.go delete mode 100644 initialize/statistics.go delete mode 100644 internal/config/constant.go create mode 100644 internal/handler/admin/application/createSubscribeApplicationHandler.go create mode 100644 internal/handler/admin/application/deleteSubscribeApplicationHandler.go create mode 100644 internal/handler/admin/application/getSubscribeApplicationListHandler.go create mode 100644 internal/handler/admin/application/previewSubscribeTemplateHandler.go create mode 100644 internal/handler/admin/application/updateSubscribeApplicationHandler.go create mode 100644 internal/handler/admin/log/filterBalanceLogHandler.go create mode 100644 internal/handler/admin/log/filterCommissionLogHandler.go create mode 100644 internal/handler/admin/log/filterEmailLogHandler.go create mode 100644 internal/handler/admin/log/filterGiftLogHandler.go create mode 100644 internal/handler/admin/log/filterLoginLogHandler.go create mode 100644 internal/handler/admin/log/filterMobileLogHandler.go create mode 100644 internal/handler/admin/log/filterRegisterLogHandler.go create mode 100644 internal/handler/admin/log/filterResetSubscribeLogHandler.go create mode 100644 internal/handler/admin/log/filterServerTrafficLogHandler.go create mode 100644 internal/handler/admin/log/filterSubscribeLogHandler.go create mode 100644 internal/handler/admin/log/filterTrafficLogDetailsHandler.go create mode 100644 internal/handler/admin/log/filterUserSubscribeTrafficLogHandler.go create mode 100644 internal/handler/admin/log/getLogSettingHandler.go create mode 100644 internal/handler/admin/log/updateLogSettingHandler.go create mode 100644 internal/handler/admin/marketing/createBatchSendEmailTaskHandler.go create mode 100644 internal/handler/admin/marketing/createQuotaTaskHandler.go create mode 100644 internal/handler/admin/marketing/getBatchSendEmailTaskListHandler.go create mode 100644 internal/handler/admin/marketing/getBatchSendEmailTaskStatusHandler.go create mode 100644 internal/handler/admin/marketing/getPreSendEmailCountHandler.go create mode 100644 internal/handler/admin/marketing/queryQuotaTaskListHandler.go create mode 100644 internal/handler/admin/marketing/queryQuotaTaskPreCountHandler.go create mode 100644 internal/handler/admin/marketing/queryQuotaTaskStatusHandler.go create mode 100644 internal/handler/admin/marketing/stopBatchSendEmailTaskHandler.go delete mode 100644 internal/handler/admin/server/batchDeleteNodeGroupHandler.go delete mode 100644 internal/handler/admin/server/batchDeleteNodeHandler.go delete mode 100644 internal/handler/admin/server/createNodeGroupHandler.go delete mode 100644 internal/handler/admin/server/createRuleGroupHandler.go create mode 100644 internal/handler/admin/server/createServerHandler.go delete mode 100644 internal/handler/admin/server/deleteNodeGroupHandler.go delete mode 100644 internal/handler/admin/server/deleteRuleGroupHandler.go create mode 100644 internal/handler/admin/server/deleteServerHandler.go create mode 100644 internal/handler/admin/server/filterNodeListHandler.go create mode 100644 internal/handler/admin/server/filterServerListHandler.go delete mode 100644 internal/handler/admin/server/getNodeDetailHandler.go delete mode 100644 internal/handler/admin/server/getNodeGroupListHandler.go delete mode 100644 internal/handler/admin/server/getNodeListHandler.go delete mode 100644 internal/handler/admin/server/getNodeTagListHandler.go delete mode 100644 internal/handler/admin/server/getRuleGroupListHandler.go create mode 100644 internal/handler/admin/server/getServerProtocolsHandler.go create mode 100644 internal/handler/admin/server/hasMigrateSeverNodeHandler.go create mode 100644 internal/handler/admin/server/migrateServerNodeHandler.go delete mode 100644 internal/handler/admin/server/nodeSortHandler.go create mode 100644 internal/handler/admin/server/queryNodeTagHandler.go create mode 100644 internal/handler/admin/server/resetSortWithNodeHandler.go create mode 100644 internal/handler/admin/server/resetSortWithServerHandler.go create mode 100644 internal/handler/admin/server/toggleNodeStatusHandler.go delete mode 100644 internal/handler/admin/server/updateNodeGroupHandler.go delete mode 100644 internal/handler/admin/server/updateRuleGroupHandler.go create mode 100644 internal/handler/admin/server/updateServerHandler.go delete mode 100644 internal/handler/admin/system/createApplicationHandler.go delete mode 100644 internal/handler/admin/system/createApplicationVersionHandler.go delete mode 100644 internal/handler/admin/system/deleteApplicationHandler.go delete mode 100644 internal/handler/admin/system/deleteApplicationVersionHandler.go delete mode 100644 internal/handler/admin/system/getApplicationConfigHandler.go delete mode 100644 internal/handler/admin/system/getApplicationHandler.go delete mode 100644 internal/handler/admin/system/getSubscribeTypeHandler.go create mode 100644 internal/handler/admin/system/preViewNodeMultiplierHandler.go delete mode 100644 internal/handler/admin/system/updateApplicationConfigHandler.go delete mode 100644 internal/handler/admin/system/updateApplicationHandler.go delete mode 100644 internal/handler/admin/system/updateApplicationVersionHandler.go create mode 100644 internal/handler/admin/tool/getVersionHandler.go create mode 100644 internal/handler/admin/user/getUserSubscribeResetTrafficLogsHandler.go delete mode 100644 internal/handler/app/announcement/queryannouncementhandler.go delete mode 100644 internal/handler/app/auth/checkHandler.go delete mode 100644 internal/handler/app/auth/getAppConfigHandler.go delete mode 100644 internal/handler/app/auth/loginHandler.go delete mode 100644 internal/handler/app/auth/registerHandler.go delete mode 100644 internal/handler/app/auth/resetPasswordHandler.go delete mode 100644 internal/handler/app/document/querydocumentdetailhandler.go delete mode 100644 internal/handler/app/document/querydocumentlisthandler.go delete mode 100644 internal/handler/app/node/getNodeListHandler.go delete mode 100644 internal/handler/app/node/getRuleGroupListHandler.go delete mode 100644 internal/handler/app/order/checkoutorderhandler.go delete mode 100644 internal/handler/app/order/closeorderhandler.go delete mode 100644 internal/handler/app/order/precreateorderhandler.go delete mode 100644 internal/handler/app/order/purchasehandler.go delete mode 100644 internal/handler/app/order/queryorderdetailhandler.go delete mode 100644 internal/handler/app/order/queryorderlisthandler.go delete mode 100644 internal/handler/app/order/rechargehandler.go delete mode 100644 internal/handler/app/order/renewalhandler.go delete mode 100644 internal/handler/app/order/resettraffichandler.go delete mode 100644 internal/handler/app/payment/getavailablepaymentmethodshandler.go delete mode 100644 internal/handler/app/subscribe/queryApplicationConfigHandler.go delete mode 100644 internal/handler/app/subscribe/querySubscribeGroupListHandler.go delete mode 100644 internal/handler/app/subscribe/querySubscribeListHandler.go delete mode 100644 internal/handler/app/subscribe/queryUserAlreadySubscribeHandler.go delete mode 100644 internal/handler/app/subscribe/queryUserAvailableUserSubscribeHandler.go delete mode 100644 internal/handler/app/subscribe/resetUserSubscribePeriodHandler.go delete mode 100644 internal/handler/app/user/deleteAccountHandler.go delete mode 100644 internal/handler/app/user/getuseronlinetimestatisticshandler.go delete mode 100644 internal/handler/app/user/getusersubscribetrafficlogshandler.go delete mode 100644 internal/handler/app/user/queryUserInfoHandler.go delete mode 100644 internal/handler/app/user/queryuseraffiliatehandler.go delete mode 100644 internal/handler/app/user/queryuseraffiliatelisthandler.go delete mode 100644 internal/handler/app/user/updatePasswordHandler.go delete mode 100644 internal/handler/app/ws/appWsHandler.go create mode 100644 internal/handler/auth/deviceLoginHandler.go delete mode 100644 internal/handler/common/getApplicationHandler.go create mode 100644 internal/handler/common/getClientHandler.go delete mode 100644 internal/handler/common/getSubscriptionHandler.go delete mode 100644 internal/handler/public/subscribe/queryApplicationConfigHandler.go create mode 100644 internal/handler/public/user/getDeviceListHandler.go create mode 100644 internal/handler/public/user/unbindDeviceHandler.go create mode 100644 internal/handler/server/queryServerProtocolConfigHandler.go create mode 100644 internal/logic/admin/application/createSubscribeApplicationLogic.go create mode 100644 internal/logic/admin/application/deleteSubscribeApplicationLogic.go create mode 100644 internal/logic/admin/application/getSubscribeApplicationListLogic.go create mode 100644 internal/logic/admin/application/previewSubscribeTemplateLogic.go create mode 100644 internal/logic/admin/application/updateSubscribeApplicationLogic.go create mode 100644 internal/logic/admin/log/filterBalanceLogLogic.go create mode 100644 internal/logic/admin/log/filterCommissionLogLogic.go create mode 100644 internal/logic/admin/log/filterEmailLogLogic.go create mode 100644 internal/logic/admin/log/filterGiftLogLogic.go create mode 100644 internal/logic/admin/log/filterLoginLogLogic.go create mode 100644 internal/logic/admin/log/filterMobileLogLogic.go create mode 100644 internal/logic/admin/log/filterRegisterLogLogic.go create mode 100644 internal/logic/admin/log/filterResetSubscribeLogLogic.go create mode 100644 internal/logic/admin/log/filterServerTrafficLogLogic.go create mode 100644 internal/logic/admin/log/filterSubscribeLogLogic.go create mode 100644 internal/logic/admin/log/filterTrafficLogDetailsLogic.go create mode 100644 internal/logic/admin/log/filterUserSubscribeTrafficLogLogic.go create mode 100644 internal/logic/admin/log/getLogSettingLogic.go create mode 100644 internal/logic/admin/log/updateLogSettingLogic.go create mode 100644 internal/logic/admin/marketing/createBatchSendEmailTaskLogic.go create mode 100644 internal/logic/admin/marketing/createQuotaTaskLogic.go create mode 100644 internal/logic/admin/marketing/getBatchSendEmailTaskListLogic.go create mode 100644 internal/logic/admin/marketing/getBatchSendEmailTaskStatusLogic.go create mode 100644 internal/logic/admin/marketing/getPreSendEmailCountLogic.go create mode 100644 internal/logic/admin/marketing/queryQuotaTaskListLogic.go create mode 100644 internal/logic/admin/marketing/queryQuotaTaskPreCountLogic.go create mode 100644 internal/logic/admin/marketing/queryQuotaTaskStatusLogic.go create mode 100644 internal/logic/admin/marketing/stopBatchSendEmailTaskLogic.go delete mode 100644 internal/logic/admin/server/batchDeleteNodeGroupLogic.go delete mode 100644 internal/logic/admin/server/batchDeleteNodeLogic.go create mode 100644 internal/logic/admin/server/constant.go delete mode 100644 internal/logic/admin/server/createNodeGroupLogic.go delete mode 100644 internal/logic/admin/server/createRuleGroupLogic.go create mode 100644 internal/logic/admin/server/createServerLogic.go delete mode 100644 internal/logic/admin/server/deleteNodeGroupLogic.go delete mode 100644 internal/logic/admin/server/deleteRuleGroupLogic.go create mode 100644 internal/logic/admin/server/deleteServerLogic.go create mode 100644 internal/logic/admin/server/filterNodeListLogic.go create mode 100644 internal/logic/admin/server/filterServerListLogic.go delete mode 100644 internal/logic/admin/server/getNodeDetailLogic.go delete mode 100644 internal/logic/admin/server/getNodeGroupListLogic.go delete mode 100644 internal/logic/admin/server/getNodeListLogic.go delete mode 100644 internal/logic/admin/server/getNodeTagListLogic.go delete mode 100644 internal/logic/admin/server/getRuleGroupListLogic.go create mode 100644 internal/logic/admin/server/getServerProtocolsLogic.go create mode 100644 internal/logic/admin/server/hasMigrateSeverNodeLogic.go create mode 100644 internal/logic/admin/server/migrateServerNodeLogic.go create mode 100644 internal/logic/admin/server/queryNodeTagLogic.go rename internal/logic/admin/server/{nodeSortLogic.go => resetSortWithNodeLogic.go} (59%) create mode 100644 internal/logic/admin/server/resetSortWithServerLogic.go create mode 100644 internal/logic/admin/server/toggleNodeStatusLogic.go delete mode 100644 internal/logic/admin/server/updateNodeGroupLogic.go delete mode 100644 internal/logic/admin/server/updateRuleGroupLogic.go create mode 100644 internal/logic/admin/server/updateServerLogic.go delete mode 100644 internal/logic/admin/system/createApplicationLogic.go delete mode 100644 internal/logic/admin/system/createApplicationVersionLogic.go delete mode 100644 internal/logic/admin/system/deleteApplicationLogic.go delete mode 100644 internal/logic/admin/system/deleteApplicationVersionLogic.go delete mode 100644 internal/logic/admin/system/getApplicationConfigLogic.go delete mode 100644 internal/logic/admin/system/getApplicationLogic.go delete mode 100644 internal/logic/admin/system/getSubscribeTypeLogic.go create mode 100644 internal/logic/admin/system/preViewNodeMultiplierLogic.go delete mode 100644 internal/logic/admin/system/updateApplicationConfigLogic.go delete mode 100644 internal/logic/admin/system/updateApplicationLogic.go delete mode 100644 internal/logic/admin/system/updateApplicationVersionLogic.go create mode 100644 internal/logic/admin/tool/getVersionLogic.go create mode 100644 internal/logic/admin/user/getUserSubscribeResetTrafficLogsLogic.go delete mode 100644 internal/logic/app/announcement/queryAnnouncementLogic.go delete mode 100644 internal/logic/app/auth/checkLogic.go delete mode 100644 internal/logic/app/auth/findUserByMethod.go delete mode 100644 internal/logic/app/auth/getAppConfigLogic.go delete mode 100644 internal/logic/app/auth/loginLogic.go delete mode 100644 internal/logic/app/auth/registerLogic.go delete mode 100644 internal/logic/app/auth/resetPasswordLogic.go delete mode 100644 internal/logic/app/document/queryDocumentDetailLogic.go delete mode 100644 internal/logic/app/document/queryDocumentListLogic.go delete mode 100644 internal/logic/app/node/getNodeListLogic.go delete mode 100644 internal/logic/app/node/getRuleGroupListLogic.go delete mode 100644 internal/logic/app/order/calculateCoupon.go delete mode 100644 internal/logic/app/order/calculateFee.go delete mode 100644 internal/logic/app/order/checkoutOrderLogic.go delete mode 100644 internal/logic/app/order/closeOrderLogic.go delete mode 100644 internal/logic/app/order/getDiscount.go delete mode 100644 internal/logic/app/order/preCreateOrderLogic.go delete mode 100644 internal/logic/app/order/purchaseLogic.go delete mode 100644 internal/logic/app/order/queryOrderDetailLogic.go delete mode 100644 internal/logic/app/order/queryOrderListLogic.go delete mode 100644 internal/logic/app/order/rechargeLogic.go delete mode 100644 internal/logic/app/order/renewalLogic.go delete mode 100644 internal/logic/app/order/resetTrafficLogic.go delete mode 100644 internal/logic/app/payment/getAvailablePaymentMethodsLogic.go delete mode 100644 internal/logic/app/subscribe/queryApplicationConfigLogic.go delete mode 100644 internal/logic/app/subscribe/querySubscribeGroupListLogic.go delete mode 100644 internal/logic/app/subscribe/querySubscribeListLogic.go delete mode 100644 internal/logic/app/subscribe/queryUserAlreadySubscribeLogic.go delete mode 100644 internal/logic/app/subscribe/queryUserAvailableUserSubscribeLogic.go delete mode 100644 internal/logic/app/subscribe/resetUserSubscribePeriodLogic.go delete mode 100644 internal/logic/app/user/deleteAccountLogic.go delete mode 100644 internal/logic/app/user/getuseronlinetimestatisticslogic.go delete mode 100644 internal/logic/app/user/getusersubscribetrafficlogslogic.go delete mode 100644 internal/logic/app/user/queryUserAffiliateListLogic.go delete mode 100644 internal/logic/app/user/queryUserInfoLogic.go delete mode 100644 internal/logic/app/user/queryuseraffiliatelogic.go delete mode 100644 internal/logic/app/user/updatePasswordLogic.go delete mode 100644 internal/logic/app/ws/appWsLogic.go create mode 100644 internal/logic/auth/bindDeviceLogic.go create mode 100644 internal/logic/auth/deviceLoginLogic.go delete mode 100644 internal/logic/common/getApplicationLogic.go create mode 100644 internal/logic/common/getClientLogic.go delete mode 100644 internal/logic/common/getSubscriptionLogic.go delete mode 100644 internal/logic/public/subscribe/queryApplicationConfigLogic.go create mode 100644 internal/logic/public/user/getDeviceListLogic.go create mode 100644 internal/logic/public/user/unbindDeviceLogic.go create mode 100644 internal/logic/server/queryServerProtocolConfigLogic.go rename internal/middleware/{appMiddleware.go => deviceMiddleware.go} (85%) delete mode 100644 internal/model/application/application.go delete mode 100644 internal/model/application/default.go delete mode 100644 internal/model/application/model.go delete mode 100644 internal/model/cache/constant.go delete mode 100644 internal/model/cache/node.go delete mode 100644 internal/model/cache/node_test.go delete mode 100644 internal/model/cache/types.go create mode 100644 internal/model/client/application.go create mode 100644 internal/model/client/default.go create mode 100644 internal/model/node/cache.go create mode 100644 internal/model/node/default.go create mode 100644 internal/model/node/model.go create mode 100644 internal/model/node/node.go create mode 100644 internal/model/node/server.go delete mode 100644 internal/model/subscribeType/default.go delete mode 100644 internal/model/subscribeType/model.go delete mode 100644 internal/model/subscribeType/subscribeType.go create mode 100644 internal/model/task/task.go create mode 100644 internal/model/user/cache.go delete mode 100644 internal/model/user/log.go delete mode 100644 pkg/adapter/adapter.go delete mode 100644 pkg/adapter/adapter_test.go delete mode 100644 pkg/adapter/clash/clash.go delete mode 100644 pkg/adapter/clash/clash_test.go delete mode 100644 pkg/adapter/clash/default.go delete mode 100644 pkg/adapter/clash/model.go delete mode 100644 pkg/adapter/clash/parse.go delete mode 100644 pkg/adapter/clash/tool.go delete mode 100644 pkg/adapter/general/uri.go delete mode 100644 pkg/adapter/general/uri_test.go delete mode 100644 pkg/adapter/loon/build.go delete mode 100644 pkg/adapter/loon/hysteria2.go delete mode 100644 pkg/adapter/loon/loon_test.go delete mode 100644 pkg/adapter/loon/shadowsocks.go delete mode 100644 pkg/adapter/loon/trojan.go delete mode 100644 pkg/adapter/loon/vless.go delete mode 100644 pkg/adapter/loon/vmess.go delete mode 100644 pkg/adapter/proxy/proxy.go delete mode 100644 pkg/adapter/quantumultx/build.go delete mode 100644 pkg/adapter/quantumultx/quantumux_test.go delete mode 100644 pkg/adapter/quantumultx/shadowsocks.go delete mode 100644 pkg/adapter/quantumultx/trojan.go delete mode 100644 pkg/adapter/quantumultx/vmess.go delete mode 100644 pkg/adapter/shadowrocket/build.go delete mode 100644 pkg/adapter/shadowrocket/shadowrocket_test.go delete mode 100644 pkg/adapter/shadowrocket/vmess.go delete mode 100644 pkg/adapter/singbox/build.go delete mode 100644 pkg/adapter/singbox/default.go delete mode 100644 pkg/adapter/singbox/hysteria2.go delete mode 100644 pkg/adapter/singbox/multiplex.go delete mode 100644 pkg/adapter/singbox/rule.go delete mode 100644 pkg/adapter/singbox/rule_test.go delete mode 100644 pkg/adapter/singbox/shadowsocks.go delete mode 100644 pkg/adapter/singbox/singbox.go delete mode 100644 pkg/adapter/singbox/singbox_test.go delete mode 100644 pkg/adapter/singbox/tls.go delete mode 100644 pkg/adapter/singbox/tool.go delete mode 100644 pkg/adapter/singbox/trojan.go delete mode 100644 pkg/adapter/singbox/tuic.go delete mode 100644 pkg/adapter/singbox/v2rayTransport.go delete mode 100644 pkg/adapter/singbox/vless.go delete mode 100644 pkg/adapter/singbox/vmess.go delete mode 100644 pkg/adapter/surfboard/build.go delete mode 100644 pkg/adapter/surfboard/build_test.go delete mode 100644 pkg/adapter/surfboard/default.tpl delete mode 100644 pkg/adapter/surfboard/model.go delete mode 100644 pkg/adapter/surfboard/shadowsocks.go delete mode 100644 pkg/adapter/surfboard/shadowsocks_test.go delete mode 100644 pkg/adapter/surfboard/trojan.go delete mode 100644 pkg/adapter/surfboard/trojan_test.go delete mode 100644 pkg/adapter/surfboard/vmess.go delete mode 100644 pkg/adapter/surfboard/vmess_test.go delete mode 100644 pkg/adapter/surge/default.tpl delete mode 100644 pkg/adapter/surge/hysteria2.go delete mode 100644 pkg/adapter/surge/hysteria2_test.go delete mode 100644 pkg/adapter/surge/shadowsocks.go delete mode 100644 pkg/adapter/surge/surge.go delete mode 100644 pkg/adapter/surge/surge_test.go delete mode 100644 pkg/adapter/surge/trojan.go delete mode 100644 pkg/adapter/surge/vmess.go delete mode 100644 pkg/adapter/uilts.go create mode 100644 pkg/cache/cacheopt.go create mode 100644 pkg/cache/cacheopt_test.go delete mode 100644 pkg/countryCenter/county_center.go delete mode 100644 pkg/countryCenter/county_center_test.go create mode 100644 pkg/deduction/deduction_test.go create mode 100644 pkg/email/manager.go create mode 100644 pkg/email/worker.go create mode 100644 pkg/logger/read.go create mode 100644 pkg/logger/read_test.go delete mode 100644 pkg/payment/payssion/payssion.go delete mode 100644 pkg/payment/payssion/payssion_test.go create mode 100644 queue/logic/email/batchEmailLogic.go create mode 100644 queue/logic/order/activateOrderLogic.go_bak delete mode 100644 queue/logic/order/checkOrderLogic.go create mode 100644 queue/logic/task/quotaLogic.go create mode 100644 queue/logic/traffic/resetTrafficLogic.go create mode 100644 queue/logic/traffic/trafficStatLogic.go create mode 100644 queue/types/task.go create mode 100644 readme_zh.md mode change 100644 => 100755 script/generate.sh diff --git a/1.conf b/1.conf new file mode 100644 index 0000000..6e7a5e3 --- /dev/null +++ b/1.conf @@ -0,0 +1,67 @@ +server { + listen 80; + server_name api.hifast.biz 4d3vsw8.88xgaen.hifast.biz; + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + server_name api.hifast.biz; + client_max_body_size 150M; + + ssl_certificate /etc/letsencrypt/live/api.hifast.biz/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/api.hifast.biz/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; + } +} + +# de99e242子域名指向3001 (管理界面) +server { + listen 443 ssl http2; + server_name 4d3vsw8.88xgaen.hifast.biz; + client_max_body_size 150M; + + ssl_certificate /etc/letsencrypt/live/4d3vsw8.88xgaen.hifast.biz/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/4d3vsw8.88xgaen.hifast.biz/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; + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6aabce2..74d41df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,7 @@ FROM golang:alpine AS builder LABEL stage=gobuilder ARG TARGETARCH +ARG VERSION ENV CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} # Combine apk commands into one to reduce layer size @@ -18,8 +19,9 @@ RUN go mod download # Copy the rest of the application code COPY . . -# Build the binary with optimization flags to reduce binary size -RUN go build -ldflags="-s -w" -o /app/ppanel ppanel.go +# Build the binary with version and build time +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 scratch @@ -34,11 +36,11 @@ ENV TZ=Asia/Shanghai WORKDIR /app COPY --from=builder /app/ppanel /app/ppanel -COPY --from=builder /etc /app/etc +COPY --from=builder /build/etc /app/etc # Expose the port (optional) EXPOSE 8080 # Specify entry point ENTRYPOINT ["/app/ppanel"] -CMD ["run", "--config", "etc/ppanel.yaml"] +CMD ["run", "--config", "etc/ppanel.yaml"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..47b9849 --- /dev/null +++ b/Makefile @@ -0,0 +1,84 @@ +NAME="ppanel-server" +BINDIR=bin +VERSION=$(shell git describe --tags || echo "unknown version") +BUILDTIME=$(shell date -u) +GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/perfect-panel/server/pkg/constant.Version=$(VERSION)" \ + -X "github.com/perfect-panel/server/pkg/constant.BuildTime=$(BUILDTIME)" \ + -w -s -buildid=' + +PLATFORM_LIST = \ + darwin-amd64 \ + darwin-amd64-v3 \ + darwin-arm64 \ + linux-386 \ + linux-amd64 \ + linux-amd64-v3 \ + linux-armv5 \ + linux-armv6 \ + linux-armv7 \ + linux-arm64 \ + +WINDOWS_ARCH_LIST = \ + windows-386 \ + windows-amd64 \ + windows-amd64-v3 \ + windows-arm64 \ + windows-armv7 + +all: linux-amd64 darwin-amd64 windows-amd64 # Most used + +darwin-amd64: + GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +darwin-amd64-v3: + GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +darwin-arm64: + GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-386: + GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-amd64: + GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-amd64-v3: + GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-armv5: + GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-armv6: + GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-armv7: + GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +linux-arm64: + GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ + +windows-386: + GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe + +windows-amd64: + GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe + +windows-amd64-v3: + GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe + +windows-arm64: + GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe + +windows-armv7: + GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe + + +gz_releases=$(addsuffix .gz, $(PLATFORM_LIST)) +zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST)) + +$(gz_releases): %.gz : % + chmod +x $(BINDIR)/$(NAME)-$(basename $@) + gzip -f -S -$(VERSION).gz $(BINDIR)/$(NAME)-$(basename $@) + +$(zip_releases): %.zip : % + zip -m -j $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).zip $(BINDIR)/$(NAME)-$(basename $@).exe diff --git a/adapter/adapter.go b/adapter/adapter.go new file mode 100644 index 0000000..1acc674 --- /dev/null +++ b/adapter/adapter.go @@ -0,0 +1,140 @@ +package adapter + +import ( + "strings" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/pkg/logger" +) + +type Adapter struct { + SiteName string // 站点名称 + Servers []*node.Node // 服务器列表 + UserInfo User // 用户信息 + ClientTemplate string // 客户端配置模板 + OutputFormat string // 输出格式,默认是 base64 + SubscribeName string // 订阅名称 +} + +type Option func(*Adapter) + +// WithServers 设置服务器列表 +func WithServers(servers []*node.Node) Option { + return func(opts *Adapter) { + opts.Servers = servers + } +} + +// WithUserInfo 设置用户信息 +func WithUserInfo(user User) Option { + return func(opts *Adapter) { + opts.UserInfo = user + } +} + +// WithOutputFormat 设置输出格式 +func WithOutputFormat(format string) Option { + return func(opts *Adapter) { + opts.OutputFormat = format + } +} + +// WithSiteName 设置站点名称 +func WithSiteName(name string) Option { + return func(opts *Adapter) { + opts.SiteName = name + } +} + +// WithSubscribeName 设置订阅名称 +func WithSubscribeName(name string) Option { + return func(opts *Adapter) { + opts.SubscribeName = name + } +} + +func NewAdapter(tpl string, opts ...Option) *Adapter { + adapter := &Adapter{ + Servers: []*node.Node{}, + UserInfo: User{}, + ClientTemplate: tpl, + OutputFormat: "base64", // 默认输出格式 + } + + for _, opt := range opts { + opt(adapter) + } + + return adapter +} + +func (adapter *Adapter) Client() (*Client, error) { + client := &Client{ + SiteName: adapter.SiteName, + SubscribeName: adapter.SubscribeName, + ClientTemplate: adapter.ClientTemplate, + OutputFormat: adapter.OutputFormat, + Proxies: []Proxy{}, + UserInfo: adapter.UserInfo, + } + + proxies, err := adapter.Proxies(adapter.Servers) + if err != nil { + return nil, err + } + client.Proxies = proxies + return client, nil +} + +func (adapter *Adapter) Proxies(servers []*node.Node) ([]Proxy, error) { + var proxies []Proxy + + for _, item := range servers { + if item.Server == nil { + logger.Errorf("[Adapter] Server is nil for node ID: %d", item.Id) + continue + } + protocols, err := item.Server.UnmarshalProtocols() + if err != nil { + logger.Errorf("[Adapter] Unmarshal Protocols error: %s; server id : %d", err.Error(), item.ServerId) + continue + } + 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, + }) + } + } + } + + return proxies, nil +} diff --git a/adapter/adapter_test.go b/adapter/adapter_test.go new file mode 100644 index 0000000..d45649b --- /dev/null +++ b/adapter/adapter_test.go @@ -0,0 +1,34 @@ +package adapter + +import ( + "testing" + "time" +) + +func TestAdapter_Client(t *testing.T) { + servers := getServers() + if len(servers) == 0 { + t.Errorf("[Test] No servers found") + return + } + a := NewAdapter(tpl, WithServers(servers), WithUserInfo(User{ + Password: "test-password", + ExpiredAt: time.Now().AddDate(1, 0, 0), + Download: 0, + Upload: 0, + Traffic: 1000, + SubscribeURL: "https://example.com/subscribe", + })) + client, err := a.Client() + if err != nil { + t.Errorf("[Test] Failed to get client: %v", err.Error()) + return + } + bytes, err := client.Build() + if err != nil { + t.Errorf("[Test] Failed to build client config: %v", err.Error()) + return + } + t.Logf("[Test] Client config built successfully: %s", string(bytes)) + +} diff --git a/adapter/client.go b/adapter/client.go new file mode 100644 index 0000000..e456898 --- /dev/null +++ b/adapter/client.go @@ -0,0 +1,146 @@ +package adapter + +import ( + "bytes" + "encoding/base64" + "reflect" + "text/template" + "time" + + "github.com/Masterminds/sprig/v3" +) + +type Proxy struct { + Sort int + Name string + Server string + Port uint16 + Type string + Tags []string + + // Security Options + Security string + SNI string // Server Name Indication for TLS + AllowInsecure bool // Allow insecure connections (skip certificate verification) + Fingerprint string // Client fingerprint for TLS connections + RealityServerAddr string // Reality server address + RealityServerPort int // Reality server port + RealityPrivateKey string // Reality private key for authentication + RealityPublicKey string // Reality public key for authentication + RealityShortId string // Reality short ID for authentication + // Transport Options + Transport string // Transport protocol (e.g., ws, http, grpc) + Host string // For WebSocket/HTTP/HTTPS + Path string // For HTTP/HTTPS + ServiceName string // For gRPC + // Shadowsocks Options + Method string + ServerKey string // For Shadowsocks 2022 + + // Vmess/Vless/Trojan Options + Flow string // Flow for Vmess/Vless/Trojan + // Hysteria2 Options + HopPorts string // Comma-separated list of hop ports + HopInterval int // Interval for hop ports in seconds + ObfsPassword string // Obfuscation password for Hysteria2 + UpMbps int // Upload speed in Mbps + DownMbps int // Download speed in Mbps + + // Tuic Options + DisableSNI bool // Disable SNI + ReduceRtt bool // Reduce RTT + UDPRelayMode string // UDP relay mode (e.g., "full", "partial") + CongestionController string // Congestion controller (e.g., "cubic", "bbr") + + // AnyTLS + PaddingScheme string + + // Mieru + Multiplex string + + // Obfs + //Obfs string // obfs, 'none', 'http', 'tls' + //ObfsHost string // obfs host + //ObfsPath string // obfs path + + // Vless + XhttpMode string // xhttp mode + XhttpExtra string // xhttp path + + // encryption + Encryption string // encryption,'none', 'mlkem768x25519plus' + EncryptionMode string // encryption mode,'native', 'xorpub', 'random' + EncryptionRtt string // encryption rtt,'0rtt', '1rtt' + EncryptionTicket string // encryption ticket + EncryptionServerPadding string // encryption server padding + EncryptionPrivateKey string // encryption private key + EncryptionClientPadding string // encryption client padding + EncryptionPassword string // encryption password + + Ratio float64 // Traffic ratio, default is 1 + CertMode string // Certificate mode, `none`|`http`|`dns`|`self` + CertDNSProvider string // DNS provider for certificate + CertDNSEnv string // Environment for DNS provider +} + +type User struct { + Password string + ExpiredAt time.Time + Download int64 + Upload int64 + Traffic int64 + SubscribeURL string +} + +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 +} + +func (c *Client) Build() ([]byte, error) { + var buf bytes.Buffer + tmpl, err := template.New("client").Funcs(sprig.TxtFuncMap()).Parse(c.ClientTemplate) + if err != nil { + return nil, err + } + + proxies := make([]map[string]interface{}, len(c.Proxies)) + for i, p := range c.Proxies { + proxies[i] = StructToMap(p) + } + + err = tmpl.Execute(&buf, map[string]interface{}{ + "SiteName": c.SiteName, + "SubscribeName": c.SubscribeName, + "OutputFormat": c.OutputFormat, + "Proxies": proxies, + "UserInfo": c.UserInfo, + }) + if err != nil { + return nil, err + } + + result := buf.String() + if c.OutputFormat == "base64" { + encoded := base64.StdEncoding.EncodeToString([]byte(result)) + return []byte(encoded), nil + } + + return buf.Bytes(), nil +} + +func StructToMap(obj interface{}) map[string]interface{} { + m := make(map[string]interface{}) + v := reflect.ValueOf(obj) + t := reflect.TypeOf(obj) + + for i := 0; i < v.NumField(); i++ { + field := t.Field(i) + m[field.Name] = v.Field(i).Interface() + } + return m +} diff --git a/adapter/client_test.go b/adapter/client_test.go new file mode 100644 index 0000000..beb9145 --- /dev/null +++ b/adapter/client_test.go @@ -0,0 +1,153 @@ +package adapter + +import ( + "testing" + "time" +) + +var tpl = ` +{{- range $n := .Proxies }} + {{- $dn := urlquery (default "node" $n.Name) -}} + {{- $sni := default $n.Host $n.SNI -}} + + {{- if eq $n.Type "shadowsocks" -}} + {{- $userinfo := b64enc (print $n.Method ":" $.UserInfo.Password) -}} + {{- printf "ss://%s@%s:%v#%s" $userinfo $n.Host $n.Port $dn -}} + {{- "\n" -}} + {{- end -}} + + {{- if eq $n.Type "trojan" -}} + {{- $qs := "security=tls" -}} + {{- if $sni }}{{ $qs = printf "%s&sni=%s" $qs (urlquery $sni) }}{{ end -}} + {{- if $n.AllowInsecure }}{{ $qs = printf "%s&allowInsecure=%v" $qs $n.AllowInsecure }}{{ end -}} + {{- if $n.Fingerprint }}{{ $qs = printf "%s&fp=%s" $qs (urlquery $n.Fingerprint) }}{{ end -}} + {{- printf "trojan://%s@%s:%v?%s#%s" $.UserInfo.Password $n.Host $n.Port $qs $dn -}} + {{- "\n" -}} + {{- end -}} + + {{- if eq $n.Type "vless" -}} + {{- $qs := "encryption=none" -}} + {{- if $n.RealityPublicKey -}} + {{- $qs = printf "%s&security=reality" $qs -}} + {{- $qs = printf "%s&pbk=%s" $qs (urlquery $n.RealityPublicKey) -}} + {{- if $n.RealityShortId }}{{ $qs = printf "%s&sid=%s" $qs (urlquery $n.RealityShortId) }}{{ end -}} + {{- else -}} + {{- if or $n.SNI $n.Fingerprint $n.AllowInsecure }} + {{- $qs = printf "%s&security=tls" $qs -}} + {{- end -}} + {{- end -}} + {{- if $n.SNI }}{{ $qs = printf "%s&sni=%s" $qs (urlquery $n.SNI) }}{{ end -}} + {{- if $n.AllowInsecure }}{{ $qs = printf "%s&allowInsecure=%v" $qs $n.AllowInsecure }}{{ end -}} + {{- if $n.Fingerprint }}{{ $qs = printf "%s&fp=%s" $qs (urlquery $n.Fingerprint) }}{{ end -}} + {{- if $n.Network }}{{ $qs = printf "%s&type=%s" $qs $n.Network }}{{ end -}} + {{- if $n.Path }}{{ $qs = printf "%s&path=%s" $qs (urlquery $n.Path) }}{{ end -}} + {{- if $n.ServiceName }}{{ $qs = printf "%s&serviceName=%s" $qs (urlquery $n.ServiceName) }}{{ end -}} + {{- if $n.Flow }}{{ $qs = printf "%s&flow=%s" $qs (urlquery $n.Flow) }}{{ end -}} + {{- printf "vless://%s@%s:%v?%s#%s" $n.ServerKey $n.Host $n.Port $qs $dn -}} + {{- "\n" -}} + {{- end -}} + + {{- if eq $n.Type "vmess" -}} + {{- $obj := dict + "v" "2" + "ps" $n.Name + "add" $n.Host + "port" $n.Port + "id" $n.ServerKey + "aid" 0 + "net" (or $n.Network "tcp") + "type" "none" + "path" (or $n.Path "") + "host" $n.Host + -}} + {{- if or $n.SNI $n.Fingerprint $n.AllowInsecure }}{{ set $obj "tls" "tls" }}{{ end -}} + {{- if $n.SNI }}{{ set $obj "sni" $n.SNI }}{{ end -}} + {{- if $n.Fingerprint }}{{ set $obj "fp" $n.Fingerprint }}{{ end -}} + {{- printf "vmess://%s" (b64enc (toJson $obj)) -}} + {{- "\n" -}} + {{- end -}} + + {{- if or (eq $n.Type "hysteria2") (eq $n.Type "hy2") -}} + {{- $qs := "" -}} + {{- if $n.SNI }}{{ $qs = printf "sni=%s" (urlquery $n.SNI) }}{{ end -}} + {{- if $n.AllowInsecure }}{{ $qs = printf "%s&insecure=%v" $qs $n.AllowInsecure }}{{ end -}} + {{- if $n.ObfsPassword }}{{ $qs = printf "%s&obfs-password=%s" $qs (urlquery $n.ObfsPassword) }}{{ end -}} + {{- printf "hy2://%s@%s:%v%s#%s" + $.UserInfo.Password + $n.Host + $n.Port + (ternary (gt (len $qs) 0) (print "?" $qs) "") + $dn -}} + {{- "\n" -}} + {{- end -}} + + {{- if eq $n.Type "tuic" -}} + {{- $qs := "" -}} + {{- if $n.SNI }}{{ $qs = printf "sni=%s" (urlquery $n.SNI) }}{{ end -}} + {{- if $n.AllowInsecure }}{{ $qs = printf "%s&insecure=%v" $qs $n.AllowInsecure }}{{ end -}} + {{- printf "tuic://%s:%s@%s:%v%s#%s" + $n.ServerKey + $.UserInfo.Password + $n.Host + $n.Port + (ternary (gt (len $qs) 0) (print "?" $qs) "") + $dn -}} + {{- "\n" -}} + {{- end -}} + + {{- if eq $n.Type "anytls" -}} + {{- $qs := "" -}} + {{- if $n.SNI }}{{ $qs = printf "sni=%s" (urlquery $n.SNI) }}{{ end -}} + {{- printf "anytls://%s@%s:%v%s#%s" + $.UserInfo.Password + $n.Host + $n.Port + (ternary (gt (len $qs) 0) (print "?" $qs) "") + $dn -}} + {{- "\n" -}} + {{- end -}} + +{{- end }} +` + +func TestClient_Build(t *testing.T) { + client := &Client{ + SiteName: "TestSite", + SubscribeName: "TestSubscribe", + ClientTemplate: tpl, + Proxies: []Proxy{ + { + Name: "TestShadowSocks", + Type: "shadowsocks", + Host: "127.0.0.1", + Port: 1234, + Method: "aes-256-gcm", + }, + { + Name: "TestTrojan", + Type: "trojan", + Host: "example.com", + Port: 443, + AllowInsecure: true, + Security: "tls", + Transport: "tcp", + SNI: "v1-dy.ixigua.com", + }, + }, + UserInfo: User{ + Password: "testpassword", + ExpiredAt: time.Now().Add(24 * time.Hour), + Download: 1000000, + Upload: 500000, + Traffic: 1500000, + SubscribeURL: "https://example.com/subscribe", + }, + } + buf, err := client.Build() + if err != nil { + t.Fatalf("Failed to build client: %v", err) + } + + t.Logf("[测试] 输出: %s", buf) + +} diff --git a/adapter/utils.go b/adapter/utils.go new file mode 100644 index 0000000..b8e8da3 --- /dev/null +++ b/adapter/utils.go @@ -0,0 +1 @@ +package adapter diff --git a/adapter/utils_test.go b/adapter/utils_test.go new file mode 100644 index 0000000..7a4e32c --- /dev/null +++ b/adapter/utils_test.go @@ -0,0 +1,46 @@ +package adapter + +import ( + "testing" + + "github.com/perfect-panel/server/internal/model/server" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func TestAdapterProxy(t *testing.T) { + + servers := getServers() + if len(servers) == 0 { + t.Fatal("no servers found") + } + for _, srv := range servers { + proxy, err := adapterProxy(*srv, "example.com", 0) + if err != nil { + t.Errorf("failed to adapt server %s: %v", srv.Name, err) + } + t.Logf("[测试] 适配服务器 %s 成功: %+v", srv.Name, proxy) + } + +} + +func getServers() []*server.Server { + db, err := connectMySQL("root:mylove520@tcp(localhost:3306)/perfectlink?charset=utf8mb4&parseTime=True&loc=Local") + if err != nil { + return nil + } + var servers []*server.Server + if err = db.Model(&server.Server{}).Find(&servers).Error; err != nil { + return nil + } + return servers +} +func connectMySQL(dsn string) (*gorm.DB, error) { + db, err := gorm.Open(mysql.New(mysql.Config{ + DSN: dsn, + }), &gorm.Config{}) + if err != nil { + return nil, err + } + return db, nil +} diff --git a/apis/admin/application.api b/apis/admin/application.api new file mode 100644 index 0000000..8ac5355 --- /dev/null +++ b/apis/admin/application.api @@ -0,0 +1,96 @@ +syntax = "v1" + +info ( + title: "Application API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +import "../types.api" + +type ( + SubscribeApplication { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + UserAgent string `json:"user_agent"` + IsDefault bool `json:"is_default"` + SubscribeTemplate string `json:"template"` + OutputFormat string `json:"output_format"` + DownloadLink DownloadLink `json:"download_link,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + GetSubscribeApplicationListResponse { + Total int64 `json:"total"` + List []SubscribeApplication `json:"list"` + } + CreateSubscribeApplicationRequest { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + UserAgent string `json:"user_agent"` + IsDefault bool `json:"is_default"` + SubscribeTemplate string `json:"template"` + OutputFormat string `json:"output_format"` + DownloadLink DownloadLink `json:"download_link"` + } + UpdateSubscribeApplicationRequest { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + UserAgent string `json:"user_agent"` + IsDefault bool `json:"is_default"` + SubscribeTemplate string `json:"template"` + OutputFormat string `json:"output_format"` + DownloadLink DownloadLink `json:"download_link,omitempty"` + } + DeleteSubscribeApplicationRequest { + Id int64 `json:"id"` + } + GetSubscribeApplicationListRequest { + Page int `form:"page"` + Size int `form:"size"` + } + PreviewSubscribeTemplateRequest { + Id int64 `form:"id"` + } + PreviewSubscribeTemplateResponse { + Template string `json:"template"` // 预览的模板内容 + } +) + +@server ( + prefix: v1/admin/application + group: admin/application + middleware: AuthMiddleware +) +service ppanel { + @doc "Create subscribe application" + @handler CreateSubscribeApplication + post / (CreateSubscribeApplicationRequest) returns (SubscribeApplication) + + @doc "Update subscribe application" + @handler UpdateSubscribeApplication + put /subscribe_application (UpdateSubscribeApplicationRequest) returns (SubscribeApplication) + + @doc "Get subscribe application list" + @handler GetSubscribeApplicationList + get /subscribe_application_list (GetSubscribeApplicationListRequest) returns (GetSubscribeApplicationListResponse) + + @doc "Delete subscribe application" + @handler DeleteSubscribeApplication + delete /subscribe_application (DeleteSubscribeApplicationRequest) + + @doc "Preview Template" + @handler PreviewSubscribeTemplate + get /preview (PreviewSubscribeTemplateRequest) returns (PreviewSubscribeTemplateResponse) +} + diff --git a/apis/admin/auth.api b/apis/admin/auth.api index 4161cb5..f1cd0d3 100644 --- a/apis/admin/auth.api +++ b/apis/admin/auth.api @@ -25,17 +25,14 @@ type ( GetAuthMethodListResponse { List []AuthMethodConfig `json:"list"` } - - TestSmsSendRequest { - AreaCode string `json:"area_code" validate:"required"` + AreaCode string `json:"area_code" validate:"required"` Telephone string `json:"telephone" validate:"required"` } // Test email smtp request TestEmailSendRequest { Email string `json:"email" validate:"required"` } - ) @server ( @@ -56,7 +53,6 @@ service ppanel { @handler UpdateAuthMethodConfig put /config (UpdateAuthMethodConfigRequest) returns (AuthMethodConfig) - @doc "Test sms send" @handler TestSmsSend post /test_sms_send (TestSmsSendRequest) diff --git a/apis/admin/console.api b/apis/admin/console.api index c20bb14..1a8ded7 100644 --- a/apis/admin/console.api +++ b/apis/admin/console.api @@ -21,7 +21,7 @@ type ( Download int64 `json:"download"` } ServerTotalDataResponse { - OnlineUserIPs int64 `json:"online_user_ips"` + OnlineUsers int64 `json:"online_users"` OnlineServers int64 `json:"online_servers"` OfflineServers int64 `json:"offline_servers"` TodayUpload int64 `json:"today_upload"` diff --git a/apis/admin/coupon.api b/apis/admin/coupon.api index 92516aa..7715f4d 100644 --- a/apis/admin/coupon.api +++ b/apis/admin/coupon.api @@ -21,8 +21,8 @@ type ( ExpireTime int64 `json:"expire_time" validate:"required"` UserLimit int64 `json:"user_limit,omitempty"` Subscribe []int64 `json:"subscribe,omitempty"` - UsedCount int64 `json:"used_count,omitempty"` - Enable *bool `json:"enable,omitempty"` + UsedCount int64 `json:"used_count,omitempty"` + Enable *bool `json:"enable,omitempty"` } UpdateCouponRequest { Id int64 `json:"id" validate:"required"` @@ -35,8 +35,8 @@ type ( ExpireTime int64 `json:"expire_time" validate:"required"` UserLimit int64 `json:"user_limit,omitempty"` Subscribe []int64 `json:"subscribe,omitempty"` - UsedCount int64 `json:"used_count,omitempty"` - Enable *bool `json:"enable,omitempty"` + UsedCount int64 `json:"used_count,omitempty"` + Enable *bool `json:"enable,omitempty"` } DeleteCouponRequest { Id int64 `json:"id" validate:"required"` diff --git a/apis/admin/device.api b/apis/admin/device.api index 1e5fb59..5301ab6 100644 --- a/apis/admin/device.api +++ b/apis/admin/device.api @@ -1,13 +1,10 @@ syntax = "v1" -info( - title: "Device API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" +info ( + title: "Device API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" ) -type ( - -) \ No newline at end of file diff --git a/apis/admin/log.api b/apis/admin/log.api index 12fc312..3b117c8 100644 --- a/apis/admin/log.api +++ b/apis/admin/log.api @@ -12,19 +12,184 @@ import "../types.api" type ( GetMessageLogListRequest { - Page int `form:"page"` - Size int `form:"size"` - Type string `form:"type"` - Platform string `form:"platform,omitempty"` - To string `form:"to,omitempty"` - Subject string `form:"subject,omitempty"` - Content string `form:"content,omitempty"` - Status int `form:"status,omitempty"` + Page int `form:"page"` + Size int `form:"size"` + Type uint8 `form:"type"` + Search string `form:"search,optional"` } GetMessageLogListResponse { Total int64 `json:"total"` List []MessageLog `json:"list"` } + FilterLogParams { + Page int `form:"page"` + Size int `form:"size"` + Date string `form:"date,optional"` + Search string `form:"search,optional"` + } + FilterEmailLogResponse { + Total int64 `json:"total"` + List []MessageLog `json:"list"` + } + FilterMobileLogResponse { + Total int64 `json:"total"` + List []MessageLog `json:"list"` + } + SubscribeLog { + UserId int64 `json:"user_id"` + Token string `json:"token"` + UserAgent string `json:"user_agent"` + ClientIP string `json:"client_ip"` + UserSubscribeId int64 `json:"user_subscribe_id"` + Timestamp int64 `json:"timestamp"` + } + FilterSubscribeLogRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + UserSubscribeId int64 `form:"user_subscribe_id,optional"` + } + FilterSubscribeLogResponse { + Total int64 `json:"total"` + List []SubscribeLog `json:"list"` + } + LoginLog { + UserId int64 `json:"user_id"` + Method string `json:"method"` + LoginIP string `json:"login_ip"` + UserAgent string `json:"user_agent"` + Success bool `json:"success"` + Timestamp int64 `json:"timestamp"` + } + FilterLoginLogRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + } + FilterLoginLogResponse { + Total int64 `json:"total"` + List []LoginLog `json:"list"` + } + RegisterLog { + UserId int64 `json:"user_id"` + AuthMethod string `json:"auth_method"` + Identifier string `json:"identifier"` + RegisterIP string `json:"register_ip"` + UserAgent string `json:"user_agent"` + Timestamp int64 `json:"timestamp"` + } + FilterRegisterLogRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + } + FilterRegisterLogResponse { + Total int64 `json:"total"` + List []RegisterLog `json:"list"` + } + ResetSubscribeLog { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + UserSubscribeId int64 `json:"user_subscribe_id"` + OrderNo string `json:"order_no,omitempty"` + Timestamp int64 `json:"timestamp"` + } + FilterResetSubscribeLogRequest { + FilterLogParams + UserSubscribeId int64 `form:"user_subscribe_id,optional"` + } + FilterResetSubscribeLogResponse { + Total int64 `json:"total"` + List []ResetSubscribeLog `json:"list"` + } + UserSubscribeTrafficLog { + SubscribeId int64 `json:"subscribe_id"` // Subscribe ID + UserId int64 `json:"user_id"` // User ID + Upload int64 `json:"upload"` // Upload traffic in bytes + Download int64 `json:"download"` // Download traffic in bytes + Total int64 `json:"total"` // Total traffic in bytes (Upload + Download) + Date string `json:"date"` // Date in YYYY-MM-DD format + Details bool `json:"details"` // Whether to show detailed traffic + } + FilterSubscribeTrafficRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + UserSubscribeId int64 `form:"user_subscribe_id,optional"` + } + FilterSubscribeTrafficResponse { + Total int64 `json:"total"` + List []UserSubscribeTrafficLog `json:"list"` + } + ServerTrafficLog { + ServerId int64 `json:"server_id"` // Server ID + Upload int64 `json:"upload"` // Upload traffic in bytes + Download int64 `json:"download"` // Download traffic in bytes + Total int64 `json:"total"` // Total traffic in bytes (Upload + Download) + Date string `json:"date"` // Date in YYYY-MM-DD format + Details bool `json:"details"` // Whether to show detailed traffic + } + FilterServerTrafficLogRequest { + FilterLogParams + ServerId int64 `form:"server_id,optional"` + } + FilterServerTrafficLogResponse { + Total int64 `json:"total"` + List []ServerTrafficLog `json:"list"` + } + FilterBalanceLogRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + } + FilterBalanceLogResponse { + Total int64 `json:"total"` + List []BalanceLog `json:"list"` + } + FilterCommissionLogRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + } + FilterCommissionLogResponse { + Total int64 `json:"total"` + List []CommissionLog `json:"list"` + } + GiftLog { + Type uint16 `json:"type"` + userId int64 `json:"user_id"` + OrderNo string `json:"order_no"` + SubscribeId int64 `json:"subscribe_id"` + Amount int64 `json:"amount"` + Balance int64 `json:"balance"` + Remark string `json:"remark,omitempty"` + Timestamp int64 `json:"timestamp"` + } + FilterGiftLogRequest { + FilterLogParams + UserId int64 `form:"user_id,optional"` + } + FilterGiftLogResponse { + Total int64 `json:"total"` + List []GiftLog `json:"list"` + } + TrafficLogDetails { + Id int64 `json:"id"` + ServerId int64 `json:"server_id"` + UserId int64 `json:"user_id"` + SubscribeId int64 `json:"subscribe_id"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Timestamp int64 `json:"timestamp"` + } + FilterTrafficLogDetailsRequest { + FilterLogParams + ServerId int64 `form:"server_id,optional"` + SubscribeId int64 `form:"subscribe_id,optional"` + UserId int64 `form:"user_id,optional"` + } + FilterTrafficLogDetailsResponse { + Total int64 `json:"total"` + List []TrafficLogDetails `json:"list"` + } + LogSetting { + AutoClear *bool `json:"auto_clear"` + ClearDays int64 `json:"clear_days"` + } ) @server ( @@ -36,5 +201,61 @@ service ppanel { @doc "Get message log list" @handler GetMessageLogList get /message/list (GetMessageLogListRequest) returns (GetMessageLogListResponse) + + @doc "Filter email log" + @handler FilterEmailLog + get /email/list (FilterLogParams) returns (FilterEmailLogResponse) + + @doc "Filter mobile log" + @handler FilterMobileLog + get /mobile/list (FilterLogParams) returns (FilterMobileLogResponse) + + @doc "Filter subscribe log" + @handler FilterSubscribeLog + get /subscribe/list (FilterSubscribeLogRequest) returns (FilterSubscribeLogResponse) + + @doc "Filter login log" + @handler FilterLoginLog + get /login/list (FilterLoginLogRequest) returns (FilterLoginLogResponse) + + @doc "Filter register log" + @handler FilterRegisterLog + get /register/list (FilterRegisterLogRequest) returns (FilterRegisterLogResponse) + + @doc "Filter reset subscribe log" + @handler FilterResetSubscribeLog + get /subscribe/reset/list (FilterResetSubscribeLogRequest) returns (FilterResetSubscribeLogResponse) + + @doc "Filter user subscribe traffic log" + @handler FilterUserSubscribeTrafficLog + get /subscribe/traffic/list (FilterSubscribeTrafficRequest) returns (FilterSubscribeTrafficResponse) + + @doc "Filter server traffic log" + @handler FilterServerTrafficLog + get /server/traffic/list (FilterServerTrafficLogRequest) returns (FilterServerTrafficLogResponse) + + @doc "Filter balance log" + @handler FilterBalanceLog + get /balance/list (FilterBalanceLogRequest) returns (FilterBalanceLogResponse) + + @doc "Filter commission log" + @handler FilterCommissionLog + get /commission/list (FilterCommissionLogRequest) returns (FilterCommissionLogResponse) + + @doc "Filter gift log" + @handler FilterGiftLog + get /gift/list (FilterGiftLogRequest) returns (FilterGiftLogResponse) + + @doc "Filter traffic log details" + @handler FilterTrafficLogDetails + get /traffic/details (FilterTrafficLogDetailsRequest) returns (FilterTrafficLogDetailsResponse) + + @doc "Get log setting" + @handler GetLogSetting + get /setting returns (LogSetting) + + @doc "Update log setting" + @handler UpdateLogSetting + post /setting (LogSetting) } diff --git a/apis/admin/marketing.api b/apis/admin/marketing.api new file mode 100644 index 0000000..8014c0d --- /dev/null +++ b/apis/admin/marketing.api @@ -0,0 +1,167 @@ +syntax = "v1" + +info ( + title: "Marketing API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" +) + +type ( + CreateBatchSendEmailTaskRequest { + Subject string `json:"subject"` + Content string `json:"content"` + Scope int8 `json:"scope"` + RegisterStartTime int64 `json:"register_start_time,omitempty"` + RegisterEndTime int64 `json:"register_end_time,omitempty"` + Additional string `json:"additional,omitempty"` + Scheduled int64 `json:"scheduled,omitempty"` + Interval uint8 `json:"interval,omitempty"` + Limit uint64 `json:"limit,omitempty"` + } + BatchSendEmailTask { + Id int64 `json:"id"` + Subject string `json:"subject"` + Content string `json:"content"` + Recipients string `json:"recipients"` + Scope int8 `json:"scope"` + RegisterStartTime int64 `json:"register_start_time"` + RegisterEndTime int64 `json:"register_end_time"` + Additional string `json:"additional"` + Scheduled int64 `json:"scheduled"` + Interval uint8 `json:"interval"` + Limit uint64 `json:"limit"` + Status uint8 `json:"status"` + Errors string `json:"errors"` + Total uint64 `json:"total"` + Current uint64 `json:"current"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + GetBatchSendEmailTaskListRequest { + Page int `form:"page"` + Size int `form:"size"` + Scope *int8 `form:"scope,omitempty"` + Status *uint8 `form:"status,omitempty"` + } + GetBatchSendEmailTaskListResponse { + Total int64 `json:"total"` + List []BatchSendEmailTask `json:"list"` + } + StopBatchSendEmailTaskRequest { + Id int64 `json:"id"` + } + GetPreSendEmailCountRequest { + Scope int8 `json:"scope"` + RegisterStartTime int64 `json:"register_start_time,omitempty"` + RegisterEndTime int64 `json:"register_end_time,omitempty"` + } + GetPreSendEmailCountResponse { + Count int64 `json:"count"` + } + GetBatchSendEmailTaskStatusRequest { + Id int64 `json:"id"` + } + GetBatchSendEmailTaskStatusResponse { + Status uint8 `json:"status"` + Current int64 `json:"current"` + Total int64 `json:"total"` + Errors string `json:"errors"` + } + CreateQuotaTaskRequest { + Subscribers []int64 `json:"subscribers"` + IsActive *bool `json:"is_active"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + ResetTraffic bool `json:"reset_traffic"` + Days uint64 `json:"days"` + GiftType uint8 `json:"gift_type"` + GiftValue uint64 `json:"gift_value"` + } + QuotaTask { + Id int64 `json:"id"` + Subscribers []int64 `json:"subscribers"` + IsActive *bool `json:"is_active"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + ResetTraffic bool `json:"reset_traffic"` + Days uint64 `json:"days"` + GiftType uint8 `json:"gift_type"` + GiftValue uint64 `json:"gift_value"` + Objects []int64 `json:"objects"` // UserSubscribe IDs + Status uint8 `json:"status"` + Total int64 `json:"total"` + Current int64 `json:"current"` + Errors string `json:"errors"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + QueryQuotaTaskPreCountRequest { + Subscribers []int64 `json:"subscribers"` + IsActive *bool `json:"is_active"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + } + QueryQuotaTaskPreCountResponse { + Count int64 `json:"count"` + } + QueryQuotaTaskListRequest { + Page int `form:"page"` + Size int `form:"size"` + Status *uint8 `form:"status,omitempty"` + } + QueryQuotaTaskListResponse { + Total int64 `json:"total"` + List []QuotaTask `json:"list"` + } + QueryQuotaTaskStatusRequest { + Id int64 `json:"id"` + } + QueryQuotaTaskStatusResponse { + Status uint8 `json:"status"` + Current int64 `json:"current"` + Total int64 `json:"total"` + Errors string `json:"errors"` + } +) + +@server ( + prefix: v1/admin/marketing + group: admin/marketing + middleware: AuthMiddleware +) +service ppanel { + @doc "Create a batch send email task" + @handler CreateBatchSendEmailTask + post /email/batch/send (CreateBatchSendEmailTaskRequest) + + @doc "Get batch send email task list" + @handler GetBatchSendEmailTaskList + get /email/batch/list (GetBatchSendEmailTaskListRequest) returns (GetBatchSendEmailTaskListResponse) + + @doc "Stop a batch send email task" + @handler StopBatchSendEmailTask + post /email/batch/stop (StopBatchSendEmailTaskRequest) + + @doc "Get pre-send email count" + @handler GetPreSendEmailCount + post /email/batch/pre-send-count (GetPreSendEmailCountRequest) returns (GetPreSendEmailCountResponse) + + @doc "Get batch send email task status" + @handler GetBatchSendEmailTaskStatus + post /email/batch/status (GetBatchSendEmailTaskStatusRequest) returns (GetBatchSendEmailTaskStatusResponse) + + @doc "Create a quota task" + @handler CreateQuotaTask + post /quota/create (CreateQuotaTaskRequest) + + @doc "Query quota task pre-count" + @handler QueryQuotaTaskPreCount + post /quota/pre-count (QueryQuotaTaskPreCountRequest) returns (QueryQuotaTaskPreCountResponse) + + @doc "Query quota task list" + @handler QueryQuotaTaskList + get /quota/list (QueryQuotaTaskListRequest) returns (QueryQuotaTaskListResponse) +} + diff --git a/apis/admin/server.api b/apis/admin/server.api index d6181b0..2877427 100644 --- a/apis/admin/server.api +++ b/apis/admin/server.api @@ -11,104 +11,134 @@ info ( import "../types.api" type ( - GetNodeServerListRequest { - Page int `form:"page" validate:"required"` - Size int `form:"size" validate:"required"` - Tag string `form:"tag,omitempty"` - GroupId int64 `form:"group_id,omitempty"` - Search string `form:"search,omitempty"` + ServerOnlineIP { + IP string `json:"ip"` + Protocol string `json:"protocol"` } - GetNodeServerListResponse { + ServerOnlineUser { + IP []ServerOnlineIP `json:"ip"` + UserId int64 `json:"user_id"` + Subscribe string `json:"subscribe"` + SubscribeId int64 `json:"subscribe_id"` + Traffic int64 `json:"traffic"` + ExpiredAt int64 `json:"expired_at"` + } + ServerStatus { + Cpu float64 `json:"cpu"` + Mem float64 `json:"mem"` + Disk float64 `json:"disk"` + Protocol string `json:"protocol"` + Online []ServerOnlineUser `json:"online"` + Status string `json:"status"` + } + Server { + Id int64 `json:"id"` + Name string `json:"name"` + Country string `json:"country"` + City string `json:"city"` + Address string `json:"address"` + Sort int `json:"sort"` + Protocols []Protocol `json:"protocols"` + LastReportedAt int64 `json:"last_reported_at"` + Status ServerStatus `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + } + CreateServerRequest { + Name string `json:"name"` + Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` + Address string `json:"address"` + Sort int `json:"sort,omitempty"` + Protocols []Protocol `json:"protocols"` + } + UpdateServerRequest { + Id int64 `json:"id"` + Name string `json:"name"` + Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` + Address string `json:"address"` + Sort int `json:"sort,omitempty"` + Protocols []Protocol `json:"protocols"` + } + DeleteServerRequest { + Id int64 `json:"id"` + } + FilterServerListRequest { + Page int `form:"page"` + Size int `form:"size"` + Search string `form:"search,omitempty"` + } + FilterServerListResponse { Total int64 `json:"total"` List []Server `json:"list"` } - UpdateNodeRequest { - Id int64 `json:"id" validate:"required"` - Tags []string `json:"tags"` - Country string `json:"country"` - City string `json:"city"` - Name string `json:"name" validate:"required"` - ServerAddr string `json:"server_addr" validate:"required"` - RelayMode string `json:"relay_mode"` - RelayNode []NodeRelay `json:"relay_node"` - SpeedLimit int `json:"speed_limit"` - TrafficRatio float32 `json:"traffic_ratio"` - GroupId int64 `json:"group_id"` - Protocol string `json:"protocol" validate:"required"` - Config interface{} `json:"config" validate:"required"` - Enable *bool `json:"enable"` - Sort int64 `json:"sort"` + GetServerProtocolsRequest { + Id int64 `form:"id"` + } + GetServerProtocolsResponse { + Protocols []Protocol `json:"protocols"` + } + Node { + Id int64 `json:"id"` + Name string `json:"name"` + Tags []string `json:"tags"` + Port uint16 `json:"port"` + Address string `json:"address"` + ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` + Enabled *bool `json:"enabled"` + Sort int `json:"sort,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } CreateNodeRequest { - Name string `json:"name" validate:"required"` - Tags []string `json:"tags"` - Country string `json:"country"` - City string `json:"city"` - ServerAddr string `json:"server_addr" validate:"required"` - RelayMode string `json:"relay_mode"` - RelayNode []NodeRelay `json:"relay_node"` - SpeedLimit int `json:"speed_limit"` - TrafficRatio float32 `json:"traffic_ratio"` - GroupId int64 `json:"group_id"` - Protocol string `json:"protocol" validate:"required"` - Config interface{} `json:"config" validate:"required"` - Enable *bool `json:"enable"` - Sort int64 `json:"sort"` + Name string `json:"name"` + Tags []string `json:"tags,omitempty"` + Port uint16 `json:"port"` + Address string `json:"address"` + ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` + Enabled *bool `json:"enabled"` + } + UpdateNodeRequest { + Id int64 `json:"id"` + Name string `json:"name"` + Tags []string `json:"tags,omitempty"` + Port uint16 `json:"port"` + Address string `json:"address"` + ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` + Enabled *bool `json:"enabled"` + } + ToggleNodeStatusRequest { + Id int64 `json:"id"` + Enable *bool `json:"enable"` } DeleteNodeRequest { - Id int64 `json:"id" validate:"required"` + Id int64 `json:"id"` } - GetNodeGroupListResponse { - Total int64 `json:"total"` - List []ServerGroup `json:"list"` + FilterNodeListRequest { + Page int `form:"page"` + Size int `form:"size"` + Search string `form:"search,omitempty"` } - CreateNodeGroupRequest { - Name string `json:"name" validate:"required"` - Description string `json:"description"` + FilterNodeListResponse { + Total int64 `json:"total"` + List []Node `json:"list"` } - UpdateNodeGroupRequest { - Id int64 `json:"id" validate:"required"` - Name string `json:"name" validate:"required"` - Description string `json:"description"` + HasMigrateSeverNodeResponse { + HasMigrate bool `json:"has_migrate"` } - DeleteNodeGroupRequest { - Id int64 `json:"id" validate:"required"` + MigrateServerNodeResponse { + Succee uint64 `json:"succee"` + Fail uint64 `json:"fail"` + Message string `json:"message,omitempty"` } - BatchDeleteNodeRequest { - Ids []int64 `json:"ids" validate:"required"` - } - BatchDeleteNodeGroupRequest { - Ids []int64 `json:"ids" validate:"required"` - } - GetNodeDetailRequest { - Id int64 `form:"id" validate:"required"` - } - NodeSortRequest { + ResetSortRequest { Sort []SortItem `json:"sort"` } - CreateRuleGroupRequest { - Name string `json:"name" validate:"required"` - Icon string `json:"icon"` - Tags []string `json:"tags"` - Rules string `json:"rules"` - Enable bool `json:"enable"` - } - UpdateRuleGroupRequest { - Id int64 `json:"id" validate:"required"` - Icon string `json:"icon"` - Name string `json:"name" validate:"required"` - Tags []string `json:"tags"` - Rules string `json:"rules"` - Enable bool `json:"enable"` - } - DeleteRuleGroupRequest { - Id int64 `json:"id" validate:"required"` - } - GetRuleGroupResponse { - Total int64 `json:"total"` - List []ServerRuleGroup `json:"list"` - } - GetNodeTagListResponse { + QueryNodeTagResponse { Tags []string `json:"tags"` } ) @@ -119,72 +149,64 @@ type ( middleware: AuthMiddleware ) service ppanel { - @doc "Get node tag list" - @handler GetNodeTagList - get /tag/list returns (GetNodeTagListResponse) + @doc "Create Server" + @handler CreateServer + post /create (CreateServerRequest) - @doc "Get node list" - @handler GetNodeList - get /list (GetNodeServerListRequest) returns (GetNodeServerListResponse) + @doc "Update Server" + @handler UpdateServer + post /update (UpdateServerRequest) - @doc "Get node detail" - @handler GetNodeDetail - get /detail (GetNodeDetailRequest) returns (Server) + @doc "Delete Server" + @handler DeleteServer + post /delete (DeleteServerRequest) - @doc "Update node" - @handler UpdateNode - put / (UpdateNodeRequest) + @doc "Filter Server List" + @handler FilterServerList + get /list (FilterServerListRequest) returns (FilterServerListResponse) - @doc "Create node" + @doc "Get Server Protocols" + @handler GetServerProtocols + get /protocols (GetServerProtocolsRequest) returns (GetServerProtocolsResponse) + + @doc "Create Node" @handler CreateNode - post / (CreateNodeRequest) + post /node/create (CreateNodeRequest) - @doc "Delete node" + @doc "Update Node" + @handler UpdateNode + post /node/update (UpdateNodeRequest) + + @doc "Delete Node" @handler DeleteNode - delete / (DeleteNodeRequest) + post /node/delete (DeleteNodeRequest) - @doc "Batch delete node" - @handler BatchDeleteNode - delete /batch (BatchDeleteNodeRequest) + @doc "Filter Node List" + @handler FilterNodeList + get /node/list (FilterNodeListRequest) returns (FilterNodeListResponse) - @doc "Get node group list" - @handler GetNodeGroupList - get /group/list returns (GetNodeGroupListResponse) + @doc "Toggle Node Status" + @handler ToggleNodeStatus + post /node/status/toggle (ToggleNodeStatusRequest) - @doc "Create node group" - @handler CreateNodeGroup - post /group (CreateNodeGroupRequest) + @doc "Check if there is any server or node to migrate" + @handler HasMigrateSeverNode + get /migrate/has returns (HasMigrateSeverNodeResponse) - @doc "Update node group" - @handler UpdateNodeGroup - put /group (UpdateNodeGroupRequest) + @doc "Migrate server and node data to new database" + @handler MigrateServerNode + post /migrate/run returns (MigrateServerNodeResponse) - @doc "Delete node group" - @handler DeleteNodeGroup - delete /group (DeleteNodeGroupRequest) + @doc "Reset server sort" + @handler ResetSortWithServer + post /server/sort (ResetSortRequest) - @doc "Batch delete node group" - @handler BatchDeleteNodeGroup - delete /group/batch (BatchDeleteNodeGroupRequest) + @doc "Reset node sort" + @handler ResetSortWithNode + post /node/sort (ResetSortRequest) - @doc "Node sort " - @handler NodeSort - post /sort (NodeSortRequest) - - @doc "Create rule group" - @handler CreateRuleGroup - post /rule_group (CreateRuleGroupRequest) - - @doc "Update rule group" - @handler UpdateRuleGroup - put /rule_group (UpdateRuleGroupRequest) - - @doc "Delete rule group" - @handler DeleteRuleGroup - delete /rule_group (DeleteRuleGroupRequest) - - @doc "Get rule group list" - @handler GetRuleGroupList - get /rule_group_list returns (GetRuleGroupResponse) + @doc "Query all node tags" + @handler QueryNodeTag + get /node/tags returns (QueryNodeTagResponse) } diff --git a/apis/admin/subscribe.api b/apis/admin/subscribe.api index d95ab39..1d08b65 100644 --- a/apis/admin/subscribe.api +++ b/apis/admin/subscribe.api @@ -34,70 +34,67 @@ type ( Ids []int64 `json:"ids" validate:"required"` } CreateSubscribeRequest { - Name string `json:"name" validate:"required"` - 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"` - GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` - 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"` } UpdateSubscribeRequest { - Id int64 `json:"id" validate:"required"` - Name string `json:"name" validate:"required"` - 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"` - GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` - 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"` } SubscribeSortRequest { Sort []SortItem `json:"sort"` } GetSubscribeListRequest { - Page int64 `form:"page" validate:"required"` - Size int64 `form:"size" validate:"required"` - GroupId int64 `form:"group_id,omitempty"` - Search string `form:"search,omitempty"` + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + Language string `form:"language,omitempty"` + Search string `form:"search,omitempty"` } - SubscribeItem { - Subscribe - - Sold int64 `json:"sold"` + Subscribe + Sold int64 `json:"sold"` } - GetSubscribeListResponse { List []SubscribeItem `json:"list"` - Total int64 `json:"total"` + Total int64 `json:"total"` } DeleteSubscribeRequest { Id int64 `json:"id" validate:"required"` diff --git a/apis/admin/system.api b/apis/admin/system.api index 09323c3..a300cda 100644 --- a/apis/admin/system.api +++ b/apis/admin/system.api @@ -11,50 +11,6 @@ info ( import "../types.api" type ( - // Update application request - UpdateApplicationRequest { - Id int64 `json:"id" validate:"required"` - Icon string `json:"icon"` - Name string `json:"name"` - Description string `json:"description"` - SubscribeType string `json:"subscribe_type"` - Platform ApplicationPlatform `json:"platform"` - } - // Create application request - CreateApplicationRequest { - Icon string `json:"icon"` - Name string `json:"name"` - Description string `json:"description"` - SubscribeType string `json:"subscribe_type"` - Platform ApplicationPlatform `json:"platform"` - } - // Update application request - UpdateApplicationVersionRequest { - Id int64 `json:"id" validate:"required"` - Url string `json:"url"` - Version string `json:"version" validate:"required"` - Description string `json:"description"` - Platform string `json:"platform" validate:"required,oneof=windows mac linux android ios harmony"` - IsDefault bool `json:"is_default"` - ApplicationId int64 `json:"application_id" validate:"required"` - } - // Create application request - CreateApplicationVersionRequest { - Url string `json:"url"` - Version string `json:"version" validate:"required"` - Description string `json:"description"` - Platform string `json:"platform" validate:"required,oneof=windows mac linux android ios harmony"` - IsDefault bool `json:"is_default"` - ApplicationId int64 `json:"application_id" validate:"required"` - } - // Delete application request - DeleteApplicationRequest { - Id int64 `json:"id" validate:"required"` - } - // Delete application request - DeleteApplicationVersionRequest { - Id int64 `json:"id" validate:"required"` - } GetNodeMultiplierResponse { Periods []TimePeriod `json:"periods"` } @@ -62,6 +18,10 @@ type ( SetNodeMultiplierRequest { Periods []TimePeriod `json:"periods"` } + PreViewNodeMultiplierResponse { + CurrentTime string `json:"current_time"` + Ratio float32 `json:"ratio"` + } ) @server ( @@ -86,46 +46,6 @@ service ppanel { @handler UpdateSubscribeConfig put /subscribe_config (SubscribeConfig) - @doc "Get subscribe type" - @handler GetSubscribeType - get /subscribe_type returns (SubscribeType) - - @doc "update application config" - @handler UpdateApplicationConfig - put /application_config (ApplicationConfig) - - @doc "get application config" - @handler GetApplicationConfig - get /application_config returns (ApplicationConfig) - - @doc "Get application" - @handler GetApplication - get /application returns (ApplicationResponse) - - @doc "Update application" - @handler UpdateApplication - put /application (UpdateApplicationRequest) - - @doc "Create application" - @handler CreateApplication - post /application (CreateApplicationRequest) - - @doc "Delete application" - @handler DeleteApplication - delete /application (DeleteApplicationRequest) - - @doc "Update application version" - @handler UpdateApplicationVersion - put /application_version (UpdateApplicationVersionRequest) - - @doc "Create application version" - @handler CreateApplicationVersion - post /application_version (CreateApplicationVersionRequest) - - @doc "Delete application" - @handler DeleteApplicationVersion - delete /application_version (DeleteApplicationVersionRequest) - @doc "Get register config" @handler GetRegisterConfig get /register_config returns (RegisterConfig) @@ -201,5 +121,9 @@ service ppanel { @doc "Update Verify Code Config" @handler UpdateVerifyCodeConfig put /verify_code_config (VerifyCodeConfig) + + @doc "PreView Node Multiplier" + @handler PreViewNodeMultiplier + get /node_multiplier/preview returns (PreViewNodeMultiplierResponse) } diff --git a/apis/admin/ticket.api b/apis/admin/ticket.api index 59ae897..c325bf0 100644 --- a/apis/admin/ticket.api +++ b/apis/admin/ticket.api @@ -12,14 +12,14 @@ import "../types.api" type ( UpdateTicketStatusRequest { - Id int64 `json:"id" validate:"required"` + Id int64 `json:"id" validate:"required"` Status *uint8 `json:"status" validate:"required"` } GetTicketListRequest { Page int64 `form:"page"` Size int64 `form:"size"` UserId int64 `form:"user_id,omitempty"` - Status *uint8 `form:"status,omitempty"` + Status *uint8 `form:"status,omitempty"` Search string `form:"search,omitempty"` } GetTicketListResponse { diff --git a/apis/admin/tool.api b/apis/admin/tool.api index f54ee61..da1916b 100644 --- a/apis/admin/tool.api +++ b/apis/admin/tool.api @@ -1,33 +1,40 @@ syntax = "v1" -info( - title: "Tools Api" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" +info ( + title: "Tools Api" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" ) import "../types.api" type ( - LogResponse { - List interface{} `json:"list"` - } + LogResponse { + List interface{} `json:"list"` + } + VersionResponse { + Version string `json:"version"` + } ) @server ( - prefix: v1/admin/tool - group: admin/tool - middleware: AuthMiddleware + prefix: v1/admin/tool + group: admin/tool + middleware: AuthMiddleware ) - service ppanel { - @doc "Get System Log" - @handler GetSystemLog - get /log returns (LogResponse) + @doc "Get System Log" + @handler GetSystemLog + get /log returns (LogResponse) - @doc "Restart System" - @handler RestartSystem - get /restart + @doc "Restart System" + @handler RestartSystem + get /restart + + @doc "Get Version" + @handler GetVersion + get /version returns (VersionResponse) } + diff --git a/apis/admin/user.api b/apis/admin/user.api index 92149fd..dc0c5e8 100644 --- a/apis/admin/user.api +++ b/apis/admin/user.api @@ -32,17 +32,19 @@ type ( Id int64 `form:"id" validate:"required"` } UpdateUserBasiceInfoRequest { - UserId int64 `json:"user_id" validate:"required"` - Password string `json:"password"` - Avatar string `json:"avatar"` - Balance int64 `json:"balance"` - Commission int64 `json:"commission"` - GiftAmount int64 `json:"gift_amount"` - Telegram int64 `json:"telegram"` - ReferCode string `json:"refer_code"` - RefererId int64 `json:"referer_id"` - Enable bool `json:"enable"` - IsAdmin bool `json:"is_admin"` + UserId int64 `json:"user_id" validate:"required"` + Password string `json:"password"` + Avatar string `json:"avatar"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + ReferralPercentage uint8 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` + GiftAmount int64 `json:"gift_amount"` + Telegram int64 `json:"telegram"` + ReferCode string `json:"refer_code"` + RefererId int64 `json:"referer_id"` + Enable bool `json:"enable"` + IsAdmin bool `json:"is_admin"` } UpdateUserNotifySettingRequest { UserId int64 `json:"user_id" validate:"required"` @@ -52,18 +54,20 @@ type ( EnableTradeNotify bool `json:"enable_trade_notify"` } CreateUserRequest { - Email string `json:"email"` - Telephone string `json:"telephone"` - TelephoneAreaCode string `json:"telephone_area_code"` - Password string `json:"password"` - ProductId int64 `json:"product_id"` - Duration int64 `json:"duration"` - RefererUser string `json:"referer_user"` - ReferCode string `json:"refer_code"` - Balance int64 `json:"balance"` - Commission int64 `json:"commission"` - GiftAmount int64 `json:"gift_amount"` - IsAdmin bool `json:"is_admin"` + Email string `json:"email"` + Telephone string `json:"telephone"` + TelephoneAreaCode string `json:"telephone_area_code"` + Password string `json:"password"` + ProductId int64 `json:"product_id"` + Duration int64 `json:"duration"` + ReferralPercentage uint8 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` + RefererUser string `json:"referer_user"` + ReferCode string `json:"refer_code"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + GiftAmount int64 `json:"gift_amount"` + IsAdmin bool `json:"is_admin"` } UserSubscribeDetail { Id int64 `json:"id"` @@ -164,6 +168,15 @@ type ( List []UserLoginLog `json:"list"` Total int64 `json:"total"` } + GetUserSubscribeResetTrafficLogsRequest { + Page int `form:"page"` + Size int `form:"size"` + UserSubscribeId int64 `form:"user_subscribe_id"` + } + GetUserSubscribeResetTrafficLogsResponse { + List []ResetSubscribeTrafficLog `json:"list"` + Total int64 `json:"total"` + } DeleteUserSubscribeRequest { UserSubscribeId int64 `json:"user_subscribe_id"` } @@ -251,6 +264,10 @@ service ppanel { @handler GetUserSubscribeLogs get /subscribe/logs (GetUserSubscribeLogsRequest) returns (GetUserSubscribeLogsResponse) + @doc "Get user subcribe reset traffic logs" + @handler GetUserSubscribeResetTrafficLogs + get /subscribe/reset/logs (GetUserSubscribeResetTrafficLogsRequest) returns (GetUserSubscribeResetTrafficLogsResponse) + @doc "Get user subcribe traffic logs" @handler GetUserSubscribeTrafficLogs get /subscribe/traffic_logs (GetUserSubscribeTrafficLogsRequest) returns (GetUserSubscribeTrafficLogsResponse) diff --git a/apis/app/announcement.api b/apis/app/announcement.api deleted file mode 100644 index 2decd09..0000000 --- a/apis/app/announcement.api +++ /dev/null @@ -1,24 +0,0 @@ -syntax = "v1" - -info ( - title: "Announcement API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import "../types.api" - - -@server ( - prefix: v1/app/announcement - group: app/announcement - middleware: AppMiddleware,AuthMiddleware -) -service ppanel { - @doc "Query announcement" - @handler QueryAnnouncement - get /list (QueryAnnouncementRequest) returns (QueryAnnouncementResponse) -} - diff --git a/apis/app/auth.api b/apis/app/auth.api deleted file mode 100644 index 015eb64..0000000 --- a/apis/app/auth.api +++ /dev/null @@ -1,105 +0,0 @@ -syntax = "v1" - -info( - title: "App Auth Api" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import ( - "../types.api" -) - -type ( - AppAuthCheckRequest { - Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"` - Account string `json:"account"` - Identifier string `json:"identifier" validate:"required"` - UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` - AreaCode string `json:"area_code"` - } - AppAuthCheckResponse { - Status bool - } - AppAuthRequest { - Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"` - Account string `json:"account"` - Password string `json:"password"` - Identifier string `json:"identifier" validate:"required"` - UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` - Code string `json:"code"` - Invite string `json:"invite"` - AreaCode string `json:"area_code"` - CfToken string `json:"cf_token,optional"` - } - AppAuthRespone { - Token string `json:"token"` - } - AppSendCodeRequest { - Method string `json:"method" validate:"required" validate:"required,oneof=email mobile"` - Account string `json:"account"` - AreaCode string `json:"area_code"` - CfToken string `json:"cf_token,optional"` - } - AppSendCodeRespone { - Status bool `json:"status"` - Code string `json:"code,omitempty"` - } - AppConfigRequest { - UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` - } - AppConfigResponse { - EncryptionKey string `json:"encryption_key"` - EncryptionMethod string `json:"encryption_method"` - Domains []string `json:"domains"` - StartupPicture string `json:"startup_picture"` - StartupPictureSkipTime int64 `json:"startup_picture_skip_time"` - Application AppInfo `json:"applications"` - OfficialEmail string `json:"official_email"` - OfficialWebsite string `json:"official_website"` - OfficialTelegram string `json:"official_telegram"` - OfficialTelephone string `json:"official_telephone"` - InvitationLink string `json:"invitation_link"` - KrWebsiteId string `json:"kr_website_id"` - } - AppInfo { - Id int64 `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Url string `json:"url"` - Version string `json:"version"` - VersionReview string `json:"version_review"` - VersionDescription string `json:"version_description"` - IsDefault bool `json:"is_default"` - } -) - -@server( - prefix: v1/app/auth - group: app/auth - middleware: AppMiddleware -) -service ppanel { - @doc "Check Account" - @handler Check - post /check (AppAuthCheckRequest) returns (AppAuthCheckResponse) - - @doc "Login" - @handler Login - post /login (AppAuthRequest) returns (AppAuthRespone) - - @doc "Register" - @handler Register - post /register (AppAuthRequest) returns (AppAuthRespone) - - @doc "Reset Password" - @handler ResetPassword - post /reset_password (AppAuthRequest) returns (AppAuthRespone) - - @doc "GetAppConfig" - @handler GetAppConfig - post /config (AppConfigRequest) returns (AppConfigResponse) -} - diff --git a/apis/app/document.api b/apis/app/document.api deleted file mode 100644 index 6cc4c71..0000000 --- a/apis/app/document.api +++ /dev/null @@ -1,27 +0,0 @@ -syntax = "v1" - -info( - title: "Document API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import "../types.api" - -@server ( - prefix: v1/app/document - group: app/document - middleware: AppMiddleware,AuthMiddleware -) - -service ppanel { - @doc "Get document list" - @handler QueryDocumentList - get /list returns (QueryDocumentListResponse) - - @doc "Get document detail" - @handler QueryDocumentDetail - get /detail (QueryDocumentDetailRequest) returns (Document) -} \ No newline at end of file diff --git a/apis/app/node.api b/apis/app/node.api deleted file mode 100644 index b2434b2..0000000 --- a/apis/app/node.api +++ /dev/null @@ -1,49 +0,0 @@ -syntax = "v1" - - -info( - title: "App Node Api" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import "../types.api" - -type( - - - AppRuleGroupListResponse { - Total int64 `json:"total"` - List []ServerRuleGroup `json:"list"` - } - - AppUserSubscbribeNodeRequest { - Id int64 `form:"id" validate:"required"` - } - - AppUserSubscbribeNodeResponse{ - List []AppUserSubscbribeNode `json:"list"` - } -) - -@server ( - prefix: v1/app/node - group: app/node - middleware: AppMiddleware,AuthMiddleware -) - -service ppanel { - - - - @doc "Get Node list" - @handler GetNodeList - get /list (AppUserSubscbribeNodeRequest) returns(AppUserSubscbribeNodeResponse) - - @doc "Get rule group list" - @handler GetRuleGroupList - get /rule_group_list returns (AppRuleGroupListResponse) - -} \ No newline at end of file diff --git a/apis/app/order.api b/apis/app/order.api deleted file mode 100644 index 959ff40..0000000 --- a/apis/app/order.api +++ /dev/null @@ -1,58 +0,0 @@ -syntax = "v1" - -info ( - title: "Order API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import ( - "../types.api" -) - - -@server ( - prefix: v1/app/order - group: app/order - middleware: AppMiddleware,AuthMiddleware -) -service ppanel { - @doc "Pre create order" - @handler PreCreateOrder - post /pre (PurchaseOrderRequest) returns (PreOrderResponse) - - @doc "purchase Subscription" - @handler Purchase - post /purchase (PurchaseOrderRequest) returns (PurchaseOrderResponse) - - @doc "Renewal Subscription" - @handler Renewal - post /renewal (RenewalOrderRequest) returns (RenewalOrderResponse) - - @doc "Reset traffic" - @handler ResetTraffic - post /reset (ResetTrafficOrderRequest) returns (ResetTrafficOrderResponse) - - @doc "Recharge" - @handler Recharge - post /recharge (RechargeOrderRequest) returns (RechargeOrderResponse) - - @doc "Checkout order" - @handler CheckoutOrder - post /checkout (CheckoutOrderRequest) returns (CheckoutOrderResponse) - - @doc "Close order" - @handler CloseOrder - post /close (CloseOrderRequest) - - @doc "Get order" - @handler QueryOrderDetail - get /detail (QueryOrderDetailRequest) returns (OrderDetail) - - @doc "Get order list" - @handler QueryOrderList - get /list (QueryOrderListRequest) returns (QueryOrderListResponse) -} - diff --git a/apis/app/payment.api b/apis/app/payment.api deleted file mode 100644 index 9769a47..0000000 --- a/apis/app/payment.api +++ /dev/null @@ -1,23 +0,0 @@ -syntax = "v1" - -info ( - title: "payment API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import "../types.api" - -@server ( - prefix: v1/app/payment - group: app/payment - middleware: AppMiddleware,AuthMiddleware -) -service ppanel { - @doc "Get available payment methods" - @handler GetAvailablePaymentMethods - get /methods returns (GetAvailablePaymentMethodsResponse) -} - diff --git a/apis/app/subscribe.api b/apis/app/subscribe.api deleted file mode 100644 index 0a4a05a..0000000 --- a/apis/app/subscribe.api +++ /dev/null @@ -1,75 +0,0 @@ -syntax = "v1" - -info( - title: "Subscribe API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import "../types.api" - - -type ( - QueryUserSubscribeResp { - Data []UserSubscribeData `json:"data"` - } - - UserSubscribeData { - SubscribeId int64 `json:"subscribe_id"` - UserSubscribeId int64 `json:"user_subscribe_id"` - } - - - UserSubscribeResetPeriodRequest { - UserSubscribeId int64 `json:"user_subscribe_id"` - } - - UserSubscribeResetPeriodResponse { - Status bool `json:"status"` - } - - AppUserSubscribeRequest { - ContainsNodes *bool `form:"contains_nodes"` - } - - AppUserSubscbribeResponse { - List []AppUserSubcbribe `json:"list"` - } -) - -@server( - prefix: v1/app/subscribe - group: app/subscribe - middleware: AppMiddleware,AuthMiddleware -) - - -service ppanel { - @doc "Get subscribe list" - @handler QuerySubscribeList - get /list returns (QuerySubscribeListResponse) - - @doc "Get subscribe group list" - @handler QuerySubscribeGroupList - get /group/list returns (QuerySubscribeGroupListResponse) - - @doc "Get application config" - @handler QueryApplicationConfig - get /application/config returns (ApplicationResponse) - - @doc "Get Already subscribed to package" - @handler QueryUserAlreadySubscribe - get /user/already_subscribe returns (QueryUserSubscribeResp) - - - @doc "Get Available subscriptions for users" - @handler QueryUserAvailableUserSubscribe - get /user/available_subscribe (AppUserSubscribeRequest) returns (AppUserSubscbribeResponse) - - @doc "Reset user subscription period" - @handler ResetUserSubscribePeriod - post /reset/period (UserSubscribeResetPeriodRequest) returns (UserSubscribeResetPeriodResponse) -} - diff --git a/apis/app/user.api b/apis/app/user.api deleted file mode 100644 index 67fb380..0000000 --- a/apis/app/user.api +++ /dev/null @@ -1,90 +0,0 @@ -syntax = "v1" - -info ( - title: "App User Api" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import ( - "../types.api" -) - -type ( - UserInfoResponse { - Id int64 `json:"id"` - Balance int64 `json:"balance"` - Email string `json:"email"` - RefererId int64 `json:"referer_id"` - ReferCode string `json:"refer_code"` - Avatar string `json:"avatar"` - AreaCode string `json:"area_code"` - Telephone string `json:"telephone"` - Devices []UserDevice `json:"devices"` - AuthMethods []UserAuthMethod `json:"auth_methods"` - } - UpdatePasswordRequeset { - Password string `json:"password"` - NewPassword string `json:"new_password"` - } - DeleteAccountRequest { - Method string `json:"method" validate:"required" validate:"required,oneof=email telephone device"` - Code string `json:"code"` - } - - GetUserOnlineTimeStatisticsResponse{ - 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 ( - prefix: v1/app/user - group: app/user - middleware: AppMiddleware,AuthMiddleware -) -service ppanel { - @doc "query user info" - @handler QueryUserInfo - get /info returns (UserInfoResponse) - - @doc "Update Password " - @handler UpdatePassword - put /password (UpdatePasswordRequeset) - - @doc "Delete Account" - @handler DeleteAccount - delete /account (DeleteAccountRequest) - - @doc "Get user subcribe traffic logs" - @handler GetUserSubscribeTrafficLogs - get /subscribe/traffic_logs (GetUserSubscribeTrafficLogsRequest) returns (GetUserSubscribeTrafficLogsResponse) - - @doc "Get user online time total" - @handler GetUserOnlineTimeStatistics - get /online_time/statistics returns (GetUserOnlineTimeStatisticsResponse) - - @doc "Query User Affiliate List" - @handler QueryUserAffiliateList - get /affiliate/list (QueryUserAffiliateListRequest) returns (QueryUserAffiliateListResponse) - - @doc "Query User Affiliate Count" - @handler QueryUserAffiliate - get /affiliate/count returns (QueryUserAffiliateCountResponse) - - -} - diff --git a/apis/app/ws.api b/apis/app/ws.api deleted file mode 100644 index a3a26be..0000000 --- a/apis/app/ws.api +++ /dev/null @@ -1,23 +0,0 @@ -syntax = "v1" - -info( - title: "App Heartbeat Api" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - - -@server( - prefix: v1/app/ws - group: app/ws - middleware: AuthMiddleware -) - - -service ppanel { - @doc "App heartbeat" - @handler AppWs - get /:userid/:identifier -} \ No newline at end of file diff --git a/apis/auth/auth.api b/apis/auth/auth.api index 22dbe10..154c878 100644 --- a/apis/auth/auth.api +++ b/apis/auth/auth.api @@ -11,11 +11,13 @@ info ( type ( // User login request UserLoginRequest { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } // Check user is exist request CheckUserRequest { @@ -23,50 +25,55 @@ type ( } // User login response CheckUserResponse { - exist bool `json:"exist"` + Exist bool `json:"exist"` } // User login response UserRegisterRequest { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Invite string `json:"invite,optional"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Invite string `json:"invite,optional"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } // User login response ResetPasswordRequest { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } LoginResponse { Token string `json:"token"` } OAthLoginRequest { - Method string `json:"method" validate:"required"` // google, facebook, apple, telegram, github etc. + Method string `json:"method" validate:"required"` // google, facebook, apple, telegram, github etc. Redirect string `json:"redirect"` } OAuthLoginResponse { Redirect string `json:"redirect"` } - OAuthLoginGetTokenRequest { - Method string `json:"method" validate:"required"` // google, facebook, apple, telegram, github etc. + Method string `json:"method" validate:"required"` // google, facebook, apple, telegram, github etc. Callback interface{} `json:"callback" validate:"required"` } - // login request TelephoneLoginRequest { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneCode string `json:"telephone_code"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password"` IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` CfToken string `json:"cf_token,optional"` } // Check user is exist request @@ -76,25 +83,31 @@ type ( } // User login response TelephoneCheckUserResponse { - exist bool `json:"exist"` + Exist bool `json:"exist"` } // User login response TelephoneRegisterRequest { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` Invite string `json:"invite,optional"` Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } // User login response TelephoneResetPasswordRequest { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } AppleLoginCallbackRequest { @@ -103,14 +116,21 @@ type ( State string `form:"state"` } GoogleLoginCallbackRequest { - Code string `form:"code"` + Code string `form:"code"` State string `form:"state"` } + DeviceLoginRequest { + Identifier string `json:"identifier" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `json:"user_agent" validate:"required"` + CfToken string `json:"cf_token,optional"` + } ) @server ( prefix: v1/auth group: auth + middleware: DeviceMiddleware ) service ppanel { @doc "User login" @@ -144,6 +164,10 @@ service ppanel { @doc "Reset password" @handler TelephoneResetPassword post /reset/telephone (TelephoneResetPasswordRequest) returns (LoginResponse) + + @doc "Device Login" + @handler DeviceLogin + post /login/device (DeviceLoginRequest) returns (LoginResponse) } @server ( diff --git a/apis/common.api b/apis/common.api index 545c3c9..d246099 100644 --- a/apis/common.api +++ b/apis/common.api @@ -35,10 +35,6 @@ type ( GetTosResponse { TosContent string `json:"tos_content"` } - GetAppcationResponse { - Config ApplicationConfig `json:"config"` - Applications []ApplicationResponseInfo `json:"applications"` - } // GetCodeRequest Get code request SendCodeRequest { Email string `json:"email" validate:"required"` @@ -70,30 +66,39 @@ type ( List []Ads `json:"list"` } CheckVerificationCodeRequest { - Method string `json:"method" validate:"required,oneof=email mobile"` + Method string `json:"method" validate:"required,oneof=email mobile"` Account string `json:"account" validate:"required"` - Code string `json:"code" validate:"required"` - Type uint8 `json:"type" validate:"required"` + Code string `json:"code" validate:"required"` + Type uint8 `json:"type" validate:"required"` } - - CheckVerificationCodeRespone{ - Status bool `json:"status"` + CheckVerificationCodeRespone { + Status bool `json:"status"` + } + SubscribeClient { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + IsDefault bool `json:"is_default"` + DownloadLink DownloadLink `json:"download_link,omitempty"` + } + GetSubscribeClientResponse { + Total int64 `json:"total"` + List []SubscribeClient `json:"list"` } ) @server ( prefix: v1/common group: common + middleware: DeviceMiddleware ) service ppanel { @doc "Get global config" @handler GetGlobalConfig get /site/config returns (GetGlobalConfigResponse) - @doc "Get Tos Content" - @handler GetApplication - get /application returns (GetAppcationResponse) - @doc "Get Tos Content" @handler GetTos get /site/tos returns (GetTosResponse) @@ -121,5 +126,9 @@ service ppanel { @doc "Check verification code" @handler CheckVerificationCode post /check_verification_code (CheckVerificationCodeRequest) returns (CheckVerificationCodeRespone) + + @doc "Get Client" + @handler GetClient + get /client returns (GetSubscribeClientResponse) } diff --git a/apis/node/node.api b/apis/node/node.api index 8195854..8dee713 100644 --- a/apis/node/node.api +++ b/apis/node/node.api @@ -11,6 +11,10 @@ info ( import "../types.api" type ( + OnlineUser { + SID int64 `json:"uid"` + IP string `json:"ip"` + } ShadowsocksProtocol { Port int `json:"port"` Method string `json:"method"` @@ -44,9 +48,9 @@ type ( ServerCommon } GetServerConfigResponse { - Basic ServerBasic `json:"basic"` - Protocol string `json:"protocol"` - Config interface{} `json:"config"` + Basic ServerBasic `json:"basic"` + Protocol string `json:"protocol"` + Config interface{} `json:"config"` } ServerBasic { PushInterval int64 `json:"push_interval"` @@ -60,8 +64,8 @@ type ( ServerUser { Id int64 `json:"id"` UUID string `json:"uuid"` - SpeedLimit int64 `json:"speed_limit"` - DeviceLimit int64 `json:"device_limit"` + SpeedLimit int64 `json:"speed_limit"` + DeviceLimit int64 `json:"device_limit"` } GetServerUserListRequest { ServerCommon @@ -78,7 +82,6 @@ type ( ServerCommon Traffic []UserTraffic `json:"traffic"` } - ServerPushStatusRequest { ServerCommon Cpu float64 `json:"cpu"` @@ -90,11 +93,25 @@ type ( ServerCommon Users []OnlineUser `json:"users"` } + QueryServerConfigRequest { + ServerID int64 `path:"server_id"` + SecretKey string `form:"secret_key"` + Protocols []string `form:"protocols,omitempty"` + } + QueryServerConfigResponse { + TrafficReportThreshold int64 `json:"traffic_report_threshold"` + IPStrategy string `json:"ip_strategy"` + DNS []NodeDNS `json:"dns"` + Block []string `json:"block"` + Outbound []NodeOutbound `json:"outbound"` + Protocols []Protocol `json:"protocols"` + Total int64 `json:"total"` + } ) @server ( - prefix: v1/server - group: server + prefix: v1/server + group: server middleware: ServerMiddleware ) service ppanel { @@ -119,3 +136,13 @@ service ppanel { post /online (OnlineUsersRequest) } +@server ( + prefix: v2/server + group: server +) +service ppanel { + @doc "Get Server Protocol Config" + @handler QueryServerProtocolConfig + get /:server_id (QueryServerConfigRequest) returns (QueryServerConfigResponse) +} + diff --git a/apis/public/announcement.api b/apis/public/announcement.api index 664ac4c..7122e4e 100644 --- a/apis/public/announcement.api +++ b/apis/public/announcement.api @@ -10,11 +10,10 @@ info ( import "../types.api" - @server ( prefix: v1/public/announcement group: public/announcement - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Query announcement" diff --git a/apis/public/document.api b/apis/public/document.api index 41d7291..660bea6 100644 --- a/apis/public/document.api +++ b/apis/public/document.api @@ -1,28 +1,27 @@ syntax = "v1" -info( - title: "Document API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" +info ( + title: "Document API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" ) import "../types.api" - @server ( - prefix: v1/public/document - group: public/document - middleware: AuthMiddleware + prefix: v1/public/document + group: public/document + middleware: AuthMiddleware,DeviceMiddleware ) - service ppanel { - @doc "Get document list" - @handler QueryDocumentList - get /list returns (QueryDocumentListResponse) + @doc "Get document list" + @handler QueryDocumentList + get /list returns (QueryDocumentListResponse) + + @doc "Get document detail" + @handler QueryDocumentDetail + get /detail (QueryDocumentDetailRequest) returns (Document) +} - @doc "Get document detail" - @handler QueryDocumentDetail - get /detail (QueryDocumentDetailRequest) returns (Document) -} \ No newline at end of file diff --git a/apis/public/order.api b/apis/public/order.api index 0db556f..4e83b0f 100644 --- a/apis/public/order.api +++ b/apis/public/order.api @@ -13,7 +13,7 @@ import "../types.api" @server ( prefix: v1/public/order group: public/order - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Pre create order" diff --git a/apis/public/payment.api b/apis/public/payment.api index 247f9d4..a4893ab 100644 --- a/apis/public/payment.api +++ b/apis/public/payment.api @@ -10,11 +10,10 @@ info ( import "../types.api" - @server ( prefix: v1/public/payment group: public/payment - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Get available payment methods" diff --git a/apis/public/portal.api b/apis/public/portal.api index 709e48b..aba8e25 100644 --- a/apis/public/portal.api +++ b/apis/public/portal.api @@ -19,16 +19,20 @@ type ( SubscribeId int64 `json:"subscribe_id"` Quantity int64 `json:"quantity"` Coupon string `json:"coupon,omitempty"` + InviteCode string `json:"invite_code,omitempty"` TurnstileToken string `json:"turnstile_token,omitempty"` } PortalPurchaseResponse { OrderNo string `json:"order_no"` } + GetSubscriptionRequest { + Language string `form:"language"` + } GetSubscriptionResponse { List []Subscribe `json:"list"` } PrePurchaseOrderRequest { - Payment int64 `json:"payment,omitempty"` + Payment int64 `json:"payment,omitempty"` SubscribeId int64 `json:"subscribe_id"` Quantity int64 `json:"quantity"` Coupon string `json:"coupon,omitempty"` @@ -66,6 +70,7 @@ type ( @server ( prefix: v1/public/portal group: public/portal + middleware: DeviceMiddleware ) service ppanel { @doc "Get available payment methods" @@ -74,7 +79,7 @@ service ppanel { @doc "Get Subscription" @handler GetSubscription - get /subscribe returns (GetSubscriptionResponse) + get /subscribe (GetSubscriptionRequest) returns (GetSubscriptionResponse) @doc "Pre Purchase Order" @handler PrePurchaseOrder diff --git a/apis/public/subscribe.api b/apis/public/subscribe.api index 98fd2c2..13024f8 100644 --- a/apis/public/subscribe.api +++ b/apis/public/subscribe.api @@ -9,23 +9,21 @@ info ( ) import "../types.api" - + +type ( + QuerySubscribeListRequest { + Language string `form:"language"` + } +) + @server ( prefix: v1/public/subscribe group: public/subscribe - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Get subscribe list" @handler QuerySubscribeList - get /list returns (QuerySubscribeListResponse) - - @doc "Get subscribe group list" - @handler QuerySubscribeGroupList - get /group/list returns (QuerySubscribeGroupListResponse) - - @doc "Get application config" - @handler QueryApplicationConfig - get /application/config returns (ApplicationResponse) + get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse) } diff --git a/apis/public/ticket.api b/apis/public/ticket.api index b5a846b..69bff62 100644 --- a/apis/public/ticket.api +++ b/apis/public/ticket.api @@ -11,10 +11,9 @@ info ( import "../types.api" type ( - GetUserTicketListResponse { - Total int64 `json:"total"` - List []Ticket `json:"list"` + Total int64 `json:"total"` + List []Ticket `json:"list"` } CreateUserTicketRequest { Title string `json:"title"` @@ -34,17 +33,17 @@ type ( Status *uint8 `json:"status" validate:"required"` } CreateUserTicketFollowRequest { - TicketId int64 `json:"ticket_id"` - From string `json:"from"` - Type uint8 `json:"type"` - Content string `json:"content"` + TicketId int64 `json:"ticket_id"` + From string `json:"from"` + Type uint8 `json:"type"` + Content string `json:"content"` } ) @server ( prefix: v1/public/ticket group: public/ticket - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Get ticket list" diff --git a/apis/public/user.api b/apis/public/user.api index a2c5a53..1686b32 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -25,16 +25,8 @@ type ( Total int64 `json:"total"` } QueryUserBalanceLogListResponse { - List []UserBalanceLog `json:"list"` - Total int64 `json:"total"` - } - - CommissionLog { - Id int64 `json:"id"` - UserId int64 `json:"user_id"` - OrderNo string `json:"order_no"` - Amount int64 `json:"amount"` - CreatedAt int64 `json:"created_at"` + List []BalanceLog `json:"list"` + Total int64 `json:"total"` } QueryUserCommissionLogListRequest { Page int `form:"page"` @@ -77,47 +69,49 @@ type ( ResetUserSubscribeTokenRequest { UserSubscribeId int64 `json:"user_subscribe_id"` } - GetLoginLogRequest { Page int `form:"page"` Size int `form:"size"` } - GetLoginLogResponse { List []UserLoginLog `json:"list"` Total int64 `json:"total"` } - GetSubscribeLogRequest { Page int `form:"page"` Size int `form:"size"` } - GetSubscribeLogResponse { List []UserSubscribeLog `json:"list"` - Total int64 `json:"total"` + Total int64 `json:"total"` } - - UpdateBindMobileRequest{ - AreaCode string `json:"area_code" validate:"required"` - Mobile string `json:"mobile" validate:"required"` - Code string `json:"code" validate:"required"` + UpdateBindMobileRequest { + AreaCode string `json:"area_code" validate:"required"` + Mobile string `json:"mobile" validate:"required"` + Code string `json:"code" validate:"required"` } - - UpdateBindEmailRequest{ - Email string `json:"email" validate:"required"` + UpdateBindEmailRequest { + Email string `json:"email" validate:"required"` } - VerifyEmailRequest { - Email string `json:"email" validate:"required"` - Code string `json:"code" validate:"required"` + 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"` + } ) @server ( prefix: v1/public/user group: public/user - middleware: AuthMiddleware + middleware: AuthMiddleware,DeviceMiddleware ) service ppanel { @doc "Query User Info" @@ -207,5 +201,13 @@ service ppanel { @doc "Update Bind Email" @handler UpdateBindEmail put /bind_email (UpdateBindEmailRequest) + + @doc "Get Device List" + @handler GetDeviceList + get /devices returns (GetDeviceListResponse) + + @doc "Unbind Device" + @handler UnbindDevice + put /unbind_device (UnbindDeviceRequest) } diff --git a/apis/swagger_admin.api b/apis/swagger_admin.api index c56b302..e61a903 100644 --- a/apis/swagger_admin.api +++ b/apis/swagger_admin.api @@ -1,26 +1,30 @@ syntax = "v1" -info( - title: "admin API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" +info ( + title: "admin API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" ) + import ( - "./admin/system.api" - "./admin/user.api" - "./admin/server.api" - "./admin/subscribe.api" - "./admin/payment.api" - "./admin/coupon.api" - "./admin/order.api" - "./admin/ticket.api" - "./admin/announcement.api" - "./admin/document.api" - "./admin/tool.api" - "./admin/console.api" - "./admin/auth.api" - "./admin/log.api" - "./admin/ads.api" -) \ No newline at end of file + "./admin/system.api" + "./admin/user.api" + "./admin/server.api" + "./admin/subscribe.api" + "./admin/payment.api" + "./admin/coupon.api" + "./admin/order.api" + "./admin/ticket.api" + "./admin/announcement.api" + "./admin/document.api" + "./admin/tool.api" + "./admin/console.api" + "./admin/auth.api" + "./admin/log.api" + "./admin/ads.api" + "./admin/marketing.api" + "./admin/application.api" +) + diff --git a/apis/swagger_app.api b/apis/swagger_app.api deleted file mode 100644 index c4ff674..0000000 --- a/apis/swagger_app.api +++ /dev/null @@ -1,21 +0,0 @@ -syntax = "v1" - -info( - title: "App API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" -) - -import ( - "./app/auth.api" - "./app/user.api" - "./app/node.api" - "./app/ws.api" - "./app/order.api" - "./app/announcement.api" - "./app/payment.api" - "./app/document.api" - "./app/subscribe.api" -) \ No newline at end of file diff --git a/apis/swagger_common.api b/apis/swagger_common.api index 518b655..ffd0d6d 100644 --- a/apis/swagger_common.api +++ b/apis/swagger_common.api @@ -1,14 +1,15 @@ syntax = "v1" -info( - title: "common API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" +info ( + title: "common API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" ) import ( - "./common.api" - "./auth/auth.api" -) \ No newline at end of file + "./common.api" + "./auth/auth.api" +) + diff --git a/apis/swagger_node.api b/apis/swagger_node.api index 6a2ec35..46e77cf 100644 --- a/apis/swagger_node.api +++ b/apis/swagger_node.api @@ -1,11 +1,11 @@ syntax = "v1" -info( - title: "Node API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" +info ( + title: "Node API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" ) -import "./node/node.api" \ No newline at end of file +import "./node/node.api" diff --git a/apis/swagger_user.api b/apis/swagger_user.api index bd27b82..099a53f 100644 --- a/apis/swagger_user.api +++ b/apis/swagger_user.api @@ -1,19 +1,21 @@ syntax = "v1" -info( - title: "User API" - desc: "API for ppanel" - author: "Tension" - email: "tension@ppanel.com" - version: "0.0.1" +info ( + title: "User API" + desc: "API for ppanel" + author: "Tension" + email: "tension@ppanel.com" + version: "0.0.1" ) + import ( - "./public/user.api" - "./public/subscribe.api" - "./public/order.api" - "./public/announcement.api" - "./public/ticket.api" - "./public/payment.api" - "./public/document.api" - "./public/portal.api" -) \ No newline at end of file + "./public/user.api" + "./public/subscribe.api" + "./public/order.api" + "./public/announcement.api" + "./public/ticket.api" + "./public/payment.api" + "./public/document.api" + "./public/portal.api" +) + diff --git a/apis/types.api b/apis/types.api index 5d3d9a5..477e150 100644 --- a/apis/types.api +++ b/apis/types.api @@ -14,6 +14,8 @@ type ( Avatar string `json:"avatar"` Balance int64 `json:"balance"` Commission int64 `json:"commission"` + ReferralPercentage uint8 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` GiftAmount int64 `json:"gift_amount"` Telegram int64 `json:"telegram"` ReferCode string `json:"refer_code"` @@ -63,6 +65,8 @@ type ( SubscribePath string `json:"subscribe_path"` SubscribeDomain string `json:"subscribe_domain"` PanDomain bool `json:"pan_domain"` + UserAgentLimit bool `json:"user_agent_limit"` + UserAgentList string `json:"user_agent_list"` } VerifyCodeConfig { VerifyCodeExpireTime int64 `json:"verify_code_expire_time"` @@ -111,6 +115,7 @@ type ( AuthConfig { Mobile MobileAuthenticateConfig `json:"mobile"` Email EmailAuthticateConfig `json:"email"` + Device DeviceAuthticateConfig `json:"device"` Register PubilcRegisterConfig `json:"register"` } PubilcRegisterConfig { @@ -130,6 +135,14 @@ type ( EnableDomainSuffix bool `json:"enable_domain_suffix"` DomainSuffixList string `json:"domain_suffix_list"` } + + DeviceAuthticateConfig { + Enable bool `json:"enable"` + ShowAds bool `json:"show_ads"` + EnableSecurity bool `json:"enable_security"` + OnlyRealDevice bool `json:"only_real_device"` + } + RegisterConfig { StopRegister bool `json:"stop_register"` EnableTrial bool `json:"enable_trial"` @@ -148,9 +161,27 @@ type ( EnableResetPasswordVerify bool `json:"enable_reset_password_verify"` } NodeConfig { - NodeSecret string `json:"node_secret"` - NodePullInterval int64 `json:"node_pull_interval"` - NodePushInterval int64 `json:"node_push_interval"` + NodeSecret string `json:"node_secret"` + NodePullInterval int64 `json:"node_pull_interval"` + NodePushInterval int64 `json:"node_push_interval"` + TrafficReportThreshold int64 `json:"traffic_report_threshold"` + IPStrategy string `json:"ip_strategy"` + DNS []NodeDNS `json:"dns"` + Block []string `json:"block"` + Outbound []NodeOutbound `json:"outbound"` + } + NodeDNS { + Proto string `json:"proto"` + Address string `json:"address"` + Domains []string `json:"domains"` + } + NodeOutbound { + Name string `json:"name"` + Protocol string `json:"protocol"` + Address string `json:"address"` + Port int64 `json:"port"` + Password string `json:"password"` + Rules []string `json:"rules"` } InviteConfig { ForcedInvite bool `json:"forced_invite"` @@ -181,6 +212,7 @@ type ( 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"` @@ -191,9 +223,8 @@ type ( SpeedLimit int64 `json:"speed_limit"` DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` - GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show bool `json:"show"` Sell bool `json:"sell"` Sort int64 `json:"sort"` @@ -246,6 +277,14 @@ type ( SecurityConfig SecurityConfig `json:"security_config"` } Tuic { + Port int `json:"port" validate:"required"` + DisableSNI bool `json:"disable_sni"` + ReduceRtt bool `json:"reduce_rtt"` + UDPRelayMode string `json:"udp_relay_mode"` + CongestionController string `json:"congestion_controller"` + SecurityConfig SecurityConfig `json:"security_config"` + } + AnyTLS { Port int `json:"port" validate:"required"` SecurityConfig SecurityConfig `json:"security_config"` } @@ -264,37 +303,37 @@ type ( Host string `json:"host"` ServiceName string `json:"service_name"` } - Server { - Id int64 `json:"id"` - Tags []string `json:"tags"` - Country string `json:"country"` - City string `json:"city"` - Name string `json:"name"` - ServerAddr string `json:"server_addr"` - RelayMode string `json:"relay_mode"` - RelayNode []NodeRelay `json:"relay_node"` - SpeedLimit int `json:"speed_limit"` - TrafficRatio float32 `json:"traffic_ratio"` - GroupId int64 `json:"group_id"` - Protocol string `json:"protocol"` - Config interface{} `json:"config"` - Enable *bool `json:"enable"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - Status *NodeStatus `json:"status"` - Sort int64 `json:"sort"` - } - OnlineUser { - SID int64 `json:"uid"` - IP string `json:"ip"` - } - NodeStatus { - Online interface{} `json:"online"` - Cpu float64 `json:"cpu"` - Mem float64 `json:"mem"` - Disk float64 `json:"disk"` - UpdatedAt int64 `json:"updated_at"` - } + // Server { + // Id int64 `json:"id"` + // Tags []string `json:"tags"` + // Country string `json:"country"` + // City string `json:"city"` + // Name string `json:"name"` + // ServerAddr string `json:"server_addr"` + // RelayMode string `json:"relay_mode"` + // RelayNode []NodeRelay `json:"relay_node"` + // SpeedLimit int `json:"speed_limit"` + // TrafficRatio float32 `json:"traffic_ratio"` + // GroupId int64 `json:"group_id"` + // Protocol string `json:"protocol"` + // Config interface{} `json:"config"` + // Enable *bool `json:"enable"` + // CreatedAt int64 `json:"created_at"` + // UpdatedAt int64 `json:"updated_at"` + // Status *NodeStatus `json:"status"` + // Sort int64 `json:"sort"` + // } + // OnlineUser { + // SID int64 `json:"uid"` + // IP string `json:"ip"` + // } + // NodeStatus { + // Online interface{} `json:"online"` + // Cpu float64 `json:"cpu"` + // Mem float64 `json:"mem"` + // Disk float64 `json:"disk"` + // UpdatedAt int64 `json:"updated_at"` + // } ServerGroup { Id int64 `json:"id"` Name string `json:"name"` @@ -426,7 +465,7 @@ type ( Subscribe Subscribe `json:"subscribe"` StartTime int64 `json:"start_time"` ExpireTime int64 `json:"expire_time"` - FinishedAt int64 `json:"finished_at"` + FinishedAt int64 `json:"finished_at"` ResetTime int64 `json:"reset_time"` Traffic int64 `json:"traffic"` Download int64 `json:"download"` @@ -436,15 +475,6 @@ type ( CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } - UserBalanceLog { - Id int64 `json:"id"` - UserId int64 `json:"user_id"` - Amount int64 `json:"amount"` - Type uint8 `json:"type"` - OrderId int64 `json:"order_id"` - Balance int64 `json:"balance"` - CreatedAt int64 `json:"created_at"` - } UserAffiliate { Avatar string `json:"avatar"` Identifier string `json:"identifier"` @@ -465,14 +495,6 @@ type ( Port int `json:"port"` Prefix string `json:"prefix"` } - ApplicationConfig { - AppId int64 `json:"app_id"` - EncryptionKey string `json:"encryption_key"` - EncryptionMethod string `json:"encryption_method"` - Domains []string `json:"domains" validate:"required"` - StartupPicture string `json:"startup_picture"` - StartupPictureSkipTime int64 `json:"startup_picture_skip_time"` - } UserDevice { Id int64 `json:"id"` Ip string `json:"ip"` @@ -505,11 +527,13 @@ type ( } ServerRuleGroup { Id int64 `json:"id"` - Icon string `json:"icon"` + Icon string `json:"icon"` Name string `json:"name" validate:"required"` + Type string `json:"type"` Tags []string `json:"tags"` Rules string `json:"rules"` Enable bool `json:"enable"` + Default bool `json:"default"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } @@ -520,7 +544,7 @@ type ( Token string `json:"token"` IP string `json:"ip"` UserAgent string `json:"user_agent"` - CreatedAt int64 `json:"created_at"` + Timestamp int64 `json:"timestamp"` } UserLoginLog { Id int64 `json:"id"` @@ -528,18 +552,17 @@ type ( LoginIP string `json:"login_ip"` UserAgent string `json:"user_agent"` Success bool `json:"success"` - CreatedAt int64 `json:"created_at"` + Timestamp int64 `json:"timestamp"` } MessageLog { - Id int64 `json:"id"` - Type string `json:"type"` - Platform string `json:"platform"` - To string `json:"to"` - Subject string `json:"subject"` - Content string `json:"content"` - Status int `json:"status"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + Id int64 `json:"id"` + Type uint8 `json:"type"` + Platform string `json:"platform"` + To string `json:"to"` + Subject string `json:"subject"` + Content interface{} `json:"content"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` } Ads { Id int `json:"id"` @@ -557,8 +580,8 @@ type ( //public order PurchaseOrderRequest { SubscribeId int64 `json:"subscribe_id"` - Quantity int64 `json:"quantity"` - Payment int64 `json:"payment,omitempty"` + Quantity int64 `json:"quantity" validate:"required,gt=0"` + Payment int64 `json:"payment,omitempty"` Coupon string `json:"coupon,omitempty"` } PreOrderResponse { @@ -707,46 +730,120 @@ type ( Telephone string `json:"telephone"` Address string `json:"address"` } - QueryUserAffiliateCountResponse { Registers int64 `json:"registers"` TotalCommission int64 `json:"total_commission"` } - - AppUserSubcbribe{ - Id int64 `json:"id"` - Name string `json:"name"` - Upload int64 `json:"upload"` - Traffic int64 `json:"traffic"` - Download int64 `json:"download"` - DeviceLimit int64 `json:"device_limit"` - StartTime string `json:"start_time"` - ExpireTime string `json:"expire_time"` - List []AppUserSubscbribeNode `json:"list"` + AppUserSubcbribe { + Id int64 `json:"id"` + Name string `json:"name"` + Upload int64 `json:"upload"` + Traffic int64 `json:"traffic"` + Download int64 `json:"download"` + DeviceLimit int64 `json:"device_limit"` + StartTime string `json:"start_time"` + ExpireTime string `json:"expire_time"` + List []AppUserSubscbribeNode `json:"list"` } - - AppUserSubscbribeNode{ - Id int64 `json:"id"` - Name string `json:"name"` - Uuid string `json:"uuid"` - Protocol string `json:"protocol"` - RelayMode string `json:"relay_mode"` - RelayNode string `json:"relay_node"` - ServerAddr string `json:"server_addr"` - SpeedLimit int `json:"speed_limit"` - Tags []string `json:"tags"` - Traffic int64 `json:"traffic"` - TrafficRatio float64 `json:"traffic_ratio"` - Upload int64 `json:"upload"` - Config string `json:"config"` - Country string `json:"country"` - City string `json:"city"` - Latitude string `json:"latitude"` - Longitude string `json:"longitude"` - LatitudeCountry string `json:"latitudeCountry"` - LongitudeCountry string `json:"longitudeCountry"` - CreatedAt int64 `json:"created_at"` - Download int64 `json:"download"` + AppUserSubscbribeNode { + Id int64 `json:"id"` + Name string `json:"name"` + Uuid string `json:"uuid"` + Protocol string `json:"protocol"` + RelayMode string `json:"relay_mode"` + RelayNode string `json:"relay_node"` + ServerAddr string `json:"server_addr"` + SpeedLimit int `json:"speed_limit"` + Tags []string `json:"tags"` + Traffic int64 `json:"traffic"` + TrafficRatio float64 `json:"traffic_ratio"` + Upload int64 `json:"upload"` + Config string `json:"config"` + Country string `json:"country"` + City string `json:"city"` + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` + CreatedAt int64 `json:"created_at"` + Download int64 `json:"download"` + } + DownloadLink { + IOS string `json:"ios,omitempty"` + Android string `json:"android,omitempty"` + Windows string `json:"windows,omitempty"` + Mac string `json:"mac,omitempty"` + Linux string `json:"linux,omitempty"` + Harmony string `json:"harmony,omitempty"` + } + ResetSubscribeTrafficLog { + Id int64 `json:"id"` + Type uint16 `json:"type"` + UserSubscribeId int64 `json:"user_subscribe_id"` + OrderNo string `json:"order_no,omitempty"` + Timestamp int64 `json:"timestamp"` + } + BalanceLog { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + OrderNo string `json:"order_no,omitempty"` + Balance int64 `json:"balance"` + Timestamp int64 `json:"timestamp"` + } + CommissionLog { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + OrderNo string `json:"order_no"` + Timestamp int64 `json:"timestamp"` + } + Protocol { + Type string `json:"type"` + Port uint16 `json:"port"` + Enable bool `json:"enable"` + Security string `json:"security,omitempty"` + SNI string `json:"sni,omitempty"` + AllowInsecure bool `json:"allow_insecure,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + RealityServerAddr string `json:"reality_server_addr,omitempty"` + RealityServerPort int `json:"reality_server_port,omitempty"` + RealityPrivateKey string `json:"reality_private_key,omitempty"` + RealityPublicKey string `json:"reality_public_key,omitempty"` + RealityShortId string `json:"reality_short_id,omitempty"` + Transport string `json:"transport,omitempty"` + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + ServiceName string `json:"service_name,omitempty"` + Cipher string `json:"cipher,omitempty"` + ServerKey string `json:"server_key,omitempty"` + Flow string `json:"flow,omitempty"` + HopPorts string `json:"hop_ports,omitempty"` + HopInterval int `json:"hop_interval,omitempty"` + ObfsPassword string `json:"obfs_password,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ReduceRtt bool `json:"reduce_rtt,omitempty"` + UDPRelayMode string `json:"udp_relay_mode,omitempty"` + CongestionController string `json:"congestion_controller,omitempty"` + Multiplex string `json:"multiplex,omitempty"` // mux, eg: off/low/medium/high + PaddingScheme string `json:"padding_scheme,omitempty"` // padding scheme + UpMbps int `json:"up_mbps,omitempty"` // upload speed limit + DownMbps int `json:"down_mbps,omitempty"` // download speed limit + Obfs string `json:"obfs,omitempty"` // obfs, 'none', 'http', 'tls' + ObfsHost string `json:"obfs_host,omitempty"` // obfs host + ObfsPath string `json:"obfs_path,omitempty"` // obfs path + XhttpMode string `json:"xhttp_mode,omitempty"` // xhttp mode + XhttpExtra string `json:"xhttp_extra,omitempty"` // xhttp extra path + Encryption string `json:"encryption,omitempty"` // encryption,'none', 'mlkem768x25519plus' + EncryptionMode string `json:"encryption_mode,omitempty"` // encryption mode,'native', 'xorpub', 'random' + EncryptionRtt string `json:"encryption_rtt,omitempty"` // encryption rtt,'0rtt', '1rtt' + EncryptionTicket string `json:"encryption_ticket,omitempty"` // encryption ticket + EncryptionServerPadding string `json:"encryption_server_padding,omitempty"` // encryption server padding + EncryptionPrivateKey string `json:"encryption_private_key,omitempty"` // encryption private key + EncryptionClientPadding string `json:"encryption_client_padding,omitempty"` // encryption client padding + EncryptionPassword string `json:"encryption_password,omitempty"` // encryption password + Ratio float64 `json:"ratio,omitempty"` // Traffic ratio, default is 1 + CertMode string `json:"cert_mode,omitempty"` // Certificate mode, `none`|`http`|`dns`|`self` + CertDNSProvider string `json:"cert_dns_provider,omitempty"` // DNS provider for certificate + CertDNSEnv string `json:"cert_dns_env,omitempty"` // Environment for DNS provider } ) diff --git a/cmd/run.go b/cmd/run.go index a96d3d8..13a0a1c 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/perfect-panel/server/pkg/constant" + "log" "os" "os/signal" @@ -12,18 +14,17 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" - "github.com/perfect-panel/ppanel-server/initialize" - "github.com/perfect-panel/ppanel-server/internal" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/conf" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/orm" - "github.com/perfect-panel/ppanel-server/pkg/service" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/queue" - "github.com/perfect-panel/ppanel-server/scheduler" + "github.com/perfect-panel/server/initialize" + "github.com/perfect-panel/server/internal" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/conf" + "github.com/perfect-panel/server/pkg/logger" + "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/queue" + "github.com/perfect-panel/server/scheduler" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) @@ -40,7 +41,7 @@ var startCmd = &cobra.Command{ Use: "run", Short: "start PPanel", Run: func(cmd *cobra.Command, args []string) { - fmt.Println("[PPanel version] v" + constant.Version) + fmt.Println("[PPanel version] v" + fmt.Sprintf("%s(%s)", constant.Version, constant.BuildTime)) run() }, } diff --git a/cmd/version.go b/cmd/version.go index 804c976..73c3025 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -3,7 +3,8 @@ package cmd import ( "fmt" - "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/server/pkg/constant" + "github.com/spf13/cobra" ) @@ -11,6 +12,6 @@ var versionCmd = &cobra.Command{ Use: "version", Short: "PPanel version", Run: func(cmd *cobra.Command, args []string) { - fmt.Println("[PPanel version] " + config.Version) + fmt.Println("[PPanel version] " + constant.Version + " (" + constant.BuildTime + ")") }, } diff --git a/doc/config-zh.md b/doc/config-zh.md index 1f2e409..55b7adf 100644 --- a/doc/config-zh.md +++ b/doc/config-zh.md @@ -1,88 +1,161 @@ -### 配置文件说明 +# PPanel 配置指南 -#### 1. 配置文件路径 +本文件为 PPanel 应用程序的配置文件提供全面指南。配置文件采用 YAML 格式,定义了服务器、日志、数据库、Redis 和管理员访问的相关设置。 -配置文件默认路径为:`./etc/ppanel.yaml`,可通过启动参数 `--config` 指定配置文件路径。 +## 1. 配置文件概述 -#### 2. 配置文件格式 - - 配置文件为yaml格式,支持注释,命名为xxx.yaml。 +- **默认路径**:`./etc/ppanel.yaml` +- **自定义路径**:通过启动参数 `--config` 指定配置文件路径。 +- **格式**:YAML 格式,支持注释,文件名需以 `.yaml` 结尾。 + +## 2. 配置文件结构 + +以下是配置文件示例,包含默认值和说明: ```yaml -# 配置文件示例 -Host: # 服务监听地址,默认: 0.0.0.0 -Port: # 服务监听端口,默认: 8080 -Debug: # 是否开启调试模式,开启后无法使用后台日志功能, 默认: false -JwtAuth: # JWT认证配置 - AccessSecret: # 访问令牌密钥, 默认: 随机生成 - AccessExpire: # 访问令牌过期时间,单位秒, 默认: 604800 -Logger: # 日志配置 - FilePath: # 日志文件路径, 默认: ./ppanel.log - MaxSize: # 日志文件最大大小,单位MB, 默认: 50 - MaxBackup: # 日志文件最大备份数, 默认: 3 - MaxAge: # 日志文件最大保存时间,单位天, 默认: 30 - Compress: # 是否压缩日志文件, 默认: true - Level: # 日志级别, 默认: info, 可选: debug, info, warn, error, panic, panic, fatal -MySQL: - Addr: # MySQL地址, 必填 - Username: # MySQL用户名, 必填 - Password: # MySQL密码, 必填 - Dbname: # MySQL数据库名, 必填 - Config: # Mysql配置默认值 charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai - MaxIdleConns: # 最大空闲连接数, 默认: 10 - MaxOpenConns: # 最大打开连接数, 默认: 100 - LogMode: # 日志级别, 默认: info, 可选: debug, error, warn, info - LogZap: # 是否使用zap日志记录sql, 默认: true - SlowThreshold: # 慢查询阈值,单位毫秒, 默认: 1000 -Redis: - Host: # Redis地址, 默认:localhost:6379 - Pass: # Redis密码, 默认: "" - DB: # Redis数据库, 默认: 0 - -Administer: - Email: # 后台登录邮箱, 默认: admin@ppanel.dev - Password: # 后台登录密码, 默认: password - +# PPanel 配置文件 +Host: "0.0.0.0" # 服务监听地址 +Port: 8080 # 服务监听端口 +Debug: false # 是否开启调试模式(禁用后台日志) +JwtAuth: # JWT 认证配置 + AccessSecret: "" # 访问令牌密钥(为空时随机生成) + AccessExpire: 604800 # 访问令牌过期时间(秒) +Logger: # 日志配置 + ServiceName: "" # 日志服务标识名称 + Mode: "console" # 日志输出模式(console、file、volume) + Encoding: "json" # 日志格式(json、plain) + TimeFormat: "2006-01-02T15:04:05.000Z07:00" # 自定义时间格式 + Path: "logs" # 日志文件目录 + Level: "info" # 日志级别(info、error、severe) + Compress: false # 是否压缩日志文件 + KeepDays: 7 # 日志保留天数 + StackCooldownMillis: 100 # 堆栈日志冷却时间(毫秒) + MaxBackups: 3 # 最大日志备份数 + MaxSize: 50 # 最大日志文件大小(MB) + Rotation: "daily" # 日志轮转策略(daily、size) +MySQL: # MySQL 数据库配置 + Addr: "" # MySQL 地址(必填) + Username: "" # MySQL 用户名(必填) + Password: "" # MySQL 密码(必填) + Dbname: "" # MySQL 数据库名(必填) + Config: "charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai" # MySQL 连接参数 + MaxIdleConns: 10 # 最大空闲连接数 + MaxOpenConns: 100 # 最大打开连接数 + LogMode: "info" # 日志级别(debug、error、warn、info) + LogZap: true # 是否使用 Zap 记录 SQL 日志 + SlowThreshold: 1000 # 慢查询阈值(毫秒) +Redis: # Redis 配置 + Host: "localhost:6379" # Redis 地址 + Pass: "" # Redis 密码 + DB: 0 # Redis 数据库索引 +Administer: # 管理员登录配置 + Email: "admin@ppanel.dev" # 管理员登录邮箱 + Password: "password" # 管理员登录密码 ``` -#### 3. 配置文件说明 +## 3. 配置项说明 -- `Host`: 服务监听地址,默认: **0.0.0.0** -- `Port`: 服务监听端口,默认: **8080** -- `Debug`: 是否开启调试模式,开启后无法使用后台日志功能, 默认: **false** -- `JwtAuth`: JWT认证配置 - - `AccessSecret`: 访问令牌密钥, 默认: **随机生成** - - `AccessExpire`: 访问令牌过期时间,单位秒, 默认: **604800** -- `Logger`: 日志配置 -- `FilePath`: 日志文件路径, 默认: **./ppanel.log** -- `MaxSize`: 日志文件最大大小,单位MB, 默认: **50** -- `MaxBackup`: 日志文件最大备份数, 默认: **3** -- `MaxAge`: 日志文件最大保存时间,单位天, 默认: **30** -- `Compress`: 是否压缩日志文件, 默认: **true** -- `Level`: 日志级别, 默认: **info**, 可选: **debug, info, warn, error, panic, panic, fatal** -- `MySQL`: MySQL配置 - - `Addr`: MySQL地址, 必填 - - `Username`: MySQL用户名, 必填 - - `Password`: MySQL密码, 必填 - - `Dbname`: MySQL数据库名, 必填 - - `Config`: Mysql配置默认值 charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai - - `MaxIdleConns`: 最大空闲连接数, 默认: **10** - - `MaxOpenConns`: 最大打开连接数, 默认: **100** - - `LogMode`: 日志级别, 默认: **info**, 可选: **debug, error, warn, info** - - `LogZap`: 是否使用zap日志记录sql, 默认: **true** - - `SlowThreshold`: 慢查询阈值,单位毫秒, 默认: **1000** -- `Redis`: Redis配置 -- `Host`: Redis地址, 默认: **localhost:6379** -- `Pass`: Redis密码, 默认: **""** -- `DB`: Redis数据库, 默认: **0** -- `Administer`: 后台登录配置 - - `Email`: 后台登录邮箱, 默认: **admin@ppanel.dev** - - `Password`: 后台登录密码, 默认: **password** +### 3.1 服务器设置 -#### 4. 环境变量 +- **`Host`**:服务监听的地址。 + - 默认:`0.0.0.0`(监听所有网络接口)。 +- **`Port`**:服务监听的端口。 + - 默认:`8080`。 +- **`Debug`**:是否开启调试模式,开启后禁用后台日志功能。 + - 默认:`false`。 -支持的环境变量如下: +### 3.2 JWT 认证 (`JwtAuth`) -| 环境变量 | 配置项 | 示例 | -|--------------|---------|:-------------------------------------------| -| PPANEL_DB | MySQL配置 | root:password@tcp(localhost:3306)/vpnboard | -| PPANEL_REDIS | Redis配置 | redis://localhost:6379" | +- **`AccessSecret`**:访问令牌的密钥。 + - 默认:为空时随机生成。 +- **`AccessExpire`**:令牌过期时间(秒)。 + - 默认:`604800`(7天)。 + +### 3.3 日志配置 (`Logger`) + +- **`ServiceName`**:日志的服务标识名称,在 `volume` 模式下用作日志文件名。 + - 默认:`""`。 +- **`Mode`**:日志输出方式。 + - 选项:`console`(标准输出/错误输出)、`file`(写入指定目录)、`volume`(Docker 卷)。 + - 默认:`console`。 +- **`Encoding`**:日志格式。 + - 选项:`json`(结构化 JSON)、`plain`(纯文本,带颜色)。 + - 默认:`json`。 +- **`TimeFormat`**:日志时间格式。 + - 默认:`2006-01-02T15:04:05.000Z07:00`。 +- **`Path`**:日志文件存储目录。 + - 默认:`logs`。 +- **`Level`**:日志过滤级别。 + - 选项:`info`(记录所有日志)、`error`(仅错误和严重日志)、`severe`(仅严重日志)。 + - 默认:`info`。 +- **`Compress`**:是否压缩日志文件(仅在 `file` 模式下生效)。 + - 默认:`false`。 +- **`KeepDays`**:日志文件保留天数。 + - 默认:`7`。 +- **`StackCooldownMillis`**:堆栈日志冷却时间(毫秒),防止日志过多。 + - 默认:`100`。 +- **`MaxBackups`**:最大日志备份数量(仅在 `size` 轮转时生效)。 + - 默认:`3`。 +- **`MaxSize`**:日志文件最大大小(MB,仅在 `size` 轮转时生效)。 + - 默认:`50`。 +- **`Rotation`**:日志轮转策略。 + - 选项:`daily`(按天轮转)、`size`(按大小轮转)。 + - 默认:`daily`。 + +### 3.4 MySQL 数据库 (`MySQL`) + +- **`Addr`**:MySQL 服务器地址。 + - 必填。 +- **`Username`**:MySQL 用户名。 + - 必填。 +- **`Password`**:MySQL 密码。 + - 必填。 +- **`Dbname`**:MySQL 数据库名。 + - 必填。 +- **`Config`**:MySQL 连接参数。 + - 默认:`charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai`。 +- **`MaxIdleConns`**:最大空闲连接数。 + - 默认:`10`。 +- **`MaxOpenConns`**:最大打开连接数。 + - 默认:`100`。 +- **`LogMode`**:SQL 日志级别。 + - 选项:`debug`、`error`、`warn`、`info`。 + - 默认:`info`。 +- **`LogZap`**:是否使用 Zap 记录 SQL 日志。 + - 默认:`true`。 +- **`SlowThreshold`**:慢查询阈值(毫秒)。 + - 默认:`1000`。 + +### 3.5 Redis 配置 (`Redis`) + +- **`Host`**:Redis 服务器地址。 + - 默认:`localhost:6379`。 +- **`Pass`**:Redis 密码。 + - 默认:`""`(无密码)。 +- **`DB`**:Redis 数据库索引。 + - 默认:`0`。 + +### 3.6 管理员登录 (`Administer`) + +- **`Email`**:管理员登录邮箱。 + - 默认:`admin@ppanel.dev`。 +- **`Password`**:管理员登录密码。 + - 默认:`password`。 + +## 4. 环境变量 + +以下环境变量可用于覆盖配置文件中的设置: + +| 环境变量 | 配置项 | 示例值 | +|----------------|----------|----------------------------------------------| +| `PPANEL_DB` | MySQL 配置 | `root:password@tcp(localhost:3306)/vpnboard` | +| `PPANEL_REDIS` | Redis 配置 | `redis://localhost:6379` | + +## 5. 最佳实践 + +- **安全性**:生产环境中避免使用默认的 `Administer` 凭据,更新 `Email` 和 `Password` 为安全值。 +- **日志**:生产环境中建议使用 `file` 或 `volume` 模式持久化日志,将 `Level` 设置为 `error` 或 `severe` 以减少日志量。 +- **数据库**:确保 `MySQL` 和 `Redis` 凭据安全,避免在版本控制中暴露。 +- **JWT**:为 `JwtAuth` 的 `AccessSecret` 设置强密钥以增强安全性。 + +如需进一步帮助,请参考 PPanel 官方文档或联系支持团队。 \ No newline at end of file diff --git a/doc/config.md b/doc/config.md index ebb52c1..99b6b3c 100644 --- a/doc/config.md +++ b/doc/config.md @@ -1,116 +1,164 @@ -### Configuration File Instructions -#### Configuration File Path +# PPanel Configuration Guide -The default configuration file path is ./etc/ppanel.yaml. You can specify a custom path using the --config startup parameter. +This document provides a comprehensive guide to the configuration file for the PPanel application. The configuration +file is in YAML format and defines settings for the server, logging, database, Redis, and admin access. -#### Configuration File Format +## 1. Configuration File Overview -The configuration file uses the YAML format, supports comments, and should be named xxx.yaml. +- **Default Path**: `./etc/ppanel.yaml` +- **Custom Path**: Specify a custom path using the `--config` startup parameter. +- **Format**: YAML, supports comments, and must be named with a `.yaml` extension. + +## 2. Configuration File Structure + +Below is an example of the configuration file with default values and explanations: ```yaml -# Sample Configuration File -Host: # Service listening address, default: 0.0.0.0 -Port: # Service listening port, default: 8080 -Debug: # Enable debug mode; disables backend logging when enabled, default: false -JwtAuth: # JWT authentication settings - AccessSecret: # Access token secret, default: randomly generated - AccessExpire: # Access token expiration time in seconds, default: 604800 -Logger: # Logging configuration - FilePath: # Log file path, default: ./ppanel.log - MaxSize: # Maximum log file size in MB, default: 50 - MaxBackup: # Maximum number of log file backups, default: 3 - MaxAge: # Maximum log file retention time in days, default: 30 - Compress: # Whether to compress log files, default: true - Level: # Logging level, default: info; options: debug, info, warn, error, panic, fatal -MySQL: - Addr: # MySQL address, required - Username: # MySQL username, required - Password: # MySQL password, required - Dbname: # MySQL database name, required - Config: # MySQL configuration, default: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai - MaxIdleConns: # Maximum idle connections, default: 10 - MaxOpenConns: # Maximum open connections, default: 100 - LogMode: # Log level, default: info; options: debug, error, warn, info - LogZap: # Whether to use zap for SQL logging, default: true - SlowThreshold: # Slow query threshold in milliseconds, default: 1000 -Redis: - Host: # Redis address, default: localhost:6379 - Pass: # Redis password, default: "" - DB: # Redis database, default: 0 - -Administer: - Email: # Admin login email, default: admin@ppanel.dev - Password: # Admin login password, default: password +# PPanel Configuration +Host: "0.0.0.0" # Server listening address +Port: 8080 # Server listening port +Debug: false # Enable debug mode (disables background logging) +JwtAuth: # JWT authentication settings + AccessSecret: "" # Access token secret (randomly generated if empty) + AccessExpire: 604800 # Access token expiration (seconds) +Logger: # Logging configuration + ServiceName: "" # Service name for log identification + Mode: "console" # Log output mode (console, file, volume) + Encoding: "json" # Log format (json, plain) + TimeFormat: "2006-01-02T15:04:05.000Z07:00" # Custom time format + Path: "logs" # Log file directory + Level: "info" # Log level (info, error, severe) + Compress: false # Enable log compression + KeepDays: 7 # Log retention period (days) + StackCooldownMillis: 100 # Stack trace cooldown (milliseconds) + MaxBackups: 3 # Maximum number of log backups + MaxSize: 50 # Maximum log file size (MB) + Rotation: "daily" # Log rotation strategy (daily, size) +MySQL: # MySQL database configuration + Addr: "" # MySQL address (required) + Username: "" # MySQL username (required) + Password: "" # MySQL password (required) + Dbname: "" # MySQL database name (required) + Config: "charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai" # MySQL connection parameters + MaxIdleConns: 10 # Maximum idle connections + MaxOpenConns: 100 # Maximum open connections + LogMode: "info" # Log level (debug, error, warn, info) + LogZap: true # Enable Zap logging for SQL + SlowThreshold: 1000 # Slow query threshold (milliseconds) +Redis: # Redis configuration + Host: "localhost:6379" # Redis address + Pass: "" # Redis password + DB: 0 # Redis database index +Administer: # Admin login configuration + Email: "admin@ppanel.dev" # Admin login email + Password: "password" # Admin login password ``` -#### 3.Configuration Descriptions +## 3. Configuration Details -- Host: Service listening address, default: 0.0.0.0 +### 3.1 Server Settings -- Port: Service listening port, default: 8080 -- Debug: Enable debug mode; disables backend logging when enabled, default: false +- **`Host`**: Address the server listens on. + - Default: `0.0.0.0` (all network interfaces). +- **`Port`**: Port the server listens on. + - Default: `8080`. +- **`Debug`**: Enables debug mode, disabling background logging. + - Default: `false`. -- JwtAuth: JWT authentication settings +### 3.2 JWT Authentication (`JwtAuth`) - - AccessSecret: Access token secret, default: randomly generated - - - AccessExpire: Access token expiration time in seconds, default: 604800 - - - Logger: Logging configuration - - - FilePath: Log file path, default: ./ppanel.log - - - MaxSize: Maximum log file size in MB, default: 50 - - - MaxBackup: Maximum number of log file backups, default: 3 - - - MaxAge: Maximum log file retention time in days, default: 30 - - - Compress: Whether to compress log files, default: true - - - Level: Logging level, default: info; options: debug, info, warn, error, panic, fatal - -- MySQL: MySQL configuration - - - Addr: MySQL address, required - - - Username: MySQL username, required +- **`AccessSecret`**: Secret key for access tokens. + - Default: Randomly generated if not specified. +- **`AccessExpire`**: Token expiration time in seconds. + - Default: `604800` (7 days). - - Password: MySQL password, required +### 3.3 Logging (`Logger`) - - Dbname: MySQL database name, required - - - Config: MySQL configuration, default: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai - - - MaxIdleConns: Maximum idle connections, default: 10 - - - MaxOpenConns: Maximum open connections, default: 100 - - - LogMode: Log level, default: info; options: debug, error, warn, info - - - LogZap: Whether to use zap for SQL logging, default: true - - - SlowThreshold: Slow query threshold in milliseconds, default: 1000 - -- Redis: Redis configuration +- **`ServiceName`**: Identifier for logs, used as the log filename in `volume` mode. + - Default: `""`. +- **`Mode`**: Log output destination. + - Options: `console` (stdout/stderr), `file` (to a directory), `volume` (Docker volume). + - Default: `console`. +- **`Encoding`**: Log format. + - Options: `json` (structured JSON), `plain` (plain text with colors). + - Default: `json`. +- **`TimeFormat`**: Custom time format for logs. + - Default: `2006-01-02T15:04:05.000Z07:00`. +- **`Path`**: Directory for log files. + - Default: `logs`. +- **`Level`**: Log filtering level. + - Options: `info` (all logs), `error` (error and severe), `severe` (severe only). + - Default: `info`. +- **`Compress`**: Enable compression for log files (only in `file` mode). + - Default: `false`. +- **`KeepDays`**: Retention period for log files (in days). + - Default: `7`. +- **`StackCooldownMillis`**: Cooldown for stack trace logging to prevent log flooding. + - Default: `100`. +- **`MaxBackups`**: Maximum number of log backups (for `size` rotation). + - Default: `3`. +- **`MaxSize`**: Maximum log file size in MB (for `size` rotation). + - Default: `50`. +- **`Rotation`**: Log rotation strategy. + - Options: `daily` (rotate daily), `size` (rotate by size). + - Default: `daily`. - - Host: Redis address, default: localhost:6379 - - - Pass: Redis password, default: "" - - - DB: Redis database, default: 0 - -- Administer: Admin login configuration +### 3.4 MySQL Database (`MySQL`) - - Email: Admin login email, default: admin@ppanel.dev +- **`Addr`**: MySQL server address. + - Required. +- **`Username`**: MySQL username. + - Required. +- **`Password`**: MySQL password. + - Required. +- **`Dbname`**: MySQL database name. + - Required. +- **`Config`**: MySQL connection parameters. + - Default: `charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai`. +- **`MaxIdleConns`**: Maximum idle connections. + - Default: `10`. +- **`MaxOpenConns`**: Maximum open connections. + - Default: `100`. +- **`LogMode`**: SQL logging level. + - Options: `debug`, `error`, `warn`, `info`. + - Default: `info`. +- **`LogZap`**: Enable Zap logging for SQL queries. + - Default: `true`. +- **`SlowThreshold`**: Threshold for slow query logging (in milliseconds). + - Default: `1000`. - - Password: Admin login password, default: password +### 3.5 Redis (`Redis`) -#### 4. Environment Variables +- **`Host`**: Redis server address. + - Default: `localhost:6379`. +- **`Pass`**: Redis password. + - Default: `""` (no password). +- **`DB`**: Redis database index. + - Default: `0`. -Supported environment variables are as follows: +### 3.6 Admin Login (`Administer`) -| Environment Variable | Configuration | Example | -|-----------------------|---------------|:-------------------------------------------| -| PPANEL_DB | MySQL config | root:password@tcp(localhost:3306)/vpnboard | -| PPANEL_REDIS | Redis config | redis://localhost:6379" | +- **`Email`**: Admin login email. + - Default: `admin@ppanel.dev`. +- **`Password`**: Admin login password. + - Default: `password`. + +## 4. Environment Variables + +The following environment variables can be used to override configuration settings: + +| Environment Variable | Configuration Section | Example Value | +|----------------------|-----------------------|----------------------------------------------| +| `PPANEL_DB` | MySQL | `root:password@tcp(localhost:3306)/vpnboard` | +| `PPANEL_REDIS` | Redis | `redis://localhost:6379` | + +## 5. Best Practices + +- **Security**: Avoid using default `Administer` credentials in production. Update `Email` and `Password` to secure + values. +- **Logging**: Use `file` or `volume` mode for production to persist logs. Adjust `Level` to `error` or `severe` to + reduce log volume. +- **Database**: Ensure `MySQL` and `Redis` credentials are secure and not exposed in version control. +- **JWT**: Specify a strong `AccessSecret` for `JwtAuth` to enhance security. + +For further assistance, refer to the official PPanel documentation or contact support. \ No newline at end of file diff --git a/doc/image/architecture-en.png b/doc/image/architecture-en.png new file mode 100644 index 0000000000000000000000000000000000000000..978774c8020bcaf0d2510939b51b47e75e22dc50 GIT binary patch literal 566266 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ⅇ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 UO3H9AwTtS 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�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�eQt735}r; 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

vhSMcv{3T!&>&`KlG+ZcvJqit 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

nA!Roen$;xR3Fvo^hjCo2>d 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 LUdR75=F5KpD*;<> literal 0 HcmV?d00001 diff --git a/doc/install-zh.md b/doc/install-zh.md index 7410580..10d8292 100644 --- a/doc/install-zh.md +++ b/doc/install-zh.md @@ -6,14 +6,14 @@ #### 二进制安装 1.确定系统架构,并下载对应的二进制文件 -下载地址:`https://github.com/perfect-panel/ppanel/releases` +下载地址:`https://github.com/perfect-panel/server/releases` 示例说明:系统:Linux amd64,用户:root,当前目录:/root - 下载二进制文件 ```shell -$ wget https://github.com/perfect-panel/ppanel/releases/download/v0.1.0/ppanel-server-linux-amd64.tar.gz +$ wget https://github.com/perfect-panel/server/releases/download/v1.0.0/ppanel-server-linux-amd64.tar.gz ``` - 解压二进制文件 diff --git a/doc/install.md b/doc/install.md index e5ab48c..bcecce9 100644 --- a/doc/install.md +++ b/doc/install.md @@ -8,14 +8,14 @@ 1. Determine your system architecture and download the corresponding binary file. -Download URL: `https://github.com/perfect-panel/ppanel/releases` +Download URL: `https://github.com/perfect-panel/server/releases` Example setup: OS: Linux amd64, User: root, Current directory: `/root` - Download the binary file: ```shell -$ wget https://github.com/perfect-panel/ppanel/releases/download/v0.1.0/ppanel-server-linux-amd64.tar.gz +$ wget https://github.com/perfect-panel/server/releases/download/v1.0.0/ppanel-server-linux-amd64.tar.gz ``` - Extract the binary file: diff --git a/generate/gopure-amd64.exe b/generate/gopure-amd64.exe old mode 100644 new mode 100755 diff --git a/generate/gopure-arm64.exe b/generate/gopure-arm64.exe old mode 100644 new mode 100755 diff --git a/generate/gopure-darwin-amd64 b/generate/gopure-darwin-amd64 old mode 100644 new mode 100755 diff --git a/generate/gopure-darwin-arm64 b/generate/gopure-darwin-arm64 old mode 100644 new mode 100755 diff --git a/generate/gopure-linux-amd64 b/generate/gopure-linux-amd64 old mode 100644 new mode 100755 diff --git a/generate/gopure-linux-arm64 b/generate/gopure-linux-arm64 old mode 100644 new mode 100755 diff --git a/go.mod b/go.mod index 5f32d38..04daadd 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/perfect-panel/ppanel-server +module github.com/perfect-panel/server go 1.23.3 @@ -20,7 +20,7 @@ require ( 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.1 + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/hibiken/asynq v0.24.1 @@ -28,21 +28,21 @@ require ( 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.6.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.10.0 github.com/stripe/stripe-go/v81 v81.1.0 github.com/twilio/twilio-go v1.23.11 - go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel v1.29.0 go.opentelemetry.io/otel/exporters/jaeger v1.17.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 go.opentelemetry.io/otel/exporters/zipkin v1.24.0 - go.opentelemetry.io/otel/sdk v1.24.0 - go.opentelemetry.io/otel/trace v1.24.0 + go.opentelemetry.io/otel/sdk v1.29.0 + go.opentelemetry.io/otel/trace v1.29.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.32.0 golang.org/x/oauth2 v0.25.0 @@ -56,16 +56,21 @@ require ( ) require ( + github.com/Masterminds/sprig/v3 v3.3.0 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/spaolacci/murmur3 v1.1.0 - google.golang.org/grpc v1.61.1 + google.golang.org/grpc v1.64.1 google.golang.org/protobuf v1.36.3 ) require ( cloud.google.com/go/compute/metadata v0.6.0 // indirect + dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect @@ -88,14 +93,17 @@ 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.1.2 // 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 github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.2.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -104,12 +112,15 @@ 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/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.2 // 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 + github.com/shopspring/decimal v1.4.0 // indirect github.com/smartwalle/ncrypto v1.0.4 // indirect github.com/smartwalle/ngx v1.0.9 // indirect github.com/smartwalle/nsign v1.0.9 // indirect @@ -119,17 +130,18 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/proto/otlp v1.1.0 // indirect + 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.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.34.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // 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 ) diff --git a/go.sum b/go.sum index 841511b..9e9f2c5 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,23 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f h1:RDkg3pyE1qGbBpRWmvSN9RNZC5nUrOaEPiEpEb8y2f0= github.com/GUAIK-ORG/go-snowflake v0.0.0-20200116064823-220c4260e85f/go.mod h1:zA7AF9RTfpluCfz0omI4t5KCMaWHUMicsZoMccnaT44= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8= github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g= @@ -87,11 +99,23 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8= +github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/forgoer/openssl v1.6.0 h1:IueL+UfH0hKo99xFPojHLlO3QzRBQqFY+Cht0WwtOC0= github.com/forgoer/openssl v1.6.0/go.mod h1:9DZ4yOsQmveP0aXC/BpQ++Y5TKaz5yR9+emcxmIZNZs= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -128,12 +152,16 @@ github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +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.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= -github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +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= @@ -179,10 +207,17 @@ github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTj github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw= github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= @@ -212,6 +247,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q= github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -220,8 +257,16 @@ 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.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/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= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -229,9 +274,15 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nyaruka/phonenumbers v1.5.0 h1:0M+Gd9zl53QC4Nl5z1Yj1O/zPk2XXBUwR/vlzdXSJv4= github.com/nyaruka/phonenumbers v1.5.0/go.mod h1:gv+CtldaFz+G3vHHnasBSirAi3O2XLqZzVWz4V1pl2E= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +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.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= @@ -243,13 +294,15 @@ 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.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= -github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +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= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/smartwalle/alipay/v3 v3.2.23 h1:i1VwJeu70EmwpsXXz6GZZnMAtRx5MTfn2dPoql/L3zE= github.com/smartwalle/alipay/v3 v3.2.23/go.mod h1:lVqFiupPf8YsAXaq5JXcwqnOUC2MCF+2/5vub+RlagE= github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8= @@ -306,12 +359,14 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= @@ -320,14 +375,16 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDO go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY= go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= -go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= +go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +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.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= @@ -446,18 +503,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 v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= -google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY= -google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM= -google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= +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.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY= -google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= +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= diff --git a/initialize/config.go b/initialize/config.go index 1df7ec1..025220f 100644 --- a/initialize/config.go +++ b/initialize/config.go @@ -9,16 +9,16 @@ import ( "net/http" "os" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "gorm.io/driver/mysql" "github.com/gin-gonic/gin" "github.com/google/uuid" - "github.com/perfect-panel/ppanel-server/initialize/migrate" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/pkg/conf" - "github.com/perfect-panel/ppanel-server/pkg/orm" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/initialize/migrate" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/pkg/conf" + "github.com/perfect-panel/server/pkg/orm" + "github.com/perfect-panel/server/pkg/tool" "github.com/pkg/errors" "gopkg.in/yaml.v3" "gorm.io/gorm" @@ -141,12 +141,25 @@ func handleInitConfig(c *gin.Context) { c.Abort() return } - - // init - if err := initMysql(db, request.AdminEmail, request.AdminPassword); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", request.MysqlUser, request.MysqlPassword, request.MysqlHost, request.MysqlPort, request.MysqlDatabase) + // migrate database + if err = migrate.Migrate(dsn).Up(); err != nil { + logger.Errorf("[Init Mysql] Migrate failed: %v", err.Error()) + c.JSON(http.StatusOK, gin.H{ "code": 500, - "msg": "MySQL initialization failed", + "msg": "Database migration failed", + "data": nil, + }) + c.Abort() + return + } + + // create admin user + if err = migrate.CreateAdminUser(request.AdminEmail, request.AdminPassword, db); err != nil { + logger.Errorf("[Init Mysql] Create admin user failed: %v", err.Error()) + c.JSON(http.StatusOK, gin.H{ + "code": 500, + "msg": "Admin user creation failed", "data": nil, }) c.Abort() @@ -154,7 +167,7 @@ func handleInitConfig(c *gin.Context) { } // write to file - if err := os.WriteFile(configPath, fileData, 0644); err != nil { + if err = os.WriteFile(configPath, fileData, 0644); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "code": 500, "msg": "Configuration initialization failed", @@ -258,20 +271,3 @@ func HandleRedisTest(c *gin.Context) { "status": true, }) } - -func initMysql(tx *gorm.DB, email, password string) error { - tables, err := tx.Migrator().GetTables() - if err != nil { - return fmt.Errorf("database table validation failed: %w", err) - } - if len(tables) > 0 { - return errors.New("the database contains existing data. Please clear it before proceeding with the installation") - } - if err := migrate.InitPPanelSQL(tx); err != nil { - return fmt.Errorf("failed to initialize database: %w", err) - } - if err := migrate.CreateAdminUser(email, password, tx); err != nil { - return fmt.Errorf("failed to create admin user: %w", err) - } - return nil -} diff --git a/initialize/device.go b/initialize/device.go new file mode 100644 index 0000000..1b8c527 --- /dev/null +++ b/initialize/device.go @@ -0,0 +1,26 @@ +package initialize + +import ( + "context" + + "github.com/perfect-panel/server/pkg/logger" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/tool" +) + +func Device(ctx *svc.ServiceContext) { + logger.Debug("device config initialization") + method, err := ctx.AuthModel.FindOneByMethod(context.Background(), "device") + if err != nil { + panic(err) + } + var cfg config.DeviceConfig + var deviceConfig auth.DeviceConfig + deviceConfig.Unmarshal(method.Config) + tool.DeepCopy(&cfg, deviceConfig) + cfg.Enable = *method.Enabled + ctx.Config.Device = cfg +} diff --git a/initialize/email.go b/initialize/email.go index b740cca..f57d30a 100644 --- a/initialize/email.go +++ b/initialize/email.go @@ -5,12 +5,11 @@ import ( "encoding/json" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/logger" - - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" ) // Email get email smtp config @@ -18,13 +17,11 @@ func Email(ctx *svc.ServiceContext) { logger.Debug("Email config initialization") method, err := ctx.AuthModel.FindOneByMethod(context.Background(), "email") if err != nil { - panic(fmt.Sprintf("failed to find email auth method: %v", err.Error())) + panic(fmt.Sprintf("[Error] Initialization Failed to find email auth method: %v", err.Error())) } var cfg config.EmailConfig var emailConfig = new(auth.EmailAuthConfig) - if err := emailConfig.Unmarshal(method.Config); err != nil { - panic(fmt.Sprintf("failed to unmarshal email auth config: %v", err.Error())) - } + emailConfig.Unmarshal(method.Config) tool.DeepCopy(&cfg, emailConfig) cfg.Enable = *method.Enabled value, _ := json.Marshal(emailConfig.PlatformConfig) diff --git a/initialize/init.go b/initialize/init.go index 00453d5..8023ce5 100644 --- a/initialize/init.go +++ b/initialize/init.go @@ -1,20 +1,20 @@ package initialize -import "github.com/perfect-panel/ppanel-server/internal/svc" +import ( + "github.com/perfect-panel/server/internal/svc" +) func StartInitSystemConfig(svc *svc.ServiceContext) { - // Initialize the system configuration - Mysql(svc) - VerifyVersion(svc) + Migrate(svc) Site(svc) Node(svc) Email(svc) + Device(svc) Invite(svc) Verify(svc) Subscribe(svc) Register(svc) Mobile(svc) - TrafficDataToRedis(svc) if !svc.Config.Debug { Telegram(svc) } diff --git a/initialize/invite.go b/initialize/invite.go index c696097..9375417 100644 --- a/initialize/invite.go +++ b/initialize/invite.go @@ -3,10 +3,10 @@ package initialize import ( "context" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "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 Invite(ctx *svc.ServiceContext) { diff --git a/initialize/migrate/database/00001_init_schema.down.sql b/initialize/migrate/database/00001_init_schema.down.sql new file mode 100644 index 0000000..4b8470b --- /dev/null +++ b/initialize/migrate/database/00001_init_schema.down.sql @@ -0,0 +1,36 @@ +-- 000001_init_schema.down.sql +SET FOREIGN_KEY_CHECKS = 0; + +DROP TABLE IF EXISTS `user_subscribe_log`; +DROP TABLE IF EXISTS `user_subscribe`; +DROP TABLE IF EXISTS `user_login_log`; +DROP TABLE IF EXISTS `user_gift_amount_log`; +DROP TABLE IF EXISTS `user_device`; +DROP TABLE IF EXISTS `user_commission_log`; +DROP TABLE IF EXISTS `user_balance_log`; +DROP TABLE IF EXISTS `user_auth_methods`; +DROP TABLE IF EXISTS `user`; +DROP TABLE IF EXISTS `traffic_log`; +DROP TABLE IF EXISTS `ticket_follow`; +DROP TABLE IF EXISTS `ticket`; +DROP TABLE IF EXISTS `system`; +DROP TABLE IF EXISTS `subscribe_type`; +DROP TABLE IF EXISTS `subscribe_group`; +DROP TABLE IF EXISTS `subscribe`; +DROP TABLE IF EXISTS `sms`; +DROP TABLE IF EXISTS `server_rule_group`; +DROP TABLE IF EXISTS `server_group`; +DROP TABLE IF EXISTS `server`; +DROP TABLE IF EXISTS `payment`; +DROP TABLE IF EXISTS `order`; +DROP TABLE IF EXISTS `message_log`; +DROP TABLE IF EXISTS `document`; +DROP TABLE IF EXISTS `coupon`; +DROP TABLE IF EXISTS `auth_method`; +DROP TABLE IF EXISTS `application_version`; +DROP TABLE IF EXISTS `application_config`; +DROP TABLE IF EXISTS `application`; +DROP TABLE IF EXISTS `announcement`; +DROP TABLE IF EXISTS `ads`; + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/initialize/migrate/database/00001_init_schema.up.sql b/initialize/migrate/database/00001_init_schema.up.sql new file mode 100644 index 0000000..9be1ddb --- /dev/null +++ b/initialize/migrate/database/00001_init_schema.up.sql @@ -0,0 +1,555 @@ +-- 000001_init_schema.up.sql +SET FOREIGN_KEY_CHECKS = 0; + +CREATE TABLE IF NOT EXISTS `ads` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Ads title', + `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Ads type', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Ads content', + `target_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Ads target url', + `start_time` datetime DEFAULT NULL COMMENT 'Ads start time', + `end_time` datetime DEFAULT NULL COMMENT 'Ads end time', + `status` tinyint(1) DEFAULT '0' COMMENT 'Ads status,0 disable,1 enable', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `announcement` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Title', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Content', + `show` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Show', + `pinned` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Pinned', + `popup` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Popup', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `application` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用名称', + `icon` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '应用图标', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '更新描述', + `subscribe_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅类型', + `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `application_config` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `app_id` bigint NOT NULL DEFAULT '0' COMMENT 'App id', + `encryption_key` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Encryption Key', + `encryption_method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Encryption Method', + `domains` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + `startup_picture` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + `startup_picture_skip_time` bigint NOT NULL DEFAULT '0' COMMENT 'Startup Picture Skip Time', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `application_version` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用地址', + `version` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用版本', + `platform` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用平台', + `is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT '默认版本', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '更新描述', + `application_id` bigint DEFAULT NULL COMMENT '所属应用', + `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `fk_application_application_versions` (`application_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `auth_method` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'method', + `config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'OAuth Configuration', + `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Enabled', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_auth_method` (`method`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `coupon` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Coupon Name', + `code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Coupon Code', + `count` bigint NOT NULL DEFAULT '0' COMMENT 'Count Limit', + `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Coupon Type: 1: Percentage 2: Fixed Amount', + `discount` bigint NOT NULL DEFAULT '0' COMMENT 'Coupon Discount', + `start_time` bigint NOT NULL DEFAULT '0' COMMENT 'Start Time', + `expire_time` bigint NOT NULL DEFAULT '0' COMMENT 'Expire Time', + `user_limit` bigint NOT NULL DEFAULT '0' COMMENT 'User Limit', + `subscribe` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subscribe Limit', + `used_count` bigint NOT NULL DEFAULT '0' COMMENT 'Used Count', + `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enable', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_coupon_code` (`code`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `document` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Document Title', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Document Content', + `tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Document Tags', + `show` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Show', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `message_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'email' COMMENT 'Message Type', + `platform` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'smtp' COMMENT 'Platform', + `to` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'To', + `subject` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subject', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Content', + `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Status', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `order` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `parent_id` bigint DEFAULT NULL COMMENT 'Parent Order Id', + `user_id` bigint NOT NULL DEFAULT '0' COMMENT 'User Id', + `order_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Order No', + `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Order Type: 1: Subscribe, 2: Renewal, 3: ResetTraffic, 4: Recharge', + `quantity` bigint NOT NULL DEFAULT '1' COMMENT 'Quantity', + `price` bigint NOT NULL DEFAULT '0' COMMENT 'Original price', + `amount` bigint NOT NULL DEFAULT '0' COMMENT 'Order Amount', + `gift_amount` bigint NOT NULL DEFAULT '0' COMMENT 'User Gift Amount', + `discount` bigint NOT NULL DEFAULT '0' COMMENT 'Discount Amount', + `coupon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Coupon', + `coupon_discount` bigint NOT NULL DEFAULT '0' COMMENT 'Coupon Discount Amount', + `commission` bigint NOT NULL DEFAULT '0' COMMENT 'Order Commission', + `payment_id` bigint NOT NULL DEFAULT '-1' COMMENT 'Payment Id', + `method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Payment Method', + `fee_amount` bigint NOT NULL DEFAULT '0' COMMENT 'Fee Amount', + `trade_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Trade No', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Order Status: 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished', + `subscribe_id` bigint NOT NULL DEFAULT '0' COMMENT 'Subscribe Id', + `subscribe_token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Renewal Subscribe Token', + `is_new` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is New Order', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_order_order_no` (`order_no`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `payment` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Payment Name', + `platform` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Payment Platform', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Payment Description', + `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Payment Icon', + `domain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Notification Domain', + `config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Payment Configuration', + `fee_mode` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Fee Mode: 0: No Fee 1: Percentage 2: Fixed Amount 3: Percentage + Fixed Amount', + `fee_percent` bigint DEFAULT '0' COMMENT 'Fee Percentage', + `fee_amount` bigint DEFAULT '0' COMMENT 'Fixed Fee Amount', + `enable` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Enabled', + `token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Payment Token', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_payment_token` (`token`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `server` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Node Name', + `tags` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Tags', + `country` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Country', + `city` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'City', + `latitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'latitude', + `longitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'longitude', + `server_addr` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Address', + `relay_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'none' COMMENT 'Relay Mode', + `relay_node` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Relay Node', + `speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit', + `traffic_ratio` decimal(4, 2) NOT NULL DEFAULT '0.00' COMMENT 'Traffic Ratio', + `group_id` bigint DEFAULT NULL COMMENT 'Group ID', + `protocol` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Protocol', + `config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Config', + `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enabled', + `sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort', + `last_reported_at` datetime(3) DEFAULT NULL COMMENT 'Last Reported Time', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + KEY `idx_group_id` (`group_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `server_group` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Group Name', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Group Description', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +-- if `sms` not exist, create it +CREATE TABLE IF NOT EXISTS `sms` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + `platform` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `area_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `telephone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `status` tinyint(1) DEFAULT '1', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `subscribe` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subscribe Name', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Subscribe Description', + `unit_price` bigint NOT NULL DEFAULT '0' COMMENT 'Unit Price', + `unit_time` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Unit Time', + `discount` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Discount', + `replacement` bigint NOT NULL DEFAULT '0' COMMENT 'Replacement', + `inventory` bigint NOT NULL DEFAULT '0' COMMENT 'Inventory', + `traffic` bigint NOT NULL DEFAULT '0' COMMENT 'Traffic', + `speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit', + `device_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Device Limit', + `quota` bigint NOT NULL DEFAULT '0' COMMENT 'Quota', + `group_id` bigint DEFAULT NULL COMMENT 'Group Id', + `server_group` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Server Group', + `server` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Server', + `show` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Show portal page', + `sell` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Sell', + `sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort', + `deduction_ratio` bigint DEFAULT '0' COMMENT 'Deduction Ratio', + `allow_deduction` tinyint(1) DEFAULT '1' COMMENT 'Allow deduction', + `reset_cycle` bigint DEFAULT '0' COMMENT 'Reset Cycle: 0: No Reset, 1: 1st, 2: Monthly, 3: Yearly', + `renewal_reset` tinyint(1) DEFAULT '0' COMMENT 'Renew Reset', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `subscribe_group` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Group Name', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Group Description', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `subscribe_type` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅类型', + `mark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅标识', + `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `system` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Category', + `key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Key Name', + `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Key Value', + `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Type', + `desc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Description', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_system_key` (`key`), + KEY `index_key` (`key`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `ticket` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Title', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Description', + `user_id` bigint NOT NULL DEFAULT '0' COMMENT 'UserId', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Status', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `ticket_follow` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `ticket_id` bigint NOT NULL DEFAULT '0' COMMENT 'TicketId', + `from` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'From', + `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Type: 1 text, 2 image', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Content', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + PRIMARY KEY (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `traffic_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `server_id` bigint NOT NULL COMMENT 'Server ID', + `user_id` bigint NOT NULL COMMENT 'User ID', + `subscribe_id` bigint NOT NULL COMMENT 'Subscription ID', + `download` bigint DEFAULT '0' COMMENT 'Download Traffic', + `upload` bigint DEFAULT '0' COMMENT 'Upload Traffic', + `timestamp` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'Traffic Log Time', + PRIMARY KEY (`id`), + KEY `idx_subscribe_id` (`subscribe_id`), + KEY `idx_server_id` (`server_id`), + KEY `idx_user_id` (`user_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'User Password', + `avatar` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'User Avatar', + `balance` bigint DEFAULT '0' COMMENT 'User Balance', + `telegram` bigint DEFAULT NULL COMMENT 'Telegram Account', + `refer_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Referral Code', + `referer_id` bigint DEFAULT NULL COMMENT 'Referrer ID', + `commission` bigint DEFAULT '0' COMMENT 'Commission', + `gift_amount` bigint DEFAULT '0' COMMENT 'User Gift Amount', + `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Is Account Enabled', + `is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Admin', + `valid_email` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Email Verified', + `enable_email_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Email Notifications', + `enable_telegram_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Telegram Notifications', + `enable_balance_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Balance Change Notifications', + `enable_login_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Login Notifications', + `enable_subscribe_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Subscription Notifications', + `enable_trade_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Trade Notifications', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + `deleted_at` datetime(3) DEFAULT NULL COMMENT 'Deletion Time', + `is_del` bigint unsigned DEFAULT NULL COMMENT '1: Normal 0: Deleted', + PRIMARY KEY (`id`), + KEY `idx_referer` (`referer_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_auth_methods` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `auth_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Auth Type 1: apple 2: google 3: github 4: facebook 5: telegram 6: email 7: phone', + `auth_identifier` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Auth Identifier', + `verified` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Verified', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `idx_auth_identifier` (`auth_identifier`), + KEY `idx_user_id` (`user_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_balance_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `amount` bigint NOT NULL COMMENT 'Amount', + `type` tinyint(1) NOT NULL COMMENT 'Type: 1: Recharge 2: Withdraw 3: Payment 4: Refund 5: Reward', + `order_id` bigint DEFAULT NULL COMMENT 'Order ID', + `balance` bigint NOT NULL COMMENT 'Balance', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_commission_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `order_no` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.', + `amount` bigint NOT NULL COMMENT 'Amount', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_device` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `subscribe_id` bigint DEFAULT NULL COMMENT 'Subscribe ID', + `ip` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Ip.', + `Identifier` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Identifier.', + `user_agent` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device User Agent.', + `online` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Online', + `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'EnableDeviceNumber', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_gift_amount_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `user_subscribe_id` bigint DEFAULT NULL COMMENT 'Deduction User Subscribe ID', + `order_no` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.', + `type` tinyint(1) NOT NULL COMMENT 'Type: 1: Increase 2: Reduce', + `amount` bigint NOT NULL COMMENT 'Amount', + `balance` bigint NOT NULL COMMENT 'Balance', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Remark', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_login_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `login_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Login IP', + `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent', + `success` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Login Success', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_subscribe` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `order_id` bigint NOT NULL COMMENT 'Order ID', + `subscribe_id` bigint NOT NULL COMMENT 'Subscription ID', + `start_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'Subscription Start Time', + `expire_time` datetime(3) DEFAULT NULL COMMENT 'Subscription Expire Time', + `traffic` bigint DEFAULT '0' COMMENT 'Traffic', + `download` bigint DEFAULT '0' COMMENT 'Download Traffic', + `upload` bigint DEFAULT '0' COMMENT 'Upload Traffic', + `token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Token', + `uuid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'UUID', + `status` tinyint(1) DEFAULT '0' COMMENT 'Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_user_subscribe_token` (`token`), + UNIQUE KEY `uni_user_subscribe_uuid` (`uuid`), + KEY `idx_user_id` (`user_id`), + KEY `idx_order_id` (`order_id`), + KEY `idx_subscribe_id` (`subscribe_id`), + KEY `idx_token` (`token`), + KEY `idx_uuid` (`uuid`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_subscribe_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `user_subscribe_id` bigint NOT NULL COMMENT 'User Subscribe ID', + `token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Token', + `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'IP', + `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_user_subscribe_id` (`user_subscribe_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `server_rule_group` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Rule Group Name', + `icon` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Rule Group Icon', + `tags` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Selected Node Tags', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Rule Group Description', + `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Rule Group Enable', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `unique_name` (`name`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/initialize/migrate/database/00002_init_basic_data.down.sql b/initialize/migrate/database/00002_init_basic_data.down.sql new file mode 100644 index 0000000..7d749b2 --- /dev/null +++ b/initialize/migrate/database/00002_init_basic_data.down.sql @@ -0,0 +1,21 @@ +-- 000002_init_data.down.sql +SET +FOREIGN_KEY_CHECKS = 0; + +DELETE +FROM `auth_method` +WHERE `id` IN (1, 2, 3, 4, 5, 6, 7, 8); +DELETE +FROM `payment` +WHERE `id` = -1; +DELETE +FROM `subscribe_type` +WHERE `id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14); +DELETE +FROM `system` +WHERE `id` IN + (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41); + +SET +FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/initialize/migrate/database/00002_init_basic_data.up.sql b/initialize/migrate/database/00002_init_basic_data.up.sql new file mode 100644 index 0000000..83cdda6 --- /dev/null +++ b/initialize/migrate/database/00002_init_basic_data.up.sql @@ -0,0 +1,127 @@ +-- 000002_init_data.up.sql +SET FOREIGN_KEY_CHECKS = 0; + +-- auth_method +INSERT IGNORE INTO `auth_method` (`id`, `method`, `config`, `enabled`, `created_at`, `updated_at`) +VALUES (1, 'email', + '{"platform":"smtp","platform_config":{"host":"","port":0,"user":"","pass":"","from":"","ssl":false},"enable_verify":false,"enable_notify":false,"enable_domain_suffix":false,"domain_suffix_list":"","verify_email_template":"","expiration_email_template":"","maintenance_email_template":"","traffic_exceed_email_template":""}', + 1, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'), + (2, 'mobile', + '{"platform":"AlibabaCloud","platform_config":{"access":"","secret":"","sign_name":"","endpoint":"","template_code":""},"enable_whitelist":false,"whitelist":[]}', + 0, '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'), + (3, 'apple', '{"team_id":"","key_id":"","client_id":"","client_secret":"","redirect_url":""}', 0, + '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'), + (4, 'google', '{"client_id":"","client_secret":"","redirect_url":""}', 0, '2025-04-22 14:25:16.642', + '2025-04-22 14:25:16.642'), + (5, 'github', '{"client_id":"","client_secret":"","redirect_url":""}', 0, '2025-04-22 14:25:16.642', + '2025-04-22 14:25:16.642'), + (6, 'facebook', '{"client_id":"","client_secret":"","redirect_url":""}', 0, '2025-04-22 14:25:16.642', + '2025-04-22 14:25:16.642'), + (7, 'telegram', '{"bot_token":"","enable_notify":false,"webhook_domain":""}', 0, '2025-04-22 14:25:16.642', + '2025-04-22 14:25:16.642'), + (8, 'device', '{"show_ads":false,"only_real_device":false,"enable_security":false,"security_secret":""}', 0, + '2025-04-22 14:25:16.642', '2025-04-22 14:25:16.642'); + +-- payment +INSERT IGNORE INTO `payment` (`id`, `name`, `platform`, `description`, `icon`, `domain`, `config`, `fee_mode`, + `fee_percent`, `fee_amount`, `enable`, `token`) +VALUES (-1, 'Balance', 'balance', '', '', '', '', 0, 0, 0, 1, ''); + +-- subscribe_type +INSERT IGNORE INTO `subscribe_type` (`id`, `name`, `mark`, `created_at`, `updated_at`) +VALUES (1, 'Clash', 'Clash', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'), + (2, 'Hiddify', 'Hiddify', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'), + (3, 'Loon', 'Loon', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'), + (4, 'NekoBox', 'NekoBox', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'), + (5, 'NekoRay', 'NekoRay', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'), + (6, 'Netch', 'Netch', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'), + (7, 'Quantumult', 'Quantumult', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'), + (8, 'Shadowrocket', 'Shadowrocket', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'), + (9, 'SingBox', ' SingBox', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'), + (10, 'Surfboard', 'Surfboard', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'), + (11, 'Surge', 'Surge', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'), + (12, 'V2box', 'V2box', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'), + (13, 'V2rayN', 'V2rayN', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'), + (14, 'V2rayNg', 'V2rayNg', '2025-04-22 14:25:16.648', '2025-04-22 14:25:16.648'); + +-- system +INSERT IGNORE INTO `system` (`id`, `category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +VALUES (1, 'site', 'SiteLogo', '/favicon.svg', 'string', 'Site Logo', '2025-04-22 14:25:16.637', + '2025-04-22 14:25:16.637'), + (2, 'site', 'SiteName', 'Perfect Panel', 'string', 'Site Name', '2025-04-22 14:25:16.637', + '2025-04-22 14:25:16.637'), + (3, 'site', 'SiteDesc', + 'PPanel is a pure, professional, and perfect open-source proxy panel tool, designed to be your ideal choice for learning and practical use.', + 'string', 'Site Description', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'), + (4, 'site', 'Host', '', 'string', 'Site Host', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'), + (5, 'site', 'Keywords', 'Perfect Panel,PPanel', 'string', 'Site Keywords', '2025-04-22 14:25:16.637', + '2025-04-22 14:25:16.637'), + (6, 'site', 'CustomHTML', '', 'string', 'Custom HTML', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'), + (7, 'tos', 'TosContent', 'Welcome to use Perfect Panel', 'string', 'Terms of Service', '2025-04-22 14:25:16.637', + '2025-04-22 14:25:16.637'), + (8, 'tos', 'PrivacyPolicy', '', 'string', 'PrivacyPolicy', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'), + (9, 'ad', 'WebAD', 'false', 'bool', 'Display ad on the web', '2025-04-22 14:25:16.637', + '2025-04-22 14:25:16.637'), + (10, 'subscribe', 'SingleModel', 'false', 'bool', '是否单订阅模式', '2025-04-22 14:25:16.639', + '2025-04-22 14:25:16.639'), + (11, 'subscribe', 'SubscribePath', '/api/subscribe', 'string', '订阅路径', '2025-04-22 14:25:16.639', + '2025-04-22 14:25:16.639'), + (12, 'subscribe', 'SubscribeDomain', '', 'string', '订阅域名', '2025-04-22 14:25:16.639', + '2025-04-22 14:25:16.639'), + (13, 'subscribe', 'PanDomain', 'false', 'bool', '是否使用泛域名', '2025-04-22 14:25:16.639', + '2025-04-22 14:25:16.639'), + (14, 'verify', 'TurnstileSiteKey', '', 'string', 'TurnstileSiteKey', '2025-04-22 14:25:16.639', + '2025-04-22 14:25:16.639'), + (15, 'verify', 'TurnstileSecret', '', 'string', 'TurnstileSecret', '2025-04-22 14:25:16.639', + '2025-04-22 14:25:16.639'), + (16, 'verify', 'EnableLoginVerify', 'false', 'bool', 'is enable login verify', '2025-04-22 14:25:16.639', + '2025-04-22 14:25:16.639'), + (17, 'verify', 'EnableRegisterVerify', 'false', 'bool', 'is enable register verify', '2025-04-22 14:25:16.639', + '2025-04-22 14:25:16.639'), + (18, 'verify', 'EnableResetPasswordVerify', 'false', 'bool', 'is enable reset password verify', + '2025-04-22 14:25:16.639', '2025-04-22 14:25:16.639'), + (19, 'server', 'NodeSecret', '12345678', 'string', 'node secret', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (20, 'server', 'NodePullInterval', '10', 'int', 'node pull interval', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (21, 'server', 'NodePushInterval', '60', 'int', 'node push interval', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (22, 'server', 'NodeMultiplierConfig', '[]', 'string', 'node multiplier config', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (23, 'invite', 'ForcedInvite', 'false', 'bool', 'Forced invite', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (24, 'invite', 'ReferralPercentage', '20', 'int', 'Referral percentage', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (25, 'invite', 'OnlyFirstPurchase', 'false', 'bool', 'Only first purchase', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (26, 'register', 'StopRegister', 'false', 'bool', 'is stop register', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (27, 'register', 'EnableTrial', 'false', 'bool', 'is enable trial', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (28, 'register', 'TrialSubscribe', '', 'int', 'Trial subscription', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (29, 'register', 'TrialTime', '24', 'int', 'Trial time', '2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'), + (30, 'register', 'TrialTimeUnit', 'Hour', 'string', 'Trial time unit', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (31, 'register', 'EnableIpRegisterLimit', 'false', 'bool', 'is enable IP register limit', + '2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'), + (32, 'register', 'IpRegisterLimit', '3', 'int', 'IP Register Limit', '2025-04-22 14:25:16.640', + '2025-04-22 14:25:16.640'), + (33, 'register', 'IpRegisterLimitDuration', '64', 'int', 'IP Register Limit Duration (minutes)', + '2025-04-22 14:25:16.640', '2025-04-22 14:25:16.640'), + (34, 'currency', 'Currency', 'USD', 'string', 'Currency', '2025-04-22 14:25:16.641', '2025-04-22 14:25:16.641'), + (35, 'currency', 'CurrencySymbol', '$', 'string', 'Currency Symbol', '2025-04-22 14:25:16.641', + '2025-04-22 14:25:16.641'), + (36, 'currency', 'CurrencyUnit', 'USD', 'string', 'Currency Unit', '2025-04-22 14:25:16.641', + '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', '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'), + (40, 'verify_code', 'VerifyCodeInterval', '60', 'int', 'Interval of verify code', '2025-04-22 14:25:16.641', + '2025-04-22 14:25:16.641'), + (41, 'system', 'Version', '0.2.0(02002)', 'string', 'System Version', '2025-04-22 14:25:16.642', + '2025-04-22 14:25:16.642'); +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/initialize/migrate/database/01200-patch.sql b/initialize/migrate/database/01200-patch.sql deleted file mode 100644 index 7c2c000..0000000 --- a/initialize/migrate/database/01200-patch.sql +++ /dev/null @@ -1,54 +0,0 @@ --- 先检查 `email` 列是否存在,再删除 -SELECT COUNT(*) INTO @col_exists FROM information_schema.columns -WHERE table_schema = DATABASE() AND table_name = 'user' AND column_name = 'email'; - -SET @sql = IF(@col_exists > 0, 'ALTER TABLE `user` DROP COLUMN `email`', 'SELECT 1'); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; - --- 先检查 `telephone` 列是否存在,再删除 -SELECT COUNT(*) INTO @col_exists FROM information_schema.columns -WHERE table_schema = DATABASE() AND table_name = 'user' AND column_name = 'telephone'; - -SET @sql = IF(@col_exists > 0, 'ALTER TABLE `user` DROP COLUMN `telephone`', 'SELECT 1'); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; - --- 先检查 `telephone_area_code` 列是否存在,再删除 -SELECT COUNT(*) INTO @col_exists FROM information_schema.columns -WHERE table_schema = DATABASE() AND table_name = 'user' AND column_name = 'telephone_area_code'; - -SET @sql = IF(@col_exists > 0, 'ALTER TABLE `user` DROP COLUMN `telephone_area_code`', 'SELECT 1'); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; - - --- 先检查 `idx_email` 索引是否存在,再删除 -SELECT COUNT(*) INTO @idx_exists FROM information_schema.statistics -WHERE table_schema = DATABASE() AND table_name = 'user' AND index_name = 'idx_email'; - -SET @sql = IF(@idx_exists > 0, 'ALTER TABLE `user` DROP INDEX `idx_email`', 'SELECT 1'); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; - --- 先检查 `idx_telephone` 索引是否存在,再删除 -SELECT COUNT(*) INTO @idx_exists FROM information_schema.statistics -WHERE table_schema = DATABASE() AND table_name = 'user' AND index_name = 'idx_telephone'; - -SET @sql = IF(@idx_exists > 0, 'ALTER TABLE `user` DROP INDEX `idx_telephone`', 'SELECT 1'); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; - --- 先检查 `idx_telephone_area_code` 索引是否存在,再删除 -SELECT COUNT(*) INTO @idx_exists FROM information_schema.statistics -WHERE table_schema = DATABASE() AND table_name = 'user' AND index_name = 'idx_telephone_area_code'; - -SET @sql = IF(@idx_exists > 0, 'ALTER TABLE `user` DROP INDEX `idx_telephone_area_code`', 'SELECT 1'); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; diff --git a/initialize/migrate/database/01201-patch.sql b/initialize/migrate/database/01201-patch.sql deleted file mode 100644 index d2f4364..0000000 --- a/initialize/migrate/database/01201-patch.sql +++ /dev/null @@ -1,118 +0,0 @@ -SET NAMES utf8mb4; -SET FOREIGN_KEY_CHECKS = 0; - --- 检查表是否存在,如果存在则跳过创建 -CREATE TABLE IF NOT EXISTS `oauth_config` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `platform` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'platform', - `config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'OAuth Configuration', - `redirect` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Redirect URL', - `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Enabled', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`), - UNIQUE KEY `uni_oauth_config_platform` (`platform`) - ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- 插入记录时忽略重复记录 -BEGIN; -INSERT IGNORE INTO `oauth_config` (`id`, `platform`, `config`, `redirect`, `enabled`, `created_at`, `updated_at`) VALUES -(1, 'apple', '{\"team_id\":\"\",\"key_id\":\"\",\"client_id\":\"\",\"client_secret\":\"\"}', '', 0, '2025-01-26 20:11:15.292', '2025-01-26 20:11:15.292'), -(2, 'google', '{\"client_id\":\"\",\"client_secret\":\"\"}', '', 0, '2025-01-26 20:11:15.292', '2025-01-26 20:11:15.292'), -(3, 'github', '{\"client_id\":\"\",\"client_secret\":\"\"}', '', 0, '2025-01-26 20:11:15.292', '2025-01-26 20:11:15.292'), -(4, 'facebook', '{\"client_id\":\"\",\"client_secret\":\"\"}', '', 0, '2025-01-26 20:11:15.292', '2025-01-26 20:11:15.292'), -(5, 'telegram', '{\"bot\":\"\",\"bot_token\":\"\"}', '', 0, '2025-01-26 20:11:15.292', '2025-01-26 20:11:15.292'); -COMMIT; - --- 检测更新设置表 -BEGIN; -INSERT IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) VALUES -('sms', 'SmsEnabled', 'false', 'bool', '是否启用短信功能', NOW(), NOW()), -('sms', 'SmsKey', 'your-key', 'string', '短信服务用户名或Key',NOW(), NOW()), -('sms', 'SmsSecret', 'your-secret', 'string', '短信服务密码或Secret', NOW(), NOW()), -('sms', 'SmsSign', 'your-sign', 'string', '短信签名', NOW(), NOW()), -('sms', 'SmsTemplate', 'your-template', 'string', '短信模板ID', NOW(), NOW()), -('sms', 'SmsRegion', 'cn-hangzhou', 'string', '短信服务所在区域(适用于阿里云)', NOW(), NOW()), -('sms', 'SmsTemplate', '您的验证码是{{.Code}},请在5分钟内使用。', 'string', '自定义短信模板', NOW(), NOW()), -('sms', 'SmsTemplateCode', 'SMS_12345678', 'string', '阿里云国内短信模板代码',NOW(),NOW()), -('sms', 'SmsTemplateParam', '{\"code\":{{.Code}}}', 'string', '短信模板参数', NOW(), NOW()), -('sms', 'SmsPlatform', 'smsbao', 'string', '当前使用的短信平台', NOW(), NOW()), -('sms', 'SmsLimit', '10', 'int64', '可以发送的短信最大数量', NOW(), NOW()), -('sms', 'SmsInterval', '60', 'int64', '发送短信的时间间隔(单位:秒)',NOW(), NOW()), -('sms', 'SmsExpireTime', '300', 'int64', '短信验证码的过期时间(单位:秒)',NOW(), NOW()), -('email', 'EmailEnabled', 'true', 'bool', '启用邮箱登陆',NOW(), NOW()), -('email', 'EmailSmtpHost', '', 'string', '邮箱服务器地址', NOW(), NOW()), -('email', 'EmailSmtpPort', '465', 'int', '邮箱服务器端口',NOW(), NOW()), -('email', 'EmailSmtpUser', 'domain@f1shyu.com', 'string', '邮箱服务器用户名', NOW(), NOW()), -('email', 'EmailSmtpPass', 'password', 'string', '邮箱服务器密码', NOW(), NOW()), -('email', 'EmailSmtpFrom', 'domain@f1shyu.com', 'string', '发送邮件的邮箱',NOW(), NOW()), -('email', 'EmailSmtpSSL', 'true', 'bool', '邮箱服务器加密方式',NOW(), NOW()), -('email', 'EmailTemplate', '%s', 'string', '邮件模板',NOW(), NOW()), -('email', 'VerifyEmailTemplate', '', 'string', 'Verify Email template',NOW(), NOW()), -('email', 'MaintenanceEmailTemplate', '', 'string', 'Maintenance Email template',NOW(), NOW()), -('email', 'ExpirationEmailTemplate', '', 'string', 'Expiration Email template', NOW(), NOW()), -('email', 'EmailEnableVerify', 'true', 'bool', '是否开启邮箱验证', NOW(), NOW()), -('email', 'EmailEnableDomainSuffix', 'false', 'bool', '是否开启邮箱域名后缀限制',NOW(), NOW()), -('email', 'EmailDomainSuffixList', 'qq.com', 'string', '邮箱域名后缀列表',NOW(), NOW()); -COMMIT; - --- User Device -CREATE TABLE IF NOT EXISTS `user_device` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL COMMENT 'User ID', - `device_number` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Number.', - `online` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Online', - `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'EnableDeviceNumber', - `last_online` datetime(3) DEFAULT NULL COMMENT 'Last Online', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`), - CONSTRAINT `fk_user_user_devices` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- Mobile -CREATE TABLE IF NOT EXISTS `sms` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `content` text COLLATE utf8mb4_general_ci, - `platform` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, - `area_code` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, - `telephone` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, - `status` tinyint(1) DEFAULT '1', - `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- Application Config -CREATE TABLE IF NOT EXISTS `application_config` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `app_id` bigint NOT NULL DEFAULT '0' COMMENT 'App id', - `encryption_key` text COLLATE utf8mb4_general_ci COMMENT 'Encryption Key', - `encryption_method` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Encryption Method', - `domains` text COLLATE utf8mb4_general_ci, - `startup_picture` text COLLATE utf8mb4_general_ci, - `startup_picture_skip_time` bigint NOT NULL DEFAULT '0' COMMENT 'Startup Picture Skip Time', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- Application Version -CREATE TABLE IF NOT EXISTS `application_version` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `url` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用地址', - `version` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用版本', - `platform` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用平台', - `is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT '默认版本', - `description` text COLLATE utf8mb4_general_ci COMMENT '更新描述', - `application_id` bigint DEFAULT NULL COMMENT '所属应用', - `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间', - `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间', - PRIMARY KEY (`id`), - KEY `fk_application_application_versions` (`application_id`), - CONSTRAINT `fk_application_application_versions` FOREIGN KEY (`application_id`) REFERENCES `application` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - -UPDATE `subscribe` SET `unit_time`='Month' WHERE unit_time = ''; - -SET FOREIGN_KEY_CHECKS = 1; diff --git a/initialize/migrate/database/01202-patch.sql b/initialize/migrate/database/01202-patch.sql deleted file mode 100644 index 95c3a14..0000000 --- a/initialize/migrate/database/01202-patch.sql +++ /dev/null @@ -1,44 +0,0 @@ - -SET NAMES utf8mb4; -SET FOREIGN_KEY_CHECKS = 0; - -DROP TABLE IF EXISTS `user_device`; --- User Device -CREATE TABLE IF NOT EXISTS `user_device` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL COMMENT 'User ID', - `subscribe_id` bigint DEFAULT NULL COMMENT 'Subscribe ID', - `ip` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Ip.', - `Identifier` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Identifier.', - `user_agent` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device User Agent.', - `online` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Online', - `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'EnableDeviceNumber', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`), - CONSTRAINT `fk_user_user_devices` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for server_rule_group --- ---------------------------- -DROP TABLE IF EXISTS `server_rule_group`; -CREATE TABLE `server_rule_group` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Rule Group Name', - `icon` text COLLATE utf8mb4_general_ci COMMENT 'Rule Group Icon', - `description` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Rule Group Description', - `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Rule Group Enable', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`), - UNIQUE KEY `unique_name` (`name`) -- Add unique constraint to `name` -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; --- ---------------------------- --- Records of server_rule_group --- ---------------------------- -BEGIN; -COMMIT; - -SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/initialize/migrate/database/02003_update_payment.down.sql b/initialize/migrate/database/02003_update_payment.down.sql new file mode 100644 index 0000000..51715c2 --- /dev/null +++ b/initialize/migrate/database/02003_update_payment.down.sql @@ -0,0 +1,72 @@ +-- migrations/02003_update_payment.down.sql +-- Purpose: Revert updates to payment and order tables +-- Author: PPanel Team, 2025-04-21 + +SET FOREIGN_KEY_CHECKS = 0; + +-- Drop payment_id column from order table (if exists) +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'order' + AND COLUMN_NAME = 'payment_id'); +SET @sql = IF(@column_exists > 0, + 'ALTER TABLE `order` DROP COLUMN `payment_id`', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Drop platform column from payment table (if exists) +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'payment' + AND COLUMN_NAME = 'platform'); +SET @sql = IF(@column_exists > 0, + 'ALTER TABLE `payment` DROP COLUMN `platform`', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Drop description column from payment table (if exists) +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'payment' + AND COLUMN_NAME = 'description'); +SET @sql = IF(@column_exists > 0, + 'ALTER TABLE `payment` DROP COLUMN `description`', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Drop token column from payment table (if exists) +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'payment' + AND COLUMN_NAME = 'token'); +SET @sql = IF(@column_exists > 0, + 'ALTER TABLE `payment` DROP COLUMN `token`', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Optionally restore mark column (if needed, adjust definition as per original schema) +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'payment' + AND COLUMN_NAME = 'mark'); +SET @sql = IF(@column_exists = 0, + 'ALTER TABLE `payment` ADD COLUMN `mark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT \'Payment Mark\' AFTER `name`', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/initialize/migrate/database/02003_update_payment.up.sql b/initialize/migrate/database/02003_update_payment.up.sql new file mode 100644 index 0000000..3991cff --- /dev/null +++ b/initialize/migrate/database/02003_update_payment.up.sql @@ -0,0 +1,72 @@ +-- 2025-04-22 16:16:00 +-- Purpose: Update payment table +-- Author: PPanel Team, 2025-04-21 + +SET FOREIGN_KEY_CHECKS = 0; + +-- Alter the order table to add a payment_id column (if not exists) +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'order' + AND COLUMN_NAME = 'payment_id'); +SET @sql = IF(@column_exists = 0, + 'ALTER TABLE `order` ADD COLUMN `payment_id` bigint NOT NULL DEFAULT \'-1\' COMMENT \'Payment Id\' AFTER `commission`', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Alter the payment table to add a platform column (if not exists) +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'payment' + AND COLUMN_NAME = 'platform'); +SET @sql = IF(@column_exists = 0, + 'ALTER TABLE `payment` ADD COLUMN `platform` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT \'Payment Platform\' AFTER `name`', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Drop the mark column from the payment table (only if exists) +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'payment' + AND COLUMN_NAME = 'mark'); +SET @sql = IF(@column_exists > 0, + 'ALTER TABLE `payment` DROP COLUMN `mark`', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Alter the payment table to add a description column (if not exists) +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'payment' + AND COLUMN_NAME = 'description'); +SET @sql = IF(@column_exists = 0, + 'ALTER TABLE `payment` ADD COLUMN `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT \'Payment Description\' AFTER `platform`', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Alter the payment table to add a token column (if not exists) +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'payment' + AND COLUMN_NAME = 'token'); +SET @sql = IF(@column_exists = 0, + 'ALTER TABLE `payment` ADD COLUMN `token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT \'Payment Token\' AFTER `description`', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/initialize/migrate/database/02004_rebuild_rule.down.sql b/initialize/migrate/database/02004_rebuild_rule.down.sql new file mode 100644 index 0000000..a818ad8 --- /dev/null +++ b/initialize/migrate/database/02004_rebuild_rule.down.sql @@ -0,0 +1,4 @@ +-- migrations/02003_rebuild_rule.up.sql +-- Purpose: Back rebuilding server rule table +-- Author: PPanel Team, 2025-04-21 +DROP TABLE IF EXISTS server_rule_group; \ No newline at end of file diff --git a/initialize/migrate/database/02004_rebuild_rule.up.sql b/initialize/migrate/database/02004_rebuild_rule.up.sql new file mode 100644 index 0000000..72f9b0f --- /dev/null +++ b/initialize/migrate/database/02004_rebuild_rule.up.sql @@ -0,0 +1,22 @@ +-- migrations/02003_rebuild_rule.up.sql +-- Purpose: rebuilding server rule table +-- Author: PPanel Team, 2025-04-21 + +DROP TABLE IF EXISTS `server_rule_group`; + +CREATE TABLE `server_rule_group` +( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Rule Group Name', + `icon` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Rule Group Icon', + `tags` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Selected Node Tags', + `rules` MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Rules', + `enable` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Rule Group Enable', + `created_at` DATETIME(3) COMMENT 'Creation Time', + `updated_at` DATETIME(3) COMMENT 'Update Time', + PRIMARY KEY (`id`), + UNIQUE KEY `uni_server_rule_group_name` (`name`), + INDEX `idx_enable` (`enable`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; \ No newline at end of file diff --git a/initialize/migrate/database/02005_device_online_record.down.sql b/initialize/migrate/database/02005_device_online_record.down.sql new file mode 100644 index 0000000..e571443 --- /dev/null +++ b/initialize/migrate/database/02005_device_online_record.down.sql @@ -0,0 +1,52 @@ +-- migrations/02004_create_user_device_online_record.down.sql +-- Purpose: Drop user device online record table +-- Author: PPanel Team, 2025-04-22 + +DROP TABLE IF EXISTS `user_device_online_record`; + +-- User subscribe table migration for removing finished_at column +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'user_subscribe' + AND COLUMN_NAME = 'finished_at'); +SET @sql = IF(@column_exists > 0, + 'ALTER TABLE `user_subscribe` DROP COLUMN `finished_at`', + 'SELECT 1' + ); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Application config table migration for removing invitation_link column + +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'application_config' + AND COLUMN_NAME = 'invitation_link'); + +SET @sql = IF(@column_exists > 0, + 'ALTER TABLE `application_config` DROP COLUMN `invitation_link`', + 'SELECT 1' + ); + +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Application config table migration for removing kr_website_id column +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'application_config' + AND COLUMN_NAME = 'kr_website_id'); + +SET @sql = IF(@column_exists > 0, + 'ALTER TABLE `application_config` DROP COLUMN `kr_website_id`', + 'SELECT 1' + ); + +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; \ No newline at end of file diff --git a/initialize/migrate/database/02005_device_online_record.up.sql b/initialize/migrate/database/02005_device_online_record.up.sql new file mode 100644 index 0000000..e149f12 --- /dev/null +++ b/initialize/migrate/database/02005_device_online_record.up.sql @@ -0,0 +1,69 @@ +-- migrations/02005_create_user_device_online_record.up.sql +-- Purpose: Create table for tracking user device online records +-- Author: PPanel Team, 2025-04-22 + +CREATE TABLE IF NOT EXISTS `user_device_online_record` +( + `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `user_id` BIGINT NOT NULL COMMENT 'User ID', + `identifier` VARCHAR(255) NOT NULL COMMENT 'Device Identifier', + `online_time` DATETIME COMMENT 'Online Time', + `offline_time` DATETIME COMMENT 'Offline Time', + `online_seconds` BIGINT COMMENT 'Offline Seconds', + `duration_days` BIGINT COMMENT 'Duration Days', + `created_at` DATETIME COMMENT 'Creation Time' +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + + +-- User subscribe table migration for adding finished_at column + +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'user_subscribe' + AND COLUMN_NAME = 'finished_at'); + +SET @sql = IF(@column_exists = 0, + 'ALTER TABLE `user_subscribe` ADD COLUMN `finished_at` DATETIME NULL COMMENT ''Subscribe Finished Time'' AFTER `expire_time`', + 'SELECT 1' + ); + +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + + +-- Application config table migration for adding Link column + +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'application_config' + AND COLUMN_NAME = 'invitation_link'); + +SET @sql = IF(@column_exists = 0, + 'ALTER TABLE `application_config` ADD COLUMN `invitation_link` TEXT NULL DEFAULT NULL COMMENT ''Invitation Link'' AFTER `startup_picture_skip_time`', + 'SELECT 1' + ); + +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Application config table migration for adding kr_website_id column +SET @column_exists = (SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'application_config' + AND COLUMN_NAME = 'kr_website_id'); + +SET @sql = IF(@column_exists = 0, + 'ALTER TABLE `application_config` ADD COLUMN `kr_website_id` VARCHAR(255) NULL DEFAULT NULL COMMENT ''KR Website ID'' AFTER `invitation_link`', + 'SELECT 1' + ); + +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/initialize/migrate/database/02006_reset_subscribe_record.down.sql b/initialize/migrate/database/02006_reset_subscribe_record.down.sql new file mode 100644 index 0000000..38af374 --- /dev/null +++ b/initialize/migrate/database/02006_reset_subscribe_record.down.sql @@ -0,0 +1,5 @@ +-- migrations/02008_create_user_reset_subscribe_log.down.sql +-- Purpose: Drop user_reset_subscribe_log table +-- Author: PPanel Team, 2025-04-22 + +DROP TABLE IF EXISTS `user_reset_subscribe_log`; diff --git a/initialize/migrate/database/02006_reset_subscribe_record.up.sql b/initialize/migrate/database/02006_reset_subscribe_record.up.sql new file mode 100644 index 0000000..ef5affd --- /dev/null +++ b/initialize/migrate/database/02006_reset_subscribe_record.up.sql @@ -0,0 +1,17 @@ +-- migrations/02008_create_user_reset_subscribe_log.up.sql +-- Purpose: Create user_reset_subscribe_log table +-- Author: PPanel Team, 2025-04-22 + +CREATE TABLE IF NOT EXISTS `user_reset_subscribe_log` +( + `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `user_id` BIGINT NOT NULL COMMENT 'User ID', + `type` TINYINT(1) NOT NULL COMMENT 'Type: 1: Auto 2: Advance 3: Paid', + `order_no` VARCHAR(255) DEFAULT NULL COMMENT 'Order No.', + `user_subscribe_id` BIGINT NOT NULL COMMENT 'User Subscribe ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation Time', + INDEX `idx_user_id` (`user_id`), + INDEX `idx_user_subscribe_id` (`user_subscribe_id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; diff --git a/initialize/migrate/database/02007_adapte_rule.down.sql b/initialize/migrate/database/02007_adapte_rule.down.sql new file mode 100644 index 0000000..ed5d22f --- /dev/null +++ b/initialize/migrate/database/02007_adapte_rule.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE `server_rule_group` +DROP COLUMN `default`, +DROP COLUMN `type`; diff --git a/initialize/migrate/database/02007_adapte_rule.up.sql b/initialize/migrate/database/02007_adapte_rule.up.sql new file mode 100644 index 0000000..0c30d54 --- /dev/null +++ b/initialize/migrate/database/02007_adapte_rule.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `server_rule_group` +ADD COLUMN `default` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Is Default Group', +ADD COLUMN `type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'Rule Group Type'; \ No newline at end of file diff --git a/initialize/migrate/database/02100_task.down.sql b/initialize/migrate/database/02100_task.down.sql new file mode 100644 index 0000000..0d1f763 --- /dev/null +++ b/initialize/migrate/database/02100_task.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `email_task`; \ No newline at end of file diff --git a/initialize/migrate/database/02100_task.up.sql b/initialize/migrate/database/02100_task.up.sql new file mode 100644 index 0000000..d35e8c9 --- /dev/null +++ b/initialize/migrate/database/02100_task.up.sql @@ -0,0 +1,23 @@ +DROP TABLE IF EXISTS `email_task`; +CREATE TABLE `email_task` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `subject` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Email Subject', + `content` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Email Content', + `recipient` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Email Recipient', + `scope` varchar(50) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Email Scope', + `register_start_time` datetime(3) DEFAULT NULL COMMENT 'Register Start Time', + `register_end_time` datetime(3) DEFAULT NULL COMMENT 'Register End Time', + `additional` text COLLATE utf8mb4_general_ci COMMENT 'Additional Information', + `scheduled` datetime(3) NOT NULL COMMENT 'Scheduled Time', + `interval` tinyint unsigned NOT NULL COMMENT 'Interval in Seconds', + `limit` bigint unsigned NOT NULL COMMENT 'Daily send limit', + `status` tinyint unsigned NOT NULL COMMENT 'Daily Status', + `errors` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Errors', + `total` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Total Number', + `current` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Current Number', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/initialize/migrate/database/02101_subscribe_application.down.sql b/initialize/migrate/database/02101_subscribe_application.down.sql new file mode 100644 index 0000000..bca110a --- /dev/null +++ b/initialize/migrate/database/02101_subscribe_application.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `subscribe_application`; \ No newline at end of file diff --git a/initialize/migrate/database/02101_subscribe_application.up.sql b/initialize/migrate/database/02101_subscribe_application.up.sql new file mode 100644 index 0000000..c46bc2b --- /dev/null +++ b/initialize/migrate/database/02101_subscribe_application.up.sql @@ -0,0 +1,27 @@ +DROP TABLE IF EXISTS `subscribe_application`; +CREATE TABLE IF NOT EXISTS `subscribe_application` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Application Name', + `icon` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Application Icon', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Application Description', + `scheme` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Application Scheme', + `user_agent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'User Agent', + `is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Default Application', + `subscribe_template` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Subscribe Template', + `output_format` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'yaml' COMMENT 'Output Format', + `download_link` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Download Link', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +-- ---------------------------- +-- Records of subscribe_application +-- ---------------------------- +BEGIN; +INSERT INTO `subscribe_application` (`id`, `name`, `icon`, `description`, `scheme`, `user_agent`, `is_default`, `subscribe_template`, `output_format`, `download_link`, `created_at`, `updated_at`) VALUES (1, 'Default', '', '', '', 'default', 1, '{{- $GiB := 1073741824.0 -}}\n{{- $used := printf \"%.2f\" (divf (add (.UserInfo.Download | default 0 | float64) (.UserInfo.Upload | default 0 | float64)) $GiB) -}}\n{{- $traffic := (.UserInfo.Traffic | default 0 | float64) -}}\n{{- $total := printf \"%.2f\" (divf $traffic $GiB) -}}\n\n{{- $exp := \"\" -}}\n{{- $expStr := printf \"%v\" .UserInfo.ExpiredAt -}}\n{{- if regexMatch `^[0-9]+$` $expStr -}}\n {{- $ts := $expStr | float64 -}}\n {{- $sec := ternary (divf $ts 1000.0) $ts (ge (len $expStr) 13) -}}\n {{- $exp = (date \"2006-01-02 15:04:05\" (unixEpoch ($sec | int64))) -}}\n{{- else -}}\n {{- $exp = $expStr -}}\n{{- end -}}\n\nREMARKS={{ .SiteName }}-{{ .SubscribeName }}\nSTATUS=Traffic: {{ $used }} GiB/{{ $total }} GiB | Expires: {{ $exp }}\n\n{{- range $proxy := .Proxies }}\n {{- $server := $proxy.Server -}}\n {{- if and (contains $proxy.Server \":\") (not (hasPrefix \"[\" $proxy.Server)) -}}\n {{- $server = printf \"[%s]\" $proxy.Server -}}\n {{- end -}}\n\n {{- $sni := default \"\" $proxy.SNI -}}\n {{- if eq $sni \"\" -}}\n {{- $sni = default \"\" $proxy.Host -}}\n {{- end -}}\n {{- if and (eq $sni \"\") (not (or (regexMatch \"^[0-9]+\\\\.[0-9]+\\\\.[0-9]+\\\\.[0-9]+$\" $proxy.Server) (contains $proxy.Server \":\"))) -}}\n {{- $sni = $proxy.Server -}}\n {{- end -}}\n\n {{- $common := \"udp=1&tfo=1\" -}}\n\n {{- $password := $.UserInfo.Password -}}\n {{- if and (eq $proxy.Type \"shadowsocks\") (ne (default \"\" $proxy.ServerKey) \"\") -}}\n {{- $method := $proxy.Method -}}\n {{- if or (hasPrefix \"2022-blake3-\" $method) (eq $method \"2022-blake3-aes-128-gcm\") (eq $method \"2022-blake3-aes-256-gcm\") -}}\n {{- $userKeyLen := ternary 16 32 (hasSuffix \"128-gcm\" $method) -}}\n {{- $pwdStr := printf \"%s\" $password -}}\n {{- $userKey := ternary $pwdStr (trunc $userKeyLen $pwdStr) (le (len $pwdStr) $userKeyLen) -}}\n {{- $serverB64 := b64enc $proxy.ServerKey -}}\n {{- $userB64 := b64enc $userKey -}}\n {{- $password = printf \"%s:%s\" $serverB64 $userB64 -}}\n {{- end -}}\n {{- end -}}\n\n {{- if eq $proxy.Type \"shadowsocks\" }}\nss://{{ printf \"%s:%s\" $proxy.Method $password | b64enc }}@{{ $server }}:{{ $proxy.Port }}?{{ $common }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"vmess\" }}\nvmess://{{ (dict \"v\" \"2\" \"ps\" $proxy.Name \"add\" $proxy.Server \"port\" (printf \"%d\" $proxy.Port) \"id\" $password \"aid\" \"0\" \"net\" (default \"tcp\" $proxy.Transport) \"type\" \"none\" \"host\" (default \"\" $proxy.Host) \"path\" (default \"\" $proxy.Path) \"tls\" (ternary \"tls\" \"\" (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\"))) \"sni\" $sni) | toJson | b64enc }}\n {{- else if eq $proxy.Type \"vless\" }}\nvless://{{ $password }}@{{ $server }}:{{ $proxy.Port }}?encryption=none{{- if ne (default \"\" $proxy.Flow) \"\" }}&flow={{ $proxy.Flow }}{{- end }}{{- if ne $proxy.Transport \"\" }}&type={{ $proxy.Transport }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Host) \"\") }}&host={{ $proxy.Host }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Path) \"\") }}&path={{ $proxy.Path | urlquery }}{{- end }}{{- if and (eq $proxy.Transport \"grpc\") (ne (default \"\" $proxy.ServiceName) \"\") }}&serviceName={{ $proxy.ServiceName }}{{- end }}{{- if or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\") }}&security={{ $proxy.Security }}{{- end }}{{- if ne $sni \"\" }}&sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}&allowInsecure=1{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}&fp={{ $proxy.Fingerprint }}{{- end }}{{- if and (eq $proxy.Security \"reality\") (ne (default \"\" $proxy.RealityPublicKey) \"\") }}&pbk={{ $proxy.RealityPublicKey }}{{- end }}{{- if and (eq $proxy.Security \"reality\") (ne (default \"\" $proxy.RealityShortId) \"\") }}&sid={{ $proxy.RealityShortId }}{{- end }}&{{ $common }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"trojan\" }}\ntrojan://{{ $password }}@{{ $server }}:{{ $proxy.Port }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") }}?{{- end }}{{- if and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\") }}sni={{ $sni }}{{- end }}{{- if and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure }}{{- if and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\") }}&{{- end }}allowInsecure=1{{- end }}{{- if ne $proxy.Transport \"\" }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) }}&{{- end }}type={{ $proxy.Transport }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Host) \"\") }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") }}&{{- end }}host={{ $proxy.Host }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Path) \"\") }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") (and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Host) \"\")) }}&{{- end }}path={{ $proxy.Path | urlquery }}{{- end }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"hysteria2\" }}\nhysteria2://{{ $server }}:{{ $proxy.Port }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.ObfsPassword) \"\") (ne (default \"\" $proxy.HopPorts) \"\") }}?{{- end }}{{- if ne $password \"\" }}auth={{ $password }}{{- end }}{{- if ne $sni \"\" }}{{- if ne $password \"\" }}&{{- end }}sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}{{- if or (ne $password \"\") (ne $sni \"\") }}&{{- end }}insecure=1{{- end }}{{- if ne (default \"\" $proxy.ObfsPassword) \"\" }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure }}&{{- end }}obfs=salamander&obfs-password={{ $proxy.ObfsPassword }}{{- end }}{{- if ne (default \"\" $proxy.HopPorts) \"\" }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.ObfsPassword) \"\") }}&{{- end }}mport={{ $proxy.HopPorts }}{{- end }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.ObfsPassword) \"\") (ne (default \"\" $proxy.HopPorts) \"\") }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"tuic\" }}\ntuic://{{ $password }}:{{ $password }}@{{ $server }}:{{ $proxy.Port }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI (ne $sni \"\") $proxy.AllowInsecure }}?{{- end }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}congestion_controller={{ $proxy.CongestionController }}{{- end }}{{- if ne (default \"\" $proxy.UDPRelayMode) \"\" }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}&{{- end }}udp_relay_mode={{ $proxy.UDPRelayMode }}{{- end }}{{- if $proxy.ReduceRtt }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") }}&{{- end }}reduce_rtt=1{{- end }}{{- if $proxy.DisableSNI }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt }}&{{- end }}disable_sni=1{{- end }}{{- if ne $sni \"\" }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI }}&{{- end }}sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI (ne $sni \"\") }}&{{- end }}allow_insecure=1{{- end }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI (ne $sni \"\") $proxy.AllowInsecure }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"anytls\" }}\nanytls://{{ $password }}@{{ $server }}:{{ $proxy.Port }}{{- if ne $sni \"\" }}?sni={{ $sni }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- end }}\n{{- end }}\n\n{{- range $proxy := .Proxies }}\n {{- if not (or (eq $proxy.Type \"shadowsocks\") (eq $proxy.Type \"vmess\") (eq $proxy.Type \"vless\") (eq $proxy.Type \"trojan\") (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"tuic\") (eq $proxy.Type \"anytls\")) }}\n# Skipped (unsupported protocol): {{ $proxy.Name }} ({{ $proxy.Type }})\n {{- end }}\n{{- end }}\n', 'base64', '{}', '2025-08-12 22:57:56.711', '2025-08-15 21:45:20.181'); +INSERT INTO `subscribe_application` (`id`, `name`, `icon`, `description`, `scheme`, `user_agent`, `is_default`, `subscribe_template`, `output_format`, `download_link`, `created_at`, `updated_at`) VALUES (2, 'Shadowrocket', '', '', 'shadowrocket://add/sub://${window.btoa(url)}?remark=${encodeURIComponent(name)}', 'Shadowrocket', 0, '{{- $GiB := 1073741824.0 -}}\n{{- $used := printf \"%.2f\" (divf (add (.UserInfo.Download | default 0 | float64) (.UserInfo.Upload | default 0 | float64)) $GiB) -}}\n{{- $traffic := (.UserInfo.Traffic | default 0 | float64) -}}\n{{- $total := printf \"%.2f\" (divf $traffic $GiB) -}}\n\n{{- $exp := \"\" -}}\n{{- $expStr := printf \"%v\" .UserInfo.ExpiredAt -}}\n{{- if regexMatch `^[0-9]+$` $expStr -}}\n {{- $ts := $expStr | float64 -}}\n {{- $sec := ternary (divf $ts 1000.0) $ts (ge (len $expStr) 13) -}}\n {{- $exp = (date \"2006-01-02 15:04:05\" (unixEpoch ($sec | int64))) -}}\n{{- else -}}\n {{- $exp = $expStr -}}\n{{- end -}}\n\nREMARKS={{ .SiteName }}-{{ .SubscribeName }}\nSTATUS=Traffic: {{ $used }} GiB/{{ $total }} GiB | Expires: {{ $exp }}\n\n{{- range $proxy := .Proxies }}\n {{- $server := $proxy.Server -}}\n {{- if and (contains $proxy.Server \":\") (not (hasPrefix \"[\" $proxy.Server)) -}}\n {{- $server = printf \"[%s]\" $proxy.Server -}}\n {{- end -}}\n\n {{- $sni := default \"\" $proxy.SNI -}}\n {{- if eq $sni \"\" -}}\n {{- $sni = default \"\" $proxy.Host -}}\n {{- end -}}\n {{- if and (eq $sni \"\") (not (or (regexMatch \"^[0-9]+\\\\.[0-9]+\\\\.[0-9]+\\\\.[0-9]+$\" $proxy.Server) (contains $proxy.Server \":\"))) -}}\n {{- $sni = $proxy.Server -}}\n {{- end -}}\n\n {{- $common := \"udp=1&tfo=1\" -}}\n\n {{- $password := $.UserInfo.Password -}}\n {{- if and (eq $proxy.Type \"shadowsocks\") (ne (default \"\" $proxy.ServerKey) \"\") -}}\n {{- $method := $proxy.Method -}}\n {{- if or (hasPrefix \"2022-blake3-\" $method) (eq $method \"2022-blake3-aes-128-gcm\") (eq $method \"2022-blake3-aes-256-gcm\") -}}\n {{- $userKeyLen := ternary 16 32 (hasSuffix \"128-gcm\" $method) -}}\n {{- $pwdStr := printf \"%s\" $password -}}\n {{- $userKey := ternary $pwdStr (trunc $userKeyLen $pwdStr) (le (len $pwdStr) $userKeyLen) -}}\n {{- $serverB64 := b64enc $proxy.ServerKey -}}\n {{- $userB64 := b64enc $userKey -}}\n {{- $password = printf \"%s:%s\" $serverB64 $userB64 -}}\n {{- end -}}\n {{- end -}}\n\n {{- if eq $proxy.Type \"shadowsocks\" }}\nss://{{ printf \"%s:%s\" (default \"aes-128-gcm\" $proxy.Method) $password | b64enc }}@{{ $server }}:{{ $proxy.Port }}?{{ $common }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"vmess\" }}\nvmess://{{ (dict \"v\" \"2\" \"ps\" $proxy.Name \"add\" $proxy.Server \"port\" (printf \"%d\" $proxy.Port) \"id\" $password \"aid\" \"0\" \"net\" (default \"tcp\" $proxy.Transport) \"type\" \"none\" \"host\" (default \"\" $proxy.Host) \"path\" (default \"\" $proxy.Path) \"tls\" (ternary \"tls\" \"\" (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\"))) \"sni\" $sni) | toJson | b64enc }}\n {{- else if eq $proxy.Type \"vless\" }}\nvless://{{ $password }}@{{ $server }}:{{ $proxy.Port }}?encryption=none{{- if ne (default \"\" $proxy.Flow) \"\" }}&flow={{ $proxy.Flow }}{{- end }}{{- if ne $proxy.Transport \"\" }}&type={{ $proxy.Transport }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Host) \"\") }}&host={{ $proxy.Host }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Path) \"\") }}&path={{ $proxy.Path | urlquery }}{{- end }}{{- if and (eq $proxy.Transport \"grpc\") (ne (default \"\" $proxy.ServiceName) \"\") }}&serviceName={{ $proxy.ServiceName }}{{- end }}{{- if or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\") }}&security={{ $proxy.Security }}{{- end }}{{- if ne $sni \"\" }}&sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}&allowInsecure=1{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}&fp={{ $proxy.Fingerprint }}{{- end }}{{- if and (eq $proxy.Security \"reality\") (ne (default \"\" $proxy.RealityPublicKey) \"\") }}&pbk={{ $proxy.RealityPublicKey }}{{- end }}{{- if and (eq $proxy.Security \"reality\") (ne (default \"\" $proxy.RealityShortId) \"\") }}&sid={{ $proxy.RealityShortId }}{{- end }}&{{ $common }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"trojan\" }}\ntrojan://{{ $password }}@{{ $server }}:{{ $proxy.Port }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") }}?{{- end }}{{- if and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\") }}sni={{ $sni }}{{- end }}{{- if and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure }}{{- if and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\") }}&{{- end }}allowInsecure=1{{- end }}{{- if ne $proxy.Transport \"\" }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) }}&{{- end }}type={{ $proxy.Transport }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Host) \"\") }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") }}&{{- end }}host={{ $proxy.Host }}{{- end }}{{- if and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Path) \"\") }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") (and (eq $proxy.Transport \"ws\") (ne (default \"\" $proxy.Host) \"\")) }}&{{- end }}path={{ $proxy.Path | urlquery }}{{- end }}{{- if or (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\")) (and (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) $proxy.AllowInsecure) (ne $proxy.Transport \"\") }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"hysteria2\" }}\nhysteria2://{{ $server }}:{{ $proxy.Port }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.ObfsPassword) \"\") (ne (default \"\" $proxy.HopPorts) \"\") }}?{{- end }}{{- if ne $password \"\" }}auth={{ $password }}{{- end }}{{- if ne $sni \"\" }}{{- if ne $password \"\" }}&{{- end }}sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}{{- if or (ne $password \"\") (ne $sni \"\") }}&{{- end }}insecure=1{{- end }}{{- if ne (default \"\" $proxy.ObfsPassword) \"\" }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure }}&{{- end }}obfs=salamander&obfs-password={{ $proxy.ObfsPassword }}{{- end }}{{- if ne (default \"\" $proxy.HopPorts) \"\" }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.ObfsPassword) \"\") }}&{{- end }}mport={{ $proxy.HopPorts }}{{- end }}{{- if or (ne $password \"\") (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.ObfsPassword) \"\") (ne (default \"\" $proxy.HopPorts) \"\") }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"tuic\" }}\ntuic://{{ $password }}:{{ $password }}@{{ $server }}:{{ $proxy.Port }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI (ne $sni \"\") $proxy.AllowInsecure }}?{{- end }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}congestion_controller={{ $proxy.CongestionController }}{{- end }}{{- if ne (default \"\" $proxy.UDPRelayMode) \"\" }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}&{{- end }}udp_relay_mode={{ $proxy.UDPRelayMode }}{{- end }}{{- if $proxy.ReduceRtt }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") }}&{{- end }}reduce_rtt=1{{- end }}{{- if $proxy.DisableSNI }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt }}&{{- end }}disable_sni=1{{- end }}{{- if ne $sni \"\" }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI }}&{{- end }}sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI (ne $sni \"\") }}&{{- end }}allow_insecure=1{{- end }}{{- if or (ne (default \"\" $proxy.CongestionController) \"\") (ne (default \"\" $proxy.UDPRelayMode) \"\") $proxy.ReduceRtt $proxy.DisableSNI (ne $sni \"\") $proxy.AllowInsecure }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else if eq $proxy.Type \"anytls\" }}\nanytls://{{ $password }}@{{ $server }}:{{ $proxy.Port }}{{- if ne $sni \"\" }}?sni={{ $sni }}&{{ $common }}{{- else }}?{{ $common }}{{- end }}#{{ $proxy.Name }}\n {{- else }}\n# Unsupported protocol: {{ $proxy.Type }} - {{ $proxy.Name }}\n {{- end }}\n{{- end }}', 'base64', '{}', '2025-08-12 23:03:50.004', '2025-08-15 22:01:39.221'); +INSERT INTO `subscribe_application` (`id`, `name`, `icon`, `description`, `scheme`, `user_agent`, `is_default`, `subscribe_template`, `output_format`, `download_link`, `created_at`, `updated_at`) VALUES (3, 'Clash', '', '', 'clash://install-config?url=${url}&name=${name}', 'Clash', 0, '{{- $GiB := 1073741824.0 -}}\n{{- $used := printf \"%.2f\" (divf (add (.UserInfo.Download | default 0 | float64) (.UserInfo.Upload | default 0 | float64)) $GiB) -}}\n{{- $traffic := (.UserInfo.Traffic | default 0 | float64) -}}\n{{- $total := printf \"%.2f\" (divf $traffic $GiB) -}}\n\n{{- $exp := \"\" -}}\n{{- $expStr := printf \"%v\" .UserInfo.ExpiredAt -}}\n{{- if regexMatch `^[0-9]+$` $expStr -}}\n {{- $ts := $expStr | float64 -}}\n {{- $sec := ternary (divf $ts 1000.0) $ts (ge (len $expStr) 13) -}}\n {{- $exp = (date \"2006-01-02 15:04:05\" (unixEpoch ($sec | int64))) -}}\n{{- else -}}\n {{- $exp = $expStr -}}\n{{- end -}}\n\n{{- $supportedProxies := list -}}\n{{- range $proxy := .Proxies -}}\n {{- if or (eq $proxy.Type \"shadowsocks\") (eq $proxy.Type \"vmess\") (eq $proxy.Type \"vless\") (eq $proxy.Type \"trojan\") (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"tuic\") -}}\n {{- $supportedProxies = append $supportedProxies $proxy -}}\n {{- end -}}\n{{- end -}}\n\n{{- $proxyNames := \"\" -}}\n{{- range $proxy := $supportedProxies -}}\n {{- if eq $proxyNames \"\" -}}\n {{- $proxyNames = $proxy.Name -}}\n {{- else -}}\n {{- $proxyNames = printf \"%s, %s\" $proxyNames $proxy.Name -}}\n {{- end -}}\n{{- end -}}\n\n# {{ .SiteName }}-{{ .SubscribeName }}\n# Traffic: {{ $used }} GiB/{{ $total }} GiB | Expires: {{ $exp }}\n\nmode: rule\nipv6: true\nallow-lan: true\nbind-address: ''*''\nmixed-port: 6088\nlog-level: error\nunified-delay: true\ntcp-concurrent: true\nexternal-controller: ''0.0.0.0:9090''\ntun:\n enable: true\n stack: system\n auto-route: true\ndns:\n enable: true\n cache-algorithm: arc\n listen: ''0.0.0.0:1053''\n ipv6: true\n enhanced-mode: fake-ip\n fake-ip-range: 198.18.0.1/16\n fake-ip-filter: [''*.lan'', lens.l.google.com, ''*.srv.nintendo.net'', ''*.stun.playstation.net'', ''xbox.*.*.microsoft.com'', ''*.xboxlive.com'', ''*.msftncsi.com'', ''*.msftconnecttest.com'']\n default-nameserver: [119.29.29.29, 223.5.5.5]\n nameserver: [system, 119.29.29.29, 223.5.5.5]\n fallback: [8.8.8.8, 1.1.1.1]\n fallback-filter: { geoip: true, geoip-code: CN }\n\nproxies:\n{{- range $proxy := $supportedProxies }}\n {{- $server := $proxy.Server -}}\n {{- if and (contains $proxy.Server \":\") (not (hasPrefix \"[\" $proxy.Server)) -}}\n {{- $server = printf \"[%s]\" $proxy.Server -}}\n {{- end -}}\n\n {{- $sni := default \"\" $proxy.SNI -}}\n {{- if eq $sni \"\" -}}\n {{- $sni = default \"\" $proxy.Host -}}\n {{- end -}}\n {{- if and (eq $sni \"\") (not (or (regexMatch \"^[0-9]+\\\\.[0-9]+\\\\.[0-9]+\\\\.[0-9]+$\" $proxy.Server) (contains $proxy.Server \":\"))) -}}\n {{- $sni = $proxy.Server -}}\n {{- end -}}\n\n {{- $password := $.UserInfo.Password -}}\n {{- if and (eq $proxy.Type \"shadowsocks\") (ne (default \"\" $proxy.ServerKey) \"\") -}}\n {{- $method := $proxy.Method -}}\n {{- if or (hasPrefix \"2022-blake3-\" $method) (eq $method \"2022-blake3-aes-128-gcm\") (eq $method \"2022-blake3-aes-256-gcm\") -}}\n {{- $userKeyLen := ternary 16 32 (hasSuffix \"128-gcm\" $method) -}}\n {{- $pwdStr := printf \"%s\" $password -}}\n {{- $userKey := ternary $pwdStr (trunc $userKeyLen $pwdStr) (le (len $pwdStr) $userKeyLen) -}}\n {{- $serverB64 := b64enc $proxy.ServerKey -}}\n {{- $userB64 := b64enc $userKey -}}\n {{- $password = printf \"%s:%s\" $serverB64 $userB64 -}}\n {{- end -}}\n {{- end -}}\n\n {{- $common := \"udp: true, tfo: true\" -}}\n\n {{- if eq $proxy.Type \"shadowsocks\" }}\n - { name: {{ $proxy.Name | quote }}, type: ss, server: {{ $server }}, port: {{ $proxy.Port }}, cipher: {{ default \"aes-128-gcm\" $proxy.Method }}, password: {{ $password }}, {{ $common }}{{- if ne (default \"\" $proxy.Transport) \"\" }}, plugin: obfs, plugin-opts: { mode: http, host: {{ $sni }} }{{- end }} }\n {{- else if eq $proxy.Type \"vmess\" }}\n - { name: {{ $proxy.Name | quote }}, type: vmess, server: {{ $server }}, port: {{ $proxy.Port }}, uuid: {{ $password }}, alterId: 0, cipher: auto, {{ $common }}{{- if or (eq $proxy.Transport \"websocket\") (eq $proxy.Transport \"ws\") }}, network: ws, ws-opts: { path: {{ default \"/\" $proxy.Path }}{{- if ne (default \"\" $proxy.Host) \"\" }}, headers: { Host: {{ $proxy.Host }} }{{- end }} }{{- else if eq $proxy.Transport \"http\" }}, network: http, http-opts: { method: GET, path: [{{ default \"/\" $proxy.Path | quote }}]{{- if ne (default \"\" $proxy.Host) \"\" }}, headers: { Host: [{{ $proxy.Host | quote }}] }{{- end }} }{{- else if eq $proxy.Transport \"grpc\" }}, network: grpc, grpc-opts: { grpc-service-name: {{ default \"grpc\" $proxy.ServiceName }} }{{- end }}{{- if or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\") }}, tls: true{{- end }}{{- if ne $sni \"\" }}, servername: {{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify: true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, fingerprint: {{ $proxy.Fingerprint }}{{- end }} }\n {{- else if eq $proxy.Type \"vless\" }}\n - { name: {{ $proxy.Name | quote }}, type: vless, server: {{ $server }}, port: {{ $proxy.Port }}, uuid: {{ $password }}, {{ $common }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, network: ws, ws-opts: { path: {{ default \"/\" $proxy.Path }}{{- if ne (default \"\" $proxy.Host) \"\" }}, headers: { Host: {{ $proxy.Host }} }{{- end }} }{{- else if eq $proxy.Transport \"http\" }}, network: http, http-opts: { method: GET, path: [{{ default \"/\" $proxy.Path | quote }}]{{- if ne (default \"\" $proxy.Host) \"\" }}, headers: { Host: [{{ $proxy.Host | quote }}] }{{- end }} }{{- else if eq $proxy.Transport \"grpc\" }}, network: grpc, grpc-opts: { grpc-service-name: {{ default \"grpc\" $proxy.ServiceName }} }{{- end }}{{- if ne $sni \"\" }}, servername: {{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify: true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, fingerprint: {{ $proxy.Fingerprint }}{{- end }}{{- if and (eq $proxy.Security \"reality\") (ne (default \"\" $proxy.RealityPublicKey) \"\") }}, reality-opts: { public-key: {{ $proxy.RealityPublicKey }}{{- if ne (default \"\" $proxy.RealityShortId) \"\" }}, short-id: {{ $proxy.RealityShortId }}{{- end }} }{{- end }}{{- if ne (default \"\" $proxy.Flow) \"\" }}, flow: {{ $proxy.Flow }}{{- end }} }\n {{- else if eq $proxy.Type \"trojan\" }}\n - { name: {{ $proxy.Name | quote }}, type: trojan, server: {{ $server }}, port: {{ $proxy.Port }}, password: {{ $password }}, {{ $common }}{{- if ne $sni \"\" }}, sni: {{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify: true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, fingerprint: {{ $proxy.Fingerprint }}{{- end }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, network: ws, ws-opts: { path: {{ default \"/\" $proxy.Path }}{{- if ne (default \"\" $proxy.Host) \"\" }}, headers: { Host: {{ $proxy.Host }} }{{- end }} }{{- else if eq $proxy.Transport \"http\" }}, network: http, http-opts: { method: GET, path: [{{ default \"/\" $proxy.Path | quote }}]{{- if ne (default \"\" $proxy.Host) \"\" }}, headers: { Host: [{{ $proxy.Host | quote }}] }{{- end }} }{{- else if eq $proxy.Transport \"grpc\" }}, network: grpc, grpc-opts: { grpc-service-name: {{ default \"grpc\" $proxy.ServiceName }} }{{- end }} }\n {{- else if or (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"hy2\") }}\n - { name: {{ $proxy.Name | quote }}, type: hysteria2, server: {{ $server }}, port: {{ $proxy.Port }}, password: {{ $password }}, {{ $common }}{{- if ne $sni \"\" }}, sni: {{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify: true{{- end }}{{- if ne (default \"\" $proxy.ObfsPassword) \"\" }}, obfs: salamander, obfs-password: {{ $proxy.ObfsPassword }}{{- end }}{{- if ne (default \"\" $proxy.HopPorts) \"\" }}, ports: {{ $proxy.HopPorts }}{{- end }}{{- if ne (default 0 $proxy.HopInterval) 0 }}, hop-interval: {{ $proxy.HopInterval }}{{- end }} }\n {{- else if eq $proxy.Type \"tuic\" }}\n - { name: {{ $proxy.Name | quote }}, type: tuic, server: {{ $server }}, port: {{ $proxy.Port }}, uuid: {{ default \"\" $proxy.ServerKey }}, password: {{ $password }}, {{ $common }}{{- if ne $sni \"\" }}, sni: {{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify: true{{- end }}{{- if $proxy.DisableSNI }}, disable-sni: true{{- end }}{{- if $proxy.ReduceRtt }}, reduce-rtt: true{{- end }}{{- if ne (default \"\" $proxy.UDPRelayMode) \"\" }}, udp-relay-mode: {{ $proxy.UDPRelayMode }}{{- end }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}, congestion-controller: {{ $proxy.CongestionController }}{{- end }} }\n {{- else if eq $proxy.Type \"wireguard\" }}\n - { name: {{ $proxy.Name | quote }}, type: wireguard, server: {{ $server }}, port: {{ $proxy.Port }}, private-key: {{ default \"\" $proxy.ServerKey }}, public-key: {{ default \"\" $proxy.RealityPublicKey }}, {{ $common }}{{- if ne (default \"\" $proxy.Path) \"\" }}, preshared-key: {{ $proxy.Path }}{{- end }}{{- if ne (default \"\" $proxy.RealityServerAddr) \"\" }}, ip: {{ $proxy.RealityServerAddr }}{{- end }}{{- if ne (default 0 $proxy.RealityServerPort) 0 }}, ipv6: {{ $proxy.RealityServerPort }}{{- end }} }\n {{- else if eq $proxy.Type \"anytls\" }}\n - { name: {{ $proxy.Name | quote }}, type: anytls, server: {{ $server }}, port: {{ $proxy.Port }}, password: {{ $password }}, {{ $common }} }\n {{- else }}\n - { name: {{ $proxy.Name | quote }}, type: {{ $proxy.Type }}, server: {{ $server }}, port: {{ $proxy.Port }}, {{ $common }} }\n {{- end }}\n{{- end }}\n\n{{- range $proxy := .Proxies }}\n {{- if not (or (eq $proxy.Type \"shadowsocks\") (eq $proxy.Type \"vmess\") (eq $proxy.Type \"vless\") (eq $proxy.Type \"trojan\") (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"tuic\")) }}\n# Skipped (unsupported by Clash): {{ $proxy.Name }} ({{ $proxy.Type }})\n {{- end }}\n{{- end }}\n\nproxy-groups:\n - { name: 🚀 Proxy, type: select, proxies: [🌏 Auto, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🍎 Apple, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🔍 Google, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🪟 Microsoft, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 📺 GlobalMedia, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 📟 Telegram, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🤖 AI, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🪙 Crypto, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🎮 Game, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🇨🇳 China, type: select, proxies: [🎯 Direct, 🚀 Proxy, {{ $proxyNames }}] }\n - { name: 🎯 Direct, type: select, proxies: [DIRECT], hidden: true }\n - { name: 🐠 Final, type: select, proxies: [🚀 Proxy, 🎯 Direct, {{ $proxyNames }}] }\n - { name: 🌏 Auto, type: url-test, proxies: [{{ $proxyNames }}] }\n\nrules:\n - RULE-SET, Apple, 🍎 Apple\n - RULE-SET, Google, 🔍 Google\n - RULE-SET, Microsoft, 🪟 Microsoft\n - RULE-SET, Github, 🪟 Microsoft\n - RULE-SET, HBO, 📺 GlobalMedia\n - RULE-SET, Disney, 📺 GlobalMedia\n - RULE-SET, TikTok, 📺 GlobalMedia\n - RULE-SET, Netflix, 📺 GlobalMedia\n - RULE-SET, GlobalMedia, 📺 GlobalMedia\n - RULE-SET, Telegram, 📟 Telegram\n - RULE-SET, OpenAI, 🤖 AI\n - RULE-SET, Gemini, 🤖 AI\n - RULE-SET, Copilot, 🤖 AI\n - RULE-SET, Claude, 🤖 AI\n - RULE-SET, Crypto, 🪙 Crypto\n - RULE-SET, Cryptocurrency, 🪙 Crypto\n - RULE-SET, Game, 🎮 Game\n - RULE-SET, Global, 🚀 Proxy\n - RULE-SET, ChinaMax, 🇨🇳 China\n - RULE-SET, Lan, 🎯 Direct\n - GEOIP, CN, 🇨🇳 China\n - MATCH, 🐠 Final\n\nrule-providers:\n Apple:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Apple/Apple_Classical_No_Resolve.yaml\n interval: 86400\n Google:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Google/Google_No_Resolve.yaml\n interval: 86400\n Microsoft:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Microsoft/Microsoft.yaml\n interval: 86400\n Github:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/GitHub/GitHub.yaml\n interval: 86400\n HBO:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/HBO/HBO.yaml\n interval: 86400\n Disney:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Disney/Disney.yaml\n interval: 86400\n TikTok:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/TikTok/TikTok.yaml\n interval: 86400\n Netflix:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Netflix/Netflix.yaml\n interval: 86400\n GlobalMedia:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/GlobalMedia/GlobalMedia_Classical_No_Resolve.yaml\n interval: 86400\n Telegram:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Telegram/Telegram_No_Resolve.yaml\n interval: 86400\n OpenAI:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/OpenAI/OpenAI.yaml\n interval: 86400\n Gemini:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Gemini/Gemini.yaml\n interval: 86400\n Copilot:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Copilot/Copilot.yaml\n interval: 86400\n Claude:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Claude/Claude.yaml\n interval: 86400\n Crypto:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Crypto/Crypto.yaml\n interval: 86400\n Cryptocurrency:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Cryptocurrency/Cryptocurrency.yaml\n interval: 86400\n Game:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Game/Game.yaml\n interval: 86400\n Global:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Global/Global_Classical_No_Resolve.yaml\n interval: 86400\n ChinaMax:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/ChinaMax/ChinaMax_Classical_No_Resolve.yaml\n interval: 86400\n Lan:\n type: http\n behavior: classical\n format: yaml\n url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Clash/Lan/Lan.yaml\n interval: 86400\n\nurl-rewrite:\n - ^https?:\\/\\/(www.)?g\\.cn https://www.google.com 302\n - ^https?:\\/\\/(www.)?google\\.cn https://www.google.com 302\n', 'yaml', '{}', '2025-08-12 23:10:00.487', '2025-08-15 22:01:27.031'); +INSERT INTO `subscribe_application` (`id`, `name`, `icon`, `description`, `scheme`, `user_agent`, `is_default`, `subscribe_template`, `output_format`, `download_link`, `created_at`, `updated_at`) VALUES (4, 'SingBox', '', '', 'sing-box://import-remote-profile?url=${encodeURIComponent(url)}#${name}', 'sing-box', 0, '{{- $GiB := 1073741824.0 -}}\n{{- $used := printf \"%.2f\" (divf (add (.UserInfo.Download | default 0 | float64) (.UserInfo.Upload | default 0 | float64)) $GiB) -}}\n{{- $traffic := (.UserInfo.Traffic | default 0 | float64) -}}\n{{- $total := printf \"%.2f\" (divf $traffic $GiB) -}}\n\n{{- $exp := \"\" -}}\n{{- $expStr := printf \"%v\" .UserInfo.ExpiredAt -}}\n{{- if regexMatch `^[0-9]+$` $expStr -}}\n {{- $ts := $expStr | float64 -}}\n {{- $sec := ternary (divf $ts 1000.0) $ts (ge (len $expStr) 13) -}}\n {{- $exp = (date \"2006-01-02 15:04:05\" (unixEpoch ($sec | int64))) -}}\n{{- else -}}\n {{- $exp = $expStr -}}\n{{- end -}}\n\n{{- $supportedProxies := list -}}\n{{- range $proxy := .Proxies -}}\n {{- if or (eq $proxy.Type \"shadowsocks\") (eq $proxy.Type \"vmess\") (eq $proxy.Type \"vless\") (eq $proxy.Type \"trojan\") (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"hy2\") (eq $proxy.Type \"tuic\") -}}\n {{- $supportedProxies = append $supportedProxies $proxy -}}\n {{- end -}}\n{{- end -}}\n\n{{- $proxyNames := \"\" -}}\n{{- if gt (len $supportedProxies) 0 -}}\n {{- range $proxy := $supportedProxies -}}\n {{- if eq $proxyNames \"\" -}}\n {{- $proxyNames = printf \"\\\"%s\\\"\" $proxy.Name -}}\n {{- else -}}\n {{- $proxyNames = printf \"%s, \\\"%s\\\"\" $proxyNames $proxy.Name -}}\n {{- end -}}\n {{- end -}}\n {{- $proxyNames = printf \", %s\" $proxyNames -}}\n{{- end -}}\n\n\n{\n \"log\": {\"level\": \"info\", \"timestamp\": true},\n \"experimental\": {\n \"cache_file\": {\"enabled\": true, \"path\": \"cache.db\", \"cache_id\": \"my_profile\", \"store_fakeip\": false},\n \"clash_api\": {\"external_controller\": \"127.0.0.1:9090\", \"external_ui\": \"ui\", \"secret\": \"\", \"external_ui_download_url\": \"https://mirror.ghproxy.com/https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip\", \"external_ui_download_detour\": \"direct\", \"default_mode\": \"rule\"}\n },\n \"dns\": {\n \"servers\": [\n {\"tag\": \"dns_proxy\",\"address\": \"tls://8.8.8.8\",\"detour\": \"Proxy\"},\n {\"tag\": \"dns_direct\",\"address\": \"https://223.5.5.5/dns-query\",\"detour\": \"direct\"}\n ],\n \"rules\": [\n {\"rule_set\": \"geosite-cn\", \"server\": \"dns_direct\"},\n {\"clash_mode\": \"direct\", \"server\": \"dns_direct\"},\n {\"clash_mode\": \"global\", \"server\": \"dns_proxy\"},\n {\"rule_set\": \"geosite-geolocation-!cn\", \"server\": \"dns_proxy\"}\n ],\n \"final\": \"dns_direct\",\n \"strategy\": \"ipv4_only\"\n },\n \"inbounds\": [\n {\"tag\": \"tun-in\", \"type\": \"tun\", \"address\": [\"172.18.0.1/30\",\"fdfe:dcba:9876::1/126\"], \"auto_route\": true, \"strict_route\": true, \"stack\": \"system\",\n \"platform\": {\"http_proxy\": {\"enabled\": true, \"server\": \"127.0.0.1\", \"server_port\": 7890}}},\n {\"tag\": \"mixed-in\", \"type\": \"mixed\", \"listen\": \"127.0.0.1\", \"listen_port\": 7890}\n ],\n \"outbounds\": [\n {\"tag\": \"Proxy\", \"type\": \"selector\", \"outbounds\": [\"Auto - UrlTest\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Domestic\", \"type\": \"selector\", \"outbounds\": [\"direct\", \"Proxy\"{{ $proxyNames }}]},\n {\"tag\": \"Others\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"AI Suite\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Netflix\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Disney Plus\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"YouTube\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Max\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Spotify\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Apple\", \"type\": \"selector\", \"outbounds\": [\"direct\", \"Proxy\"{{ $proxyNames }}]},\n {\"tag\": \"Telegram\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Microsoft\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"Tiktok\", \"type\": \"selector\", \"outbounds\": [\"Proxy\", \"direct\"{{ $proxyNames }}]},\n {\"tag\": \"AdBlock\", \"type\": \"selector\", \"outbounds\": [\"block\", \"direct\", \"Proxy\"]},\n {{- if gt (len $supportedProxies) 0 }}\n {\"tag\": \"Auto - UrlTest\", \"type\": \"urltest\", \"outbounds\": [{{ $proxyNames | trimPrefix \", \" }}], \"url\": \"http://cp.cloudflare.com/\", \"interval\": \"10m\", \"tolerance\": 50}\n {{- range $i, $proxy := $supportedProxies }},\n{{- $server := $proxy.Server -}}\n{{- if and (contains $proxy.Server \":\") (not (hasPrefix \"[\" $proxy.Server)) -}}\n {{- $server = printf \"[%s]\" $proxy.Server -}}\n{{- end -}}\n\n{{- $sni := default \"\" $proxy.SNI -}}\n{{- if eq $sni \"\" -}}\n {{- $sni = default \"\" $proxy.Host -}}\n{{- end -}}\n{{- if and (eq $sni \"\") (not (or (regexMatch \"^[0-9]+\\\\.[0-9]+\\\\.[0-9]+\\\\.[0-9]+$\" $proxy.Server) (contains $proxy.Server \":\"))) -}}\n {{- $sni = $proxy.Server -}}\n{{- end -}}\n\n{{- $password := $.UserInfo.Password -}}\n{{- if and (eq $proxy.Type \"shadowsocks\") (ne (default \"\" $proxy.ServerKey) \"\") -}}\n {{- $method := $proxy.Method -}}\n {{- if or (hasPrefix \"2022-blake3-\" $method) (eq $method \"2022-blake3-aes-128-gcm\") (eq $method \"2022-blake3-aes-256-gcm\") -}}\n {{- $userKeyLen := ternary 16 32 (hasSuffix \"128-gcm\" $method) -}}\n {{- $pwdStr := printf \"%s\" $password -}}\n {{- $userKey := ternary $pwdStr (trunc $userKeyLen $pwdStr) (le (len $pwdStr) $userKeyLen) -}}\n {{- $serverB64 := b64enc $proxy.ServerKey -}}\n {{- $userB64 := b64enc $userKey -}}\n {{- $password = printf \"%s:%s\" $serverB64 $userB64 -}}\n {{- end -}}\n{{- end -}}\n\n{{- $common := `\"tcp_fast_open\": true, \"udp_over_tcp\": false` -}}\n\n{{- if eq $proxy.Type \"shadowsocks\" -}}\n {{- $method := default \"aes-128-gcm\" $proxy.Method -}}\n { \"type\": \"shadowsocks\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"method\": {{ $method | quote }}, \"password\": {{ $password | quote }}, {{ $common }} }\n{{- else if eq $proxy.Type \"trojan\" -}}\n { \"type\": \"trojan\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"password\": {{ $password | quote }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, \"transport\": {\"type\": \"ws\", \"path\": {{ default \"/\" $proxy.Path | quote }}{{- if ne (default \"\" $proxy.Host) \"\" }}, \"headers\": {\"Host\": {{ $proxy.Host | quote }} }{{- end -}}}{{- else if eq $proxy.Transport \"grpc\" }}, \"transport\": {\"type\": \"grpc\", \"service_name\": {{ default \"grpc\" $proxy.ServiceName | quote }}}{{- end }}, {{ $common }}, \"tls\": {\"enabled\": true{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }}{{- if $proxy.AllowInsecure }}, \"insecure\": true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, \"utls\": {\"enabled\": true, \"fingerprint\": {{ $proxy.Fingerprint | quote }} }{{- end }}} }\n{{- else if eq $proxy.Type \"vless\" -}}\n { \"type\": \"vless\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"uuid\": {{ $password | quote }}{{- if ne (default \"\" $proxy.Flow) \"\" }}, \"flow\": {{ $proxy.Flow | quote }}{{- end }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, \"transport\": {\"type\": \"ws\", \"path\": {{ default \"/\" $proxy.Path | quote }}{{- if ne (default \"\" $proxy.Host) \"\" }}, \"headers\": {\"Host\": {{ $proxy.Host | quote }} }{{- end -}}}{{- else if eq $proxy.Transport \"grpc\" }}, \"transport\": {\"type\": \"grpc\", \"service_name\": {{ default \"grpc\" $proxy.ServiceName | quote }}}{{- end }}, {{ $common }}{{- if ne (default \"\" $proxy.RealityPublicKey) \"\" }}, \"reality\": { \"enabled\": true, \"public_key\": {{ $proxy.RealityPublicKey | quote }}{{- if ne (default \"\" $proxy.RealityShortId) \"\" }}, \"short_id\": {{ $proxy.RealityShortId | quote }}{{- end }}{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }} }{{- else if or (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.Fingerprint) \"\") }}, \"tls\": {\"enabled\": true{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }}{{- if $proxy.AllowInsecure }}, \"insecure\": true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, \"utls\": {\"enabled\": true, \"fingerprint\": {{ $proxy.Fingerprint | quote }} }{{- end }}}{{- end }} }\n{{- else if eq $proxy.Type \"vmess\" -}}\n { \"type\": \"vmess\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"uuid\": {{ $password | quote }}, \"security\": \"auto\", {{ $common }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, \"transport\": {\"type\": \"ws\", \"path\": {{ default \"/\" $proxy.Path | quote }}{{- if ne (default \"\" $proxy.Host) \"\" }}, \"headers\": {\"Host\": {{ $proxy.Host | quote }} }{{- end -}}}{{- else if eq $proxy.Transport \"grpc\" }}, \"transport\": {\"type\": \"grpc\", \"service_name\": {{ default \"grpc\" $proxy.ServiceName | quote }}}{{- end }}{{- if or (or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\")) (ne $sni \"\") $proxy.AllowInsecure (ne (default \"\" $proxy.Fingerprint) \"\") }}, \"tls\": {\"enabled\": true{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }}{{- if $proxy.AllowInsecure }}, \"insecure\": true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, \"utls\": {\"enabled\": true, \"fingerprint\": {{ $proxy.Fingerprint | quote }} }{{- end }}}{{- end }} }\n{{- else if or (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"hy2\") -}}\n { \"type\": \"hysteria2\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"password\": {{ $password | quote }}{{- if ne (default \"\" $proxy.ObfsPassword) \"\" }}, \"obfs\": { \"type\": \"salamander\", \"password\": {{ $proxy.ObfsPassword | quote }} }{{- end }}{{- if ne (default \"\" $proxy.HopPorts) \"\" }}, \"ports\": {{ $proxy.HopPorts | quote }}{{- end }}{{- if ne (default 0 $proxy.HopInterval) 0 }}, \"hop_interval\": {{ $proxy.HopInterval }}{{- end }}, {{ $common }}, \"tls\": {\"enabled\": true{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }}{{- if $proxy.AllowInsecure }}, \"insecure\": true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, \"utls\": {\"enabled\": true, \"fingerprint\": {{ $proxy.Fingerprint | quote }} }{{- end }}} }\n{{- else if eq $proxy.Type \"tuic\" -}}\n { \"type\": \"tuic\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"uuid\": {{ default \"\" $proxy.ServerKey | quote }}, \"password\": {{ $password | quote }}{{- if $proxy.DisableSNI }}, \"disable_sni\": true{{- end }}{{- if $proxy.ReduceRtt }}, \"reduce_rtt\": true{{- end }}{{- if ne (default \"\" $proxy.UDPRelayMode) \"\" }}, \"udp_relay_mode\": {{ $proxy.UDPRelayMode | quote }}{{- end }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}, \"congestion_control\": {{ $proxy.CongestionController | quote }}{{- end }}, {{ $common }}, \"alpn\": [\"h3\"], \"tls\": {\"enabled\": true{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }}{{- if $proxy.AllowInsecure }}, \"insecure\": true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, \"utls\": {\"enabled\": true, \"fingerprint\": {{ $proxy.Fingerprint | quote }} }{{- end }}} }\n{{- else if eq $proxy.Type \"anytls\" -}}\n { \"type\": \"anytls\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"password\": {{ $password | quote }}, {{ $common }}, \"tls\": {\"enabled\": true{{- if ne $sni \"\" }}, \"server_name\": {{ $sni | quote }}{{- end }}{{- if $proxy.AllowInsecure }}, \"insecure\": true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, \"utls\": {\"enabled\": true, \"fingerprint\": {{ $proxy.Fingerprint | quote }} }{{- end }}} }\n{{- else if eq $proxy.Type \"wireguard\" -}}\n { \"type\": \"wireguard\", \"tag\": {{ $proxy.Name | quote }}, \"server\": {{ $server | quote }}, \"server_port\": {{ $proxy.Port }}, \"private_key\": {{ default \"\" $proxy.ServerKey | quote }}, \"peer_public_key\": {{ default \"\" $proxy.RealityPublicKey | quote }}{{- if ne (default \"\" $proxy.Path) \"\" }}, \"pre_shared_key\": {{ $proxy.Path | quote }}{{- end }}{{- if ne (default \"\" $proxy.RealityServerAddr) \"\" }}, \"local_address\": [{{ $proxy.RealityServerAddr | quote }}]{{- end }}, {{ $common }} }\n{{- else -}}\n { \"type\": \"direct\", \"tag\": {{ $proxy.Name | quote }}, {{ $common }} }\n{{- end }}\n {{- end }},\n {{- end }}\n {\"type\": \"direct\", \"tag\": \"direct\"},\n {\"type\": \"block\", \"tag\": \"block\"}\n ],\n \"route\": {\n \"auto_detect_interface\": true, \"final\": \"Proxy\",\n \"rules\": [\n {\"type\": \"logical\", \"mode\": \"or\", \"rules\": [{\"port\": 53},{\"protocol\": \"dns\"}], \"action\": \"hijack-dns\"},\n {\"rule_set\": \"geosite-category-ads-all\", \"outbound\": \"AdBlock\"},\n {\"clash_mode\": \"direct\", \"outbound\": \"direct\"},\n {\"clash_mode\": \"global\", \"outbound\": \"Proxy\"},\n {\"domain\": [\"clash.razord.top\",\"yacd.metacubex.one\",\"yacd.haishan.me\",\"d.metacubex.one\"], \"outbound\": \"direct\"},\n {\"ip_is_private\": true, \"outbound\": \"direct\"},\n {\"rule_set\": [\"geoip-netflix\",\"geosite-netflix\"], \"outbound\": \"Netflix\"},\n {\"rule_set\": \"geosite-disney\", \"outbound\": \"Disney Plus\"},\n {\"rule_set\": \"geosite-youtube\", \"outbound\": \"YouTube\"},\n {\"rule_set\": \"geosite-max\", \"outbound\": \"Max\"},\n {\"rule_set\": \"geosite-spotify\", \"outbound\": \"Spotify\"},\n {\"rule_set\": [\"geoip-apple\",\"geosite-apple\"], \"outbound\": \"Apple\"},\n {\"rule_set\": [\"geoip-telegram\",\"geosite-telegram\"], \"outbound\": \"Telegram\"},\n {\"rule_set\": \"geosite-openai\", \"outbound\": \"AI Suite\"},\n {\"rule_set\": \"geosite-microsoft\", \"outbound\": \"Microsoft\"},\n {\"rule_set\": \"geosite-tiktok\", \"outbound\": \"Tiktok\"},\n {\"rule_set\": \"geosite-private\", \"outbound\": \"direct\"},\n {\"rule_set\": [\"geoip-cn\",\"geosite-cn\"], \"outbound\": \"Domestic\"},\n {\"rule_set\": \"geosite-geolocation-!cn\", \"outbound\": \"Others\"}\n ],\n \"rule_set\": [\n {\"tag\": \"geoip-cn\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-cn\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-private\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/private.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-geolocation-!cn\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/geolocation-!cn.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-category-ads-all\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ads-all.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geoip-netflix\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/netflix.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-netflix\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/netflix.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-disney\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/disney.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-youtube\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/youtube.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-max\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/hbomax.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-spotify\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/spotify.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geoip-apple\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo-lite/geoip/apple.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-apple\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/apple.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geoip-telegram\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/telegram.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-telegram\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/telegram.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-openai\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/openai.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-microsoft\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/microsoft.srs\",\"download_detour\": \"direct\"},\n {\"tag\": \"geosite-tiktok\",\"type\": \"remote\",\"format\": \"binary\",\"url\": \"https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/tiktok.srs\",\"download_detour\": \"direct\"}\n ]\n }\n}\n', 'json', '{}', '2025-08-12 23:30:10.016', '2025-08-15 22:01:10.801'); +INSERT INTO `subscribe_application` (`id`, `name`, `icon`, `description`, `scheme`, `user_agent`, `is_default`, `subscribe_template`, `output_format`, `download_link`, `created_at`, `updated_at`) VALUES (5, 'Surge', '', '', 'surge:///install-config?url=${encodeURIComponent(url)}', 'Surge', 0, '{{- $GiB := 1073741824.0 -}}\n{{- $used := printf \"%.2f\" (divf (add (.UserInfo.Download | default 0 | float64) (.UserInfo.Upload | default 0 | float64)) $GiB) -}}\n{{- $traffic := (.UserInfo.Traffic | default 0 | float64) -}}\n{{- $total := printf \"%.2f\" (divf $traffic $GiB) -}}\n\n{{- $exp := \"\" -}}\n{{- $expStr := printf \"%v\" .UserInfo.ExpiredAt -}}\n{{- if regexMatch `^[0-9]+$` $expStr -}}\n {{- $ts := $expStr | float64 -}}\n {{- $sec := ternary (divf $ts 1000.0) $ts (ge (len $expStr) 13) -}}\n {{- $exp = (date \"2006-01-02 15:04:05\" (unixEpoch ($sec | int64))) -}}\n{{- else -}}\n {{- $exp = $expStr -}}\n{{- end -}}\n\n{{- $supportedProxies := list -}}\n{{- range $proxy := .Proxies -}}\n {{- if or (eq $proxy.Type \"shadowsocks\") (eq $proxy.Type \"vmess\") (eq $proxy.Type \"trojan\") (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"tuic\") -}}\n {{- $supportedProxies = append $supportedProxies $proxy -}}\n {{- end -}}\n{{- end -}}\n\n{{- $proxyNames := \"\" -}}\n{{- range $proxy := $supportedProxies -}}\n {{- if eq $proxyNames \"\" -}}\n {{- $proxyNames = $proxy.Name -}}\n {{- else -}}\n {{- $proxyNames = printf \"%s, %s\" $proxyNames $proxy.Name -}}\n {{- end -}}\n{{- end -}}\n\n#!MANAGED-CONFIG {{ .UserInfo.SubscribeURL }} interval=86400\n\n[General]\nloglevel = notify\nexternal-controller-access = perlnk@0.0.0.0:6170\nexclude-simple-hostnames = true\nshow-error-page-for-reject = true\nudp-priority = true\nudp-policy-not-supported-behaviour = reject\nipv6 = true\nipv6-vif = auto\nproxy-test-url = http://www.gstatic.com/generate_204\ninternet-test-url = http://connectivitycheck.platform.hicloud.com/generate_204\ntest-timeout = 5\ndns-server = system, 119.29.29.29, 223.5.5.5\nencrypted-dns-server = https://dns.alidns.com/dns-query\nhijack-dns = 8.8.8.8:53, 8.8.4.4:53, 1.1.1.1:53, 1.0.0.1:53\nskip-proxy = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 127.0.0.0/8, localhost, *.local\nalways-real-ip = *.lan, lens.l.google.com, *.srv.nintendo.net, *.stun.playstation.net, *.xboxlive.com, xbox.*.*.microsoft.com, *.msftncsi.com, *.msftconnecttest.com\n\n# > Surge Mac Parameters\nhttp-listen = 0.0.0.0:6088\nsocks5-listen = 0.0.0.0:6089\n\n# > Surge iOS Parameters\nallow-wifi-access = true\nallow-hotspot-access = true\nwifi-access-http-port = 6088\nwifi-access-socks5-port = 6089\n\n[Panel]\nSubscribeInfo = title={{ .SiteName }} - {{ .SubscribeName }}, content=官方网站: perlnk.com \\n已用流量: {{ $used }} GiB/{{ $total }} GiB \\n到期时间: {{ $exp }}, style=info\n\n[Proxy]\n{{- range $proxy := $supportedProxies }}\n {{- $server := $proxy.Server -}}\n {{- if and (contains $proxy.Server \":\") (not (hasPrefix \"[\" $proxy.Server)) -}}\n {{- $server = printf \"[%s]\" $proxy.Server -}}\n {{- end -}}\n\n {{- $sni := default \"\" $proxy.SNI -}}\n {{- if eq $sni \"\" -}}\n {{- $sni = default \"\" $proxy.Host -}}\n {{- end -}}\n {{- if and (eq $sni \"\") (not (or (regexMatch \"^[0-9]+\\\\.[0-9]+\\\\.[0-9]+\\\\.[0-9]+$\" $proxy.Server) (contains $proxy.Server \":\"))) -}}\n {{- $sni = $proxy.Server -}}\n {{- end -}}\n\n {{- $password := $.UserInfo.Password -}}\n {{- if and (eq $proxy.Type \"shadowsocks\") (ne (default \"\" $proxy.ServerKey) \"\") -}}\n {{- $method := $proxy.Method -}}\n {{- if or (hasPrefix \"2022-blake3-\" $method) (eq $method \"2022-blake3-aes-128-gcm\") (eq $method \"2022-blake3-aes-256-gcm\") -}}\n {{- $userKeyLen := ternary 16 32 (hasSuffix \"128-gcm\" $method) -}}\n {{- $pwdStr := printf \"%s\" $password -}}\n {{- $userKey := ternary $pwdStr (trunc $userKeyLen $pwdStr) (le (len $pwdStr) $userKeyLen) -}}\n {{- $serverB64 := b64enc $proxy.ServerKey -}}\n {{- $userB64 := b64enc $userKey -}}\n {{- $password = printf \"%s:%s\" $serverB64 $userB64 -}}\n {{- end -}}\n {{- end -}}\n\n {{- $common := \"udp-relay=true, tfo=true\" -}}\n\n {{- if eq $proxy.Type \"shadowsocks\" }}\n{{ $proxy.Name }} = ss, {{ $server }}, {{ $proxy.Port }}, encrypt-method={{ default \"aes-128-gcm\" $proxy.Method }}, password={{ $password }}{{- if ne (default \"\" $proxy.Transport) \"\" }}, obfs={{ $proxy.Transport }}, obfs-host={{ $sni }}{{- end }}, {{ $common }}\n {{- else if eq $proxy.Type \"vmess\" }}\n{{ $proxy.Name }} = vmess, {{ $server }}, {{ $proxy.Port }}, username={{ $password }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, ws=true{{- if ne (default \"\" $proxy.Path) \"\" }}, ws-path={{ $proxy.Path }}{{- end }}{{- if ne (default \"\" $proxy.Host) \"\" }}, ws-headers=\"Host:{{ $proxy.Host }}\"{{- end }}{{- else if eq $proxy.Transport \"grpc\" }}, grpc=true{{- if ne (default \"\" $proxy.ServiceName) \"\" }}, grpc-service-name={{ $proxy.ServiceName }}{{- end }}{{- end }}{{- if or (eq $proxy.Security \"tls\") (eq $proxy.Security \"reality\") }}, tls=true{{- end }}{{- if ne $sni \"\" }}, sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify=true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, fingerprint={{ $proxy.Fingerprint }}{{- end }}, {{ $common }}\n {{- else if eq $proxy.Type \"vless\" }}\n{{ $proxy.Name }} = vless, {{ $server }}, {{ $proxy.Port }}, username={{ $password }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, ws=true{{- if ne (default \"\" $proxy.Path) \"\" }}, ws-path={{ $proxy.Path }}{{- end }}{{- if ne (default \"\" $proxy.Host) \"\" }}, ws-headers=\"Host:{{ $proxy.Host }}\"{{- end }}{{- else if eq $proxy.Transport \"grpc\" }}, grpc=true{{- if ne (default \"\" $proxy.ServiceName) \"\" }}, grpc-service-name={{ $proxy.ServiceName }}{{- end }}{{- end }}{{- if ne $sni \"\" }}, sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify=true{{- end }}{{- if ne (default \"\" $proxy.Flow) \"\" }}, flow={{ $proxy.Flow }}{{- end }}, {{ $common }}\n {{- else if eq $proxy.Type \"trojan\" }}\n{{ $proxy.Name }} = trojan, {{ $server }}, {{ $proxy.Port }}, password={{ $password }}{{- if or (eq $proxy.Transport \"ws\") (eq $proxy.Transport \"websocket\") }}, ws=true{{- if ne (default \"\" $proxy.Path) \"\" }}, ws-path={{ $proxy.Path }}{{- end }}{{- if ne (default \"\" $proxy.Host) \"\" }}, ws-headers=\"Host:{{ $proxy.Host }}\"{{- end }}{{- else if eq $proxy.Transport \"grpc\" }}, grpc=true{{- if ne (default \"\" $proxy.ServiceName) \"\" }}, grpc-service-name={{ $proxy.ServiceName }}{{- end }}{{- end }}{{- if ne $sni \"\" }}, sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify=true{{- end }}{{- if ne (default \"\" $proxy.Fingerprint) \"\" }}, fingerprint={{ $proxy.Fingerprint }}{{- end }}, {{ $common }}\n {{- else if or (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"hy2\") }}\n{{ $proxy.Name }} = hysteria2, {{ $server }}, {{ $proxy.Port }}, password={{ $password }}{{- if ne $sni \"\" }}, sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify=true{{- end }}{{- if ne (default \"\" $proxy.ObfsPassword) \"\" }}, obfs=salamander, obfs-password={{ $proxy.ObfsPassword }}{{- end }}{{- if ne (default \"\" $proxy.HopPorts) \"\" }}, ports={{ $proxy.HopPorts }}{{- end }}{{- if ne (default 0 $proxy.HopInterval) 0 }}, hop-interval={{ $proxy.HopInterval }}{{- end }}, {{ $common }}\n {{- else if eq $proxy.Type \"tuic\" }}\n{{ $proxy.Name }} = tuic, {{ $server }}, {{ $proxy.Port }}, uuid={{ default \"\" $proxy.ServerKey }}, password={{ $password }}{{- if ne $sni \"\" }}, sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify=true{{- end }}{{- if $proxy.DisableSNI }}, disable-sni=true{{- end }}{{- if $proxy.ReduceRtt }}, reduce-rtt=true{{- end }}{{- if ne (default \"\" $proxy.UDPRelayMode) \"\" }}, udp-relay-mode={{ $proxy.UDPRelayMode }}{{- end }}{{- if ne (default \"\" $proxy.CongestionController) \"\" }}, congestion-controller={{ $proxy.CongestionController }}{{- end }}, {{ $common }}\n {{- else if eq $proxy.Type \"wireguard\" }}\n{{ $proxy.Name }} = wireguard, {{ $server }}, {{ $proxy.Port }}, private-key={{ default \"\" $proxy.ServerKey }}, public-key={{ default \"\" $proxy.RealityPublicKey }}{{- if ne (default \"\" $proxy.Path) \"\" }}, preshared-key={{ $proxy.Path }}{{- end }}{{- if ne (default \"\" $proxy.RealityServerAddr) \"\" }}, ip={{ $proxy.RealityServerAddr }}{{- end }}{{- if ne (default 0 $proxy.RealityServerPort) 0 }}, ipv6={{ $proxy.RealityServerPort }}{{- end }}, {{ $common }}\n {{- else if eq $proxy.Type \"anytls\" }}\n{{ $proxy.Name }} = anytls, {{ $server }}, {{ $proxy.Port }}, password={{ $password }}{{- if ne $sni \"\" }}, sni={{ $sni }}{{- end }}{{- if $proxy.AllowInsecure }}, skip-cert-verify=true{{- end }}, {{ $common }}\n {{- else }}\n{{ $proxy.Name }} = {{ $proxy.Type }}, {{ $server }}, {{ $proxy.Port }}, {{ $common }}\n {{- end }}\n{{- end }}\n\n[Proxy Group]\n🚀 Proxy = select, 🌏 Auto, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🍎 Apple = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🔍 Google = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🪟 Microsoft = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n📺 GlobalMedia = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🤖 AI = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🪙 Crypto = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🎮 Game = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n📟 Telegram = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🇨🇳 China = select, 🎯 Direct, 🚀 Proxy, include-other-group=🇺🇳 Nodes\n🐠 Final = select, 🚀 Proxy, 🎯 Direct, include-other-group=🇺🇳 Nodes\n🌏 Auto = smart, include-other-group=🇺🇳 Nodes\n🎯 Direct = select, DIRECT, hidden=1\n🇺🇳 Nodes = select, {{ $proxyNames }}, hidden=1\n\n[Rule]\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Apple/Apple_All.list, 🍎 Apple\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Google/Google.list, 🔍 Google\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GitHub/GitHub.list, 🪟 Microsoft\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Microsoft/Microsoft.list, 🪟 Microsoft\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/HBO/HBO.list, 📺 GlobalMedia\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Disney/Disney.list, 📺 GlobalMedia\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/TikTok/TikTok.list, 📺 GlobalMedia\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Netflix/Netflix.list, 📺 GlobalMedia\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/GlobalMedia/GlobalMedia_All_No_Resolve.list, 📺 GlobalMedia\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Telegram/Telegram.list, 📟 Telegram\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/OpenAI/OpenAI.list, 🤖 AI\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Gemini/Gemini.list, 🤖 AI\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Copilot/Copilot.list, 🤖 AI\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Claude/Claude.list, 🤖 AI\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Crypto/Crypto.list, 🪙 Crypto\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Cryptocurrency/Cryptocurrency.list, 🪙 Crypto\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Game/Game.list, 🎮 Game\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Global/Global_All_No_Resolve.list, 🚀 Proxy\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/ChinaMax/ChinaMax_All_No_Resolve.list, 🇨🇳 China\nRULE-SET, https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/refs/heads/master/rule/Surge/Lan/Lan.list, 🎯 Direct\n\nGEOIP, CN, 🇨🇳 China\nFINAL, 🐠 Final, dns-failed\n\n[URL Rewrite]\n^https?:\\/\\/(www.)?g\\.cn https://www.google.com 302\n^https?:\\/\\/(www.)?google\\.cn https://www.google.com 302\n\n{{- range $proxy := $supportedProxies }}\n {{- if not (or (eq $proxy.Type \"shadowsocks\") (eq $proxy.Type \"vmess\") (eq $proxy.Type \"trojan\") (eq $proxy.Type \"hysteria2\") (eq $proxy.Type \"tuic\")) }}\n# Skipped (unsupported by Surge): {{ $proxy.Name }} ({{ $proxy.Type }})\n {{- end }}\n{{- end }}', 'conf', '{}', '2025-08-13 00:12:37.809', '2025-08-15 22:00:50.528'); +COMMIT; diff --git a/initialize/migrate/database/02102_subscribe_config.down.sql b/initialize/migrate/database/02102_subscribe_config.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02102_subscribe_config.up.sql b/initialize/migrate/database/02102_subscribe_config.up.sql new file mode 100644 index 0000000..4460100 --- /dev/null +++ b/initialize/migrate/database/02102_subscribe_config.up.sql @@ -0,0 +1,4 @@ +INSERT IGNORE INTO `system` (`id`, `category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +VALUES + (42, 'subscribe', 'UserAgentLimit', 'false', 'bool', 'User Agent Limit', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'), + (43, 'subscribe', 'UserAgentList', '', 'string', 'User Agent List', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'); \ No newline at end of file diff --git a/initialize/migrate/database/02103_delete_application.down.sql b/initialize/migrate/database/02103_delete_application.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02103_delete_application.up.sql b/initialize/migrate/database/02103_delete_application.up.sql new file mode 100644 index 0000000..1a3a778 --- /dev/null +++ b/initialize/migrate/database/02103_delete_application.up.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS `application`; +DROP TABLE IF EXISTS `application_version`; +DROP TABLE IF EXISTS `application_config`; \ No newline at end of file diff --git a/initialize/migrate/database/02104_system_log.down.sql b/initialize/migrate/database/02104_system_log.down.sql new file mode 100644 index 0000000..2682c97 --- /dev/null +++ b/initialize/migrate/database/02104_system_log.down.sql @@ -0,0 +1,106 @@ +CREATE TABLE IF NOT EXISTS `user_balance_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `amount` bigint NOT NULL COMMENT 'Amount', + `type` tinyint(1) NOT NULL COMMENT 'Type: 1: Recharge 2: Withdraw 3: Payment 4: Refund 5: Reward', + `order_id` bigint DEFAULT NULL COMMENT 'Order ID', + `balance` bigint NOT NULL COMMENT 'Balance', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_commission_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `order_no` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.', + `amount` bigint NOT NULL COMMENT 'Amount', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_gift_amount_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `user_subscribe_id` bigint DEFAULT NULL COMMENT 'Deduction User Subscribe ID', + `order_no` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.', + `type` tinyint(1) NOT NULL COMMENT 'Type: 1: Increase 2: Reduce', + `amount` bigint NOT NULL COMMENT 'Amount', + `balance` bigint NOT NULL COMMENT 'Balance', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Remark', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_login_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `login_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Login IP', + `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent', + `success` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Login Success', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_reset_subscribe_log` +( + `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `user_id` BIGINT NOT NULL COMMENT 'User ID', + `type` TINYINT(1) NOT NULL COMMENT 'Type: 1: Auto 2: Advance 3: Paid', + `order_no` VARCHAR(255) DEFAULT NULL COMMENT 'Order No.', + `user_subscribe_id` BIGINT NOT NULL COMMENT 'User Subscribe ID', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation Time', + INDEX `idx_user_id` (`user_id`), + INDEX `idx_user_subscribe_id` (`user_subscribe_id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `user_subscribe_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT 'User ID', + `user_subscribe_id` bigint NOT NULL COMMENT 'User Subscribe ID', + `token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Token', + `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'IP', + `user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_user_subscribe_id` (`user_subscribe_id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `message_log` +( + `id` bigint NOT NULL AUTO_INCREMENT, + `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'email' COMMENT 'Message Type', + `platform` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'smtp' COMMENT 'Platform', + `to` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'To', + `subject` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subject', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Content', + `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Status', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) + ) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_general_ci; + +DROP TABLE IF EXISTS `system_logs`; \ No newline at end of file diff --git a/initialize/migrate/database/02104_system_log.up.sql b/initialize/migrate/database/02104_system_log.up.sql new file mode 100644 index 0000000..b518e68 --- /dev/null +++ b/initialize/migrate/database/02104_system_log.up.sql @@ -0,0 +1,19 @@ +DROP TABLE IF EXISTS `user_balance_log`; +DROP TABLE IF EXISTS `user_commission_log`; +DROP TABLE IF EXISTS `user_gift_amount_log`; +DROP TABLE IF EXISTS `user_login_log`; +DROP TABLE IF EXISTS `user_reset_subscribe_log`; +DROP TABLE IF EXISTS `user_subscribe_log`; +DROP TABLE IF EXISTS `message_log`; +DROP TABLE IF EXISTS `system_logs`; +CREATE TABLE `system_logs` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `type` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Log Type: 1: Email Message 2: Mobile Message 3: Subscribe 4: Subscribe Traffic 5: Server Traffic 6: Login 7: Register 8: Balance 9: Commission 10: Reset Subscribe 11: Gift', + `date` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Log Date', + `object_id` bigint NOT NULL DEFAULT '0' COMMENT 'Object ID', + `content` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Log Content', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', + PRIMARY KEY (`id`), + KEY `idx_type` (`type`), + KEY `idx_object_id` (`object_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; \ No newline at end of file diff --git a/initialize/migrate/database/02105_node.down.sql b/initialize/migrate/database/02105_node.down.sql new file mode 100644 index 0000000..210462e --- /dev/null +++ b/initialize/migrate/database/02105_node.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS `nodes`; +DROP TABLE IF EXISTS `servers`; diff --git a/initialize/migrate/database/02105_node.up.sql b/initialize/migrate/database/02105_node.up.sql new file mode 100644 index 0000000..c9c310b --- /dev/null +++ b/initialize/migrate/database/02105_node.up.sql @@ -0,0 +1,28 @@ +CREATE TABLE IF NOT EXISTS `servers` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Name', + `country` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Country', + `city` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'City', + `ratio` decimal(4,2) NOT NULL DEFAULT '0.00' COMMENT 'Traffic Ratio', + `address` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Address', + `sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort', + `protocols` text COLLATE utf8mb4_general_ci COMMENT 'Protocol', + `last_reported_at` datetime(3) DEFAULT NULL COMMENT 'Last Reported Time', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `nodes` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Node Name', + `tags` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Tags', + `port` smallint unsigned NOT NULL DEFAULT '0' COMMENT 'Connect Port', + `address` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Connect Address', + `server_id` bigint NOT NULL DEFAULT '0' COMMENT 'Server ID', + `protocol` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Protocol', + `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enabled', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/initialize/migrate/database/02106_subscribe.down.sql b/initialize/migrate/database/02106_subscribe.down.sql new file mode 100644 index 0000000..20984b7 --- /dev/null +++ b/initialize/migrate/database/02106_subscribe.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE `subscribe` +DROP COLUMN `nodes`, + DROP COLUMN `node_tags`, + ADD COLUMN `server` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Server', + ADD COLUMN `server_group` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Server Group'; diff --git a/initialize/migrate/database/02106_subscribe.up.sql b/initialize/migrate/database/02106_subscribe.up.sql new file mode 100644 index 0000000..28f5db5 --- /dev/null +++ b/initialize/migrate/database/02106_subscribe.up.sql @@ -0,0 +1,7 @@ +ALTER TABLE `subscribe` +ADD COLUMN `nodes` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Node IDs', +ADD COLUMN `node_tags` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Node Tags', +DROP COLUMN `server`, +DROP COLUMN `server_group`; + +DROP TABLE IF EXISTS `server_rule_group`; diff --git a/initialize/migrate/database/02107_log_setting.down.sql b/initialize/migrate/database/02107_log_setting.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02107_log_setting.up.sql b/initialize/migrate/database/02107_log_setting.up.sql new file mode 100644 index 0000000..c1bc8e2 --- /dev/null +++ b/initialize/migrate/database/02107_log_setting.up.sql @@ -0,0 +1,4 @@ +INSERT IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +VALUES + ('log', 'AutoClear', 'true', 'bool', 'Auto Clear Log', '2025-04-22 14:25:16.637', '2025-04-22 14:25:16.637'), + ('log', 'ClearDays', '7', 'int', 'Clear Days', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'); \ No newline at end of file diff --git a/initialize/migrate/database/02108_user_referral.down.sql b/initialize/migrate/database/02108_user_referral.down.sql new file mode 100644 index 0000000..3bdac5b --- /dev/null +++ b/initialize/migrate/database/02108_user_referral.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE `user` +DROP COLUMN `referral_percentage`, +DROP COLUMN `only_first_purchase`; \ No newline at end of file diff --git a/initialize/migrate/database/02108_user_referral.up.sql b/initialize/migrate/database/02108_user_referral.up.sql new file mode 100644 index 0000000..e50f765 --- /dev/null +++ b/initialize/migrate/database/02108_user_referral.up.sql @@ -0,0 +1,7 @@ +ALTER TABLE `user` + ADD COLUMN `referral_percentage` TINYINT UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'Referral Percentage' + AFTER `commission`, + ADD COLUMN `only_first_purchase` TINYINT(1) NOT NULL DEFAULT 1 + COMMENT 'Only First Purchase' + AFTER `referral_percentage`; diff --git a/initialize/migrate/database/02109_node_sort.down.sql b/initialize/migrate/database/02109_node_sort.down.sql new file mode 100644 index 0000000..4413646 --- /dev/null +++ b/initialize/migrate/database/02109_node_sort.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `nodes` +DROP COLUMN `sort`; \ No newline at end of file diff --git a/initialize/migrate/database/02109_node_sort.up.sql b/initialize/migrate/database/02109_node_sort.up.sql new file mode 100644 index 0000000..1a993d0 --- /dev/null +++ b/initialize/migrate/database/02109_node_sort.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `nodes` + ADD COLUMN `sort` INT UNSIGNED NOT NULL DEFAULT 0 + COMMENT 'Sort' AFTER `enabled`; \ No newline at end of file diff --git a/initialize/migrate/database/02110_traffic_log_index.down.sql b/initialize/migrate/database/02110_traffic_log_index.down.sql new file mode 100644 index 0000000..f42807c --- /dev/null +++ b/initialize/migrate/database/02110_traffic_log_index.down.sql @@ -0,0 +1 @@ +DROP INDEX idx_traffic_log_time_user_sub ON traffic_log; diff --git a/initialize/migrate/database/02110_traffic_log_index.up.sql b/initialize/migrate/database/02110_traffic_log_index.up.sql new file mode 100644 index 0000000..2cf61f2 --- /dev/null +++ b/initialize/migrate/database/02110_traffic_log_index.up.sql @@ -0,0 +1 @@ +CREATE INDEX idx_traffic_log_time_user_sub ON traffic_log (timestamp, user_id, subscribe_id); diff --git a/initialize/migrate/database/02111_clear_table.down.sql b/initialize/migrate/database/02111_clear_table.down.sql new file mode 100644 index 0000000..85c9f3f --- /dev/null +++ b/initialize/migrate/database/02111_clear_table.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS `subscribe_type`; +DROP TABLE IF EXISTS `sms`; \ No newline at end of file diff --git a/initialize/migrate/database/02111_clear_table.up.sql b/initialize/migrate/database/02111_clear_table.up.sql new file mode 100644 index 0000000..85c9f3f --- /dev/null +++ b/initialize/migrate/database/02111_clear_table.up.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS `subscribe_type`; +DROP TABLE IF EXISTS `sms`; \ No newline at end of file diff --git a/initialize/migrate/database/02112_subscribe.down.sql b/initialize/migrate/database/02112_subscribe.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02112_subscribe.up.sql b/initialize/migrate/database/02112_subscribe.up.sql new file mode 100644 index 0000000..1a79dbc --- /dev/null +++ b/initialize/migrate/database/02112_subscribe.up.sql @@ -0,0 +1,7 @@ +ALTER TABLE `subscribe` +DROP COLUMN `group_id`, +ADD COLUMN `language` VARCHAR(255) NOT NULL DEFAULT '' + COMMENT 'Language' + AFTER `name`; + +DROP TABLE IF EXISTS `subscribe_group`; \ No newline at end of file diff --git a/initialize/migrate/database/02113_task.down.sql b/initialize/migrate/database/02113_task.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02113_task.up.sql b/initialize/migrate/database/02113_task.up.sql new file mode 100644 index 0000000..4e7b170 --- /dev/null +++ b/initialize/migrate/database/02113_task.up.sql @@ -0,0 +1,14 @@ +DROP TABLE IF EXISTS `email_task`; +CREATE TABLE `task` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `type` tinyint NOT NULL COMMENT 'Task Type', + `scope` text COLLATE utf8mb4_general_ci COMMENT 'Task Scope', + `content` text COLLATE utf8mb4_general_ci COMMENT 'Task Content', + `status` tinyint NOT NULL DEFAULT '0' COMMENT 'Task Status: 0: Pending, 1: In Progress, 2: Completed, 3: Failed', + `errors` text COLLATE utf8mb4_general_ci COMMENT 'Task Errors', + `total` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Total Number', + `current` bigint unsigned NOT NULL DEFAULT '0' COMMENT 'Current Number', + `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', + `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; \ No newline at end of file diff --git a/initialize/migrate/database/02114_node_config.down.sql b/initialize/migrate/database/02114_node_config.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/initialize/migrate/database/02114_node_config.up.sql b/initialize/migrate/database/02114_node_config.up.sql new file mode 100644 index 0000000..1289647 --- /dev/null +++ b/initialize/migrate/database/02114_node_config.up.sql @@ -0,0 +1,8 @@ +INSERT +IGNORE INTO `system` (`category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`) +VALUE + ('server', 'TrafficReportThreshold', '0', 'int', 'Traffic report threshold', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'), + ('server', 'IPStrategy', '', 'string', 'IP Strategy', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'), + ('server', 'DNS', '', 'string', 'DNS', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'), + ('server', 'Block', '', 'string', 'Block', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'), + ('server', 'Outbound', '', 'string', 'Proxy Outbound', '2025-04-22 14:25:16.637','2025-04-22 14:25:16.637'); \ No newline at end of file diff --git a/initialize/migrate/database/ppanel.sql b/initialize/migrate/database/ppanel.sql deleted file mode 100644 index f15cdf9..0000000 --- a/initialize/migrate/database/ppanel.sql +++ /dev/null @@ -1,562 +0,0 @@ -SET NAMES utf8mb4; -SET FOREIGN_KEY_CHECKS = 0; - --- ---------------------------- --- Table structure for ads --- ---------------------------- -CREATE TABLE IF NOT EXISTS `ads` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `title` varchar(255) COLLATE utf8mb4_german2_ci NOT NULL DEFAULT '' COMMENT 'Ads title', - `type` varchar(255) COLLATE utf8mb4_german2_ci NOT NULL DEFAULT '' COMMENT 'Ads type', - `content` text COLLATE utf8mb4_german2_ci COMMENT 'Ads content', - `target_url` varchar(512) COLLATE utf8mb4_german2_ci DEFAULT '' COMMENT 'Ads target url', - `start_time` datetime DEFAULT NULL COMMENT 'Ads start time', - `end_time` datetime DEFAULT NULL COMMENT 'Ads end time', - `status` tinyint(1) DEFAULT '0' COMMENT 'Ads status,0 disable,1 enable', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci; - - --- ---------------------------- --- Table structure for announcement --- ---------------------------- -CREATE TABLE IF NOT EXISTS `announcement` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `title` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Title', - `content` text COLLATE utf8mb4_general_ci COMMENT 'Content', - `show` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Show', - `pinned` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Pinned', - `popup` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Popup', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; --- ---------------------------- --- Table structure for application --- ---------------------------- -CREATE TABLE IF NOT EXISTS `application` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用名称', - `icon` text COLLATE utf8mb4_general_ci NOT NULL COMMENT '应用图标', - `description` text COLLATE utf8mb4_general_ci COMMENT '更新描述', - `subscribe_type` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅类型', - `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间', - `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; --- ---------------------------- --- Table structure for application_config --- ---------------------------- -CREATE TABLE IF NOT EXISTS `application_config` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `app_id` bigint NOT NULL DEFAULT '0' COMMENT 'App id', - `encryption_key` text COLLATE utf8mb4_general_ci COMMENT 'Encryption Key', - `encryption_method` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Encryption Method', - `domains` text COLLATE utf8mb4_general_ci, - `startup_picture` text COLLATE utf8mb4_general_ci, - `startup_picture_skip_time` bigint NOT NULL DEFAULT '0' COMMENT 'Startup Picture Skip Time', - `invitation_link` text COLLATE utf8mb4_general_ci COMMENT 'Invitation Link', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; --- ---------------------------- --- Table structure for application_version --- ---------------------------- -CREATE TABLE IF NOT EXISTS `application_version` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `url` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用地址', - `version` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用版本', - `platform` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '应用平台', - `is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT '默认版本', - `description` text COLLATE utf8mb4_general_ci COMMENT '更新描述', - `application_id` bigint DEFAULT NULL COMMENT '所属应用', - `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间', - `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间', - PRIMARY KEY (`id`), - KEY `fk_application_application_versions` (`application_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for coupon --- ---------------------------- -CREATE TABLE IF NOT EXISTS `coupon` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Coupon Name', - `code` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Coupon Code', - `count` bigint NOT NULL DEFAULT '0' COMMENT 'Count Limit', - `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Coupon Type: 1: Percentage 2: Fixed Amount', - `discount` bigint NOT NULL DEFAULT '0' COMMENT 'Coupon Discount', - `start_time` bigint NOT NULL DEFAULT '0' COMMENT 'Start Time', - `expire_time` bigint NOT NULL DEFAULT '0' COMMENT 'Expire Time', - `user_limit` bigint NOT NULL DEFAULT '0' COMMENT 'User Limit', - `subscribe` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subscribe Limit', - `used_count` bigint NOT NULL DEFAULT '0' COMMENT 'Used Count', - `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enable', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`), - UNIQUE KEY `uni_coupon_code` (`code`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for document --- ---------------------------- -CREATE TABLE IF NOT EXISTS `document` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `title` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Document Title', - `content` text COLLATE utf8mb4_general_ci COMMENT 'Document Content', - `tags` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Document Tags', - `show` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Show', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; --- ---------------------------- --- Table structure for auth_method --- ---------------------------- -CREATE TABLE IF NOT EXISTS `auth_method` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `method` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'method', - `config` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'OAuth Configuration', - `enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Enabled', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`), - UNIQUE KEY `uni_auth_method` (`method`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for order --- ---------------------------- -CREATE TABLE IF NOT EXISTS `order` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `parent_id` bigint DEFAULT NULL COMMENT 'Parent Order Id', - `user_id` bigint NOT NULL DEFAULT '0' COMMENT 'User Id', - `order_no` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Order No', - `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Order Type: 1: Subscribe, 2: Renewal, 3: ResetTraffic, 4: Recharge', - `quantity` bigint NOT NULL DEFAULT '1' COMMENT 'Quantity', - `price` bigint NOT NULL DEFAULT '0' COMMENT 'Original price', - `amount` bigint NOT NULL DEFAULT '0' COMMENT 'Order Amount', - `gift_amount` bigint NOT NULL DEFAULT '0' COMMENT 'User Gift Amount', - `discount` bigint NOT NULL DEFAULT '0' COMMENT 'Discount Amount', - `coupon` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Coupon', - `coupon_discount` bigint NOT NULL DEFAULT '0' COMMENT 'Coupon Discount Amount', - `commission` bigint NOT NULL DEFAULT '0' COMMENT 'Order Commission', - `payment_id` bigint NOT NULL DEFAULT '-1' COMMENT 'Payment Id', - `method` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Payment Method', - `fee_amount` bigint NOT NULL DEFAULT '0' COMMENT 'Fee Amount', - `trade_no` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Trade No', - `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Order Status: 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished', - `subscribe_id` bigint NOT NULL DEFAULT '0' COMMENT 'Subscribe Id', - `subscribe_token` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Renewal Subscribe Token', - `is_new` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is New Order', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`), - UNIQUE KEY `uni_order_order_no` (`order_no`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for payment --- ---------------------------- -CREATE TABLE IF NOT EXISTS `payment` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Payment Name', - `platform` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Payment Platform', - `description` text COLLATE utf8mb4_general_ci COMMENT 'Payment Description', - `icon` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Payment Icon', - `domain` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Notification Domain', - `config` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Payment Configuration', - `fee_mode` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Fee Mode: 0: No Fee 1: Percentage 2: Fixed Amount 3: Percentage + Fixed Amount', - `fee_percent` bigint DEFAULT '0' COMMENT 'Fee Percentage', - `fee_amount` bigint DEFAULT '0' COMMENT 'Fixed Fee Amount', - `enable` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Enabled', - `token` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Payment Token', - PRIMARY KEY (`id`), - UNIQUE KEY `uni_payment_token` (`token`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; --- ---------------------------- --- Table structure for server --- ---------------------------- -CREATE TABLE IF NOT EXISTS `server` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Node Name', - `tags` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Tags', - `country` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Country', - `city` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'City', - `latitude` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'latitude', - `longitude` varchar(128) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'longitude', - `server_addr` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Address', - `relay_mode` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'none' COMMENT 'Relay Mode', - `relay_node` text COLLATE utf8mb4_general_ci COMMENT 'Relay Node', - `speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit', - `traffic_ratio` decimal(4,2) NOT NULL DEFAULT '0.00' COMMENT 'Traffic Ratio', - `group_id` bigint DEFAULT NULL COMMENT 'Group ID', - `protocol` varchar(20) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Protocol', - `config` text COLLATE utf8mb4_general_ci COMMENT 'Config', - `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enabled', - `sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort', - `last_reported_at` datetime(3) DEFAULT NULL COMMENT 'Last Reported Time', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`), - KEY `idx_group_id` (`group_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; --- ---------------------------- --- Table structure for server_group --- ---------------------------- -CREATE TABLE IF NOT EXISTS `server_group` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Group Name', - `description` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Group Description', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; --- ---------------------------- --- Table structure for sms --- ---------------------------- -CREATE TABLE IF NOT EXISTS `sms` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `content` text COLLATE utf8mb4_general_ci, - `platform` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, - `area_code` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, - `telephone` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, - `status` tinyint(1) DEFAULT '1', - `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; --- ---------------------------- --- Table structure for subscribe --- ---------------------------- -CREATE TABLE IF NOT EXISTS `subscribe` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subscribe Name', - `description` text COLLATE utf8mb4_general_ci COMMENT 'Subscribe Description', - `unit_price` bigint NOT NULL DEFAULT '0' COMMENT 'Unit Price', - `unit_time` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Unit Time', - `discount` text COLLATE utf8mb4_general_ci COMMENT 'Discount', - `replacement` bigint NOT NULL DEFAULT '0' COMMENT 'Replacement', - `inventory` bigint NOT NULL DEFAULT '0' COMMENT 'Inventory', - `traffic` bigint NOT NULL DEFAULT '0' COMMENT 'Traffic', - `speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit', - `device_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Device Limit', - `quota` bigint NOT NULL DEFAULT '0' COMMENT 'Quota', - `group_id` bigint DEFAULT NULL COMMENT 'Group Id', - `server_group` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Server Group', - `server` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Server', - `show` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Show portal page', - `sell` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Sell', - `sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort', - `deduction_ratio` bigint DEFAULT '0' COMMENT 'Deduction Ratio', - `allow_deduction` tinyint(1) DEFAULT '1' COMMENT 'Allow deduction', - `reset_cycle` bigint DEFAULT '0' COMMENT 'Reset Cycle: 0: No Reset, 1: 1st, 2: Monthly, 3: Yearly', - `renewal_reset` tinyint(1) DEFAULT '0' COMMENT 'Renew Reset', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; --- ---------------------------- --- Table structure for subscribe_group --- ---------------------------- -CREATE TABLE IF NOT EXISTS `subscribe_group` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Group Name', - `description` text COLLATE utf8mb4_general_ci COMMENT 'Group Description', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; --- ---------------------------- --- Table structure for subscribe_type --- ---------------------------- -CREATE TABLE IF NOT EXISTS `subscribe_type` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅类型', - `mark` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '订阅标识', - `created_at` datetime(3) DEFAULT NULL COMMENT '创建时间', - `updated_at` datetime(3) DEFAULT NULL COMMENT '更新时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for system --- ---------------------------- -CREATE TABLE IF NOT EXISTS `system` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `category` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Category', - `key` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Key Name', - `value` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Key Value', - `type` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Type', - `desc` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Description', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`), - UNIQUE KEY `uni_system_key` (`key`), - KEY `index_key` (`key`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - - --- ---------------------------- --- Table structure for ticket --- ---------------------------- -CREATE TABLE IF NOT EXISTS `ticket` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `title` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Title', - `description` text COLLATE utf8mb4_general_ci COMMENT 'Description', - `user_id` bigint NOT NULL DEFAULT '0' COMMENT 'UserId', - `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Status', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for ticket_follow --- ---------------------------- -CREATE TABLE IF NOT EXISTS `ticket_follow` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `ticket_id` bigint NOT NULL DEFAULT '0' COMMENT 'TicketId', - `from` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'From', - `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Type: 1 text, 2 image', - `content` text COLLATE utf8mb4_general_ci COMMENT 'Content', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for traffic_log --- ---------------------------- -CREATE TABLE IF NOT EXISTS `traffic_log` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `server_id` bigint NOT NULL COMMENT 'Server ID', - `user_id` bigint NOT NULL COMMENT 'User ID', - `subscribe_id` bigint NOT NULL COMMENT 'Subscription ID', - `download` bigint DEFAULT '0' COMMENT 'Download Traffic', - `upload` bigint DEFAULT '0' COMMENT 'Upload Traffic', - `timestamp` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'Traffic Log Time', - PRIMARY KEY (`id`), - KEY `idx_subscribe_id` (`subscribe_id`), - KEY `idx_server_id` (`server_id`), - KEY `idx_user_id` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - - --- ---------------------------- --- Table structure for user --- ---------------------------- -CREATE TABLE IF NOT EXISTS `user` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `password` varchar(100) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'User Password', - `avatar` text COLLATE utf8mb4_general_ci COMMENT 'User Avatar', - `balance` bigint DEFAULT '0' COMMENT 'User Balance', - `telegram` bigint DEFAULT NULL COMMENT 'Telegram Account', - `refer_code` varchar(20) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Referral Code', - `referer_id` bigint DEFAULT NULL COMMENT 'Referrer ID', - `commission` bigint DEFAULT '0' COMMENT 'Commission', - `gift_amount` bigint DEFAULT '0' COMMENT 'User Gift Amount', - `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Is Account Enabled', - `is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Admin', - `valid_email` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Email Verified', - `enable_email_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Email Notifications', - `enable_telegram_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Telegram Notifications', - `enable_balance_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Balance Change Notifications', - `enable_login_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Login Notifications', - `enable_subscribe_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Subscription Notifications', - `enable_trade_notify` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Enable Trade Notifications', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - `deleted_at` datetime(3) DEFAULT NULL COMMENT 'Deletion Time', - `is_del` bigint unsigned DEFAULT NULL COMMENT '1: Normal 0: Deleted', - PRIMARY KEY (`id`), - KEY `idx_referer` (`referer_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for user_auth_methods --- ---------------------------- -CREATE TABLE IF NOT EXISTS `user_auth_methods` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL COMMENT 'User ID', - `auth_type` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Auth Type 1: apple 2: google 3: github 4: facebook 5: telegram 6: email 7: phone', - `auth_identifier` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Auth Identifier', - `verified` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Verified', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`), - UNIQUE KEY `idx_auth_identifier` (`auth_identifier`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for user_balance_log --- ---------------------------- -CREATE TABLE IF NOT EXISTS `user_balance_log` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL COMMENT 'User ID', - `amount` bigint NOT NULL COMMENT 'Amount', - `type` tinyint(1) NOT NULL COMMENT 'Type: 1: Recharge 2: Withdraw 3: Payment 4: Refund 5: Reward', - `order_id` bigint DEFAULT NULL COMMENT 'Order ID', - `balance` bigint NOT NULL COMMENT 'Balance', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for user_commission_log --- ---------------------------- -CREATE TABLE IF NOT EXISTS `user_commission_log` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL COMMENT 'User ID', - `order_no` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.', - `amount` bigint NOT NULL COMMENT 'Amount', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for user_device --- ---------------------------- -CREATE TABLE IF NOT EXISTS `user_device` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL COMMENT 'User ID', - `subscribe_id` bigint DEFAULT NULL COMMENT 'Subscribe ID', - `ip` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Ip.', - `Identifier` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device Identifier.', - `user_agent` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Device User Agent.', - `online` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Online', - `enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'EnableDeviceNumber', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for user_gift_amount_log --- ---------------------------- -CREATE TABLE IF NOT EXISTS `user_gift_amount_log` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL COMMENT 'User ID', - `user_subscribe_id` bigint DEFAULT NULL COMMENT 'Deduction User Subscribe ID', - `order_no` varchar(191) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'Order No.', - `type` tinyint(1) NOT NULL COMMENT 'Type: 1: Increase 2: Reduce', - `amount` bigint NOT NULL COMMENT 'Amount', - `balance` bigint NOT NULL COMMENT 'Balance', - `remark` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Remark', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for user_subscribe --- ---------------------------- -CREATE TABLE IF NOT EXISTS `user_subscribe` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL COMMENT 'User ID', - `order_id` bigint NOT NULL COMMENT 'Order ID', - `subscribe_id` bigint NOT NULL COMMENT 'Subscription ID', - `start_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'Subscription Start Time', - `expire_time` datetime(3) DEFAULT NULL COMMENT 'Subscription Expire Time', - `traffic` bigint DEFAULT '0' COMMENT 'Traffic', - `download` bigint DEFAULT '0' COMMENT 'Download Traffic', - `upload` bigint DEFAULT '0' COMMENT 'Upload Traffic', - `token` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Token', - `uuid` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'UUID', - `status` tinyint(1) DEFAULT '0' COMMENT 'Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - `finished_at` datetime(3) DEFAULT NULL COMMENT 'Finished At', - PRIMARY KEY (`id`), - UNIQUE KEY `uni_user_subscribe_token` (`token`), - UNIQUE KEY `uni_user_subscribe_uuid` (`uuid`), - KEY `idx_user_id` (`user_id`), - KEY `idx_order_id` (`order_id`), - KEY `idx_subscribe_id` (`subscribe_id`), - KEY `idx_token` (`token`), - KEY `idx_uuid` (`uuid`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - -CREATE TABLE IF NOT EXISTS `server_rule_group` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `name` varchar(100) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Rule Group Name', - `icon` text COLLATE utf8mb4_general_ci COMMENT 'Rule Group Icon', - `description` varchar(255) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'Rule Group Description', - `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Rule Group Enable', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`), - UNIQUE KEY `unique_name` (`name`) -- Add unique constraint to `name` -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - - --- ---------------------------- --- Table structure for user_login_log --- ---------------------------- -CREATE TABLE IF NOT EXISTS `user_login_log` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL COMMENT 'User ID', - `login_ip` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Login IP', - `user_agent` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent', - `success` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Login Success', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- ---------------------------- --- Table structure for user_subscribe_log --- ---------------------------- - -CREATE TABLE IF NOT EXISTS `user_subscribe_log` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NOT NULL COMMENT 'User ID', - `user_subscribe_id` bigint NOT NULL COMMENT 'User Subscribe ID', - `token` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'Token', - `ip` varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'IP', - `user_agent` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'UserAgent', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_user_subscribe_id` (`user_subscribe_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - - -CREATE TABLE IF NOT EXISTS `message_log` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `type` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'email' COMMENT 'Message Type', - `platform` varchar(50) COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'smtp' COMMENT 'Platform', - `to` text COLLATE utf8mb4_general_ci NOT NULL COMMENT 'To', - `subject` varchar(255) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Subject', - `content` text COLLATE utf8mb4_general_ci COMMENT 'Content', - `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Status', - `created_at` datetime(3) DEFAULT NULL COMMENT 'Create Time', - `updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - -SET NAMES utf8mb4; -SET FOREIGN_KEY_CHECKS = 0; - --- ---------------------------- --- Table structure for user_device_online_record --- ---------------------------- -CREATE TABLE IF NOT EXISTS `user_device_online_record` ( - `id` bigint NOT NULL AUTO_INCREMENT, - `user_id` bigint NULL DEFAULT NULL COMMENT 'User ID', - `identifier` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT 'Device Identifier', - `online_time` datetime(3) NULL DEFAULT NULL COMMENT 'Online Time', - `offline_time` datetime(3) NULL DEFAULT NULL COMMENT 'Offline Time', - `online_seconds` bigint NOT NULL DEFAULT '0' COMMENT 'Online Seconds ', - `duration_days` bigint NOT NULL DEFAULT '0' COMMENT 'Duration Days ', - `created_at` datetime(3) NULL DEFAULT NULL COMMENT 'Creation Time', - PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; - -SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/initialize/migrate/init.go b/initialize/migrate/init.go index 3bfeba3..d5b4344 100644 --- a/initialize/migrate/init.go +++ b/initialize/migrate/init.go @@ -1,517 +1,15 @@ package migrate import ( - "fmt" "time" - "github.com/perfect-panel/ppanel-server/internal/model/auth" - "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/model/subscribeType" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/email" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/sms" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" "gorm.io/gorm" ) -func InitPPanelSQL(db *gorm.DB) error { - logger.Info("PPanel SQL initialization started") - startTime := time.Now() - defer func() { - logger.Info("PPanel SQL initialization completed", logger.Field("duration", time.Since(startTime).String())) - - }() - return db.Transaction(func(tx *gorm.DB) error { - var err error - defer func() { - // If an error occurs, delete all tables - if err != nil { - logger.Debugf("PPanel SQL initialization completed, err: %v", err.Error()) - tables, _ := tx.Migrator().GetTables() - for _, table := range tables { - tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS `%s`", table)) - } - } - }() - // init ppanel.sql file - if err = ExecuteSQLFile(tx, "database/ppanel.sql"); err != nil { - return err - } - //Insert basic system data - if err = insertBasicSystemData(tx); err != nil { - return err - } - // insert into OAuth config - if err = insertAuthMethodConfig(tx); err != nil { - return err - } - // insert into Payment config - if err = insertPaymentConfig(tx); err != nil { - return err - } - // insert into SubscribeType - if err = insertSubscribeType(tx); err != nil { - return err - } - return err - }) -} - -func insertBasicSystemData(tx *gorm.DB) error { - if err := insertSiteConfig(tx); err != nil { - return err - } - if err := insertSubscribeConfig(tx); err != nil { - return err - } - if err := insertVerifyConfig(tx); err != nil { - return err - } - if err := insertSeverConfig(tx); err != nil { - return err - } - if err := insertInviteConfig(tx); err != nil { - return err - } - if err := insertRegisterConfig(tx); err != nil { - return err - } - if err := insertCurrencyConfig(tx); err != nil { - return err - } - if err := insertVerifyCodeConfig(tx); err != nil { - return err - } - - version := system.System{ - Category: "system", - Key: "Version", - Value: constant.Version, - Type: "string", - Desc: "System Version", - } - if err := tx.Model(&system.System{}).Save(&version).Error; err != nil { - return err - } - - return nil -} - -// insertSiteConfig -func insertSiteConfig(tx *gorm.DB) error { - siteConfig := []system.System{ - { - Category: "site", - Key: "SiteLogo", - Value: "/favicon.svg", - Type: "string", - Desc: "Site Logo", - }, - { - Category: "site", - Key: "SiteName", - Value: "Perfect Panel", - Type: "string", - Desc: "Site Name", - }, - { - Category: "site", - Key: "SiteDesc", - Value: "PPanel is a pure, professional, and perfect open-source proxy panel tool, designed to be your ideal choice for learning and practical use.", - Type: "string", - Desc: "Site Description", - }, - { - Category: "site", - Key: "Host", - Value: "", - Type: "string", - Desc: "Site Host", - }, - { - Category: "site", - Key: "Keywords", - Value: "Perfect Panel,PPanel", - Type: "string", - Desc: "Site Keywords", - }, - { - Category: "site", - Key: "CustomHTML", - Value: "", - Type: "string", - Desc: "Custom HTML", - }, - { - Category: "site", - Key: "CustomData", - Value: "{\"website\":\"\",\"contacts\":{\"email\":\"\",\"telephone\":\"\",\"address\":\"\"},\"community\":{\"telegram\":\"\",\"twitter\":\"\",\"discord\":\"\",\"instagram\":\"\",\"linkedin\":\"\",\"facebook\":\"\",\"github\":\"\"}}", - Type: "string", - Desc: "Custom data", - }, - { - Category: "tos", - Key: "TosContent", - Value: "Welcome to use Perfect Panel", - Type: "string", - Desc: "Terms of Service", - }, - { - Category: "tos", - Key: "PrivacyPolicy", - Value: "", - Type: "string", - Desc: "PrivacyPolicy", - }, - { - Category: "ad", - Key: "WebAD", - Value: "false", - Type: "bool", - Desc: "Display ad on the web", - }, - } - return tx.Model(&system.System{}).Save(&siteConfig).Error -} - -// insertSubscribeConfig -func insertSubscribeConfig(tx *gorm.DB) error { - subscribeConfig := []system.System{ - { - Category: "subscribe", - Key: "SingleModel", - Value: "false", - Type: "bool", - Desc: "是否单订阅模式", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - }, - { - Category: "subscribe", - Key: "SubscribePath", - Value: "/api/subscribe", - Type: "string", - Desc: "订阅路径", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - }, - { - Category: "subscribe", - Key: "SubscribeDomain", - Value: "", - Type: "string", - Desc: "订阅域名", - }, - { - Category: "subscribe", - Key: "PanDomain", - Value: "false", - Type: "bool", - Desc: "是否使用泛域名", - }, - } - return tx.Model(&system.System{}).Save(&subscribeConfig).Error -} - -// insertVerifyConfig -func insertVerifyConfig(tx *gorm.DB) error { - verifyConfig := []system.System{ - { - Category: "verify", - Key: "TurnstileSiteKey", - Value: "", - Type: "string", - Desc: "TurnstileSiteKey", - }, - { - Category: "verify", - Key: "TurnstileSecret", - Value: "", - Type: "string", - Desc: "TurnstileSecret", - }, - { - Category: "verify", - Key: "EnableLoginVerify", - Value: "false", - Type: "bool", - Desc: "is enable login verify", - }, - { - Category: "verify", - Key: "EnableRegisterVerify", - Value: "false", - Type: "bool", - Desc: "is enable register verify", - }, - { - Category: "verify", - Key: "EnableResetPasswordVerify", - Value: "false", - Type: "bool", - Desc: "is enable reset password verify", - }, - } - return tx.Model(&system.System{}).Save(&verifyConfig).Error -} - -// insertSeverConfig -func insertSeverConfig(tx *gorm.DB) error { - serverConfig := []system.System{ - { - Category: "server", - Key: "NodeSecret", - Value: "12345678", - Type: "string", - Desc: "node secret", - }, - { - Category: "server", - Key: "NodePullInterval", - Value: "10", - Type: "int", - Desc: "node pull interval", - }, - { - Category: "server", - Key: "NodePushInterval", - Value: "60", - Type: "int", - Desc: "node push interval", - }, - { - Category: "server", - Key: "NodeMultiplierConfig", - Value: "[]", - Type: "string", - Desc: "node multiplier config", - }, - } - return tx.Model(&system.System{}).Save(&serverConfig).Error -} - -// insertInviteConfig -func insertInviteConfig(tx *gorm.DB) error { - inviteConfig := []system.System{ - { - Category: "invite", - Key: "ForcedInvite", - Value: "false", - Type: "bool", - Desc: "Forced invite", - }, - { - Category: "invite", - Key: "ReferralPercentage", - Value: "20", - Type: "int", - Desc: "Referral percentage", - }, - { - Category: "invite", - Key: "OnlyFirstPurchase", - Value: "false", - Type: "bool", - Desc: "Only first purchase", - }, - } - return tx.Model(&system.System{}).Save(&inviteConfig).Error -} - -// insertRegisterConfig -func insertRegisterConfig(tx *gorm.DB) error { - registerConfig := []system.System{ - { - Category: "register", - Key: "StopRegister", - Value: "false", - Type: "bool", - Desc: "is stop register", - }, - { - Category: "register", - Key: "EnableTrial", - Value: "false", - Type: "bool", - Desc: "is enable trial", - }, - { - Category: "register", - Key: "TrialSubscribe", - Value: "", - Type: "int", - Desc: "Trial subscription", - }, - { - Category: "register", - Key: "TrialTime", - Value: "24", - Type: "int", - Desc: "Trial time", - }, - { - Category: "register", - Key: "TrialTimeUnit", - Value: "Hour", - Type: "string", - Desc: "Trial time unit", - }, - { - Category: "register", - Key: "EnableIpRegisterLimit", - Value: "false", - Type: "bool", - Desc: "is enable IP register limit", - }, - { - Category: "register", - Key: "IpRegisterLimit", - Value: "3", - Type: "int", - Desc: "IP Register Limit", - }, - { - Category: "register", - Key: "IpRegisterLimitDuration", - Value: "64", - Type: "int", - Desc: "IP Register Limit Duration (minutes)", - }, - } - return tx.Model(&system.System{}).Save(®isterConfig).Error -} - -// insertAuthMethodConfig -func insertAuthMethodConfig(tx *gorm.DB) error { - // insert into OAuth config - var methods []auth.Auth - methods = append(methods, []auth.Auth{ - initEmailConfig(), - initMobileConfig(), - { - Method: "apple", - Config: new(auth.AppleAuthConfig).Marshal(), - }, - { - Method: "google", - Config: new(auth.GoogleAuthConfig).Marshal(), - }, - { - Method: "github", - Config: new(auth.GithubAuthConfig).Marshal(), - }, - { - Method: "facebook", - Config: new(auth.FacebookAuthConfig).Marshal(), - }, - { - - Method: "telegram", - Config: new(auth.TelegramAuthConfig).Marshal(), - }, - { - Method: "device", - Config: new(auth.DeviceConfig).Marshal(), - }, - }...) - return tx.Model(&auth.Auth{}).Save(&methods).Error -} - -// insertPaymentConfig -func insertPaymentConfig(tx *gorm.DB) error { - enable := true - payments := []payment.Payment{ - { - Id: -1, - Name: "Balance", - Platform: "balance", - Icon: "", - Domain: "", - Config: "", - FeeMode: 0, - FeePercent: 0, - FeeAmount: 0, - Enable: &enable, - }, - } - // reset auto increment - if err := tx.Exec("ALTER TABLE `payment` AUTO_INCREMENT = 1").Error; err != nil { - logger.Errorw("Reset auto increment failed", logger.Field("error", err)) - return err - } - return tx.Model(&payment.Payment{}).Save(&payments).Error -} - -// insertSubscribeType -func insertSubscribeType(tx *gorm.DB) error { - // insert into subscribe type - var subscribeTypes []subscribeType.SubscribeType - subscribeTypes = append(subscribeTypes, []subscribeType.SubscribeType{ - { - Name: "Clash", - Mark: "Clash", - }, - { - Name: "Hiddify", - Mark: "Hiddify", - }, - { - Name: "Loon", - Mark: "Loon", - }, - { - Name: "NekoBox", - Mark: "NekoBox", - }, - { - Name: "NekoRay", - Mark: "NekoRay", - }, - { - Name: "Netch", - Mark: "Netch", - }, - { - Name: "Quantumult", - Mark: "Quantumult", - }, - { - Name: "Shadowrocket", - Mark: "Shadowrocket", - }, - { - Name: "Singbox", - Mark: "Singbox", - }, - { - Name: "Surfboard", - Mark: "Surfboard", - }, - { - Name: "Surge", - Mark: "Surge", - }, - { - Name: "V2box", - Mark: "V2box", - }, - { - Name: "V2rayN", - Mark: "V2rayN", - }, - { - Name: "V2rayNg", - Mark: "V2rayNg", - }, - }...) - // insert into payment - return tx.Save(&subscribeTypes).Error -} - // CreateAdminUser create admin user func CreateAdminUser(email, password string, tx *gorm.DB) error { enable := true @@ -542,103 +40,3 @@ func CreateAdminUser(email, password string, tx *gorm.DB) error { return nil }) } - -func initEmailConfig() auth.Auth { - enable := true - smtpConfig := new(auth.SMTPConfig) - emailConfig := auth.EmailAuthConfig{ - Platform: "smtp", - PlatformConfig: smtpConfig, - EnableVerify: false, - EnableDomainSuffix: false, - DomainSuffixList: "", - VerifyEmailTemplate: email.DefaultEmailVerifyTemplate, - ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate, - MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate, - TrafficExceedEmailTemplate: email.DefaultTrafficExceedEmailTemplate, - } - authMethod := auth.Auth{ - Method: "email", - Config: emailConfig.Marshal(), - Enabled: &enable, - } - return authMethod -} - -func initMobileConfig() auth.Auth { - cfg := new(auth.AlibabaCloudConfig) - mobileConfig := auth.MobileAuthConfig{ - Platform: sms.AlibabaCloud.String(), - PlatformConfig: cfg, - EnableWhitelist: false, - Whitelist: make([]string, 0), - } - authMethod := auth.Auth{ - Method: "mobile", - Config: mobileConfig.Marshal(), - } - return authMethod -} - -// insert into currency config -func insertCurrencyConfig(tx *gorm.DB) error { - currencyConfig := []system.System{ - { - Category: "currency", - Key: "Currency", - Value: "USD", - Type: "string", - Desc: "Currency", - }, - { - Category: "currency", - Key: "CurrencySymbol", - Value: "$", - Type: "string", - Desc: "Currency Symbol", - }, - { - Category: "currency", - Key: "CurrencyUnit", - Value: "USD", - Type: "string", - Desc: "Currency Unit", - }, - { - Category: "currency", - Key: "AccessKey", - Value: "", - Type: "string", - Desc: "Exchangerate Access Key", - }, - } - return tx.Model(&system.System{}).Save(¤cyConfig).Error -} - -// insert into verify code config -func insertVerifyCodeConfig(tx *gorm.DB) error { - verifyCodeConfig := []system.System{ - { - Category: "verify_code", - Key: "VerifyCodeExpireTime", - Value: "300", - Type: "int", - Desc: "Verify code expire time", - }, - { - Category: "verify_code", - Key: "VerifyCodeLimit", - Value: "15", - Type: "int", - Desc: "limits of verify code", - }, - { - Category: "verify_code", - Key: "VerifyCodeInterval", - Value: "60", - Type: "int", - Desc: "Interval of verify code", - }, - } - return tx.Model(&system.System{}).Save(&verifyCodeConfig).Error -} diff --git a/initialize/migrate/init_test.go b/initialize/migrate/init_test.go index 183c3f5..278a35f 100644 --- a/initialize/migrate/init_test.go +++ b/initialize/migrate/init_test.go @@ -1,37 +1 @@ package migrate - -import ( - "testing" - - "github.com/perfect-panel/ppanel-server/pkg/orm" - "gorm.io/gorm" -) - -func connMySQL() *gorm.DB { - - cfg := orm.Config{ - Addr: "127.0.0.1", - Username: "root", - Password: "mylove520", - Dbname: "ppanel", - } - db, err := orm.ConnectMysql(orm.Mysql{ - Config: cfg, - }) - if err != nil { - return nil - } - return db -} -func TestInitPPanelSQL(t *testing.T) { - t.Skipf("Skip TestInitPPanelSQL") - db := connMySQL() - if db == nil { - t.Error("connect mysql failed") - return - } - if err := InitPPanelSQL(db); err != nil { - t.Error(err) - } - t.Logf("InitPPanelSQL success") -} diff --git a/initialize/migrate/migrate.go b/initialize/migrate/migrate.go index 1e227fd..a2985f5 100644 --- a/initialize/migrate/migrate.go +++ b/initialize/migrate/migrate.go @@ -2,33 +2,28 @@ package migrate import ( "embed" - "time" + "fmt" - "github.com/perfect-panel/ppanel-server/pkg/logger" - - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/database/mysql" + "github.com/golang-migrate/migrate/v4/source/iofs" + "github.com/perfect-panel/server/pkg/logger" ) //go:embed database/*.sql var sqlFiles embed.FS +var NoChange = migrate.ErrNoChange -func Migrate(ctx *svc.ServiceContext) { - logger.Debug("SQL Migrate started") - startTime := time.Now() - defer func() { - logger.WithDuration(time.Since(startTime)).Debug("PPanel SQL Migrate completed") - }() - db := ctx.DB - if !db.Migrator().HasTable(&system.System{}) { - if err := InitPPanelSQL(db); err != nil { - logger.Error("SQL Migrate failed", logger.Field("err", err.Error())) - panic(err) - } - // create admin user - if err := CreateAdminUser(ctx.Config.Administrator.Email, ctx.Config.Administrator.Password, db); err != nil { - logger.Error("Create admin User failed", logger.Field("err", err.Error())) - panic(err) - } +func Migrate(dsn string) *migrate.Migrate { + d, err := iofs.New(sqlFiles, "database") + if err != nil { + logger.Errorf("[Migrate] iofs.New error: %v", err.Error()) + panic(err) } + client, err := migrate.NewWithSourceInstance("iofs", d, fmt.Sprintf("mysql://%s", dsn)) + if err != nil { + logger.Errorf("[Migrate] NewWithSourceInstance error: %v", err.Error()) + panic(err) + } + return client } diff --git a/initialize/migrate/migrate_test.go b/initialize/migrate/migrate_test.go new file mode 100644 index 0000000..531266e --- /dev/null +++ b/initialize/migrate/migrate_test.go @@ -0,0 +1,49 @@ +package migrate + +import ( + "testing" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/pkg/orm" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func getDSN() string { + + cfg := orm.Config{ + Addr: "127.0.0.1", + Username: "root", + Password: "mylove520", + Dbname: "vpnboard", + } + mc := orm.Mysql{ + Config: cfg, + } + return mc.Dsn() +} + +func TestMigrate(t *testing.T) { + t.Skipf("skip test") + m := Migrate(getDSN()) + err := m.Migrate(2004) + if err != nil { + t.Errorf("failed to migrate: %v", err) + } else { + t.Log("migrate success") + } +} +func TestMysql(t *testing.T) { + db, err := gorm.Open(mysql.New(mysql.Config{ + DSN: "root:mylove520@tcp(localhost:3306)/vpnboard", + })) + if err != nil { + t.Fatalf("Failed to connect to MySQL: %v", err) + } + err = db.Migrator().AutoMigrate(&node.Node{}) + if err != nil { + t.Fatalf("Failed to auto migrate: %v", err) + return + } + t.Log("MySQL connection and migration successful") +} diff --git a/initialize/migrate/patch/01703.go b/initialize/migrate/patch/01703.go deleted file mode 100644 index b49392f..0000000 --- a/initialize/migrate/patch/01703.go +++ /dev/null @@ -1,456 +0,0 @@ -package patch - -import ( - "github.com/perfect-panel/ppanel-server/initialize/migrate" - "github.com/perfect-panel/ppanel-server/internal/model/application" - "github.com/perfect-panel/ppanel-server/internal/model/auth" - "github.com/perfect-panel/ppanel-server/internal/model/log" - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/email" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/sms" - "gorm.io/gorm" -) - -func Migrate01200(db *gorm.DB) error { - var version = "0.1.2(01200)" - return db.Transaction(func(tx *gorm.DB) error { - if exists := db.Migrator().HasColumn(&user.OldUser{}, "email"); !exists { - logger.Debug("Migrate 01200 skipped", logger.Field("reason", "old user table not exists")) - return nil - } - - logger.Debug("Migrate 01200 started", logger.Field("step", "1"), logger.Field("action", "migrate old user to user auth methods")) - var users []*user.OldUser - if err := tx.Model(&user.OldUser{}).Find(&users).Error; err != nil { - return err - } - if err := tx.Migrator().AutoMigrate(&user.AuthMethods{}); err != nil { - logger.Errorw("Migrate 01200 failed", logger.Field("step", "1"), logger.Field("action", "create user auth methods table"), logger.Field("error", err.Error())) - return err - } - err := tx.Transaction(func(tx *gorm.DB) error { - for _, oldUser := range users { - if oldUser.Email == "" { - continue - } - // create user auth method - authMethod := &user.AuthMethods{ - UserId: oldUser.Id, - AuthType: "email", - AuthIdentifier: oldUser.Email, - Verified: false, - } - if err := tx.Create(authMethod).Error; err != nil { - return err - } - } - return nil - }) - if err != nil { - logger.Errorw("Migrate 01200 failed", logger.Field("step", "1"), logger.Field("action", "migrate old user to user auth methods"), logger.Field("error", err.Error())) - return err - } - logger.Debug("Migrate 01200 completed", logger.Field("step", "1"), logger.Field("action", "migrate old user to user auth methods")) - - logger.Debug("Migrate 01200 started", logger.Field("step", "2"), logger.Field("action", "exclude sql files")) - // exclude sql files - if err := migrate.ExecuteSQLFile(tx, "database/01200-patch.sql"); err != nil { - logger.Errorw("Migrate 01200 failed", logger.Field("step", "2"), logger.Field("action", "exclude sql files"), logger.Field("file", "database/01200-patch.sql"), logger.Field("error", err.Error())) - return err - } - logger.Debug("Migrate 01200 completed", logger.Field("step", "2"), logger.Field("action", "exclude sql files")) - - logger.Debug("Migrate 01200 started", logger.Field("step", "3"), logger.Field("action", "update system config")) - versionConfig := &system.System{ - Category: "system", - Key: "Version", - Value: version, - Type: "string", - Desc: "Version of the system, eg: 1.0.0(10000)", - } - // update system config - if err := tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Save(&versionConfig).Error; err != nil { - logger.Errorw("Migrate 01200 failed", logger.Field("step", "3"), logger.Field("action", "update system config"), logger.Field("error", err.Error())) - return err - } - return nil - }) -} - -func Migrate01201(db *gorm.DB) error { - version := "0.1.2(01201)" - // exclude sql files - if err := migrate.ExecuteSQLFile(db, "database/01201-patch.sql"); err != nil { - logger.Errorw("Migrate 01201 failed", logger.Field("step", "1"), logger.Field("action", "exclude sql files"), logger.Field("file", "database/01200-patch.sql"), logger.Field("error", err.Error())) - return err - } - // update system config - if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { - logger.Errorw("Migrate 01201 failed", logger.Field("step", "2"), logger.Field("action", "update system config"), logger.Field("error", err.Error())) - return err - } - return nil -} - -func Migrate01202(db *gorm.DB) error { - version := "0.1.2(01202)" - return db.Transaction(func(tx *gorm.DB) error { - // migrate email config to system config - if err := db.Migrator().AutoMigrate(&auth.Auth{}); err != nil { - logger.Errorw("Migrate01202: AutoMigrate Auth failed", logger.Field("version", version), logger.Field("error", err.Error())) - return err - } - if db.Migrator().HasColumn("oauth_config", "platform") { - if err := db.Migrator().RenameColumn("oauth_config", "platform", "method"); err != nil { - logger.Errorw("Migrate01202: RenameColumn platform to method failed", logger.Field("version", version), logger.Field("error", err.Error())) - } - } - - // init email config - if err := initEmailConfig(db); err != nil { - logger.Errorw("Migrate01202: initEmailConfig failed", logger.Field("version", version), logger.Field("error", err.Error())) - return err - } - // init mobile config - if err := initMobileConfig(db); err != nil { - logger.Errorw("Migrate01202: initMobileConfig failed", logger.Field("version", version), logger.Field("error", err.Error())) - return err - } - // drop oauth_config table - err := db.Migrator().DropTable("oauth_config") - if err != nil { - logger.Debug("Migrate01202: DropTable oauth_config failed", logger.Field("version", version), logger.Field("error", err.Error())) - } - // exclude sql files - if err := migrate.ExecuteSQLFile(db, "database/01202-patch.sql"); err != nil { - logger.Errorw("Migrate 01202 failed", logger.Field("action", "exclude sql files"), logger.Field("file", "database/012002-patch.sql"), logger.Field("error", err.Error())) - return err - } - - // update system config - if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { - logger.Errorw("Migrate 01202 failed", logger.Field("step", "2"), logger.Field("action", "update system config"), logger.Field("error", err.Error())) - return err - } - return nil - }) -} -func Migrate01203(db *gorm.DB) error { - version := "0.1.2(01203)" - return db.Transaction(func(tx *gorm.DB) error { - if err := db.AutoMigrate(&user.LoginLog{}, &user.SubscribeLog{}); err != nil { - logger.Errorw("Migrate01203: AutoMigrate LoginLog/SubscribeLog failed", logger.Field("version", version), logger.Field("error", err.Error())) - return err - } - // update version - if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { - logger.Errorw("Migrate01203: Update Version failed", logger.Field("version", version), logger.Field("error", err.Error())) - return err - } - return nil - }) -} - -func Migrate01204(db *gorm.DB) error { - version := "0.1.2(01204)" - return db.Transaction(func(tx *gorm.DB) error { - if err := db.AutoMigrate(&log.MessageLog{}); err != nil { - logger.Errorw("Migrate01204: AutoMigrate MessageLog failed", logger.Field("version", version), logger.Field("error", err.Error())) - return err - } - // Trial configuration - if err := initTrialConfig(tx); err != nil { - logger.Errorw("Migrate01204: initTrialConfig failed", logger.Field("version", version), logger.Field("error", err.Error())) - return err - } - // Add auth method with device - if err := addAuthMethodWithDevice(tx); err != nil { - logger.Errorw("Migrate01204: Add auth method with device failed", logger.Field("version", version), logger.Field("error", err.Error())) - return err - } - // update version - if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { - logger.Errorw("Migrate01204: Update Version failed", logger.Field("version", version), logger.Field("error", err.Error())) - return err - } - - return nil - }) -} - -func Migrate01205(db *gorm.DB) error { - version := "0.1.2(01205)" - return db.Transaction(func(tx *gorm.DB) error { - // Add VerifyCode public configuration - configs := []system.System{ - { - Category: "verify_code", - Key: "VerifyCodeExpireTime", - Value: "5", - Type: "int", - Desc: "Verify code expire time", - }, - { - Category: "verify_code", - Key: "VerifyCodeLimit", - Value: "15", - Type: "int", - Desc: "limits of verify code", - }, - { - Category: "verify_code", - Key: "VerifyCodeInterval", - Value: "60", - Type: "int", - Desc: "Interval of verify code", - }, - } - if err := tx.Model(&system.System{}).Save(&configs).Error; err != nil { - logger.Errorw("Migrate01205: Save VerifyCode public configuration failed", logger.Field("error", err.Error())) - return err - } - - // update version - if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { - logger.Errorw("Migrate01205: Update Version failed", logger.Field("version", version), logger.Field("error", err.Error())) - return err - } - return nil - }) -} - -func Migrate01301(db *gorm.DB) error { - version := "0.1.3(01301)" - return db.Transaction(func(tx *gorm.DB) error { - err := tx.Migrator().AlterColumn(&application.Application{}, "icon") - if err != nil { - logger.Errorw("Migrate01301: AlterColumn failed", logger.Field("error", err.Error())) - return err - } - // update version - if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { - logger.Errorw("Migrate01205: Update Version failed", logger.Field("version", version), logger.Field("error", err.Error())) - return err - } - return nil - }) -} - -func Migrate01602(db *gorm.DB) error { - version := "0.1.6(01602)" - return db.Transaction(func(tx *gorm.DB) error { - if tx.Model(&system.System{}).Where("`category` = 'tos' AND `key` = 'TosContent'").Find(&system.System{}).RowsAffected == 0 { - if err := tx.Save(&system.System{ - Category: "tos", - Key: "TosContent", - Value: "Welcome to use Perfect Panel", - Type: "string", - Desc: "Terms of Service", - }).Error; err != nil { - return err - } - } - // update version - if err := tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { - return err - } - return nil - }) -} -func Migrate01701(db *gorm.DB) error { - return db.Transaction(func(tx *gorm.DB) error { - version := "0.1.7(01701)" - if err := db.Migrator().AlterColumn(&user.User{}, "Avatar"); err != nil { - return err - } - // update version - if err := tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { - return err - } - return nil - }) - -} - -func Migrate01702(db *gorm.DB) error { - return db.Transaction(func(tx *gorm.DB) error { - version := "0.1.7(01702)" - - if tx.Model(&system.System{}).Where("`category` = 'site' AND `key` = 'Keywords'").Find(&system.System{}).RowsAffected == 0 { - if err := tx.Save(&system.System{ - Category: "site", - Key: "Keywords", - Value: "Perfect Panel,PPanel", - Type: "string", - Desc: "Keywords", - }).Error; err != nil { - return err - } - } - if tx.Model(&system.System{}).Where("`category` = 'site' AND `key` = 'CustomHTML'").Find(&system.System{}).RowsAffected == 0 { - if err := tx.Save(&system.System{ - Category: "site", - Key: "CustomHTML", - Value: "", - Type: "string", - Desc: "Custom HTML", - }).Error; err != nil { - return err - } - } - - // update version - if err := tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { - return err - } - return nil - }) -} - -func Migrate01703(db *gorm.DB) error { - version := "0.1.7(01703)" - return db.Transaction(func(tx *gorm.DB) error { - if tx.Model(&system.System{}).Where("`category` = 'tos' AND `key` = 'PrivacyPolicy'").Find(&system.System{}).RowsAffected == 0 { - if err := tx.Save(&system.System{ - Category: "tos", - Key: "PrivacyPolicy", - Value: "", - Type: "string", - Desc: "Privacy Policy", - }).Error; err != nil { - return err - } - } - return tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error - }) -} -func Migrate01704(db *gorm.DB) error { - version := "0.1.7(01704)" - - // check server table latitude column exists, if not exists, create it - if exists := db.Migrator().HasColumn(&server.Server{}, "latitude"); !exists { - if err := db.Migrator().AddColumn(&server.Server{}, "latitude"); err != nil { - logger.Errorw("Migrate 01704 failed", logger.Field("action", "add latitude column"), logger.Field("error", err.Error())) - return err - } - logger.Infow("Migrate 01704 success", logger.Field("action", "add latitude column")) - } - // check server table longitude column exists, if not exists, create it - if exists := db.Migrator().HasColumn(&server.Server{}, "longitude"); !exists { - if err := db.Migrator().AddColumn(&server.Server{}, "longitude"); err != nil { - logger.Errorw("Migrate 01704 failed", logger.Field("action", "add longitude column"), logger.Field("error", err.Error())) - return err - } - logger.Infow("Migrate 01704 success", logger.Field("action", "add longitude column")) - } - // update system config - if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { - logger.Errorw("Migrate 01704 failed", logger.Field("step", "2"), logger.Field("action", "update system config"), logger.Field("error", err.Error())) - return err - } - logger.Infow("Migrate 01704 success", logger.Field("action", "update system config")) - return nil -} - -func Migrate01705(db *gorm.DB) error { - version := "0.1.7(01705)" - // check user_device table exists, if not exists, create it - if exists := db.Migrator().HasTable(&user.Device{}); !exists { - if err := db.Migrator().CreateTable(&user.Device{}); err != nil { - logger.Errorw("Migrate 01705 failed", logger.Field("action", "create user_device table"), logger.Field("error", err.Error())) - return err - } - logger.Infow("Migrate 01705 success", logger.Field("action", "create user_device table")) - } - // check user_table exists and imei column exists, if exists, update imei column name to identifier - if exists := db.Migrator().HasColumn(&user.Device{}, "imei"); exists { - if err := db.Migrator().RenameColumn(&user.Device{}, "imei", "identifier"); err != nil { - logger.Errorw("Migrate 01705 failed", logger.Field("action", "rename imei column to identifier"), logger.Field("error", err.Error())) - return err - } - logger.Infow("Migrate 01705 success", logger.Field("action", "rename imei column to identifier")) - } - - // update system config - if err := db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error; err != nil { - logger.Errorw("Migrate 01705 failed", logger.Field("step", "2"), logger.Field("action", "update system config"), logger.Field("error", err.Error())) - return err - } - return nil -} - -func initMobileConfig(db *gorm.DB) error { - cfg := new(auth.AlibabaCloudConfig) - mobileConfig := auth.MobileAuthConfig{ - Platform: sms.AlibabaCloud.String(), - PlatformConfig: cfg.Marshal(), - EnableWhitelist: false, - Whitelist: make([]string, 0), - } - authMethod := auth.Auth{ - Method: "mobile", - Config: mobileConfig.Marshal(), - } - if err := db.Save(&authMethod).Error; err != nil { - return err - } - return nil -} - -func initEmailConfig(db *gorm.DB) error { - enable := true - smtpConfig := new(auth.SMTPConfig) - - emailConfig := auth.EmailAuthConfig{ - Platform: "smtp", - PlatformConfig: smtpConfig.Marshal(), - EnableVerify: false, - EnableDomainSuffix: false, - DomainSuffixList: "", - VerifyEmailTemplate: email.DefaultEmailVerifyTemplate, - ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate, - MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate, - } - authMethod := auth.Auth{ - Method: "email", - Config: emailConfig.Marshal(), - Enabled: &enable, - } - return db.Save(&authMethod).Error -} - -func initTrialConfig(tx *gorm.DB) error { - configs := []system.System{ - { - Category: "register", - Key: "TrialSubscribe", - Value: "", - Type: "int", - Desc: "Trial subscription", - }, - { - Category: "register", - Key: "TrialTime", - Value: "24", - Type: "int", - Desc: "Trial time", - }, - { - Category: "register", - Key: "TrialTimeUnit", - Value: "Hour", - Type: "string", - Desc: "Trial time unit", - }, - } - return tx.Model(&system.System{}).Save(&configs).Error -} - -func addAuthMethodWithDevice(tx *gorm.DB) error { - return tx.Model(&auth.Auth{}).Save(&auth.Auth{ - Method: "device", - }).Error -} diff --git a/initialize/migrate/patch/02000.go b/initialize/migrate/patch/02000.go deleted file mode 100644 index cf67ebb..0000000 --- a/initialize/migrate/patch/02000.go +++ /dev/null @@ -1,252 +0,0 @@ -package patch - -import ( - "github.com/perfect-panel/ppanel-server/internal/model/ads" - "github.com/perfect-panel/ppanel-server/internal/model/application" - "github.com/perfect-panel/ppanel-server/internal/model/auth" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "gorm.io/gorm" -) - -func Migrate02000(db *gorm.DB) error { - version := "0.2.0(02000)" - return db.Transaction(func(tx *gorm.DB) error { - if err := initDeviceConfig(tx); err != nil { - logMigrationError("Setting Device Config", err) - return err - } - logMigrationSuccess("Setting Device Config") - - if !tx.Migrator().HasTable(&ads.Ads{}) { - if err := createAdsTable(tx); err != nil { - return err - } - } - - if err := updatePaymentTable(tx); err != nil { - return err - } - - if err := tx.Migrator().AutoMigrate(&order.Order{}); err != nil { - logMigrationError("Auto Migrate Order", err) - return err - } - - return updateSystemVersion(tx, version) - }) -} - -func Migrate02001(db *gorm.DB) error { - version := "0.2.0(02001)" - return db.Transaction(func(tx *gorm.DB) error { - if tx.Model(&system.System{}).Where("`category` = 'site' AND `key` = 'CustomData'").Find(&system.System{}).RowsAffected == 0 { - if err := tx.Save(&system.System{ - Category: "site", - Key: "CustomData", - Value: "{\"website\":\"\",\"contacts\":{\"email\":\"\",\"telephone\":\"\",\"address\":\"\"},\"community\":{\"telegram\":\"\",\"twitter\":\"\",\"discord\":\"\",\"instagram\":\"\",\"linkedin\":\"\",\"facebook\":\"\",\"github\":\"\"}}", - Type: "string", - Desc: "Custom data", - }).Error; err != nil { - logMigrationError("create custom data system config", err) - return err - } - } - return updateSystemVersion(tx, version) - }) -} - -func Migrate02002(db *gorm.DB) error { - version := "0.2.0(02002)" - return db.Transaction(func(tx *gorm.DB) error { - if err := tx.Model(&system.System{}).Where("`category` = 'site' AND `key` = 'CustomData'").UpdateColumn("type", "string").Error; err != nil { - return err - } - return updateSystemVersion(tx, version) - }) -} - -func Migrate02003(db *gorm.DB) error { - version := "0.2.0(02003)" - return db.Transaction(func(tx *gorm.DB) error { - if err := addColumnIfNotExists(tx, &order.Order{}, "payment_id"); err != nil { - return err - } - if err := addColumnIfNotExists(tx, &payment.Payment{}, "platform"); err != nil { - return err - } - if err := dropColumnIfExists(tx, &payment.Payment{}, "mark"); err != nil { - return err - } - if err := addColumnIfNotExists(tx, &payment.Payment{}, "description"); err != nil { - return err - } - if err := addColumnIfNotExists(tx, &payment.Payment{}, "token"); err != nil { - return err - } - return updateSystemVersion(tx, version) - }) -} - -func Migrate02007(db *gorm.DB) error { - version := "0.2.0(02007)" - return db.Transaction(func(tx *gorm.DB) error { - if err := recreateTable(tx, &server.RuleGroup{}); err != nil { - return err - } - return updateSystemVersion(tx, version) - }) -} - -func Migrate02008(db *gorm.DB) error { - version := "0.2.0(02008)" - return db.Transaction(func(tx *gorm.DB) error { - if exists := tx.Migrator().HasColumn(&application.ApplicationConfig{}, "invitation_link"); !exists { - if err := tx.Migrator().AddColumn(&application.ApplicationConfig{}, "invitation_link"); err != nil { - logger.Errorw("Migrate 02008 failed", logger.Field("action", "add invitation_link column"), logger.Field("error", err.Error())) - return err - } - logger.Infow("Migrate 02008 success", logger.Field("action", "add invitation_link column")) - } - - if exists := tx.Migrator().HasTable(&user.DeviceOnlineRecord{}); !exists { - if err := tx.Migrator().CreateTable(&user.DeviceOnlineRecord{}); err != nil { - logger.Errorw("Migrate 02008 failed", logger.Field("action", "create device_online_record table"), logger.Field("error", err.Error())) - return err - } - } - return tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error - }) -} - -func Migrate02009(db *gorm.DB) error { - version := "0.2.0(02009)" - return db.Transaction(func(tx *gorm.DB) error { - if err := addColumnIfNotExists(tx, &user.Subscribe{}, "finished_at"); err != nil { - logger.Errorw("Migrate 02009 failed", logger.Field("action", "subscribe table add finished_at column"), logger.Field("error", err.Error())) - return err - } - return updateSystemVersion(tx, version) - }) -} - -func Migrate02010(db *gorm.DB) error { - version := "0.2.0(02010)" - return db.Transaction(func(tx *gorm.DB) error { - if err := addColumnIfNotExists(tx, &application.ApplicationConfig{}, "kr_website_id"); err != nil { - logger.Errorw("Migrate 02010 failed", logger.Field("action", "application_config table add kr_website_id column"), logger.Field("error", err.Error())) - return err - } - return updateSystemVersion(tx, version) - }) -} - -func Migrate02011(db *gorm.DB) error { - version := "0.2.0(02011)" - return db.Transaction(func(tx *gorm.DB) error { - if err := addColumnIfNotExists(tx, &user.Subscribe{}, "used_period"); err != nil { - logger.Errorw("Migrate 02011 failed", logger.Field("action", "user.Subscribe table add used_period column"), logger.Field("error", err.Error())) - return err - } - if err := addColumnIfNotExists(tx, &user.Subscribe{}, "total_period"); err != nil { - logger.Errorw("Migrate 02011 failed", logger.Field("action", "user.Subscribe table add total_period column"), logger.Field("error", err.Error())) - return err - } - return updateSystemVersion(tx, version) - }) -} - -func initDeviceConfig(db *gorm.DB) error { - cfg := new(auth.DeviceConfig) - return db.Model(&auth.Auth{}).Where("method = ?", "device").Update("config", cfg.Marshal()).Error -} - -func createAdsTable(tx *gorm.DB) error { - if err := tx.Migrator().CreateTable(&ads.Ads{}); err != nil { - logMigrationError("Create Table Ads", err) - return err - } - logMigrationSuccess("Create Table Ads") - return tx.Model(&system.System{}).Save(&system.System{ - Category: "ad", - Key: "WebAD", - Value: "false", - Type: "bool", - Desc: "Display ad on the web", - }).Error -} - -func updatePaymentTable(tx *gorm.DB) error { - if err := tx.Exec("DROP TABLE IF EXISTS `payment`").Error; err != nil { - logMigrationError("Drop Payment Table", err) - } - if err := tx.AutoMigrate(&payment.Payment{}); err != nil { - logMigrationError("Auto Migrate Payment", err) - return err - } - enable := true - return tx.Model(&payment.Payment{}).Create(&payment.Payment{ - Id: -1, - Name: "", - Platform: "balance", - Icon: "", - Domain: "", - Config: "", - FeeMode: 0, - FeePercent: 0, - FeeAmount: 0, - Enable: &enable, - }).Error -} - -func updateSystemVersion(tx *gorm.DB, version string) error { - return tx.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").Update("value", version).Error -} - -func addColumnIfNotExists(tx *gorm.DB, model interface{}, columnName string) error { - if exists := tx.Migrator().HasColumn(model, columnName); !exists { - if err := tx.Migrator().AddColumn(model, columnName); err != nil { - logMigrationError("add "+columnName+" column", err) - return err - } - logMigrationSuccess("add " + columnName + " column") - } - return nil -} - -func dropColumnIfExists(tx *gorm.DB, model interface{}, columnName string) error { - if exists := tx.Migrator().HasColumn(model, columnName); exists { - if err := tx.Migrator().DropColumn(model, columnName); err != nil { - logMigrationError("del "+columnName+" column", err) - return err - } - logMigrationSuccess("del " + columnName + " column") - } - return nil -} - -func recreateTable(tx *gorm.DB, model interface{}) error { - if exists := tx.Migrator().HasTable(model); exists { - if err := tx.Migrator().DropTable(model); err != nil { - logMigrationError("drop table", err) - return err - } - } - if err := tx.Migrator().CreateTable(model); err != nil { - logMigrationError("create table", err) - return err - } - return nil -} - -func logMigrationError(action string, err error) { - logger.Errorw("Migration failed", logger.Field("action", action), logger.Field("error", err.Error())) -} - -func logMigrationSuccess(action string) { - logger.Infow("Migration success", logger.Field("action", action)) -} diff --git a/initialize/migrate/patch/03001.go b/initialize/migrate/patch/03001.go deleted file mode 100644 index 144ba32..0000000 --- a/initialize/migrate/patch/03001.go +++ /dev/null @@ -1,30 +0,0 @@ -package patch - -import ( - "github.com/perfect-panel/ppanel-server/internal/model/application" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "gorm.io/gorm" -) - -func Migrate03001(db *gorm.DB) error { - version := "0.3.0(1)" - return db.Transaction(func(tx *gorm.DB) error { - if err := addColumnIfNotExists(tx, &user.Subscribe{}, "finished_at"); err != nil { - logger.Errorw("Migrate 03001 failed", logger.Field("action", "user.Subscribe table add finished_at column"), logger.Field("error", err.Error())) - return err - } - return updateSystemVersion(tx, version) - }) -} - -func Migrate03002(db *gorm.DB) error { - version := "0.3.0(2)" - return db.Transaction(func(tx *gorm.DB) error { - if err := addColumnIfNotExists(tx, &application.ApplicationConfig{}, "kr_website_id"); err != nil { - logger.Errorw("Migrate 03002 failed", logger.Field("action", "application.Config table add kr_website_id column"), logger.Field("error", err.Error())) - return err - } - return updateSystemVersion(tx, version) - }) -} diff --git a/initialize/mobile.go b/initialize/mobile.go index 696a725..ac784ea 100644 --- a/initialize/mobile.go +++ b/initialize/mobile.go @@ -3,14 +3,13 @@ package initialize import ( "context" "encoding/json" - "fmt" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/tool" ) func Mobile(ctx *svc.ServiceContext) { @@ -21,9 +20,7 @@ func Mobile(ctx *svc.ServiceContext) { } var cfg config.MobileConfig var mobileConfig auth.MobileAuthConfig - if err := mobileConfig.Unmarshal(method.Config); err != nil { - panic(fmt.Sprintf("failed to unmarshal mobile auth config: %v", err.Error())) - } + mobileConfig.Unmarshal(method.Config) tool.DeepCopy(&cfg, mobileConfig) cfg.Enable = *method.Enabled value, _ := json.Marshal(mobileConfig.PlatformConfig) diff --git a/initialize/mysql.go b/initialize/mysql.go index 17a45e2..723178f 100644 --- a/initialize/mysql.go +++ b/initialize/mysql.go @@ -1,10 +1 @@ package initialize - -import ( - "github.com/perfect-panel/ppanel-server/initialize/migrate" - "github.com/perfect-panel/ppanel-server/internal/svc" -) - -func Mysql(ctx *svc.ServiceContext) { - migrate.Migrate(ctx) -} diff --git a/initialize/node.go b/initialize/node.go index a8d65bb..51dce08 100644 --- a/initialize/node.go +++ b/initialize/node.go @@ -3,14 +3,13 @@ package initialize import ( "context" "encoding/json" + "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/logger" - - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/nodeMultiplier" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/nodeMultiplier" + "github.com/perfect-panel/server/pkg/tool" ) func Node(ctx *svc.ServiceContext) { @@ -19,9 +18,40 @@ func Node(ctx *svc.ServiceContext) { if err != nil { panic(err) } - var nodeConfig config.NodeConfig + var nodeConfig config.NodeDBConfig tool.SystemConfigSliceReflectToStruct(configs, &nodeConfig) - ctx.Config.Node = nodeConfig + c := config.NodeConfig{ + NodeSecret: nodeConfig.NodeSecret, + NodePullInterval: nodeConfig.NodePullInterval, + NodePushInterval: nodeConfig.NodePushInterval, + IPStrategy: nodeConfig.IPStrategy, + TrafficReportThreshold: nodeConfig.TrafficReportThreshold, + } + if nodeConfig.DNS != "" { + var dns []config.NodeDNS + err = json.Unmarshal([]byte(nodeConfig.DNS), &dns) + if err != nil { + logger.Errorf("[Node] Unmarshal DNS config error: %s", err.Error()) + panic(err) + } + c.DNS = dns + } + if nodeConfig.Block != "" { + var block []string + _ = json.Unmarshal([]byte(nodeConfig.Block), &block) + c.Block = tool.RemoveDuplicateElements(block...) + } + if nodeConfig.Outbound != "" { + var outbound []config.NodeOutbound + err = json.Unmarshal([]byte(nodeConfig.Outbound), &outbound) + if err != nil { + logger.Errorf("[Node] Unmarshal Outbound config error: %s", err.Error()) + panic(err) + } + c.Outbound = outbound + } + + ctx.Config.Node = c // Manager initialization if ctx.DB.Model(&system.System{}).Where("`key` = ?", "NodeMultiplierConfig").Find(&system.System{}).RowsAffected == 0 { @@ -39,7 +69,6 @@ func Node(ctx *svc.ServiceContext) { nodeMultiplierData, err := ctx.SystemModel.FindNodeMultiplierConfig(context.Background()) if err != nil { - logger.Error("Get Node Multiplier Config Error: ", logger.Field("error", err.Error())) return } diff --git a/initialize/oauth.go b/initialize/oauth.go index 70676ad..999d773 100644 --- a/initialize/oauth.go +++ b/initialize/oauth.go @@ -1,8 +1,8 @@ package initialize import ( - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" ) func OAuth(svc *svc.ServiceContext) { diff --git a/initialize/register.go b/initialize/register.go index d6ce119..bbe44f9 100644 --- a/initialize/register.go +++ b/initialize/register.go @@ -3,11 +3,11 @@ package initialize import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/tool" ) func Register(ctx *svc.ServiceContext) { diff --git a/initialize/site.go b/initialize/site.go index 20c82ab..6cdbdbb 100644 --- a/initialize/site.go +++ b/initialize/site.go @@ -3,11 +3,11 @@ package initialize import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/tool" ) func Site(ctx *svc.ServiceContext) { diff --git a/initialize/statistics.go b/initialize/statistics.go deleted file mode 100644 index fcb81c7..0000000 --- a/initialize/statistics.go +++ /dev/null @@ -1,57 +0,0 @@ -package initialize - -import ( - "context" - "time" - - "github.com/perfect-panel/ppanel-server/internal/model/cache" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -func TrafficDataToRedis(svcCtx *svc.ServiceContext) { - ctx := context.Background() - // 统计昨天的节点流量数据排行榜前10 - nodeData, err := svcCtx.TrafficLogModel.TopServersTrafficByDay(ctx, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-1, 0, 0, 0, 0, time.Local), 10) - if err != nil { - logger.Errorw("统计昨天的流量数据失败", logger.Field("error", err.Error())) - } - var nodeCacheData []cache.NodeTodayTrafficRank - for _, node := range nodeData { - serverInfo, err := svcCtx.ServerModel.FindOne(ctx, node.ServerId) - if err != nil { - logger.Errorw("查询节点信息失败", logger.Field("error", err.Error())) - continue - } - nodeCacheData = append(nodeCacheData, cache.NodeTodayTrafficRank{ - ID: node.ServerId, - Name: serverInfo.Name, - Upload: node.Upload, - Download: node.Download, - Total: node.Upload + node.Download, - }) - } - // 写入缓存 - if err = svcCtx.NodeCache.UpdateYesterdayNodeTotalTrafficRank(ctx, nodeCacheData); err != nil { - logger.Errorw("写入昨天的流量数据到缓存失败", logger.Field("error", err.Error())) - } - // 统计昨天的用户流量数据排行榜前10 - userData, err := svcCtx.TrafficLogModel.TopUsersTrafficByDay(ctx, time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day()-1, 0, 0, 0, 0, time.Local), 10) - if err != nil { - logger.Errorw("统计昨天的流量数据失败", logger.Field("error", err.Error())) - } - var userCacheData []cache.UserTodayTrafficRank - for _, user := range userData { - userCacheData = append(userCacheData, cache.UserTodayTrafficRank{ - SID: user.SubscribeId, - Upload: user.Upload, - Download: user.Download, - Total: user.Upload + user.Download, - }) - } - // 写入缓存 - if err = svcCtx.NodeCache.UpdateYesterdayUserTotalTrafficRank(ctx, userCacheData); err != nil { - logger.Errorw("写入昨天的流量数据到缓存失败", logger.Field("error", err.Error())) - } - logger.Infow("初始化昨天的流量数据到缓存成功") -} diff --git a/initialize/subscribe.go b/initialize/subscribe.go index a584a3d..7c5f2fe 100644 --- a/initialize/subscribe.go +++ b/initialize/subscribe.go @@ -3,11 +3,11 @@ package initialize import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/tool" ) func Subscribe(svc *svc.ServiceContext) { diff --git a/initialize/telegram.go b/initialize/telegram.go index 3a2e3a8..56c7086 100644 --- a/initialize/telegram.go +++ b/initialize/telegram.go @@ -4,14 +4,14 @@ import ( "context" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/logic/telegram" - "github.com/perfect-panel/ppanel-server/internal/model/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "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" + "github.com/perfect-panel/server/pkg/tool" ) func Telegram(svc *svc.ServiceContext) { diff --git a/initialize/verify.go b/initialize/verify.go index c7a2f1a..965e021 100644 --- a/initialize/verify.go +++ b/initialize/verify.go @@ -3,11 +3,11 @@ package initialize import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/tool" ) type verifyConfig struct { diff --git a/initialize/version.go b/initialize/version.go index fbabfac..9eac7d4 100644 --- a/initialize/version.go +++ b/initialize/version.go @@ -1,127 +1,45 @@ package initialize import ( - "fmt" - - "github.com/perfect-panel/ppanel-server/pkg/logger" + "errors" + "github.com/perfect-panel/server/internal/model/user" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/initialize/migrate/patch" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/initialize/migrate" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/orm" ) -func VerifyVersion(ctx *svc.ServiceContext) { - var configVersion system.System - err := ctx.DB.Transaction(func(db *gorm.DB) error { - db.Model(&system.System{}).Where("`category` = 'system' AND `key` = 'Version'").First(&configVersion) - if configVersion.Value != constant.Version { - // Version eg: 1.0.0(10000) - current := tool.ExtractVersionNumber(constant.Version) - sqlVersion := tool.ExtractVersionNumber(configVersion.Value) - logger.Infof("Verify System Version, current version: %d, datebase version: %d", current, sqlVersion) - if current > sqlVersion { - // Migrate to Milestone Version - // - // Migrate SQL to 0.1.7(01703) - if sqlVersion < 1705 { - if err := migrate01701(db, sqlVersion); err != nil { - return err - } - } - // 重新执行2000版本的迁移 - if sqlVersion == 2002 { - sqlVersion = 2000 - } - // Migrate SQL to 0.2.0(02000) - if sqlVersion < 2009 { - if err := migrate02000(db, sqlVersion); err != nil { - return err - } - } - // Migrate SQL to 0.3.0(03000) - if sqlVersion < 3002 { - if err := migrate03000(db, sqlVersion); err != nil { - return err - } - } - +func Migrate(ctx *svc.ServiceContext) { + mc := orm.Mysql{ + Config: ctx.Config.MySQL, + } + if err := migrate.Migrate(mc.Dsn()).Up(); err != nil { + if errors.Is(err, migrate.NoChange) { + logger.Info("[Migrate] database not change") + return + } + logger.Errorf("[Migrate] Up error: %v", err.Error()) + panic(err) + } + // if not found admin user + err := ctx.DB.Transaction(func(tx *gorm.DB) error { + var count int64 + if err := tx.Model(&user.User{}).Count(&count).Error; err != nil { + return err + } + if count == 0 { + if err := migrate.CreateAdminUser(ctx.Config.Administrator.Email, ctx.Config.Administrator.Password, tx); err != nil { + logger.Errorf("[Migrate] CreateAdminUser error: %v", err.Error()) + return err } + logger.Info("[Migrate] Create admin user success") } return nil }) if err != nil { - panic("update system version error:" + err.Error()) + panic(err) } } -func migrate01701(db *gorm.DB, sqlVersion int) error { - migrations := map[int]func(*gorm.DB) error{ - 1200: patch.Migrate01200, - 1201: patch.Migrate01201, - 1202: patch.Migrate01202, - 1203: patch.Migrate01203, - 1204: patch.Migrate01204, - 1205: patch.Migrate01205, - 1301: patch.Migrate01301, - 1602: patch.Migrate01602, - 1701: patch.Migrate01701, - 1702: patch.Migrate01702, - 1703: patch.Migrate01703, - 1704: patch.Migrate01704, - 1705: patch.Migrate01705, - } - - for v, migrate := range migrations { - if sqlVersion < v { - if err := migrate(db); err != nil { - return fmt.Errorf("migrator %d version error: %w", v, err) - } - logger.Infof(fmt.Sprintf("Migrate %d version success", v)) - } - } - return nil -} - -func migrate02000(db *gorm.DB, sqlVersion int) error { - migrations := map[int]func(*gorm.DB) error{ - 2000: patch.Migrate02000, - 2001: patch.Migrate02001, - 2002: patch.Migrate02002, - 2003: patch.Migrate02003, - 2007: patch.Migrate02007, - 2008: patch.Migrate02008, - 2009: patch.Migrate02009, - 2010: patch.Migrate02010, - 2011: patch.Migrate02011, - } - - for v, migrate := range migrations { - if sqlVersion < v { - if err := migrate(db); err != nil { - return fmt.Errorf("migrator %d version error: %w", v, err) - } - logger.Infof(fmt.Sprintf("Migrate %d version success", v)) - } - } - return nil -} - -func migrate03000(db *gorm.DB, sqlVersion int) error { - migrations := map[int]func(*gorm.DB) error{ - 3001: patch.Migrate03001, - 3002: patch.Migrate03002, - } - - for v, migrate := range migrations { - if sqlVersion < v { - if err := migrate(db); err != nil { - return fmt.Errorf("migrator %d version error: %w", v, err) - } - logger.Infof(fmt.Sprintf("Migrate %d version success", v)) - } - } - return nil -} diff --git a/internal/config/cacheKey.go b/internal/config/cacheKey.go index 02f5be9..655ce55 100644 --- a/internal/config/cacheKey.go +++ b/internal/config/cacheKey.go @@ -12,9 +12,6 @@ const SiteConfigKey = "system:site_config" // SubscribeConfigKey Subscribe Config Key const SubscribeConfigKey = "system:subscribe_config" -// ApplicationKey Application Key -const ApplicationKey = "system:application" - // RegisterConfigKey Register Config Key const RegisterConfigKey = "system:register_config" @@ -51,26 +48,12 @@ const AuthCodeCacheKey = "auth:verify:email" // AuthCodeTelephoneCacheKey Register Code Cache Key const AuthCodeTelephoneCacheKey = "auth:verify:telephone" -// ServerUserListCacheKey Server User List Cache Key -const ServerUserListCacheKey = "server:user_list:id:" - -// ServerConfigCacheKey Server Config Cache Key -const ServerConfigCacheKey = "server:config:id:" - -// CommonStat Cache Key +// CommonStatCacheKey CommonStat Cache Key const CommonStatCacheKey = "common:stat" -// ServerStatusCacheKey Server Status Cache Key -const ServerStatusCacheKey = "server:status:id:" - // ServerCountCacheKey Server Count Cache Key const ServerCountCacheKey = "server:count" -// UserBindTelegramCacheKey User Bind Telegram Cache Key -const UserBindTelegramCacheKey = "user:bind:telegram:code:" - -const CacheSmsCount = "cache:sms:count" - // SendIntervalKeyPrefix Auth Code Send Interval Key Prefix const SendIntervalKeyPrefix = "send:interval:" diff --git a/internal/config/config.go b/internal/config/config.go index 824d656..59ece74 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,8 +1,10 @@ package config import ( - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/orm" + "encoding/json" + + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/orm" ) type Config struct { @@ -19,12 +21,14 @@ type Config struct { Node NodeConfig `yaml:"Node"` Mobile MobileConfig `yaml:"Mobile"` Email EmailConfig `yaml:"Email"` + Device DeviceConfig `yaml:"device"` Verify Verify `yaml:"Verify"` VerifyCode VerifyCode `yaml:"VerifyCode"` Register RegisterConfig `yaml:"Register"` Subscribe SubscribeConfig `yaml:"Subscribe"` Invite InviteConfig `yaml:"Invite"` Telegram Telegram `yaml:"Telegram"` + Log Log `yaml:"Log"` Administrator struct { Email string `yaml:"Email" default:"admin@ppanel.dev"` Password string `yaml:"Password" default:"password"` @@ -52,9 +56,11 @@ type Verify struct { type SubscribeConfig struct { SingleModel bool `yaml:"SingleModel" default:"false"` - SubscribePath string `yaml:"SubscribePath" default:"/api/subscribe"` + SubscribePath string `yaml:"SubscribePath" default:"/v1/subscribe/config"` SubscribeDomain string `yaml:"SubscribeDomain" default:""` PanDomain bool `yaml:"PanDomain" default:"false"` + UserAgentLimit bool `yaml:"UserAgentLimit" default:"false"` + UserAgentList string `yaml:"UserAgentList" default:""` } type RegisterConfig struct { @@ -91,6 +97,14 @@ type MobileConfig struct { Whitelist []string `yaml:"whitelist"` } +type DeviceConfig struct { + Enable bool `yaml:"enable" default:"true"` + ShowAds bool `yaml:"show_ads"` + EnableSecurity bool `yaml:"enable_security"` + OnlyRealDevice bool `yaml:"only_real_device"` + SecuritySecret string `yaml:"security_secret"` +} + type SiteConfig struct { Host string `yaml:"Host" default:""` SiteName string `yaml:"SiteName" default:""` @@ -102,9 +116,76 @@ type SiteConfig struct { } type NodeConfig struct { - NodeSecret string `yaml:"NodeSecret" default:""` - NodePullInterval int64 `yaml:"NodePullInterval" default:"60"` - NodePushInterval int64 `yaml:"NodePushInterval" default:"60"` + NodeSecret string `yaml:"NodeSecret" default:""` + NodePullInterval int64 `yaml:"NodePullInterval" default:"60"` + NodePushInterval int64 `yaml:"NodePushInterval" default:"60"` + TrafficReportThreshold int64 `yaml:"TrafficReportThreshold" default:"0"` + IPStrategy string `yaml:"IPStrategy" default:""` + DNS []NodeDNS `yaml:"DNS"` + Block []string `yaml:"Block" ` + Outbound []NodeOutbound `yaml:"Outbound"` +} + +func (n *NodeConfig) Marshal() ([]byte, error) { + type Alias NodeConfig + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(n), + }) +} + +func (n *NodeConfig) Unmarshal(data []byte) error { + type Alias NodeConfig + aux := &struct { + *Alias + }{ + Alias: (*Alias)(n), + } + return json.Unmarshal(data, &aux) +} + +type NodeDNS struct { + Proto string `json:"proto"` + Address string `json:"address"` + Domains []string `json:"domains"` +} + +func (n *NodeDNS) Marshal() ([]byte, error) { + type Alias NodeDNS + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(n), + }) +} + +func (n *NodeDNS) Unmarshal(data []byte) error { + type Alias NodeDNS + aux := &struct { + *Alias + }{ + Alias: (*Alias)(n), + } + return json.Unmarshal(data, &aux) +} + +type NodeOutbound struct { + Name string `json:"name"` + Protocol string `json:"protocol"` + Address string `json:"address"` + Port int64 `json:"port"` + Password string `json:"password"` + Rules []string `json:"rules"` +} + +func (n *NodeOutbound) Marshal() ([]byte, error) { + type Alias NodeOutbound + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(n), + }) } type File struct { @@ -144,3 +225,19 @@ type VerifyCode struct { Limit int64 `yaml:"Limit" default:"15"` Interval int64 `yaml:"Interval" default:"60"` } + +type Log struct { + AutoClear bool `yaml:"AutoClear" default:"true"` + ClearDays int64 `yaml:"ClearDays" default:"7"` +} + +type NodeDBConfig struct { + NodeSecret string + NodePullInterval int64 + NodePushInterval int64 + TrafficReportThreshold int64 + IPStrategy string + DNS string + Block string + Outbound string +} diff --git a/internal/config/constant.go b/internal/config/constant.go deleted file mode 100644 index 3f7625e..0000000 --- a/internal/config/constant.go +++ /dev/null @@ -1,5 +0,0 @@ -package config - -import "github.com/perfect-panel/ppanel-server/pkg/constant" - -const Version = constant.Version diff --git a/internal/handler/admin/ads/createAdsHandler.go b/internal/handler/admin/ads/createAdsHandler.go index d06a5ed..5d45005 100644 --- a/internal/handler/admin/ads/createAdsHandler.go +++ b/internal/handler/admin/ads/createAdsHandler.go @@ -2,10 +2,10 @@ package ads import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/ads" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/ads" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create Ads diff --git a/internal/handler/admin/ads/deleteAdsHandler.go b/internal/handler/admin/ads/deleteAdsHandler.go index efb153d..e8a804b 100644 --- a/internal/handler/admin/ads/deleteAdsHandler.go +++ b/internal/handler/admin/ads/deleteAdsHandler.go @@ -2,10 +2,10 @@ package ads import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/ads" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/ads" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Delete Ads diff --git a/internal/handler/admin/ads/getAdsDetailHandler.go b/internal/handler/admin/ads/getAdsDetailHandler.go index 3c3eb4d..bce6e38 100644 --- a/internal/handler/admin/ads/getAdsDetailHandler.go +++ b/internal/handler/admin/ads/getAdsDetailHandler.go @@ -2,10 +2,10 @@ package ads import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/ads" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/ads" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get Ads Detail diff --git a/internal/handler/admin/ads/getAdsListHandler.go b/internal/handler/admin/ads/getAdsListHandler.go index e2dc1ec..92ff173 100644 --- a/internal/handler/admin/ads/getAdsListHandler.go +++ b/internal/handler/admin/ads/getAdsListHandler.go @@ -2,10 +2,10 @@ package ads import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/ads" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/ads" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get Ads List diff --git a/internal/handler/admin/ads/updateAdsHandler.go b/internal/handler/admin/ads/updateAdsHandler.go index 801365b..8b1d28c 100644 --- a/internal/handler/admin/ads/updateAdsHandler.go +++ b/internal/handler/admin/ads/updateAdsHandler.go @@ -2,10 +2,10 @@ package ads import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/ads" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/ads" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update Ads diff --git a/internal/handler/admin/announcement/createAnnouncementHandler.go b/internal/handler/admin/announcement/createAnnouncementHandler.go index 138b23c..573da5f 100644 --- a/internal/handler/admin/announcement/createAnnouncementHandler.go +++ b/internal/handler/admin/announcement/createAnnouncementHandler.go @@ -2,10 +2,10 @@ package announcement import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/announcement" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/announcement" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create announcement diff --git a/internal/handler/admin/announcement/deleteAnnouncementHandler.go b/internal/handler/admin/announcement/deleteAnnouncementHandler.go index ae673d0..2cbb88e 100644 --- a/internal/handler/admin/announcement/deleteAnnouncementHandler.go +++ b/internal/handler/admin/announcement/deleteAnnouncementHandler.go @@ -2,10 +2,10 @@ package announcement import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/announcement" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/announcement" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Delete announcement diff --git a/internal/handler/admin/announcement/getAnnouncementHandler.go b/internal/handler/admin/announcement/getAnnouncementHandler.go index ea51c64..4525af6 100644 --- a/internal/handler/admin/announcement/getAnnouncementHandler.go +++ b/internal/handler/admin/announcement/getAnnouncementHandler.go @@ -2,10 +2,10 @@ package announcement import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/announcement" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/announcement" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get announcement diff --git a/internal/handler/admin/announcement/getAnnouncementListHandler.go b/internal/handler/admin/announcement/getAnnouncementListHandler.go index b5c0f55..64288b1 100644 --- a/internal/handler/admin/announcement/getAnnouncementListHandler.go +++ b/internal/handler/admin/announcement/getAnnouncementListHandler.go @@ -2,10 +2,10 @@ package announcement import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/announcement" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/announcement" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get announcement list diff --git a/internal/handler/admin/announcement/updateAnnouncementHandler.go b/internal/handler/admin/announcement/updateAnnouncementHandler.go index 96be68c..c6a82c2 100644 --- a/internal/handler/admin/announcement/updateAnnouncementHandler.go +++ b/internal/handler/admin/announcement/updateAnnouncementHandler.go @@ -2,10 +2,10 @@ package announcement import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/announcement" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/announcement" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update announcement diff --git a/internal/handler/admin/application/createSubscribeApplicationHandler.go b/internal/handler/admin/application/createSubscribeApplicationHandler.go new file mode 100644 index 0000000..6f9d136 --- /dev/null +++ b/internal/handler/admin/application/createSubscribeApplicationHandler.go @@ -0,0 +1,26 @@ +package application + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/application" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Create subscribe application +func CreateSubscribeApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateSubscribeApplicationRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := application.NewCreateSubscribeApplicationLogic(c.Request.Context(), svcCtx) + resp, err := l.CreateSubscribeApplication(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/application/deleteSubscribeApplicationHandler.go b/internal/handler/admin/application/deleteSubscribeApplicationHandler.go new file mode 100644 index 0000000..6e298a5 --- /dev/null +++ b/internal/handler/admin/application/deleteSubscribeApplicationHandler.go @@ -0,0 +1,26 @@ +package application + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/application" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Delete subscribe application +func DeleteSubscribeApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteSubscribeApplicationRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := application.NewDeleteSubscribeApplicationLogic(c.Request.Context(), svcCtx) + err := l.DeleteSubscribeApplication(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/application/getSubscribeApplicationListHandler.go b/internal/handler/admin/application/getSubscribeApplicationListHandler.go new file mode 100644 index 0000000..5e8222d --- /dev/null +++ b/internal/handler/admin/application/getSubscribeApplicationListHandler.go @@ -0,0 +1,26 @@ +package application + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/application" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get subscribe application list +func GetSubscribeApplicationListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetSubscribeApplicationListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := application.NewGetSubscribeApplicationListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetSubscribeApplicationList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/application/previewSubscribeTemplateHandler.go b/internal/handler/admin/application/previewSubscribeTemplateHandler.go new file mode 100644 index 0000000..6c136d2 --- /dev/null +++ b/internal/handler/admin/application/previewSubscribeTemplateHandler.go @@ -0,0 +1,27 @@ +package application + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/application" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Preview Template +func PreviewSubscribeTemplateHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.PreviewSubscribeTemplateRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := application.NewPreviewSubscribeTemplateLogic(c.Request.Context(), svcCtx) + resp, err := l.PreviewSubscribeTemplate(&req) + result.HttpResult(c, resp, err) + + } +} diff --git a/internal/handler/admin/application/updateSubscribeApplicationHandler.go b/internal/handler/admin/application/updateSubscribeApplicationHandler.go new file mode 100644 index 0000000..beaf306 --- /dev/null +++ b/internal/handler/admin/application/updateSubscribeApplicationHandler.go @@ -0,0 +1,26 @@ +package application + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/application" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Update subscribe application +func UpdateSubscribeApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateSubscribeApplicationRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := application.NewUpdateSubscribeApplicationLogic(c.Request.Context(), svcCtx) + resp, err := l.UpdateSubscribeApplication(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/authMethod/getAuthMethodConfigHandler.go b/internal/handler/admin/authMethod/getAuthMethodConfigHandler.go index ac94fc1..6b63e8d 100644 --- a/internal/handler/admin/authMethod/getAuthMethodConfigHandler.go +++ b/internal/handler/admin/authMethod/getAuthMethodConfigHandler.go @@ -2,10 +2,10 @@ package authMethod import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/authMethod" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get auth method config diff --git a/internal/handler/admin/authMethod/getAuthMethodListHandler.go b/internal/handler/admin/authMethod/getAuthMethodListHandler.go index 9e20e2f..93a64de 100644 --- a/internal/handler/admin/authMethod/getAuthMethodListHandler.go +++ b/internal/handler/admin/authMethod/getAuthMethodListHandler.go @@ -2,9 +2,9 @@ package authMethod import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/authMethod" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get auth method list diff --git a/internal/handler/admin/authMethod/getEmailPlatformHandler.go b/internal/handler/admin/authMethod/getEmailPlatformHandler.go index 97a2aba..c5a751e 100644 --- a/internal/handler/admin/authMethod/getEmailPlatformHandler.go +++ b/internal/handler/admin/authMethod/getEmailPlatformHandler.go @@ -2,9 +2,9 @@ package authMethod import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/authMethod" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get email support platform diff --git a/internal/handler/admin/authMethod/getSmsPlatformHandler.go b/internal/handler/admin/authMethod/getSmsPlatformHandler.go index 15dcd91..092c7b7 100644 --- a/internal/handler/admin/authMethod/getSmsPlatformHandler.go +++ b/internal/handler/admin/authMethod/getSmsPlatformHandler.go @@ -2,9 +2,9 @@ package authMethod import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/authMethod" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get sms support platform diff --git a/internal/handler/admin/authMethod/testEmailSendHandler.go b/internal/handler/admin/authMethod/testEmailSendHandler.go index fd98afd..ed91244 100644 --- a/internal/handler/admin/authMethod/testEmailSendHandler.go +++ b/internal/handler/admin/authMethod/testEmailSendHandler.go @@ -2,10 +2,10 @@ package authMethod import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/authMethod" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Test email send diff --git a/internal/handler/admin/authMethod/testSmsSendHandler.go b/internal/handler/admin/authMethod/testSmsSendHandler.go index 2bc551e..b74edc4 100644 --- a/internal/handler/admin/authMethod/testSmsSendHandler.go +++ b/internal/handler/admin/authMethod/testSmsSendHandler.go @@ -2,10 +2,10 @@ package authMethod import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/authMethod" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Test sms send diff --git a/internal/handler/admin/authMethod/updateAuthMethodConfigHandler.go b/internal/handler/admin/authMethod/updateAuthMethodConfigHandler.go index 0af525e..af2b9e2 100644 --- a/internal/handler/admin/authMethod/updateAuthMethodConfigHandler.go +++ b/internal/handler/admin/authMethod/updateAuthMethodConfigHandler.go @@ -2,10 +2,10 @@ package authMethod import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/authMethod" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/authMethod" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update auth method config diff --git a/internal/handler/admin/console/queryRevenueStatisticsHandler.go b/internal/handler/admin/console/queryRevenueStatisticsHandler.go index 2894000..d9c9c89 100644 --- a/internal/handler/admin/console/queryRevenueStatisticsHandler.go +++ b/internal/handler/admin/console/queryRevenueStatisticsHandler.go @@ -2,9 +2,9 @@ package console import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/console" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/console" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Query revenue statistics diff --git a/internal/handler/admin/console/queryServerTotalDataHandler.go b/internal/handler/admin/console/queryServerTotalDataHandler.go index d646cc6..1f88689 100644 --- a/internal/handler/admin/console/queryServerTotalDataHandler.go +++ b/internal/handler/admin/console/queryServerTotalDataHandler.go @@ -2,9 +2,9 @@ package console import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/console" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/console" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Query server total data diff --git a/internal/handler/admin/console/queryTicketWaitReplyHandler.go b/internal/handler/admin/console/queryTicketWaitReplyHandler.go index b39a61e..5aa6f4f 100644 --- a/internal/handler/admin/console/queryTicketWaitReplyHandler.go +++ b/internal/handler/admin/console/queryTicketWaitReplyHandler.go @@ -2,9 +2,9 @@ package console import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/console" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/console" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Query ticket wait reply diff --git a/internal/handler/admin/console/queryUserStatisticsHandler.go b/internal/handler/admin/console/queryUserStatisticsHandler.go index 1d651b3..1d738b1 100644 --- a/internal/handler/admin/console/queryUserStatisticsHandler.go +++ b/internal/handler/admin/console/queryUserStatisticsHandler.go @@ -2,9 +2,9 @@ package console import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/console" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/console" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Query user statistics diff --git a/internal/handler/admin/coupon/batchDeleteCouponHandler.go b/internal/handler/admin/coupon/batchDeleteCouponHandler.go index cbb6b0c..60f3035 100644 --- a/internal/handler/admin/coupon/batchDeleteCouponHandler.go +++ b/internal/handler/admin/coupon/batchDeleteCouponHandler.go @@ -2,10 +2,10 @@ package coupon import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/coupon" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/coupon" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Batch delete coupon diff --git a/internal/handler/admin/coupon/createCouponHandler.go b/internal/handler/admin/coupon/createCouponHandler.go index baf9c7a..6a48d68 100644 --- a/internal/handler/admin/coupon/createCouponHandler.go +++ b/internal/handler/admin/coupon/createCouponHandler.go @@ -2,10 +2,10 @@ package coupon import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/coupon" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/coupon" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create coupon diff --git a/internal/handler/admin/coupon/deleteCouponHandler.go b/internal/handler/admin/coupon/deleteCouponHandler.go index 651de28..94c59bc 100644 --- a/internal/handler/admin/coupon/deleteCouponHandler.go +++ b/internal/handler/admin/coupon/deleteCouponHandler.go @@ -2,10 +2,10 @@ package coupon import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/coupon" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/coupon" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Delete coupon diff --git a/internal/handler/admin/coupon/getCouponListHandler.go b/internal/handler/admin/coupon/getCouponListHandler.go index ce0b1d0..b8d0d96 100644 --- a/internal/handler/admin/coupon/getCouponListHandler.go +++ b/internal/handler/admin/coupon/getCouponListHandler.go @@ -2,10 +2,10 @@ package coupon import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/coupon" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/coupon" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get coupon list diff --git a/internal/handler/admin/coupon/updateCouponHandler.go b/internal/handler/admin/coupon/updateCouponHandler.go index 3443553..412653a 100644 --- a/internal/handler/admin/coupon/updateCouponHandler.go +++ b/internal/handler/admin/coupon/updateCouponHandler.go @@ -2,10 +2,10 @@ package coupon import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/coupon" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/coupon" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update coupon diff --git a/internal/handler/admin/document/batchDeleteDocumentHandler.go b/internal/handler/admin/document/batchDeleteDocumentHandler.go index 490125c..204e499 100644 --- a/internal/handler/admin/document/batchDeleteDocumentHandler.go +++ b/internal/handler/admin/document/batchDeleteDocumentHandler.go @@ -2,10 +2,10 @@ package document import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/document" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/document" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Batch delete document diff --git a/internal/handler/admin/document/createDocumentHandler.go b/internal/handler/admin/document/createDocumentHandler.go index 08d0fcb..e7f8a72 100644 --- a/internal/handler/admin/document/createDocumentHandler.go +++ b/internal/handler/admin/document/createDocumentHandler.go @@ -2,10 +2,10 @@ package document import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/document" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/document" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create document diff --git a/internal/handler/admin/document/deleteDocumentHandler.go b/internal/handler/admin/document/deleteDocumentHandler.go index 6dd5a7b..45b0f10 100644 --- a/internal/handler/admin/document/deleteDocumentHandler.go +++ b/internal/handler/admin/document/deleteDocumentHandler.go @@ -2,10 +2,10 @@ package document import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/document" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/document" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Delete document diff --git a/internal/handler/admin/document/getDocumentDetailHandler.go b/internal/handler/admin/document/getDocumentDetailHandler.go index f272f12..7def525 100644 --- a/internal/handler/admin/document/getDocumentDetailHandler.go +++ b/internal/handler/admin/document/getDocumentDetailHandler.go @@ -2,10 +2,10 @@ package document import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/document" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/document" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get document detail diff --git a/internal/handler/admin/document/getDocumentListHandler.go b/internal/handler/admin/document/getDocumentListHandler.go index 48a3e62..c3290f5 100644 --- a/internal/handler/admin/document/getDocumentListHandler.go +++ b/internal/handler/admin/document/getDocumentListHandler.go @@ -2,10 +2,10 @@ package document import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/document" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/document" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get document list diff --git a/internal/handler/admin/document/updateDocumentHandler.go b/internal/handler/admin/document/updateDocumentHandler.go index b88798b..b478e45 100644 --- a/internal/handler/admin/document/updateDocumentHandler.go +++ b/internal/handler/admin/document/updateDocumentHandler.go @@ -2,10 +2,10 @@ package document import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/document" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/document" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update document diff --git a/internal/handler/admin/log/filterBalanceLogHandler.go b/internal/handler/admin/log/filterBalanceLogHandler.go new file mode 100644 index 0000000..c8bf7d1 --- /dev/null +++ b/internal/handler/admin/log/filterBalanceLogHandler.go @@ -0,0 +1,26 @@ +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" +) + +// Filter balance log +func FilterBalanceLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterBalanceLogRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterBalanceLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterBalanceLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/filterCommissionLogHandler.go b/internal/handler/admin/log/filterCommissionLogHandler.go new file mode 100644 index 0000000..07361cd --- /dev/null +++ b/internal/handler/admin/log/filterCommissionLogHandler.go @@ -0,0 +1,26 @@ +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" +) + +// Filter commission log +func FilterCommissionLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterCommissionLogRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterCommissionLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterCommissionLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/filterEmailLogHandler.go b/internal/handler/admin/log/filterEmailLogHandler.go new file mode 100644 index 0000000..6d9f03d --- /dev/null +++ b/internal/handler/admin/log/filterEmailLogHandler.go @@ -0,0 +1,26 @@ +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" +) + +// Filter email log +func FilterEmailLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterLogParams + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterEmailLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterEmailLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/filterGiftLogHandler.go b/internal/handler/admin/log/filterGiftLogHandler.go new file mode 100644 index 0000000..e650a27 --- /dev/null +++ b/internal/handler/admin/log/filterGiftLogHandler.go @@ -0,0 +1,26 @@ +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" +) + +// Filter gift log +func FilterGiftLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterGiftLogRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterGiftLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterGiftLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/filterLoginLogHandler.go b/internal/handler/admin/log/filterLoginLogHandler.go new file mode 100644 index 0000000..c2dae41 --- /dev/null +++ b/internal/handler/admin/log/filterLoginLogHandler.go @@ -0,0 +1,26 @@ +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" +) + +// Filter login log +func FilterLoginLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterLoginLogRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterLoginLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterLoginLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/filterMobileLogHandler.go b/internal/handler/admin/log/filterMobileLogHandler.go new file mode 100644 index 0000000..0d45e27 --- /dev/null +++ b/internal/handler/admin/log/filterMobileLogHandler.go @@ -0,0 +1,26 @@ +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" +) + +// Filter mobile log +func FilterMobileLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterLogParams + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterMobileLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterMobileLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/filterRegisterLogHandler.go b/internal/handler/admin/log/filterRegisterLogHandler.go new file mode 100644 index 0000000..a5ca9e8 --- /dev/null +++ b/internal/handler/admin/log/filterRegisterLogHandler.go @@ -0,0 +1,26 @@ +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" +) + +// Filter register log +func FilterRegisterLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterRegisterLogRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterRegisterLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterRegisterLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/filterResetSubscribeLogHandler.go b/internal/handler/admin/log/filterResetSubscribeLogHandler.go new file mode 100644 index 0000000..f4d96e5 --- /dev/null +++ b/internal/handler/admin/log/filterResetSubscribeLogHandler.go @@ -0,0 +1,26 @@ +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" +) + +// Filter reset subscribe log +func FilterResetSubscribeLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterResetSubscribeLogRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterResetSubscribeLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterResetSubscribeLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/filterServerTrafficLogHandler.go b/internal/handler/admin/log/filterServerTrafficLogHandler.go new file mode 100644 index 0000000..ec522ed --- /dev/null +++ b/internal/handler/admin/log/filterServerTrafficLogHandler.go @@ -0,0 +1,26 @@ +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" +) + +// Filter server traffic log +func FilterServerTrafficLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterServerTrafficLogRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterServerTrafficLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterServerTrafficLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/filterSubscribeLogHandler.go b/internal/handler/admin/log/filterSubscribeLogHandler.go new file mode 100644 index 0000000..f01b61e --- /dev/null +++ b/internal/handler/admin/log/filterSubscribeLogHandler.go @@ -0,0 +1,26 @@ +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" +) + +// Filter subscribe log +func FilterSubscribeLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterSubscribeLogRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterSubscribeLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterSubscribeLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/filterTrafficLogDetailsHandler.go b/internal/handler/admin/log/filterTrafficLogDetailsHandler.go new file mode 100644 index 0000000..c77a881 --- /dev/null +++ b/internal/handler/admin/log/filterTrafficLogDetailsHandler.go @@ -0,0 +1,26 @@ +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" +) + +// Filter traffic log details +func FilterTrafficLogDetailsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterTrafficLogDetailsRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterTrafficLogDetailsLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterTrafficLogDetails(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/filterUserSubscribeTrafficLogHandler.go b/internal/handler/admin/log/filterUserSubscribeTrafficLogHandler.go new file mode 100644 index 0000000..976e278 --- /dev/null +++ b/internal/handler/admin/log/filterUserSubscribeTrafficLogHandler.go @@ -0,0 +1,26 @@ +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" +) + +// Filter user subscribe traffic log +func FilterUserSubscribeTrafficLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterSubscribeTrafficRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterUserSubscribeTrafficLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterUserSubscribeTrafficLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/getLogSettingHandler.go b/internal/handler/admin/log/getLogSettingHandler.go new file mode 100644 index 0000000..50217cb --- /dev/null +++ b/internal/handler/admin/log/getLogSettingHandler.go @@ -0,0 +1,18 @@ +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" +) + +// Get log setting +func GetLogSettingHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := log.NewGetLogSettingLogic(c.Request.Context(), svcCtx) + resp, err := l.GetLogSetting() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/log/getMessageLogListHandler.go b/internal/handler/admin/log/getMessageLogListHandler.go index ab535a2..7b0dd3c 100644 --- a/internal/handler/admin/log/getMessageLogListHandler.go +++ b/internal/handler/admin/log/getMessageLogListHandler.go @@ -2,10 +2,10 @@ package log import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/log" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "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" ) // Get message log list diff --git a/internal/handler/admin/log/updateLogSettingHandler.go b/internal/handler/admin/log/updateLogSettingHandler.go new file mode 100644 index 0000000..91aa2c8 --- /dev/null +++ b/internal/handler/admin/log/updateLogSettingHandler.go @@ -0,0 +1,26 @@ +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" +) + +// Update log setting +func UpdateLogSettingHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.LogSetting + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewUpdateLogSettingLogic(c.Request.Context(), svcCtx) + err := l.UpdateLogSetting(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/marketing/createBatchSendEmailTaskHandler.go b/internal/handler/admin/marketing/createBatchSendEmailTaskHandler.go new file mode 100644 index 0000000..8057689 --- /dev/null +++ b/internal/handler/admin/marketing/createBatchSendEmailTaskHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Create a batch send email task +func CreateBatchSendEmailTaskHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateBatchSendEmailTaskRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewCreateBatchSendEmailTaskLogic(c.Request.Context(), svcCtx) + err := l.CreateBatchSendEmailTask(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/marketing/createQuotaTaskHandler.go b/internal/handler/admin/marketing/createQuotaTaskHandler.go new file mode 100644 index 0000000..0fb088b --- /dev/null +++ b/internal/handler/admin/marketing/createQuotaTaskHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Create a quota task +func CreateQuotaTaskHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateQuotaTaskRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewCreateQuotaTaskLogic(c.Request.Context(), svcCtx) + err := l.CreateQuotaTask(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/marketing/getBatchSendEmailTaskListHandler.go b/internal/handler/admin/marketing/getBatchSendEmailTaskListHandler.go new file mode 100644 index 0000000..9c13f9f --- /dev/null +++ b/internal/handler/admin/marketing/getBatchSendEmailTaskListHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get batch send email task list +func GetBatchSendEmailTaskListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetBatchSendEmailTaskListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewGetBatchSendEmailTaskListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetBatchSendEmailTaskList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/marketing/getBatchSendEmailTaskStatusHandler.go b/internal/handler/admin/marketing/getBatchSendEmailTaskStatusHandler.go new file mode 100644 index 0000000..8e5ad87 --- /dev/null +++ b/internal/handler/admin/marketing/getBatchSendEmailTaskStatusHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get batch send email task status +func GetBatchSendEmailTaskStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetBatchSendEmailTaskStatusRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewGetBatchSendEmailTaskStatusLogic(c.Request.Context(), svcCtx) + resp, err := l.GetBatchSendEmailTaskStatus(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/marketing/getPreSendEmailCountHandler.go b/internal/handler/admin/marketing/getPreSendEmailCountHandler.go new file mode 100644 index 0000000..33e5894 --- /dev/null +++ b/internal/handler/admin/marketing/getPreSendEmailCountHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get pre-send email count +func GetPreSendEmailCountHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetPreSendEmailCountRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewGetPreSendEmailCountLogic(c.Request.Context(), svcCtx) + resp, err := l.GetPreSendEmailCount(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/marketing/queryQuotaTaskListHandler.go b/internal/handler/admin/marketing/queryQuotaTaskListHandler.go new file mode 100644 index 0000000..3aaebdc --- /dev/null +++ b/internal/handler/admin/marketing/queryQuotaTaskListHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Query quota task list +func QueryQuotaTaskListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryQuotaTaskListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewQueryQuotaTaskListLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryQuotaTaskList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/marketing/queryQuotaTaskPreCountHandler.go b/internal/handler/admin/marketing/queryQuotaTaskPreCountHandler.go new file mode 100644 index 0000000..bcf6bd7 --- /dev/null +++ b/internal/handler/admin/marketing/queryQuotaTaskPreCountHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Query quota task pre-count +func QueryQuotaTaskPreCountHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryQuotaTaskPreCountRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewQueryQuotaTaskPreCountLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryQuotaTaskPreCount(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/marketing/queryQuotaTaskStatusHandler.go b/internal/handler/admin/marketing/queryQuotaTaskStatusHandler.go new file mode 100644 index 0000000..8d6cf9c --- /dev/null +++ b/internal/handler/admin/marketing/queryQuotaTaskStatusHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Query quota task status +func QueryQuotaTaskStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryQuotaTaskStatusRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewQueryQuotaTaskStatusLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryQuotaTaskStatus(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/marketing/stopBatchSendEmailTaskHandler.go b/internal/handler/admin/marketing/stopBatchSendEmailTaskHandler.go new file mode 100644 index 0000000..0129218 --- /dev/null +++ b/internal/handler/admin/marketing/stopBatchSendEmailTaskHandler.go @@ -0,0 +1,26 @@ +package marketing + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/marketing" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// StopBatchSendEmailTaskHandler Stop a batch send email task +func StopBatchSendEmailTaskHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.StopBatchSendEmailTaskRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := marketing.NewStopBatchSendEmailTaskLogic(c.Request.Context(), svcCtx) + err := l.StopBatchSendEmailTask(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/order/createOrderHandler.go b/internal/handler/admin/order/createOrderHandler.go index f48ba16..346dc99 100644 --- a/internal/handler/admin/order/createOrderHandler.go +++ b/internal/handler/admin/order/createOrderHandler.go @@ -2,10 +2,10 @@ package order import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create order diff --git a/internal/handler/admin/order/getOrderListHandler.go b/internal/handler/admin/order/getOrderListHandler.go index 9e732bb..01adc3c 100644 --- a/internal/handler/admin/order/getOrderListHandler.go +++ b/internal/handler/admin/order/getOrderListHandler.go @@ -2,10 +2,10 @@ package order import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get order list diff --git a/internal/handler/admin/order/updateOrderStatusHandler.go b/internal/handler/admin/order/updateOrderStatusHandler.go index 51a5c19..21a8596 100644 --- a/internal/handler/admin/order/updateOrderStatusHandler.go +++ b/internal/handler/admin/order/updateOrderStatusHandler.go @@ -2,10 +2,10 @@ package order import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update order status diff --git a/internal/handler/admin/payment/createPaymentMethodHandler.go b/internal/handler/admin/payment/createPaymentMethodHandler.go index 69961a7..f2004a6 100644 --- a/internal/handler/admin/payment/createPaymentMethodHandler.go +++ b/internal/handler/admin/payment/createPaymentMethodHandler.go @@ -2,10 +2,10 @@ package payment import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create Payment Method diff --git a/internal/handler/admin/payment/deletePaymentMethodHandler.go b/internal/handler/admin/payment/deletePaymentMethodHandler.go index b3d4365..4bc5af9 100644 --- a/internal/handler/admin/payment/deletePaymentMethodHandler.go +++ b/internal/handler/admin/payment/deletePaymentMethodHandler.go @@ -2,10 +2,10 @@ package payment import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Delete Payment Method diff --git a/internal/handler/admin/payment/getPaymentMethodListHandler.go b/internal/handler/admin/payment/getPaymentMethodListHandler.go index c968f53..c5b2e22 100644 --- a/internal/handler/admin/payment/getPaymentMethodListHandler.go +++ b/internal/handler/admin/payment/getPaymentMethodListHandler.go @@ -2,10 +2,10 @@ package payment import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // GetPaymentMethodListHandler Get Payment Method List diff --git a/internal/handler/admin/payment/getPaymentPlatformHandler.go b/internal/handler/admin/payment/getPaymentPlatformHandler.go index 7e8d92e..ffc4b00 100644 --- a/internal/handler/admin/payment/getPaymentPlatformHandler.go +++ b/internal/handler/admin/payment/getPaymentPlatformHandler.go @@ -2,9 +2,9 @@ package payment import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get supported payment platform diff --git a/internal/handler/admin/payment/updatePaymentMethodHandler.go b/internal/handler/admin/payment/updatePaymentMethodHandler.go index 10b7802..3c30d57 100644 --- a/internal/handler/admin/payment/updatePaymentMethodHandler.go +++ b/internal/handler/admin/payment/updatePaymentMethodHandler.go @@ -2,10 +2,10 @@ package payment import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update Payment Method diff --git a/internal/handler/admin/server/batchDeleteNodeGroupHandler.go b/internal/handler/admin/server/batchDeleteNodeGroupHandler.go deleted file mode 100644 index caa6411..0000000 --- a/internal/handler/admin/server/batchDeleteNodeGroupHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Batch delete node group -func BatchDeleteNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.BatchDeleteNodeGroupRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := server.NewBatchDeleteNodeGroupLogic(c.Request.Context(), svcCtx) - err := l.BatchDeleteNodeGroup(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/server/batchDeleteNodeHandler.go b/internal/handler/admin/server/batchDeleteNodeHandler.go deleted file mode 100644 index 221fe86..0000000 --- a/internal/handler/admin/server/batchDeleteNodeHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Batch delete node -func BatchDeleteNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.BatchDeleteNodeRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := server.NewBatchDeleteNodeLogic(c.Request.Context(), svcCtx) - err := l.BatchDeleteNode(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/server/createNodeGroupHandler.go b/internal/handler/admin/server/createNodeGroupHandler.go deleted file mode 100644 index 1523833..0000000 --- a/internal/handler/admin/server/createNodeGroupHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Create node group -func CreateNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.CreateNodeGroupRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := server.NewCreateNodeGroupLogic(c.Request.Context(), svcCtx) - err := l.CreateNodeGroup(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/server/createNodeHandler.go b/internal/handler/admin/server/createNodeHandler.go index d4b8a98..e872b09 100644 --- a/internal/handler/admin/server/createNodeHandler.go +++ b/internal/handler/admin/server/createNodeHandler.go @@ -2,13 +2,13 @@ package server import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) -// Create node +// Create Node func CreateNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { var req types.CreateNodeRequest diff --git a/internal/handler/admin/server/createRuleGroupHandler.go b/internal/handler/admin/server/createRuleGroupHandler.go deleted file mode 100644 index 21c02c4..0000000 --- a/internal/handler/admin/server/createRuleGroupHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Create rule group -func CreateRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.CreateRuleGroupRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := server.NewCreateRuleGroupLogic(c.Request.Context(), svcCtx) - err := l.CreateRuleGroup(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/server/createServerHandler.go b/internal/handler/admin/server/createServerHandler.go new file mode 100644 index 0000000..2068122 --- /dev/null +++ b/internal/handler/admin/server/createServerHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// CreateServerHandler Create Server +func CreateServerHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.CreateServerRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewCreateServerLogic(c.Request.Context(), svcCtx) + err := l.CreateServer(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/deleteNodeGroupHandler.go b/internal/handler/admin/server/deleteNodeGroupHandler.go deleted file mode 100644 index 492898f..0000000 --- a/internal/handler/admin/server/deleteNodeGroupHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Delete node group -func DeleteNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.DeleteNodeGroupRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := server.NewDeleteNodeGroupLogic(c.Request.Context(), svcCtx) - err := l.DeleteNodeGroup(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/server/deleteNodeHandler.go b/internal/handler/admin/server/deleteNodeHandler.go index 62f4a60..37ac80b 100644 --- a/internal/handler/admin/server/deleteNodeHandler.go +++ b/internal/handler/admin/server/deleteNodeHandler.go @@ -2,13 +2,13 @@ package server import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) -// Delete node +// Delete Node func DeleteNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { var req types.DeleteNodeRequest diff --git a/internal/handler/admin/server/deleteRuleGroupHandler.go b/internal/handler/admin/server/deleteRuleGroupHandler.go deleted file mode 100644 index 91b830e..0000000 --- a/internal/handler/admin/server/deleteRuleGroupHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Delete rule group -func DeleteRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.DeleteRuleGroupRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := server.NewDeleteRuleGroupLogic(c.Request.Context(), svcCtx) - err := l.DeleteRuleGroup(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/server/deleteServerHandler.go b/internal/handler/admin/server/deleteServerHandler.go new file mode 100644 index 0000000..677fb17 --- /dev/null +++ b/internal/handler/admin/server/deleteServerHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Delete Server +func DeleteServerHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeleteServerRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewDeleteServerLogic(c.Request.Context(), svcCtx) + err := l.DeleteServer(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/filterNodeListHandler.go b/internal/handler/admin/server/filterNodeListHandler.go new file mode 100644 index 0000000..5e154ca --- /dev/null +++ b/internal/handler/admin/server/filterNodeListHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Filter Node List +func FilterNodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterNodeListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewFilterNodeListLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterNodeList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/filterServerListHandler.go b/internal/handler/admin/server/filterServerListHandler.go new file mode 100644 index 0000000..9e6cb7b --- /dev/null +++ b/internal/handler/admin/server/filterServerListHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// FilterServerListHandler Filter Server List +func FilterServerListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterServerListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewFilterServerListLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterServerList(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/getNodeDetailHandler.go b/internal/handler/admin/server/getNodeDetailHandler.go deleted file mode 100644 index 8485a73..0000000 --- a/internal/handler/admin/server/getNodeDetailHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get node detail -func GetNodeDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.GetDetailRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := server.NewGetNodeDetailLogic(c.Request.Context(), svcCtx) - resp, err := l.GetNodeDetail(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/admin/server/getNodeGroupListHandler.go b/internal/handler/admin/server/getNodeGroupListHandler.go deleted file mode 100644 index 99bdf3a..0000000 --- a/internal/handler/admin/server/getNodeGroupListHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get node group list -func GetNodeGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := server.NewGetNodeGroupListLogic(c.Request.Context(), svcCtx) - resp, err := l.GetNodeGroupList() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/admin/server/getNodeListHandler.go b/internal/handler/admin/server/getNodeListHandler.go deleted file mode 100644 index 2c47f74..0000000 --- a/internal/handler/admin/server/getNodeListHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get node list -func GetNodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.GetNodeServerListRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := server.NewGetNodeListLogic(c.Request.Context(), svcCtx) - resp, err := l.GetNodeList(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/admin/server/getNodeTagListHandler.go b/internal/handler/admin/server/getNodeTagListHandler.go deleted file mode 100644 index 700d79b..0000000 --- a/internal/handler/admin/server/getNodeTagListHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get node tag list -func GetNodeTagListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := server.NewGetNodeTagListLogic(c.Request.Context(), svcCtx) - resp, err := l.GetNodeTagList() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/admin/server/getRuleGroupListHandler.go b/internal/handler/admin/server/getRuleGroupListHandler.go deleted file mode 100644 index 22b14cf..0000000 --- a/internal/handler/admin/server/getRuleGroupListHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get rule group list -func GetRuleGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := server.NewGetRuleGroupListLogic(c.Request.Context(), svcCtx) - resp, err := l.GetRuleGroupList() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/admin/server/getServerProtocolsHandler.go b/internal/handler/admin/server/getServerProtocolsHandler.go new file mode 100644 index 0000000..14238ca --- /dev/null +++ b/internal/handler/admin/server/getServerProtocolsHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get Server Protocols +func GetServerProtocolsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetServerProtocolsRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewGetServerProtocolsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetServerProtocols(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/hasMigrateSeverNodeHandler.go b/internal/handler/admin/server/hasMigrateSeverNodeHandler.go new file mode 100644 index 0000000..6088577 --- /dev/null +++ b/internal/handler/admin/server/hasMigrateSeverNodeHandler.go @@ -0,0 +1,18 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Check if there is any server or node to migrate +func HasMigrateSeverNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := server.NewHasMigrateSeverNodeLogic(c.Request.Context(), svcCtx) + resp, err := l.HasMigrateSeverNode() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/migrateServerNodeHandler.go b/internal/handler/admin/server/migrateServerNodeHandler.go new file mode 100644 index 0000000..8f8c842 --- /dev/null +++ b/internal/handler/admin/server/migrateServerNodeHandler.go @@ -0,0 +1,18 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Migrate server and node data to new database +func MigrateServerNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := server.NewMigrateServerNodeLogic(c.Request.Context(), svcCtx) + resp, err := l.MigrateServerNode() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/nodeSortHandler.go b/internal/handler/admin/server/nodeSortHandler.go deleted file mode 100644 index 84c5184..0000000 --- a/internal/handler/admin/server/nodeSortHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Node sort -func NodeSortHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.NodeSortRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := server.NewNodeSortLogic(c.Request.Context(), svcCtx) - err := l.NodeSort(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/server/queryNodeTagHandler.go b/internal/handler/admin/server/queryNodeTagHandler.go new file mode 100644 index 0000000..fa963cc --- /dev/null +++ b/internal/handler/admin/server/queryNodeTagHandler.go @@ -0,0 +1,18 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Query all node tags +func QueryNodeTagHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := server.NewQueryNodeTagLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryNodeTag() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/server/resetSortWithNodeHandler.go b/internal/handler/admin/server/resetSortWithNodeHandler.go new file mode 100644 index 0000000..4b8b14c --- /dev/null +++ b/internal/handler/admin/server/resetSortWithNodeHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Reset node sort +func ResetSortWithNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ResetSortRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewResetSortWithNodeLogic(c.Request.Context(), svcCtx) + err := l.ResetSortWithNode(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/resetSortWithServerHandler.go b/internal/handler/admin/server/resetSortWithServerHandler.go new file mode 100644 index 0000000..7adbecb --- /dev/null +++ b/internal/handler/admin/server/resetSortWithServerHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Reset server sort +func ResetSortWithServerHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ResetSortRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewResetSortWithServerLogic(c.Request.Context(), svcCtx) + err := l.ResetSortWithServer(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/toggleNodeStatusHandler.go b/internal/handler/admin/server/toggleNodeStatusHandler.go new file mode 100644 index 0000000..67144ad --- /dev/null +++ b/internal/handler/admin/server/toggleNodeStatusHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Toggle Node Status +func ToggleNodeStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ToggleNodeStatusRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewToggleNodeStatusLogic(c.Request.Context(), svcCtx) + err := l.ToggleNodeStatus(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/server/updateNodeGroupHandler.go b/internal/handler/admin/server/updateNodeGroupHandler.go deleted file mode 100644 index 4b4d2de..0000000 --- a/internal/handler/admin/server/updateNodeGroupHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Update node group -func UpdateNodeGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.UpdateNodeGroupRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := server.NewUpdateNodeGroupLogic(c.Request.Context(), svcCtx) - err := l.UpdateNodeGroup(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/server/updateNodeHandler.go b/internal/handler/admin/server/updateNodeHandler.go index 8ced9af..af19537 100644 --- a/internal/handler/admin/server/updateNodeHandler.go +++ b/internal/handler/admin/server/updateNodeHandler.go @@ -2,13 +2,13 @@ package server import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) -// Update node +// Update Node func UpdateNodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { var req types.UpdateNodeRequest diff --git a/internal/handler/admin/server/updateRuleGroupHandler.go b/internal/handler/admin/server/updateRuleGroupHandler.go deleted file mode 100644 index e77cfc1..0000000 --- a/internal/handler/admin/server/updateRuleGroupHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package server - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Update rule group -func UpdateRuleGroupHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.UpdateRuleGroupRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := server.NewUpdateRuleGroupLogic(c.Request.Context(), svcCtx) - err := l.UpdateRuleGroup(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/server/updateServerHandler.go b/internal/handler/admin/server/updateServerHandler.go new file mode 100644 index 0000000..24570c6 --- /dev/null +++ b/internal/handler/admin/server/updateServerHandler.go @@ -0,0 +1,26 @@ +package server + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Update Server +func UpdateServerHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UpdateServerRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := server.NewUpdateServerLogic(c.Request.Context(), svcCtx) + err := l.UpdateServer(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/admin/subscribe/batchDeleteSubscribeGroupHandler.go b/internal/handler/admin/subscribe/batchDeleteSubscribeGroupHandler.go index 1d2a3c9..001396d 100644 --- a/internal/handler/admin/subscribe/batchDeleteSubscribeGroupHandler.go +++ b/internal/handler/admin/subscribe/batchDeleteSubscribeGroupHandler.go @@ -2,10 +2,10 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Batch delete subscribe group diff --git a/internal/handler/admin/subscribe/batchDeleteSubscribeHandler.go b/internal/handler/admin/subscribe/batchDeleteSubscribeHandler.go index e16bb2c..46af1ef 100644 --- a/internal/handler/admin/subscribe/batchDeleteSubscribeHandler.go +++ b/internal/handler/admin/subscribe/batchDeleteSubscribeHandler.go @@ -2,10 +2,10 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Batch delete subscribe diff --git a/internal/handler/admin/subscribe/createSubscribeGroupHandler.go b/internal/handler/admin/subscribe/createSubscribeGroupHandler.go index c0dd388..4c0ede9 100644 --- a/internal/handler/admin/subscribe/createSubscribeGroupHandler.go +++ b/internal/handler/admin/subscribe/createSubscribeGroupHandler.go @@ -2,10 +2,10 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create subscribe group diff --git a/internal/handler/admin/subscribe/createSubscribeHandler.go b/internal/handler/admin/subscribe/createSubscribeHandler.go index 95c7c83..2409777 100644 --- a/internal/handler/admin/subscribe/createSubscribeHandler.go +++ b/internal/handler/admin/subscribe/createSubscribeHandler.go @@ -2,10 +2,10 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create subscribe diff --git a/internal/handler/admin/subscribe/deleteSubscribeGroupHandler.go b/internal/handler/admin/subscribe/deleteSubscribeGroupHandler.go index 54d20df..db8df90 100644 --- a/internal/handler/admin/subscribe/deleteSubscribeGroupHandler.go +++ b/internal/handler/admin/subscribe/deleteSubscribeGroupHandler.go @@ -2,10 +2,10 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Delete subscribe group diff --git a/internal/handler/admin/subscribe/deleteSubscribeHandler.go b/internal/handler/admin/subscribe/deleteSubscribeHandler.go index b25b1cb..711eae0 100644 --- a/internal/handler/admin/subscribe/deleteSubscribeHandler.go +++ b/internal/handler/admin/subscribe/deleteSubscribeHandler.go @@ -2,10 +2,10 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Delete subscribe diff --git a/internal/handler/admin/subscribe/getSubscribeDetailsHandler.go b/internal/handler/admin/subscribe/getSubscribeDetailsHandler.go index 216e753..371b149 100644 --- a/internal/handler/admin/subscribe/getSubscribeDetailsHandler.go +++ b/internal/handler/admin/subscribe/getSubscribeDetailsHandler.go @@ -2,10 +2,10 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get subscribe details diff --git a/internal/handler/admin/subscribe/getSubscribeGroupListHandler.go b/internal/handler/admin/subscribe/getSubscribeGroupListHandler.go index 980dbb4..a629f02 100644 --- a/internal/handler/admin/subscribe/getSubscribeGroupListHandler.go +++ b/internal/handler/admin/subscribe/getSubscribeGroupListHandler.go @@ -2,9 +2,9 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get subscribe group list diff --git a/internal/handler/admin/subscribe/getSubscribeListHandler.go b/internal/handler/admin/subscribe/getSubscribeListHandler.go index eedf89d..41e4e6d 100644 --- a/internal/handler/admin/subscribe/getSubscribeListHandler.go +++ b/internal/handler/admin/subscribe/getSubscribeListHandler.go @@ -2,10 +2,10 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get subscribe list diff --git a/internal/handler/admin/subscribe/subscribeSortHandler.go b/internal/handler/admin/subscribe/subscribeSortHandler.go index 8f7c5b6..d195279 100644 --- a/internal/handler/admin/subscribe/subscribeSortHandler.go +++ b/internal/handler/admin/subscribe/subscribeSortHandler.go @@ -2,10 +2,10 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Subscribe sort diff --git a/internal/handler/admin/subscribe/updateSubscribeGroupHandler.go b/internal/handler/admin/subscribe/updateSubscribeGroupHandler.go index 53d9fd1..dc37c4f 100644 --- a/internal/handler/admin/subscribe/updateSubscribeGroupHandler.go +++ b/internal/handler/admin/subscribe/updateSubscribeGroupHandler.go @@ -2,10 +2,10 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update subscribe group diff --git a/internal/handler/admin/subscribe/updateSubscribeHandler.go b/internal/handler/admin/subscribe/updateSubscribeHandler.go index c01bcc9..2c54697 100644 --- a/internal/handler/admin/subscribe/updateSubscribeHandler.go +++ b/internal/handler/admin/subscribe/updateSubscribeHandler.go @@ -2,10 +2,10 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update subscribe diff --git a/internal/handler/admin/system/createApplicationHandler.go b/internal/handler/admin/system/createApplicationHandler.go deleted file mode 100644 index f2745f1..0000000 --- a/internal/handler/admin/system/createApplicationHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Create application -func CreateApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.CreateApplicationRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := system.NewCreateApplicationLogic(c.Request.Context(), svcCtx) - err := l.CreateApplication(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/system/createApplicationVersionHandler.go b/internal/handler/admin/system/createApplicationVersionHandler.go deleted file mode 100644 index bbae577..0000000 --- a/internal/handler/admin/system/createApplicationVersionHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Create application version -func CreateApplicationVersionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.CreateApplicationVersionRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := system.NewCreateApplicationVersionLogic(c.Request.Context(), svcCtx) - err := l.CreateApplicationVersion(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/system/deleteApplicationHandler.go b/internal/handler/admin/system/deleteApplicationHandler.go deleted file mode 100644 index 3de1aa4..0000000 --- a/internal/handler/admin/system/deleteApplicationHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Delete application -func DeleteApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.DeleteApplicationRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := system.NewDeleteApplicationLogic(c.Request.Context(), svcCtx) - err := l.DeleteApplication(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/system/deleteApplicationVersionHandler.go b/internal/handler/admin/system/deleteApplicationVersionHandler.go deleted file mode 100644 index 7f9516f..0000000 --- a/internal/handler/admin/system/deleteApplicationVersionHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Delete application -func DeleteApplicationVersionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.DeleteApplicationVersionRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := system.NewDeleteApplicationVersionLogic(c.Request.Context(), svcCtx) - err := l.DeleteApplicationVersion(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/system/getApplicationConfigHandler.go b/internal/handler/admin/system/getApplicationConfigHandler.go deleted file mode 100644 index 0f8ddab..0000000 --- a/internal/handler/admin/system/getApplicationConfigHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// get application config -func GetApplicationConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := system.NewGetApplicationConfigLogic(c.Request.Context(), svcCtx) - resp, err := l.GetApplicationConfig() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/admin/system/getApplicationHandler.go b/internal/handler/admin/system/getApplicationHandler.go deleted file mode 100644 index a0eefbd..0000000 --- a/internal/handler/admin/system/getApplicationHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get application -func GetApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := system.NewGetApplicationLogic(c.Request.Context(), svcCtx) - resp, err := l.GetApplication() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/admin/system/getCurrencyConfigHandler.go b/internal/handler/admin/system/getCurrencyConfigHandler.go index a172227..3a39888 100644 --- a/internal/handler/admin/system/getCurrencyConfigHandler.go +++ b/internal/handler/admin/system/getCurrencyConfigHandler.go @@ -2,9 +2,9 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get Currency Config diff --git a/internal/handler/admin/system/getInviteConfigHandler.go b/internal/handler/admin/system/getInviteConfigHandler.go index f586852..ed777c6 100644 --- a/internal/handler/admin/system/getInviteConfigHandler.go +++ b/internal/handler/admin/system/getInviteConfigHandler.go @@ -2,9 +2,9 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get invite config diff --git a/internal/handler/admin/system/getNodeConfigHandler.go b/internal/handler/admin/system/getNodeConfigHandler.go index 0a0aba6..360b10a 100644 --- a/internal/handler/admin/system/getNodeConfigHandler.go +++ b/internal/handler/admin/system/getNodeConfigHandler.go @@ -2,9 +2,9 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get node config diff --git a/internal/handler/admin/system/getNodeMultiplierHandler.go b/internal/handler/admin/system/getNodeMultiplierHandler.go index 5268a6a..ca845a6 100644 --- a/internal/handler/admin/system/getNodeMultiplierHandler.go +++ b/internal/handler/admin/system/getNodeMultiplierHandler.go @@ -2,9 +2,9 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get Node Multiplier diff --git a/internal/handler/admin/system/getPrivacyPolicyConfigHandler.go b/internal/handler/admin/system/getPrivacyPolicyConfigHandler.go index 3f86383..62ed8d2 100644 --- a/internal/handler/admin/system/getPrivacyPolicyConfigHandler.go +++ b/internal/handler/admin/system/getPrivacyPolicyConfigHandler.go @@ -2,9 +2,9 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // get Privacy Policy Config diff --git a/internal/handler/admin/system/getRegisterConfigHandler.go b/internal/handler/admin/system/getRegisterConfigHandler.go index b8b9687..9eab273 100644 --- a/internal/handler/admin/system/getRegisterConfigHandler.go +++ b/internal/handler/admin/system/getRegisterConfigHandler.go @@ -2,9 +2,9 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get register config diff --git a/internal/handler/admin/system/getSiteConfigHandler.go b/internal/handler/admin/system/getSiteConfigHandler.go index 109a1ee..ea9a376 100644 --- a/internal/handler/admin/system/getSiteConfigHandler.go +++ b/internal/handler/admin/system/getSiteConfigHandler.go @@ -2,9 +2,9 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get site config diff --git a/internal/handler/admin/system/getSubscribeConfigHandler.go b/internal/handler/admin/system/getSubscribeConfigHandler.go index 0b5fb19..0cca035 100644 --- a/internal/handler/admin/system/getSubscribeConfigHandler.go +++ b/internal/handler/admin/system/getSubscribeConfigHandler.go @@ -2,9 +2,9 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get subscribe config diff --git a/internal/handler/admin/system/getSubscribeTypeHandler.go b/internal/handler/admin/system/getSubscribeTypeHandler.go deleted file mode 100644 index f017cf7..0000000 --- a/internal/handler/admin/system/getSubscribeTypeHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get subscribe type -func GetSubscribeTypeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := system.NewGetSubscribeTypeLogic(c.Request.Context(), svcCtx) - resp, err := l.GetSubscribeType() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/admin/system/getTosConfigHandler.go b/internal/handler/admin/system/getTosConfigHandler.go index 06c6cc0..2215991 100644 --- a/internal/handler/admin/system/getTosConfigHandler.go +++ b/internal/handler/admin/system/getTosConfigHandler.go @@ -2,9 +2,9 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get Team of Service Config diff --git a/internal/handler/admin/system/getVerifyCodeConfigHandler.go b/internal/handler/admin/system/getVerifyCodeConfigHandler.go index 4a23c1b..2936d61 100644 --- a/internal/handler/admin/system/getVerifyCodeConfigHandler.go +++ b/internal/handler/admin/system/getVerifyCodeConfigHandler.go @@ -2,9 +2,9 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get Verify Code Config diff --git a/internal/handler/admin/system/getVerifyConfigHandler.go b/internal/handler/admin/system/getVerifyConfigHandler.go index eb928dd..2a9a03b 100644 --- a/internal/handler/admin/system/getVerifyConfigHandler.go +++ b/internal/handler/admin/system/getVerifyConfigHandler.go @@ -2,9 +2,9 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get verify config diff --git a/internal/handler/admin/system/preViewNodeMultiplierHandler.go b/internal/handler/admin/system/preViewNodeMultiplierHandler.go new file mode 100644 index 0000000..8ef2089 --- /dev/null +++ b/internal/handler/admin/system/preViewNodeMultiplierHandler.go @@ -0,0 +1,18 @@ +package system + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// PreView Node Multiplier +func PreViewNodeMultiplierHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := system.NewPreViewNodeMultiplierLogic(c.Request.Context(), svcCtx) + resp, err := l.PreViewNodeMultiplier() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/system/setNodeMultiplierHandler.go b/internal/handler/admin/system/setNodeMultiplierHandler.go index 2b01f46..7abc922 100644 --- a/internal/handler/admin/system/setNodeMultiplierHandler.go +++ b/internal/handler/admin/system/setNodeMultiplierHandler.go @@ -2,10 +2,10 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Set Node Multiplier diff --git a/internal/handler/admin/system/settingTelegramBotHandler.go b/internal/handler/admin/system/settingTelegramBotHandler.go index aac5cc4..c6e9087 100644 --- a/internal/handler/admin/system/settingTelegramBotHandler.go +++ b/internal/handler/admin/system/settingTelegramBotHandler.go @@ -2,9 +2,9 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // setting telegram bot diff --git a/internal/handler/admin/system/updateApplicationConfigHandler.go b/internal/handler/admin/system/updateApplicationConfigHandler.go deleted file mode 100644 index 84a296e..0000000 --- a/internal/handler/admin/system/updateApplicationConfigHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// update application config -func UpdateApplicationConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.ApplicationConfig - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := system.NewUpdateApplicationConfigLogic(c.Request.Context(), svcCtx) - err := l.UpdateApplicationConfig(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/system/updateApplicationHandler.go b/internal/handler/admin/system/updateApplicationHandler.go deleted file mode 100644 index f7a0de0..0000000 --- a/internal/handler/admin/system/updateApplicationHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Update application -func UpdateApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.UpdateApplicationRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := system.NewUpdateApplicationLogic(c.Request.Context(), svcCtx) - err := l.UpdateApplication(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/system/updateApplicationVersionHandler.go b/internal/handler/admin/system/updateApplicationVersionHandler.go deleted file mode 100644 index fffe6e3..0000000 --- a/internal/handler/admin/system/updateApplicationVersionHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package system - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Update application version -func UpdateApplicationVersionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.UpdateApplicationVersionRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := system.NewUpdateApplicationVersionLogic(c.Request.Context(), svcCtx) - err := l.UpdateApplicationVersion(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/admin/system/updateCurrencyConfigHandler.go b/internal/handler/admin/system/updateCurrencyConfigHandler.go index 1bcae53..3c5467e 100644 --- a/internal/handler/admin/system/updateCurrencyConfigHandler.go +++ b/internal/handler/admin/system/updateCurrencyConfigHandler.go @@ -2,10 +2,10 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update Currency Config diff --git a/internal/handler/admin/system/updateInviteConfigHandler.go b/internal/handler/admin/system/updateInviteConfigHandler.go index b9e6bbb..2151edf 100644 --- a/internal/handler/admin/system/updateInviteConfigHandler.go +++ b/internal/handler/admin/system/updateInviteConfigHandler.go @@ -2,10 +2,10 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update invite config diff --git a/internal/handler/admin/system/updateNodeConfigHandler.go b/internal/handler/admin/system/updateNodeConfigHandler.go index ceb95de..01e7bbf 100644 --- a/internal/handler/admin/system/updateNodeConfigHandler.go +++ b/internal/handler/admin/system/updateNodeConfigHandler.go @@ -2,10 +2,10 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update node config diff --git a/internal/handler/admin/system/updatePrivacyPolicyConfigHandler.go b/internal/handler/admin/system/updatePrivacyPolicyConfigHandler.go index ca5168b..fc8bb33 100644 --- a/internal/handler/admin/system/updatePrivacyPolicyConfigHandler.go +++ b/internal/handler/admin/system/updatePrivacyPolicyConfigHandler.go @@ -2,10 +2,10 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update Privacy Policy Config diff --git a/internal/handler/admin/system/updateRegisterConfigHandler.go b/internal/handler/admin/system/updateRegisterConfigHandler.go index 507b7b9..fd43850 100644 --- a/internal/handler/admin/system/updateRegisterConfigHandler.go +++ b/internal/handler/admin/system/updateRegisterConfigHandler.go @@ -2,10 +2,10 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update register config diff --git a/internal/handler/admin/system/updateSiteConfigHandler.go b/internal/handler/admin/system/updateSiteConfigHandler.go index 8ecb211..6692ecf 100644 --- a/internal/handler/admin/system/updateSiteConfigHandler.go +++ b/internal/handler/admin/system/updateSiteConfigHandler.go @@ -2,10 +2,10 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update site config diff --git a/internal/handler/admin/system/updateSubscribeConfigHandler.go b/internal/handler/admin/system/updateSubscribeConfigHandler.go index 185302b..3316cc6 100644 --- a/internal/handler/admin/system/updateSubscribeConfigHandler.go +++ b/internal/handler/admin/system/updateSubscribeConfigHandler.go @@ -2,10 +2,10 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update subscribe config diff --git a/internal/handler/admin/system/updateTosConfigHandler.go b/internal/handler/admin/system/updateTosConfigHandler.go index 16c71e2..ee77fba 100644 --- a/internal/handler/admin/system/updateTosConfigHandler.go +++ b/internal/handler/admin/system/updateTosConfigHandler.go @@ -2,10 +2,10 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update Team of Service Config diff --git a/internal/handler/admin/system/updateVerifyCodeConfigHandler.go b/internal/handler/admin/system/updateVerifyCodeConfigHandler.go index 3c27154..04e2902 100644 --- a/internal/handler/admin/system/updateVerifyCodeConfigHandler.go +++ b/internal/handler/admin/system/updateVerifyCodeConfigHandler.go @@ -2,10 +2,10 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update Verify Code Config diff --git a/internal/handler/admin/system/updateVerifyConfigHandler.go b/internal/handler/admin/system/updateVerifyConfigHandler.go index 483985c..730425d 100644 --- a/internal/handler/admin/system/updateVerifyConfigHandler.go +++ b/internal/handler/admin/system/updateVerifyConfigHandler.go @@ -2,10 +2,10 @@ package system import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update verify config diff --git a/internal/handler/admin/ticket/createTicketFollowHandler.go b/internal/handler/admin/ticket/createTicketFollowHandler.go index 34b6b2d..6ede0fc 100644 --- a/internal/handler/admin/ticket/createTicketFollowHandler.go +++ b/internal/handler/admin/ticket/createTicketFollowHandler.go @@ -2,10 +2,10 @@ package ticket import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/ticket" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/ticket" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create ticket follow diff --git a/internal/handler/admin/ticket/getTicketHandler.go b/internal/handler/admin/ticket/getTicketHandler.go index 5058a7f..b8e8ccb 100644 --- a/internal/handler/admin/ticket/getTicketHandler.go +++ b/internal/handler/admin/ticket/getTicketHandler.go @@ -2,10 +2,10 @@ package ticket import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/ticket" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/ticket" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get ticket detail diff --git a/internal/handler/admin/ticket/getTicketListHandler.go b/internal/handler/admin/ticket/getTicketListHandler.go index bfca05c..2fe7264 100644 --- a/internal/handler/admin/ticket/getTicketListHandler.go +++ b/internal/handler/admin/ticket/getTicketListHandler.go @@ -2,10 +2,10 @@ package ticket import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/ticket" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/ticket" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get ticket list diff --git a/internal/handler/admin/ticket/updateTicketStatusHandler.go b/internal/handler/admin/ticket/updateTicketStatusHandler.go index f710b98..30ca534 100644 --- a/internal/handler/admin/ticket/updateTicketStatusHandler.go +++ b/internal/handler/admin/ticket/updateTicketStatusHandler.go @@ -2,10 +2,10 @@ package ticket import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/ticket" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/ticket" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update ticket status diff --git a/internal/handler/admin/tool/getSystemLogHandler.go b/internal/handler/admin/tool/getSystemLogHandler.go index 80dd1d3..fe34bca 100644 --- a/internal/handler/admin/tool/getSystemLogHandler.go +++ b/internal/handler/admin/tool/getSystemLogHandler.go @@ -2,9 +2,9 @@ package tool import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/tool" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/tool" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get System Log diff --git a/internal/handler/admin/tool/getVersionHandler.go b/internal/handler/admin/tool/getVersionHandler.go new file mode 100644 index 0000000..d1c0bce --- /dev/null +++ b/internal/handler/admin/tool/getVersionHandler.go @@ -0,0 +1,18 @@ +package tool + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/tool" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// GetVersionHandler Get Version +func GetVersionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := tool.NewGetVersionLogic(c.Request.Context(), svcCtx) + resp, err := l.GetVersion() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/tool/restartSystemHandler.go b/internal/handler/admin/tool/restartSystemHandler.go index 5d391b0..e77183c 100644 --- a/internal/handler/admin/tool/restartSystemHandler.go +++ b/internal/handler/admin/tool/restartSystemHandler.go @@ -2,9 +2,9 @@ package tool import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/tool" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/tool" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Restart System diff --git a/internal/handler/admin/user/batchDeleteUserHandler.go b/internal/handler/admin/user/batchDeleteUserHandler.go index d0f1c65..e58bdd5 100644 --- a/internal/handler/admin/user/batchDeleteUserHandler.go +++ b/internal/handler/admin/user/batchDeleteUserHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Batch delete user diff --git a/internal/handler/admin/user/createUserAuthMethodHandler.go b/internal/handler/admin/user/createUserAuthMethodHandler.go index de69a03..41e4467 100644 --- a/internal/handler/admin/user/createUserAuthMethodHandler.go +++ b/internal/handler/admin/user/createUserAuthMethodHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create user auth method diff --git a/internal/handler/admin/user/createUserHandler.go b/internal/handler/admin/user/createUserHandler.go index 4de5e76..5de76e6 100644 --- a/internal/handler/admin/user/createUserHandler.go +++ b/internal/handler/admin/user/createUserHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create user diff --git a/internal/handler/admin/user/createUserSubscribeHandler.go b/internal/handler/admin/user/createUserSubscribeHandler.go index 556c75c..3f1a34b 100644 --- a/internal/handler/admin/user/createUserSubscribeHandler.go +++ b/internal/handler/admin/user/createUserSubscribeHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create user subcribe diff --git a/internal/handler/admin/user/currentUserHandler.go b/internal/handler/admin/user/currentUserHandler.go index 3851558..9f65159 100644 --- a/internal/handler/admin/user/currentUserHandler.go +++ b/internal/handler/admin/user/currentUserHandler.go @@ -2,9 +2,9 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Current user diff --git a/internal/handler/admin/user/deleteUserAuthMethodHandler.go b/internal/handler/admin/user/deleteUserAuthMethodHandler.go index f9cde21..80a5fcc 100644 --- a/internal/handler/admin/user/deleteUserAuthMethodHandler.go +++ b/internal/handler/admin/user/deleteUserAuthMethodHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Delete user auth method diff --git a/internal/handler/admin/user/deleteUserDeviceHandler.go b/internal/handler/admin/user/deleteUserDeviceHandler.go index 6269cbe..3dd4b13 100644 --- a/internal/handler/admin/user/deleteUserDeviceHandler.go +++ b/internal/handler/admin/user/deleteUserDeviceHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Delete user device diff --git a/internal/handler/admin/user/deleteUserHandler.go b/internal/handler/admin/user/deleteUserHandler.go index 7e077b1..70a9410 100644 --- a/internal/handler/admin/user/deleteUserHandler.go +++ b/internal/handler/admin/user/deleteUserHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Delete user diff --git a/internal/handler/admin/user/deleteUserSubscribeHandler.go b/internal/handler/admin/user/deleteUserSubscribeHandler.go index 9ff53da..febcea0 100644 --- a/internal/handler/admin/user/deleteUserSubscribeHandler.go +++ b/internal/handler/admin/user/deleteUserSubscribeHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Delete user subcribe diff --git a/internal/handler/admin/user/getUserAuthMethodHandler.go b/internal/handler/admin/user/getUserAuthMethodHandler.go index 9e5c60c..f93494b 100644 --- a/internal/handler/admin/user/getUserAuthMethodHandler.go +++ b/internal/handler/admin/user/getUserAuthMethodHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get user auth method diff --git a/internal/handler/admin/user/getUserDetailHandler.go b/internal/handler/admin/user/getUserDetailHandler.go index 30a8f50..aadd63c 100644 --- a/internal/handler/admin/user/getUserDetailHandler.go +++ b/internal/handler/admin/user/getUserDetailHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get user detail diff --git a/internal/handler/admin/user/getUserListHandler.go b/internal/handler/admin/user/getUserListHandler.go index 6af432f..54eff40 100644 --- a/internal/handler/admin/user/getUserListHandler.go +++ b/internal/handler/admin/user/getUserListHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get user list diff --git a/internal/handler/admin/user/getUserLoginLogsHandler.go b/internal/handler/admin/user/getUserLoginLogsHandler.go index 99646c7..fa2ee60 100644 --- a/internal/handler/admin/user/getUserLoginLogsHandler.go +++ b/internal/handler/admin/user/getUserLoginLogsHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get user login logs diff --git a/internal/handler/admin/user/getUserSubscribeByIdHandler.go b/internal/handler/admin/user/getUserSubscribeByIdHandler.go index 49df56a..e591e59 100644 --- a/internal/handler/admin/user/getUserSubscribeByIdHandler.go +++ b/internal/handler/admin/user/getUserSubscribeByIdHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get user subcribe by id diff --git a/internal/handler/admin/user/getUserSubscribeDevicesHandler.go b/internal/handler/admin/user/getUserSubscribeDevicesHandler.go index cc342fb..4f8ecdf 100644 --- a/internal/handler/admin/user/getUserSubscribeDevicesHandler.go +++ b/internal/handler/admin/user/getUserSubscribeDevicesHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get user subcribe devices diff --git a/internal/handler/admin/user/getUserSubscribeHandler.go b/internal/handler/admin/user/getUserSubscribeHandler.go index 1bf0125..57c151f 100644 --- a/internal/handler/admin/user/getUserSubscribeHandler.go +++ b/internal/handler/admin/user/getUserSubscribeHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get user subcribe diff --git a/internal/handler/admin/user/getUserSubscribeLogsHandler.go b/internal/handler/admin/user/getUserSubscribeLogsHandler.go index a9a4263..7b8d362 100644 --- a/internal/handler/admin/user/getUserSubscribeLogsHandler.go +++ b/internal/handler/admin/user/getUserSubscribeLogsHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get user subcribe logs diff --git a/internal/handler/admin/user/getUserSubscribeResetTrafficLogsHandler.go b/internal/handler/admin/user/getUserSubscribeResetTrafficLogsHandler.go new file mode 100644 index 0000000..0f7525d --- /dev/null +++ b/internal/handler/admin/user/getUserSubscribeResetTrafficLogsHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Get user subcribe reset traffic logs +func GetUserSubscribeResetTrafficLogsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.GetUserSubscribeResetTrafficLogsRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewGetUserSubscribeResetTrafficLogsLogic(c.Request.Context(), svcCtx) + resp, err := l.GetUserSubscribeResetTrafficLogs(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/user/getUserSubscribeTrafficLogsHandler.go b/internal/handler/admin/user/getUserSubscribeTrafficLogsHandler.go index f6cf5fb..ce42b79 100644 --- a/internal/handler/admin/user/getUserSubscribeTrafficLogsHandler.go +++ b/internal/handler/admin/user/getUserSubscribeTrafficLogsHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get user subcribe traffic logs diff --git a/internal/handler/admin/user/kickOfflineByUserDeviceHandler.go b/internal/handler/admin/user/kickOfflineByUserDeviceHandler.go index ab9b4f6..d4b99c8 100644 --- a/internal/handler/admin/user/kickOfflineByUserDeviceHandler.go +++ b/internal/handler/admin/user/kickOfflineByUserDeviceHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // kick offline user device diff --git a/internal/handler/admin/user/updateUserAuthMethodHandler.go b/internal/handler/admin/user/updateUserAuthMethodHandler.go index 1f289c8..354fcf2 100644 --- a/internal/handler/admin/user/updateUserAuthMethodHandler.go +++ b/internal/handler/admin/user/updateUserAuthMethodHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update user auth method diff --git a/internal/handler/admin/user/updateUserBasicInfoHandler.go b/internal/handler/admin/user/updateUserBasicInfoHandler.go index 4187bfc..7a1fc78 100644 --- a/internal/handler/admin/user/updateUserBasicInfoHandler.go +++ b/internal/handler/admin/user/updateUserBasicInfoHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update user basic info diff --git a/internal/handler/admin/user/updateUserDeviceHandler.go b/internal/handler/admin/user/updateUserDeviceHandler.go index ae1b813..5b4e178 100644 --- a/internal/handler/admin/user/updateUserDeviceHandler.go +++ b/internal/handler/admin/user/updateUserDeviceHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // User device diff --git a/internal/handler/admin/user/updateUserNotifySettingHandler.go b/internal/handler/admin/user/updateUserNotifySettingHandler.go index 3644c5b..a4fc785 100644 --- a/internal/handler/admin/user/updateUserNotifySettingHandler.go +++ b/internal/handler/admin/user/updateUserNotifySettingHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update user notify setting diff --git a/internal/handler/admin/user/updateUserSubscribeHandler.go b/internal/handler/admin/user/updateUserSubscribeHandler.go index c1bca31..3e111d3 100644 --- a/internal/handler/admin/user/updateUserSubscribeHandler.go +++ b/internal/handler/admin/user/updateUserSubscribeHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/admin/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/admin/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update user subcribe diff --git a/internal/handler/app/announcement/queryannouncementhandler.go b/internal/handler/app/announcement/queryannouncementhandler.go deleted file mode 100644 index 7eec557..0000000 --- a/internal/handler/app/announcement/queryannouncementhandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package announcement - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/announcement" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Query announcement -func QueryAnnouncementHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.QueryAnnouncementRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := announcement.NewQueryAnnouncementLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryAnnouncement(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/auth/checkHandler.go b/internal/handler/app/auth/checkHandler.go deleted file mode 100644 index 6265f38..0000000 --- a/internal/handler/app/auth/checkHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package auth - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Check Account -func CheckHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.AppAuthCheckRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := auth.NewCheckLogic(c, svcCtx) - resp, err := l.Check(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/auth/getAppConfigHandler.go b/internal/handler/app/auth/getAppConfigHandler.go deleted file mode 100644 index 83ea8c5..0000000 --- a/internal/handler/app/auth/getAppConfigHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package auth - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// GetAppConfig -func GetAppConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.AppConfigRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := auth.NewGetAppConfigLogic(c, svcCtx) - resp, err := l.GetAppConfig(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/auth/loginHandler.go b/internal/handler/app/auth/loginHandler.go deleted file mode 100644 index b6cbe1a..0000000 --- a/internal/handler/app/auth/loginHandler.go +++ /dev/null @@ -1,42 +0,0 @@ -package auth - -import ( - "time" - - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/turnstile" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -// Login -func LoginHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.AppAuthRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - if svcCtx.Config.Verify.LoginVerify { - verifyTurns := turnstile.New(turnstile.Config{ - Secret: svcCtx.Config.Verify.TurnstileSecret, - Timeout: 3 * time.Second, - }) - if verify, err := verifyTurns.Verify(c, req.CfToken, c.ClientIP()); err != nil || !verify { - err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) - result.HttpResult(c, nil, err) - return - } - } - l := auth.NewLoginLogic(c, svcCtx) - resp, err := l.Login(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/auth/registerHandler.go b/internal/handler/app/auth/registerHandler.go deleted file mode 100644 index 530fb89..0000000 --- a/internal/handler/app/auth/registerHandler.go +++ /dev/null @@ -1,43 +0,0 @@ -package auth - -import ( - "time" - - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/turnstile" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -// Register -func RegisterHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.AppAuthRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - // get client ip - if svcCtx.Config.Verify.RegisterVerify { - verifyTurns := turnstile.New(turnstile.Config{ - Secret: svcCtx.Config.Verify.TurnstileSecret, - Timeout: 3 * time.Second, - }) - if verify, err := verifyTurns.Verify(c, req.CfToken, c.ClientIP()); err != nil || !verify { - err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) - result.HttpResult(c, nil, err) - return - } - } - - l := auth.NewRegisterLogic(c, svcCtx) - resp, err := l.Register(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/auth/resetPasswordHandler.go b/internal/handler/app/auth/resetPasswordHandler.go deleted file mode 100644 index c1c8dd2..0000000 --- a/internal/handler/app/auth/resetPasswordHandler.go +++ /dev/null @@ -1,41 +0,0 @@ -package auth - -import ( - "time" - - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/turnstile" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -// Reset Password -func ResetPasswordHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.AppAuthRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - if svcCtx.Config.Verify.ResetPasswordVerify { - verifyTurns := turnstile.New(turnstile.Config{ - Secret: svcCtx.Config.Verify.TurnstileSecret, - Timeout: 3 * time.Second, - }) - if verify, err := verifyTurns.Verify(c, req.CfToken, c.ClientIP()); err != nil || !verify { - err = errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "error: %v, verify: %v", err, verify) - result.HttpResult(c, nil, err) - return - } - } - l := auth.NewResetPasswordLogic(c, svcCtx) - resp, err := l.ResetPassword(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/document/querydocumentdetailhandler.go b/internal/handler/app/document/querydocumentdetailhandler.go deleted file mode 100644 index 36050e4..0000000 --- a/internal/handler/app/document/querydocumentdetailhandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package document - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/document" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get document detail -func QueryDocumentDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.QueryDocumentDetailRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := document.NewQueryDocumentDetailLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryDocumentDetail(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/document/querydocumentlisthandler.go b/internal/handler/app/document/querydocumentlisthandler.go deleted file mode 100644 index 842704a..0000000 --- a/internal/handler/app/document/querydocumentlisthandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package document - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/document" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get document list -func QueryDocumentListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := document.NewQueryDocumentListLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryDocumentList() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/node/getNodeListHandler.go b/internal/handler/app/node/getNodeListHandler.go deleted file mode 100644 index 8e0b3f0..0000000 --- a/internal/handler/app/node/getNodeListHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package node - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/node" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get Node list -func GetNodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.AppUserSubscbribeNodeRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := node.NewGetNodeListLogic(c.Request.Context(), svcCtx) - resp, err := l.GetNodeList(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/node/getRuleGroupListHandler.go b/internal/handler/app/node/getRuleGroupListHandler.go deleted file mode 100644 index 1b08f0a..0000000 --- a/internal/handler/app/node/getRuleGroupListHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package node - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/node" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get rule group list -func GetRuleGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := node.NewGetRuleGroupListLogic(c.Request.Context(), svcCtx) - resp, err := l.GetRuleGroupList() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/order/checkoutorderhandler.go b/internal/handler/app/order/checkoutorderhandler.go deleted file mode 100644 index ec23ebc..0000000 --- a/internal/handler/app/order/checkoutorderhandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package order - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Checkout order -func CheckoutOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.CheckoutOrderRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := order.NewCheckoutOrderLogic(c.Request.Context(), svcCtx) - resp, err := l.CheckoutOrder(&req, c.Request.Host) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/order/closeorderhandler.go b/internal/handler/app/order/closeorderhandler.go deleted file mode 100644 index 474cfdc..0000000 --- a/internal/handler/app/order/closeorderhandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package order - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Close order -func CloseOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.CloseOrderRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := order.NewCloseOrderLogic(c.Request.Context(), svcCtx) - err := l.CloseOrder(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/app/order/precreateorderhandler.go b/internal/handler/app/order/precreateorderhandler.go deleted file mode 100644 index e7a3b63..0000000 --- a/internal/handler/app/order/precreateorderhandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package order - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Pre create order -func PreCreateOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.PurchaseOrderRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := order.NewPreCreateOrderLogic(c.Request.Context(), svcCtx) - resp, err := l.PreCreateOrder(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/order/purchasehandler.go b/internal/handler/app/order/purchasehandler.go deleted file mode 100644 index 99e3d45..0000000 --- a/internal/handler/app/order/purchasehandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package order - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// purchase Subscription -func PurchaseHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.PurchaseOrderRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := order.NewPurchaseLogic(c.Request.Context(), svcCtx) - resp, err := l.Purchase(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/order/queryorderdetailhandler.go b/internal/handler/app/order/queryorderdetailhandler.go deleted file mode 100644 index 3adc5b0..0000000 --- a/internal/handler/app/order/queryorderdetailhandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package order - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get order -func QueryOrderDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.QueryOrderDetailRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := order.NewQueryOrderDetailLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryOrderDetail(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/order/queryorderlisthandler.go b/internal/handler/app/order/queryorderlisthandler.go deleted file mode 100644 index 7a37db3..0000000 --- a/internal/handler/app/order/queryorderlisthandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package order - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get order list -func QueryOrderListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.QueryOrderListRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := order.NewQueryOrderListLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryOrderList(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/order/rechargehandler.go b/internal/handler/app/order/rechargehandler.go deleted file mode 100644 index 56fc11c..0000000 --- a/internal/handler/app/order/rechargehandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package order - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Recharge -func RechargeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.RechargeOrderRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := order.NewRechargeLogic(c.Request.Context(), svcCtx) - resp, err := l.Recharge(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/order/renewalhandler.go b/internal/handler/app/order/renewalhandler.go deleted file mode 100644 index 9482181..0000000 --- a/internal/handler/app/order/renewalhandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package order - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Renewal Subscription -func RenewalHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.RenewalOrderRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := order.NewRenewalLogic(c.Request.Context(), svcCtx) - resp, err := l.Renewal(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/order/resettraffichandler.go b/internal/handler/app/order/resettraffichandler.go deleted file mode 100644 index bdffad3..0000000 --- a/internal/handler/app/order/resettraffichandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package order - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Reset traffic -func ResetTrafficHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.ResetTrafficOrderRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := order.NewResetTrafficLogic(c.Request.Context(), svcCtx) - resp, err := l.ResetTraffic(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/payment/getavailablepaymentmethodshandler.go b/internal/handler/app/payment/getavailablepaymentmethodshandler.go deleted file mode 100644 index 57e7495..0000000 --- a/internal/handler/app/payment/getavailablepaymentmethodshandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package payment - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get available payment methods -func GetAvailablePaymentMethodsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := payment.NewGetAvailablePaymentMethodsLogic(c.Request.Context(), svcCtx) - resp, err := l.GetAvailablePaymentMethods() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/subscribe/queryApplicationConfigHandler.go b/internal/handler/app/subscribe/queryApplicationConfigHandler.go deleted file mode 100644 index f663047..0000000 --- a/internal/handler/app/subscribe/queryApplicationConfigHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package subscribe - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get application config -func QueryApplicationConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := subscribe.NewQueryApplicationConfigLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryApplicationConfig() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/subscribe/querySubscribeGroupListHandler.go b/internal/handler/app/subscribe/querySubscribeGroupListHandler.go deleted file mode 100644 index 8403fe8..0000000 --- a/internal/handler/app/subscribe/querySubscribeGroupListHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package subscribe - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get subscribe group list -func QuerySubscribeGroupListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := subscribe.NewQuerySubscribeGroupListLogic(c.Request.Context(), svcCtx) - resp, err := l.QuerySubscribeGroupList() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/subscribe/querySubscribeListHandler.go b/internal/handler/app/subscribe/querySubscribeListHandler.go deleted file mode 100644 index d9f427d..0000000 --- a/internal/handler/app/subscribe/querySubscribeListHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package subscribe - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get subscribe list -func QuerySubscribeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := subscribe.NewQuerySubscribeListLogic(c.Request.Context(), svcCtx) - resp, err := l.QuerySubscribeList() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/subscribe/queryUserAlreadySubscribeHandler.go b/internal/handler/app/subscribe/queryUserAlreadySubscribeHandler.go deleted file mode 100644 index b5324dd..0000000 --- a/internal/handler/app/subscribe/queryUserAlreadySubscribeHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package subscribe - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get Already subscribed to package -func QueryUserAlreadySubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := subscribe.NewQueryUserAlreadySubscribeLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryUserAlreadySubscribe() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/subscribe/queryUserAvailableUserSubscribeHandler.go b/internal/handler/app/subscribe/queryUserAvailableUserSubscribeHandler.go deleted file mode 100644 index 9f87808..0000000 --- a/internal/handler/app/subscribe/queryUserAvailableUserSubscribeHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package subscribe - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get Available subscriptions for users -func QueryUserAvailableUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.AppUserSubscribeRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := subscribe.NewQueryUserAvailableUserSubscribeLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryUserAvailableUserSubscribe(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/subscribe/resetUserSubscribePeriodHandler.go b/internal/handler/app/subscribe/resetUserSubscribePeriodHandler.go deleted file mode 100644 index fa687b9..0000000 --- a/internal/handler/app/subscribe/resetUserSubscribePeriodHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package subscribe - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Reset user subscription period -func ResetUserSubscribePeriodHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.UserSubscribeResetPeriodRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := subscribe.NewResetUserSubscribePeriodLogic(c.Request.Context(), svcCtx) - resp, err := l.ResetUserSubscribePeriod(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/user/deleteAccountHandler.go b/internal/handler/app/user/deleteAccountHandler.go deleted file mode 100644 index b7d5337..0000000 --- a/internal/handler/app/user/deleteAccountHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package user - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Delete Account -func DeleteAccountHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.DeleteAccountRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := user.NewDeleteAccountLogic(c.Request.Context(), svcCtx) - err := l.DeleteAccount(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/app/user/getuseronlinetimestatisticshandler.go b/internal/handler/app/user/getuseronlinetimestatisticshandler.go deleted file mode 100644 index 678ffa5..0000000 --- a/internal/handler/app/user/getuseronlinetimestatisticshandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package user - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get user online time total -func GetUserOnlineTimeStatisticsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := user.NewGetUserOnlineTimeStatisticsLogic(c.Request.Context(), svcCtx) - resp, err := l.GetUserOnlineTimeStatistics() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/user/getusersubscribetrafficlogshandler.go b/internal/handler/app/user/getusersubscribetrafficlogshandler.go deleted file mode 100644 index b1cc669..0000000 --- a/internal/handler/app/user/getusersubscribetrafficlogshandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package user - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get user subcribe traffic logs -func GetUserSubscribeTrafficLogsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.GetUserSubscribeTrafficLogsRequest - _ = c.BindQuery(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := user.NewGetUserSubscribeTrafficLogsLogic(c.Request.Context(), svcCtx) - resp, err := l.GetUserSubscribeTrafficLogs(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/user/queryUserInfoHandler.go b/internal/handler/app/user/queryUserInfoHandler.go deleted file mode 100644 index c955983..0000000 --- a/internal/handler/app/user/queryUserInfoHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package user - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// query user info -func QueryUserInfoHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := user.NewQueryUserInfoLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryUserInfo() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/user/queryuseraffiliatehandler.go b/internal/handler/app/user/queryuseraffiliatehandler.go deleted file mode 100644 index c5bfd2f..0000000 --- a/internal/handler/app/user/queryuseraffiliatehandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package user - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Query User Affiliate Count -func QueryUserAffiliateHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := user.NewQueryUserAffiliateLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryUserAffiliate() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/user/queryuseraffiliatelisthandler.go b/internal/handler/app/user/queryuseraffiliatelisthandler.go deleted file mode 100644 index 6e2f6a5..0000000 --- a/internal/handler/app/user/queryuseraffiliatelisthandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package user - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Query User Affiliate List -func QueryUserAffiliateListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.QueryUserAffiliateListRequest - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := user.NewQueryUserAffiliateListLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryUserAffiliateList(&req) - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/app/user/updatePasswordHandler.go b/internal/handler/app/user/updatePasswordHandler.go deleted file mode 100644 index 532612e..0000000 --- a/internal/handler/app/user/updatePasswordHandler.go +++ /dev/null @@ -1,26 +0,0 @@ -package user - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Update Password -func UpdatePasswordHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - var req types.UpdatePasswordRequeset - _ = c.ShouldBind(&req) - validateErr := svcCtx.Validate(&req) - if validateErr != nil { - result.ParamErrorResult(c, validateErr) - return - } - - l := user.NewUpdatePasswordLogic(c.Request.Context(), svcCtx) - err := l.UpdatePassword(&req) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/app/ws/appWsHandler.go b/internal/handler/app/ws/appWsHandler.go deleted file mode 100644 index 6e90b67..0000000 --- a/internal/handler/app/ws/appWsHandler.go +++ /dev/null @@ -1,20 +0,0 @@ -package ws - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/app/ws" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// App heartbeat -func AppWsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - ctx := c.Request.Context() - - // Logic: App heartbeat - l := ws.NewAppWsLogic(ctx, svcCtx) - err := l.AppWs(c.Writer, c.Request, c.Param("userid"), c.Param("identifier")) - result.HttpResult(c, nil, err) - } -} diff --git a/internal/handler/auth/checkUserHandler.go b/internal/handler/auth/checkUserHandler.go index 840642b..86c3a7d 100644 --- a/internal/handler/auth/checkUserHandler.go +++ b/internal/handler/auth/checkUserHandler.go @@ -2,10 +2,10 @@ package auth import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Check user is exist diff --git a/internal/handler/auth/checkUserTelephoneHandler.go b/internal/handler/auth/checkUserTelephoneHandler.go index c438b84..649eba9 100644 --- a/internal/handler/auth/checkUserTelephoneHandler.go +++ b/internal/handler/auth/checkUserTelephoneHandler.go @@ -2,10 +2,10 @@ package auth import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Check user telephone is exist diff --git a/internal/handler/auth/deviceLoginHandler.go b/internal/handler/auth/deviceLoginHandler.go new file mode 100644 index 0000000..6a772bf --- /dev/null +++ b/internal/handler/auth/deviceLoginHandler.go @@ -0,0 +1,26 @@ +package auth + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Device Login +func DeviceLoginHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.DeviceLoginRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := auth.NewDeviceLoginLogic(c.Request.Context(), svcCtx) + resp, err := l.DeviceLogin(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/auth/oauth/appleLoginCallbackHandler.go b/internal/handler/auth/oauth/appleLoginCallbackHandler.go index e69764d..fe256ca 100644 --- a/internal/handler/auth/oauth/appleLoginCallbackHandler.go +++ b/internal/handler/auth/oauth/appleLoginCallbackHandler.go @@ -4,10 +4,10 @@ import ( "net/http" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/auth/oauth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/auth/oauth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Apple Login Callback diff --git a/internal/handler/auth/oauth/oAuthLoginGetTokenHandler.go b/internal/handler/auth/oauth/oAuthLoginGetTokenHandler.go index 384dafb..a08f475 100644 --- a/internal/handler/auth/oauth/oAuthLoginGetTokenHandler.go +++ b/internal/handler/auth/oauth/oAuthLoginGetTokenHandler.go @@ -2,10 +2,10 @@ package oauth import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/auth/oauth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/auth/oauth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // OAuth login get token diff --git a/internal/handler/auth/oauth/oAuthLoginHandler.go b/internal/handler/auth/oauth/oAuthLoginHandler.go index 1653f3b..588f0d6 100644 --- a/internal/handler/auth/oauth/oAuthLoginHandler.go +++ b/internal/handler/auth/oauth/oAuthLoginHandler.go @@ -2,10 +2,10 @@ package oauth import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/auth/oauth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/auth/oauth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // OAuth login diff --git a/internal/handler/auth/resetPasswordHandler.go b/internal/handler/auth/resetPasswordHandler.go index 73848b2..d4edc9b 100644 --- a/internal/handler/auth/resetPasswordHandler.go +++ b/internal/handler/auth/resetPasswordHandler.go @@ -4,12 +4,12 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/turnstile" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/logic/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" + "github.com/perfect-panel/server/pkg/turnstile" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/handler/auth/telephoneLoginHandler.go b/internal/handler/auth/telephoneLoginHandler.go index 798b1cd..44c1b53 100644 --- a/internal/handler/auth/telephoneLoginHandler.go +++ b/internal/handler/auth/telephoneLoginHandler.go @@ -4,12 +4,12 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/turnstile" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/logic/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" + "github.com/perfect-panel/server/pkg/turnstile" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/handler/auth/telephoneResetPasswordHandler.go b/internal/handler/auth/telephoneResetPasswordHandler.go index 4b870c0..16a5105 100644 --- a/internal/handler/auth/telephoneResetPasswordHandler.go +++ b/internal/handler/auth/telephoneResetPasswordHandler.go @@ -4,12 +4,12 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/turnstile" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/logic/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" + "github.com/perfect-panel/server/pkg/turnstile" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/handler/auth/telephoneUserRegisterHandler.go b/internal/handler/auth/telephoneUserRegisterHandler.go index bcaef82..45a7ba8 100644 --- a/internal/handler/auth/telephoneUserRegisterHandler.go +++ b/internal/handler/auth/telephoneUserRegisterHandler.go @@ -4,12 +4,12 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/turnstile" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/logic/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" + "github.com/perfect-panel/server/pkg/turnstile" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -25,6 +25,7 @@ func TelephoneUserRegisterHandler(svcCtx *svc.ServiceContext) func(c *gin.Contex } // get client ip req.IP = c.ClientIP() + req.UserAgent = c.Request.UserAgent() if svcCtx.Config.Verify.RegisterVerify { verifyTurns := turnstile.New(turnstile.Config{ Secret: svcCtx.Config.Verify.TurnstileSecret, diff --git a/internal/handler/auth/userLoginHandler.go b/internal/handler/auth/userLoginHandler.go index 1e857c3..20eff59 100644 --- a/internal/handler/auth/userLoginHandler.go +++ b/internal/handler/auth/userLoginHandler.go @@ -4,12 +4,12 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/turnstile" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/logic/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" + "github.com/perfect-panel/server/pkg/turnstile" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/handler/auth/userRegisterHandler.go b/internal/handler/auth/userRegisterHandler.go index 9bdcd99..ea40223 100644 --- a/internal/handler/auth/userRegisterHandler.go +++ b/internal/handler/auth/userRegisterHandler.go @@ -4,12 +4,12 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/turnstile" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/logic/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" + "github.com/perfect-panel/server/pkg/turnstile" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -20,6 +20,7 @@ func UserRegisterHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { _ = c.ShouldBind(&req) // get client ip req.IP = c.ClientIP() + req.UserAgent = c.Request.UserAgent() if svcCtx.Config.Verify.RegisterVerify { verifyTurns := turnstile.New(turnstile.Config{ Secret: svcCtx.Config.Verify.TurnstileSecret, diff --git a/internal/handler/common/checkverificationcodehandler.go b/internal/handler/common/checkverificationcodehandler.go index 0bdf6be..70c743f 100644 --- a/internal/handler/common/checkverificationcodehandler.go +++ b/internal/handler/common/checkverificationcodehandler.go @@ -2,10 +2,10 @@ package common import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Check verification code diff --git a/internal/handler/common/getAdsHandler.go b/internal/handler/common/getAdsHandler.go index 5eba200..f2c2192 100644 --- a/internal/handler/common/getAdsHandler.go +++ b/internal/handler/common/getAdsHandler.go @@ -2,10 +2,10 @@ package common import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get Ads diff --git a/internal/handler/common/getApplicationHandler.go b/internal/handler/common/getApplicationHandler.go deleted file mode 100644 index fa91a77..0000000 --- a/internal/handler/common/getApplicationHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package common - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get Tos Content -func GetApplicationHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := common.NewGetApplicationLogic(c.Request.Context(), svcCtx) - resp, err := l.GetApplication() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/common/getClientHandler.go b/internal/handler/common/getClientHandler.go new file mode 100644 index 0000000..e40b555 --- /dev/null +++ b/internal/handler/common/getClientHandler.go @@ -0,0 +1,18 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Get Client +func GetClientHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := common.NewGetClientLogic(c.Request.Context(), svcCtx) + resp, err := l.GetClient() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/common/getGlobalConfigHandler.go b/internal/handler/common/getGlobalConfigHandler.go index 0d45405..9f520f4 100644 --- a/internal/handler/common/getGlobalConfigHandler.go +++ b/internal/handler/common/getGlobalConfigHandler.go @@ -2,9 +2,9 @@ package common import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get global config diff --git a/internal/handler/common/getPrivacyPolicyHandler.go b/internal/handler/common/getPrivacyPolicyHandler.go index 09f6c6e..4674d23 100644 --- a/internal/handler/common/getPrivacyPolicyHandler.go +++ b/internal/handler/common/getPrivacyPolicyHandler.go @@ -2,9 +2,9 @@ package common import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get Privacy Policy diff --git a/internal/handler/common/getStatHandler.go b/internal/handler/common/getStatHandler.go index a370fd3..9e6b4fa 100644 --- a/internal/handler/common/getStatHandler.go +++ b/internal/handler/common/getStatHandler.go @@ -2,9 +2,9 @@ package common import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get stat diff --git a/internal/handler/common/getSubscriptionHandler.go b/internal/handler/common/getSubscriptionHandler.go deleted file mode 100644 index a63e221..0000000 --- a/internal/handler/common/getSubscriptionHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package common - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get Subscription -func GetSubscriptionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := common.NewGetSubscriptionLogic(c.Request.Context(), svcCtx) - resp, err := l.GetSubscription() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/common/getTosHandler.go b/internal/handler/common/getTosHandler.go index 2979881..caa5c5a 100644 --- a/internal/handler/common/getTosHandler.go +++ b/internal/handler/common/getTosHandler.go @@ -2,9 +2,9 @@ package common import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get Tos Content diff --git a/internal/handler/common/sendEmailCodeHandler.go b/internal/handler/common/sendEmailCodeHandler.go index 7c9d372..09b1c94 100644 --- a/internal/handler/common/sendEmailCodeHandler.go +++ b/internal/handler/common/sendEmailCodeHandler.go @@ -2,10 +2,10 @@ package common import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get verification code diff --git a/internal/handler/common/sendSmsCodeHandler.go b/internal/handler/common/sendSmsCodeHandler.go index 2b7220d..097b99f 100644 --- a/internal/handler/common/sendSmsCodeHandler.go +++ b/internal/handler/common/sendSmsCodeHandler.go @@ -2,10 +2,10 @@ package common import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get sms verification code diff --git a/internal/handler/notify.go b/internal/handler/notify.go index a73bd70..3055dcd 100644 --- a/internal/handler/notify.go +++ b/internal/handler/notify.go @@ -2,9 +2,9 @@ package handler import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/handler/notify" - "github.com/perfect-panel/ppanel-server/internal/middleware" - "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/server/internal/handler/notify" + "github.com/perfect-panel/server/internal/middleware" + "github.com/perfect-panel/server/internal/svc" ) func RegisterNotifyHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { diff --git a/internal/handler/notify/paymentNotifyHandler.go b/internal/handler/notify/paymentNotifyHandler.go index f84ad00..cd7d8b9 100644 --- a/internal/handler/notify/paymentNotifyHandler.go +++ b/internal/handler/notify/paymentNotifyHandler.go @@ -4,15 +4,15 @@ import ( "fmt" "net/http" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/notify" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/payment" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/notify" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/payment" + "github.com/perfect-panel/server/pkg/result" ) // PaymentNotifyHandler Payment Notify @@ -26,7 +26,7 @@ func PaymentNotifyHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { } switch payment.ParsePlatform(platform) { - case payment.EPay: + case payment.EPay, payment.CryptoSaaS: req := &types.EPayNotifyRequest{} if err := c.ShouldBind(req); err != nil { result.HttpResult(c, nil, err) diff --git a/internal/handler/public/announcement/queryAnnouncementHandler.go b/internal/handler/public/announcement/queryAnnouncementHandler.go index dd2a8e8..ae222f0 100644 --- a/internal/handler/public/announcement/queryAnnouncementHandler.go +++ b/internal/handler/public/announcement/queryAnnouncementHandler.go @@ -2,10 +2,10 @@ package announcement import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/announcement" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/announcement" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Query announcement diff --git a/internal/handler/public/document/queryDocumentDetailHandler.go b/internal/handler/public/document/queryDocumentDetailHandler.go index 7b2524b..92a2168 100644 --- a/internal/handler/public/document/queryDocumentDetailHandler.go +++ b/internal/handler/public/document/queryDocumentDetailHandler.go @@ -2,10 +2,10 @@ package document import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/document" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/document" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get document detail diff --git a/internal/handler/public/document/queryDocumentListHandler.go b/internal/handler/public/document/queryDocumentListHandler.go index 9a57040..9344d62 100644 --- a/internal/handler/public/document/queryDocumentListHandler.go +++ b/internal/handler/public/document/queryDocumentListHandler.go @@ -2,9 +2,9 @@ package document import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/document" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/document" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get document list diff --git a/internal/handler/public/order/closeOrderHandler.go b/internal/handler/public/order/closeOrderHandler.go index c0c9120..f92c7c8 100644 --- a/internal/handler/public/order/closeOrderHandler.go +++ b/internal/handler/public/order/closeOrderHandler.go @@ -2,10 +2,10 @@ package order import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Close order diff --git a/internal/handler/public/order/preCreateOrderHandler.go b/internal/handler/public/order/preCreateOrderHandler.go index bca9773..1e5f505 100644 --- a/internal/handler/public/order/preCreateOrderHandler.go +++ b/internal/handler/public/order/preCreateOrderHandler.go @@ -2,10 +2,10 @@ package order import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Pre create order diff --git a/internal/handler/public/order/purchaseHandler.go b/internal/handler/public/order/purchaseHandler.go index a7eb606..3dcfe40 100644 --- a/internal/handler/public/order/purchaseHandler.go +++ b/internal/handler/public/order/purchaseHandler.go @@ -2,10 +2,10 @@ package order import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // purchase Subscription diff --git a/internal/handler/public/order/queryOrderDetailHandler.go b/internal/handler/public/order/queryOrderDetailHandler.go index dcc3b9f..ff65632 100644 --- a/internal/handler/public/order/queryOrderDetailHandler.go +++ b/internal/handler/public/order/queryOrderDetailHandler.go @@ -2,10 +2,10 @@ package order import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get order diff --git a/internal/handler/public/order/queryOrderListHandler.go b/internal/handler/public/order/queryOrderListHandler.go index 4ecf7ce..e8bf02b 100644 --- a/internal/handler/public/order/queryOrderListHandler.go +++ b/internal/handler/public/order/queryOrderListHandler.go @@ -2,10 +2,10 @@ package order import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get order list diff --git a/internal/handler/public/order/rechargeHandler.go b/internal/handler/public/order/rechargeHandler.go index 3807585..e64da16 100644 --- a/internal/handler/public/order/rechargeHandler.go +++ b/internal/handler/public/order/rechargeHandler.go @@ -2,10 +2,10 @@ package order import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Recharge diff --git a/internal/handler/public/order/renewalHandler.go b/internal/handler/public/order/renewalHandler.go index 8cd6c5a..e9d769b 100644 --- a/internal/handler/public/order/renewalHandler.go +++ b/internal/handler/public/order/renewalHandler.go @@ -2,10 +2,10 @@ package order import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Renewal Subscription diff --git a/internal/handler/public/order/resetTrafficHandler.go b/internal/handler/public/order/resetTrafficHandler.go index d4cb5af..ae5d9a8 100644 --- a/internal/handler/public/order/resetTrafficHandler.go +++ b/internal/handler/public/order/resetTrafficHandler.go @@ -2,10 +2,10 @@ package order import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Reset traffic diff --git a/internal/handler/public/payment/getAvailablePaymentMethodsHandler.go b/internal/handler/public/payment/getAvailablePaymentMethodsHandler.go index 96aa03d..2bdbeb2 100644 --- a/internal/handler/public/payment/getAvailablePaymentMethodsHandler.go +++ b/internal/handler/public/payment/getAvailablePaymentMethodsHandler.go @@ -2,9 +2,9 @@ package payment import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get available payment methods diff --git a/internal/handler/public/portal/getAvailablePaymentMethodsHandler.go b/internal/handler/public/portal/getAvailablePaymentMethodsHandler.go index cd6a19a..8955117 100644 --- a/internal/handler/public/portal/getAvailablePaymentMethodsHandler.go +++ b/internal/handler/public/portal/getAvailablePaymentMethodsHandler.go @@ -2,9 +2,9 @@ package portal import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/portal" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/portal" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get available payment methods diff --git a/internal/handler/public/portal/getSubscriptionHandler.go b/internal/handler/public/portal/getSubscriptionHandler.go index 695ceba..6d7f735 100644 --- a/internal/handler/public/portal/getSubscriptionHandler.go +++ b/internal/handler/public/portal/getSubscriptionHandler.go @@ -2,16 +2,25 @@ package portal import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/portal" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/portal" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get Subscription func GetSubscriptionHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { + var req types.GetSubscriptionRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + l := portal.NewGetSubscriptionLogic(c.Request.Context(), svcCtx) - resp, err := l.GetSubscription() + resp, err := l.GetSubscription(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/public/portal/prePurchaseOrderHandler.go b/internal/handler/public/portal/prePurchaseOrderHandler.go index 37650b2..511f97c 100644 --- a/internal/handler/public/portal/prePurchaseOrderHandler.go +++ b/internal/handler/public/portal/prePurchaseOrderHandler.go @@ -2,10 +2,10 @@ package portal import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/portal" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/portal" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Pre Purchase Order diff --git a/internal/handler/public/portal/purchaseCheckoutHandler.go b/internal/handler/public/portal/purchaseCheckoutHandler.go index ec06c9e..0e34d38 100644 --- a/internal/handler/public/portal/purchaseCheckoutHandler.go +++ b/internal/handler/public/portal/purchaseCheckoutHandler.go @@ -2,13 +2,13 @@ package portal import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/portal" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/portal" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) -// Purchase Checkout +// PurchaseCheckoutHandler Purchase Checkout func PurchaseCheckoutHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { var req types.CheckoutOrderRequest diff --git a/internal/handler/public/portal/purchaseHandler.go b/internal/handler/public/portal/purchaseHandler.go index a483050..1f56f83 100644 --- a/internal/handler/public/portal/purchaseHandler.go +++ b/internal/handler/public/portal/purchaseHandler.go @@ -2,10 +2,10 @@ package portal import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/portal" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/portal" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Purchase subscription diff --git a/internal/handler/public/portal/queryPurchaseOrderHandler.go b/internal/handler/public/portal/queryPurchaseOrderHandler.go index 2e7cf2e..f134647 100644 --- a/internal/handler/public/portal/queryPurchaseOrderHandler.go +++ b/internal/handler/public/portal/queryPurchaseOrderHandler.go @@ -2,10 +2,10 @@ package portal import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/portal" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/portal" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Query Purchase Order diff --git a/internal/handler/public/subscribe/queryApplicationConfigHandler.go b/internal/handler/public/subscribe/queryApplicationConfigHandler.go deleted file mode 100644 index 7cb91c5..0000000 --- a/internal/handler/public/subscribe/queryApplicationConfigHandler.go +++ /dev/null @@ -1,18 +0,0 @@ -package subscribe - -import ( - "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" -) - -// Get application config -func QueryApplicationConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { - return func(c *gin.Context) { - - l := subscribe.NewQueryApplicationConfigLogic(c.Request.Context(), svcCtx) - resp, err := l.QueryApplicationConfig() - result.HttpResult(c, resp, err) - } -} diff --git a/internal/handler/public/subscribe/querySubscribeGroupListHandler.go b/internal/handler/public/subscribe/querySubscribeGroupListHandler.go index 8b48fc1..eca7db7 100644 --- a/internal/handler/public/subscribe/querySubscribeGroupListHandler.go +++ b/internal/handler/public/subscribe/querySubscribeGroupListHandler.go @@ -2,9 +2,9 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get subscribe group list diff --git a/internal/handler/public/subscribe/querySubscribeListHandler.go b/internal/handler/public/subscribe/querySubscribeListHandler.go index 551725d..6a68a46 100644 --- a/internal/handler/public/subscribe/querySubscribeListHandler.go +++ b/internal/handler/public/subscribe/querySubscribeListHandler.go @@ -2,17 +2,25 @@ package subscribe import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) -// Get subscribe list +// QuerySubscribeListHandler Get subscribe list func QuerySubscribeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { + var req types.QuerySubscribeListRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } l := subscribe.NewQuerySubscribeListLogic(c.Request.Context(), svcCtx) - resp, err := l.QuerySubscribeList() + resp, err := l.QuerySubscribeList(&req) result.HttpResult(c, resp, err) } } diff --git a/internal/handler/public/ticket/createUserTicketFollowHandler.go b/internal/handler/public/ticket/createUserTicketFollowHandler.go index b94334e..720e55c 100644 --- a/internal/handler/public/ticket/createUserTicketFollowHandler.go +++ b/internal/handler/public/ticket/createUserTicketFollowHandler.go @@ -2,10 +2,10 @@ package ticket import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/ticket" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/ticket" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create ticket follow diff --git a/internal/handler/public/ticket/createUserTicketHandler.go b/internal/handler/public/ticket/createUserTicketHandler.go index 2cbae74..194b37b 100644 --- a/internal/handler/public/ticket/createUserTicketHandler.go +++ b/internal/handler/public/ticket/createUserTicketHandler.go @@ -2,10 +2,10 @@ package ticket import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/ticket" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/ticket" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Create ticket diff --git a/internal/handler/public/ticket/getUserTicketDetailsHandler.go b/internal/handler/public/ticket/getUserTicketDetailsHandler.go index 6230664..e664376 100644 --- a/internal/handler/public/ticket/getUserTicketDetailsHandler.go +++ b/internal/handler/public/ticket/getUserTicketDetailsHandler.go @@ -2,10 +2,10 @@ package ticket import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/ticket" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/ticket" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get ticket detail diff --git a/internal/handler/public/ticket/getUserTicketListHandler.go b/internal/handler/public/ticket/getUserTicketListHandler.go index 9b86d0e..3e3aed5 100644 --- a/internal/handler/public/ticket/getUserTicketListHandler.go +++ b/internal/handler/public/ticket/getUserTicketListHandler.go @@ -2,10 +2,10 @@ package ticket import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/ticket" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/ticket" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get ticket list diff --git a/internal/handler/public/ticket/updateUserTicketStatusHandler.go b/internal/handler/public/ticket/updateUserTicketStatusHandler.go index 12e7c82..cb71fb5 100644 --- a/internal/handler/public/ticket/updateUserTicketStatusHandler.go +++ b/internal/handler/public/ticket/updateUserTicketStatusHandler.go @@ -2,10 +2,10 @@ package ticket import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/ticket" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/ticket" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update ticket status diff --git a/internal/handler/public/user/bindOAuthCallbackHandler.go b/internal/handler/public/user/bindOAuthCallbackHandler.go index 67cc030..adf7927 100644 --- a/internal/handler/public/user/bindOAuthCallbackHandler.go +++ b/internal/handler/public/user/bindOAuthCallbackHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Bind OAuth Callback diff --git a/internal/handler/public/user/bindOAuthHandler.go b/internal/handler/public/user/bindOAuthHandler.go index f80dfa9..3b31d4a 100644 --- a/internal/handler/public/user/bindOAuthHandler.go +++ b/internal/handler/public/user/bindOAuthHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Bind OAuth diff --git a/internal/handler/public/user/bindTelegramHandler.go b/internal/handler/public/user/bindTelegramHandler.go index 3ae8630..2e1027d 100644 --- a/internal/handler/public/user/bindTelegramHandler.go +++ b/internal/handler/public/user/bindTelegramHandler.go @@ -2,9 +2,9 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Bind Telegram diff --git a/internal/handler/public/user/getDeviceListHandler.go b/internal/handler/public/user/getDeviceListHandler.go new file mode 100644 index 0000000..deb2e3a --- /dev/null +++ b/internal/handler/public/user/getDeviceListHandler.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" +) + +// Get Device List +func GetDeviceListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + + l := user.NewGetDeviceListLogic(c.Request.Context(), svcCtx) + resp, err := l.GetDeviceList() + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/public/user/getLoginLogHandler.go b/internal/handler/public/user/getLoginLogHandler.go index 9a07df0..0188f8b 100644 --- a/internal/handler/public/user/getLoginLogHandler.go +++ b/internal/handler/public/user/getLoginLogHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get Login Log diff --git a/internal/handler/public/user/getOAuthMethodsHandler.go b/internal/handler/public/user/getOAuthMethodsHandler.go index 5e62f51..2e5fb24 100644 --- a/internal/handler/public/user/getOAuthMethodsHandler.go +++ b/internal/handler/public/user/getOAuthMethodsHandler.go @@ -2,9 +2,9 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Get OAuth Methods diff --git a/internal/handler/public/user/getSubscribeLogHandler.go b/internal/handler/public/user/getSubscribeLogHandler.go index eec3314..b417ef2 100644 --- a/internal/handler/public/user/getSubscribeLogHandler.go +++ b/internal/handler/public/user/getSubscribeLogHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Get Subscribe Log diff --git a/internal/handler/public/user/preUnsubscribeHandler.go b/internal/handler/public/user/preUnsubscribeHandler.go index a2f76b8..6f02968 100644 --- a/internal/handler/public/user/preUnsubscribeHandler.go +++ b/internal/handler/public/user/preUnsubscribeHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Pre Unsubscribe diff --git a/internal/handler/public/user/queryUserAffiliateHandler.go b/internal/handler/public/user/queryUserAffiliateHandler.go index 79f7109..3a2ee8e 100644 --- a/internal/handler/public/user/queryUserAffiliateHandler.go +++ b/internal/handler/public/user/queryUserAffiliateHandler.go @@ -2,9 +2,9 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Query User Affiliate Count diff --git a/internal/handler/public/user/queryUserAffiliateListHandler.go b/internal/handler/public/user/queryUserAffiliateListHandler.go index 8cca91b..7882ec5 100644 --- a/internal/handler/public/user/queryUserAffiliateListHandler.go +++ b/internal/handler/public/user/queryUserAffiliateListHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Query User Affiliate List diff --git a/internal/handler/public/user/queryUserBalanceLogHandler.go b/internal/handler/public/user/queryUserBalanceLogHandler.go index f83fb9d..3e7e7f3 100644 --- a/internal/handler/public/user/queryUserBalanceLogHandler.go +++ b/internal/handler/public/user/queryUserBalanceLogHandler.go @@ -2,9 +2,9 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Query User Balance Log diff --git a/internal/handler/public/user/queryUserCommissionLogHandler.go b/internal/handler/public/user/queryUserCommissionLogHandler.go index 4ca2a44..733a040 100644 --- a/internal/handler/public/user/queryUserCommissionLogHandler.go +++ b/internal/handler/public/user/queryUserCommissionLogHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Query User Commission Log diff --git a/internal/handler/public/user/queryUserInfoHandler.go b/internal/handler/public/user/queryUserInfoHandler.go index 13c284d..2169f0f 100644 --- a/internal/handler/public/user/queryUserInfoHandler.go +++ b/internal/handler/public/user/queryUserInfoHandler.go @@ -2,9 +2,9 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Query User Info diff --git a/internal/handler/public/user/queryUserSubscribeHandler.go b/internal/handler/public/user/queryUserSubscribeHandler.go index e16aadd..5d7882a 100644 --- a/internal/handler/public/user/queryUserSubscribeHandler.go +++ b/internal/handler/public/user/queryUserSubscribeHandler.go @@ -2,9 +2,9 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Query User Subscribe diff --git a/internal/handler/public/user/resetUserSubscribeTokenHandler.go b/internal/handler/public/user/resetUserSubscribeTokenHandler.go index 2002dc1..545893a 100644 --- a/internal/handler/public/user/resetUserSubscribeTokenHandler.go +++ b/internal/handler/public/user/resetUserSubscribeTokenHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Reset User Subscribe Token diff --git a/internal/handler/public/user/unbindDeviceHandler.go b/internal/handler/public/user/unbindDeviceHandler.go new file mode 100644 index 0000000..9429d72 --- /dev/null +++ b/internal/handler/public/user/unbindDeviceHandler.go @@ -0,0 +1,26 @@ +package user + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +// Unbind Device +func UnbindDeviceHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.UnbindDeviceRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := user.NewUnbindDeviceLogic(c.Request.Context(), svcCtx) + err := l.UnbindDevice(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/public/user/unbindOAuthHandler.go b/internal/handler/public/user/unbindOAuthHandler.go index de9ed68..f4aa124 100644 --- a/internal/handler/public/user/unbindOAuthHandler.go +++ b/internal/handler/public/user/unbindOAuthHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Unbind OAuth diff --git a/internal/handler/public/user/unbindTelegramHandler.go b/internal/handler/public/user/unbindTelegramHandler.go index 902b58f..b8d9c9e 100644 --- a/internal/handler/public/user/unbindTelegramHandler.go +++ b/internal/handler/public/user/unbindTelegramHandler.go @@ -2,9 +2,9 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/result" ) // Unbind Telegram diff --git a/internal/handler/public/user/unsubscribeHandler.go b/internal/handler/public/user/unsubscribeHandler.go index 1ad3bb1..e0792c1 100644 --- a/internal/handler/public/user/unsubscribeHandler.go +++ b/internal/handler/public/user/unsubscribeHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Unsubscribe diff --git a/internal/handler/public/user/updateBindEmailHandler.go b/internal/handler/public/user/updateBindEmailHandler.go index a00a5bc..3c06d0d 100644 --- a/internal/handler/public/user/updateBindEmailHandler.go +++ b/internal/handler/public/user/updateBindEmailHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update Bind Email diff --git a/internal/handler/public/user/updateBindMobileHandler.go b/internal/handler/public/user/updateBindMobileHandler.go index 045d152..333ebe2 100644 --- a/internal/handler/public/user/updateBindMobileHandler.go +++ b/internal/handler/public/user/updateBindMobileHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update Bind Mobile diff --git a/internal/handler/public/user/updateUserNotifyHandler.go b/internal/handler/public/user/updateUserNotifyHandler.go index 7b38554..8023d86 100644 --- a/internal/handler/public/user/updateUserNotifyHandler.go +++ b/internal/handler/public/user/updateUserNotifyHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update User Notify diff --git a/internal/handler/public/user/updateUserPasswordHandler.go b/internal/handler/public/user/updateUserPasswordHandler.go index 3f23c8a..dd44561 100644 --- a/internal/handler/public/user/updateUserPasswordHandler.go +++ b/internal/handler/public/user/updateUserPasswordHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Update User Password diff --git a/internal/handler/public/user/verifyEmailHandler.go b/internal/handler/public/user/verifyEmailHandler.go index d959d70..85bdf5c 100644 --- a/internal/handler/public/user/verifyEmailHandler.go +++ b/internal/handler/public/user/verifyEmailHandler.go @@ -2,10 +2,10 @@ package user import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/public/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/public/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Verify Email diff --git a/internal/handler/routes.go b/internal/handler/routes.go index c684245..c6832ec 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -5,44 +5,37 @@ package handler import ( "github.com/gin-gonic/gin" - adminAds "github.com/perfect-panel/ppanel-server/internal/handler/admin/ads" - adminAnnouncement "github.com/perfect-panel/ppanel-server/internal/handler/admin/announcement" - adminAuthMethod "github.com/perfect-panel/ppanel-server/internal/handler/admin/authMethod" - adminConsole "github.com/perfect-panel/ppanel-server/internal/handler/admin/console" - adminCoupon "github.com/perfect-panel/ppanel-server/internal/handler/admin/coupon" - adminDocument "github.com/perfect-panel/ppanel-server/internal/handler/admin/document" - adminLog "github.com/perfect-panel/ppanel-server/internal/handler/admin/log" - adminOrder "github.com/perfect-panel/ppanel-server/internal/handler/admin/order" - adminPayment "github.com/perfect-panel/ppanel-server/internal/handler/admin/payment" - adminServer "github.com/perfect-panel/ppanel-server/internal/handler/admin/server" - adminSubscribe "github.com/perfect-panel/ppanel-server/internal/handler/admin/subscribe" - adminSystem "github.com/perfect-panel/ppanel-server/internal/handler/admin/system" - adminTicket "github.com/perfect-panel/ppanel-server/internal/handler/admin/ticket" - adminTool "github.com/perfect-panel/ppanel-server/internal/handler/admin/tool" - adminUser "github.com/perfect-panel/ppanel-server/internal/handler/admin/user" - appAnnouncement "github.com/perfect-panel/ppanel-server/internal/handler/app/announcement" - appAuth "github.com/perfect-panel/ppanel-server/internal/handler/app/auth" - appDocument "github.com/perfect-panel/ppanel-server/internal/handler/app/document" - appNode "github.com/perfect-panel/ppanel-server/internal/handler/app/node" - appOrder "github.com/perfect-panel/ppanel-server/internal/handler/app/order" - appPayment "github.com/perfect-panel/ppanel-server/internal/handler/app/payment" - appSubscribe "github.com/perfect-panel/ppanel-server/internal/handler/app/subscribe" - appUser "github.com/perfect-panel/ppanel-server/internal/handler/app/user" - appWs "github.com/perfect-panel/ppanel-server/internal/handler/app/ws" - auth "github.com/perfect-panel/ppanel-server/internal/handler/auth" - authOauth "github.com/perfect-panel/ppanel-server/internal/handler/auth/oauth" - common "github.com/perfect-panel/ppanel-server/internal/handler/common" - publicAnnouncement "github.com/perfect-panel/ppanel-server/internal/handler/public/announcement" - publicDocument "github.com/perfect-panel/ppanel-server/internal/handler/public/document" - publicOrder "github.com/perfect-panel/ppanel-server/internal/handler/public/order" - publicPayment "github.com/perfect-panel/ppanel-server/internal/handler/public/payment" - publicPortal "github.com/perfect-panel/ppanel-server/internal/handler/public/portal" - publicSubscribe "github.com/perfect-panel/ppanel-server/internal/handler/public/subscribe" - publicTicket "github.com/perfect-panel/ppanel-server/internal/handler/public/ticket" - publicUser "github.com/perfect-panel/ppanel-server/internal/handler/public/user" - server "github.com/perfect-panel/ppanel-server/internal/handler/server" - "github.com/perfect-panel/ppanel-server/internal/middleware" - "github.com/perfect-panel/ppanel-server/internal/svc" + adminAds "github.com/perfect-panel/server/internal/handler/admin/ads" + adminAnnouncement "github.com/perfect-panel/server/internal/handler/admin/announcement" + adminApplication "github.com/perfect-panel/server/internal/handler/admin/application" + adminAuthMethod "github.com/perfect-panel/server/internal/handler/admin/authMethod" + adminConsole "github.com/perfect-panel/server/internal/handler/admin/console" + adminCoupon "github.com/perfect-panel/server/internal/handler/admin/coupon" + adminDocument "github.com/perfect-panel/server/internal/handler/admin/document" + adminLog "github.com/perfect-panel/server/internal/handler/admin/log" + adminMarketing "github.com/perfect-panel/server/internal/handler/admin/marketing" + adminOrder "github.com/perfect-panel/server/internal/handler/admin/order" + adminPayment "github.com/perfect-panel/server/internal/handler/admin/payment" + adminServer "github.com/perfect-panel/server/internal/handler/admin/server" + adminSubscribe "github.com/perfect-panel/server/internal/handler/admin/subscribe" + adminSystem "github.com/perfect-panel/server/internal/handler/admin/system" + adminTicket "github.com/perfect-panel/server/internal/handler/admin/ticket" + adminTool "github.com/perfect-panel/server/internal/handler/admin/tool" + adminUser "github.com/perfect-panel/server/internal/handler/admin/user" + auth "github.com/perfect-panel/server/internal/handler/auth" + authOauth "github.com/perfect-panel/server/internal/handler/auth/oauth" + common "github.com/perfect-panel/server/internal/handler/common" + publicAnnouncement "github.com/perfect-panel/server/internal/handler/public/announcement" + publicDocument "github.com/perfect-panel/server/internal/handler/public/document" + publicOrder "github.com/perfect-panel/server/internal/handler/public/order" + publicPayment "github.com/perfect-panel/server/internal/handler/public/payment" + publicPortal "github.com/perfect-panel/server/internal/handler/public/portal" + publicSubscribe "github.com/perfect-panel/server/internal/handler/public/subscribe" + publicTicket "github.com/perfect-panel/server/internal/handler/public/ticket" + publicUser "github.com/perfect-panel/server/internal/handler/public/user" + server "github.com/perfect-panel/server/internal/handler/server" + "github.com/perfect-panel/server/internal/middleware" + "github.com/perfect-panel/server/internal/svc" ) func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { @@ -86,6 +79,26 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminAnnouncementGroupRouter.GET("/list", adminAnnouncement.GetAnnouncementListHandler(serverCtx)) } + adminApplicationGroupRouter := router.Group("/v1/admin/application") + adminApplicationGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Create subscribe application + adminApplicationGroupRouter.POST("/", adminApplication.CreateSubscribeApplicationHandler(serverCtx)) + + // Preview Template + adminApplicationGroupRouter.GET("/preview", adminApplication.PreviewSubscribeTemplateHandler(serverCtx)) + + // Update subscribe application + adminApplicationGroupRouter.PUT("/subscribe_application", adminApplication.UpdateSubscribeApplicationHandler(serverCtx)) + + // Delete subscribe application + adminApplicationGroupRouter.DELETE("/subscribe_application", adminApplication.DeleteSubscribeApplicationHandler(serverCtx)) + + // Get subscribe application list + adminApplicationGroupRouter.GET("/subscribe_application_list", adminApplication.GetSubscribeApplicationListHandler(serverCtx)) + } + adminAuthMethodGroupRouter := router.Group("/v1/admin/auth-method") adminAuthMethodGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) @@ -176,8 +189,79 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminLogGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) { + // Filter balance log + adminLogGroupRouter.GET("/balance/list", adminLog.FilterBalanceLogHandler(serverCtx)) + + // Filter commission log + adminLogGroupRouter.GET("/commission/list", adminLog.FilterCommissionLogHandler(serverCtx)) + + // Filter email log + adminLogGroupRouter.GET("/email/list", adminLog.FilterEmailLogHandler(serverCtx)) + + // Filter gift log + adminLogGroupRouter.GET("/gift/list", adminLog.FilterGiftLogHandler(serverCtx)) + + // Filter login log + adminLogGroupRouter.GET("/login/list", adminLog.FilterLoginLogHandler(serverCtx)) + // Get message log list adminLogGroupRouter.GET("/message/list", adminLog.GetMessageLogListHandler(serverCtx)) + + // Filter mobile log + adminLogGroupRouter.GET("/mobile/list", adminLog.FilterMobileLogHandler(serverCtx)) + + // Filter register log + adminLogGroupRouter.GET("/register/list", adminLog.FilterRegisterLogHandler(serverCtx)) + + // Filter server traffic log + adminLogGroupRouter.GET("/server/traffic/list", adminLog.FilterServerTrafficLogHandler(serverCtx)) + + // Get log setting + adminLogGroupRouter.GET("/setting", adminLog.GetLogSettingHandler(serverCtx)) + + // Update log setting + adminLogGroupRouter.POST("/setting", adminLog.UpdateLogSettingHandler(serverCtx)) + + // Filter subscribe log + adminLogGroupRouter.GET("/subscribe/list", adminLog.FilterSubscribeLogHandler(serverCtx)) + + // Filter reset subscribe log + adminLogGroupRouter.GET("/subscribe/reset/list", adminLog.FilterResetSubscribeLogHandler(serverCtx)) + + // Filter user subscribe traffic log + adminLogGroupRouter.GET("/subscribe/traffic/list", adminLog.FilterUserSubscribeTrafficLogHandler(serverCtx)) + + // Filter traffic log details + adminLogGroupRouter.GET("/traffic/details", adminLog.FilterTrafficLogDetailsHandler(serverCtx)) + } + + adminMarketingGroupRouter := router.Group("/v1/admin/marketing") + adminMarketingGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + + { + // Get batch send email task list + adminMarketingGroupRouter.GET("/email/batch/list", adminMarketing.GetBatchSendEmailTaskListHandler(serverCtx)) + + // Get pre-send email count + adminMarketingGroupRouter.POST("/email/batch/pre-send-count", adminMarketing.GetPreSendEmailCountHandler(serverCtx)) + + // Create a batch send email task + adminMarketingGroupRouter.POST("/email/batch/send", adminMarketing.CreateBatchSendEmailTaskHandler(serverCtx)) + + // Get batch send email task status + adminMarketingGroupRouter.POST("/email/batch/status", adminMarketing.GetBatchSendEmailTaskStatusHandler(serverCtx)) + + // Stop a batch send email task + adminMarketingGroupRouter.POST("/email/batch/stop", adminMarketing.StopBatchSendEmailTaskHandler(serverCtx)) + + // Create a quota task + adminMarketingGroupRouter.POST("/quota/create", adminMarketing.CreateQuotaTaskHandler(serverCtx)) + + // Query quota task list + adminMarketingGroupRouter.GET("/quota/list", adminMarketing.QueryQuotaTaskListHandler(serverCtx)) + + // Query quota task pre-count + adminMarketingGroupRouter.POST("/quota/pre-count", adminMarketing.QueryQuotaTaskPreCountHandler(serverCtx)) } adminOrderGroupRouter := router.Group("/v1/admin/order") @@ -218,56 +302,50 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminServerGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) { - // Update node - adminServerGroupRouter.PUT("/", adminServer.UpdateNodeHandler(serverCtx)) + // Create Server + adminServerGroupRouter.POST("/create", adminServer.CreateServerHandler(serverCtx)) - // Create node - adminServerGroupRouter.POST("/", adminServer.CreateNodeHandler(serverCtx)) + // Delete Server + adminServerGroupRouter.POST("/delete", adminServer.DeleteServerHandler(serverCtx)) - // Delete node - adminServerGroupRouter.DELETE("/", adminServer.DeleteNodeHandler(serverCtx)) + // Filter Server List + adminServerGroupRouter.GET("/list", adminServer.FilterServerListHandler(serverCtx)) - // Batch delete node - adminServerGroupRouter.DELETE("/batch", adminServer.BatchDeleteNodeHandler(serverCtx)) + // Check if there is any server or node to migrate + adminServerGroupRouter.GET("/migrate/has", adminServer.HasMigrateSeverNodeHandler(serverCtx)) - // Get node detail - adminServerGroupRouter.GET("/detail", adminServer.GetNodeDetailHandler(serverCtx)) + // Migrate server and node data to new database + adminServerGroupRouter.POST("/migrate/run", adminServer.MigrateServerNodeHandler(serverCtx)) - // Create node group - adminServerGroupRouter.POST("/group", adminServer.CreateNodeGroupHandler(serverCtx)) + // Create Node + adminServerGroupRouter.POST("/node/create", adminServer.CreateNodeHandler(serverCtx)) - // Update node group - adminServerGroupRouter.PUT("/group", adminServer.UpdateNodeGroupHandler(serverCtx)) + // Delete Node + adminServerGroupRouter.POST("/node/delete", adminServer.DeleteNodeHandler(serverCtx)) - // Delete node group - adminServerGroupRouter.DELETE("/group", adminServer.DeleteNodeGroupHandler(serverCtx)) + // Filter Node List + adminServerGroupRouter.GET("/node/list", adminServer.FilterNodeListHandler(serverCtx)) - // Batch delete node group - adminServerGroupRouter.DELETE("/group/batch", adminServer.BatchDeleteNodeGroupHandler(serverCtx)) + // Reset node sort + adminServerGroupRouter.POST("/node/sort", adminServer.ResetSortWithNodeHandler(serverCtx)) - // Get node group list - adminServerGroupRouter.GET("/group/list", adminServer.GetNodeGroupListHandler(serverCtx)) + // Toggle Node Status + adminServerGroupRouter.POST("/node/status/toggle", adminServer.ToggleNodeStatusHandler(serverCtx)) - // Get node list - adminServerGroupRouter.GET("/list", adminServer.GetNodeListHandler(serverCtx)) + // Query all node tags + adminServerGroupRouter.GET("/node/tags", adminServer.QueryNodeTagHandler(serverCtx)) - // Create rule group - adminServerGroupRouter.POST("/rule_group", adminServer.CreateRuleGroupHandler(serverCtx)) + // Update Node + adminServerGroupRouter.POST("/node/update", adminServer.UpdateNodeHandler(serverCtx)) - // Update rule group - adminServerGroupRouter.PUT("/rule_group", adminServer.UpdateRuleGroupHandler(serverCtx)) + // Get Server Protocols + adminServerGroupRouter.GET("/protocols", adminServer.GetServerProtocolsHandler(serverCtx)) - // Delete rule group - adminServerGroupRouter.DELETE("/rule_group", adminServer.DeleteRuleGroupHandler(serverCtx)) + // Reset server sort + adminServerGroupRouter.POST("/server/sort", adminServer.ResetSortWithServerHandler(serverCtx)) - // Get rule group list - adminServerGroupRouter.GET("/rule_group_list", adminServer.GetRuleGroupListHandler(serverCtx)) - - // Node sort - adminServerGroupRouter.POST("/sort", adminServer.NodeSortHandler(serverCtx)) - - // Get node tag list - adminServerGroupRouter.GET("/tag/list", adminServer.GetNodeTagListHandler(serverCtx)) + // Update Server + adminServerGroupRouter.POST("/update", adminServer.UpdateServerHandler(serverCtx)) } adminSubscribeGroupRouter := router.Group("/v1/admin/subscribe") @@ -315,33 +393,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { adminSystemGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) { - // Get application - adminSystemGroupRouter.GET("/application", adminSystem.GetApplicationHandler(serverCtx)) - - // Update application - adminSystemGroupRouter.PUT("/application", adminSystem.UpdateApplicationHandler(serverCtx)) - - // Create application - adminSystemGroupRouter.POST("/application", adminSystem.CreateApplicationHandler(serverCtx)) - - // Delete application - adminSystemGroupRouter.DELETE("/application", adminSystem.DeleteApplicationHandler(serverCtx)) - - // update application config - adminSystemGroupRouter.PUT("/application_config", adminSystem.UpdateApplicationConfigHandler(serverCtx)) - - // get application config - adminSystemGroupRouter.GET("/application_config", adminSystem.GetApplicationConfigHandler(serverCtx)) - - // Update application version - adminSystemGroupRouter.PUT("/application_version", adminSystem.UpdateApplicationVersionHandler(serverCtx)) - - // Create application version - adminSystemGroupRouter.POST("/application_version", adminSystem.CreateApplicationVersionHandler(serverCtx)) - - // Delete application - adminSystemGroupRouter.DELETE("/application_version", adminSystem.DeleteApplicationVersionHandler(serverCtx)) - // Get Currency Config adminSystemGroupRouter.GET("/currency_config", adminSystem.GetCurrencyConfigHandler(serverCtx)) @@ -363,6 +414,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Update node config adminSystemGroupRouter.PUT("/node_config", adminSystem.UpdateNodeConfigHandler(serverCtx)) + // PreView Node Multiplier + adminSystemGroupRouter.GET("/node_multiplier/preview", adminSystem.PreViewNodeMultiplierHandler(serverCtx)) + // get Privacy Policy Config adminSystemGroupRouter.GET("/privacy", adminSystem.GetPrivacyPolicyConfigHandler(serverCtx)) @@ -393,9 +447,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Update subscribe config adminSystemGroupRouter.PUT("/subscribe_config", adminSystem.UpdateSubscribeConfigHandler(serverCtx)) - // Get subscribe type - adminSystemGroupRouter.GET("/subscribe_type", adminSystem.GetSubscribeTypeHandler(serverCtx)) - // Get Team of Service Config adminSystemGroupRouter.GET("/tos_config", adminSystem.GetTosConfigHandler(serverCtx)) @@ -441,6 +492,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Restart System adminToolGroupRouter.GET("/restart", adminTool.RestartSystemHandler(serverCtx)) + + // Get Version + adminToolGroupRouter.GET("/version", adminTool.GetVersionHandler(serverCtx)) } adminUserGroupRouter := router.Group("/v1/admin/user") @@ -516,158 +570,15 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get user subcribe logs adminUserGroupRouter.GET("/subscribe/logs", adminUser.GetUserSubscribeLogsHandler(serverCtx)) + // Get user subcribe reset traffic logs + adminUserGroupRouter.GET("/subscribe/reset/logs", adminUser.GetUserSubscribeResetTrafficLogsHandler(serverCtx)) + // Get user subcribe traffic logs adminUserGroupRouter.GET("/subscribe/traffic_logs", adminUser.GetUserSubscribeTrafficLogsHandler(serverCtx)) } - appAnnouncementGroupRouter := router.Group("/v1/app/announcement") - appAnnouncementGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Query announcement - appAnnouncementGroupRouter.GET("/list", appAnnouncement.QueryAnnouncementHandler(serverCtx)) - } - - appAuthGroupRouter := router.Group("/v1/app/auth") - appAuthGroupRouter.Use(middleware.AppMiddleware(serverCtx)) - - { - // Check Account - appAuthGroupRouter.POST("/check", appAuth.CheckHandler(serverCtx)) - - // GetAppConfig - appAuthGroupRouter.POST("/config", appAuth.GetAppConfigHandler(serverCtx)) - - // Login - appAuthGroupRouter.POST("/login", appAuth.LoginHandler(serverCtx)) - - // Register - appAuthGroupRouter.POST("/register", appAuth.RegisterHandler(serverCtx)) - - // Reset Password - appAuthGroupRouter.POST("/reset_password", appAuth.ResetPasswordHandler(serverCtx)) - } - - appDocumentGroupRouter := router.Group("/v1/app/document") - appDocumentGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Get document detail - appDocumentGroupRouter.GET("/detail", appDocument.QueryDocumentDetailHandler(serverCtx)) - - // Get document list - appDocumentGroupRouter.GET("/list", appDocument.QueryDocumentListHandler(serverCtx)) - } - - appNodeGroupRouter := router.Group("/v1/app/node") - appNodeGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Get Node list - appNodeGroupRouter.GET("/list", appNode.GetNodeListHandler(serverCtx)) - - // Get rule group list - appNodeGroupRouter.GET("/rule_group_list", appNode.GetRuleGroupListHandler(serverCtx)) - } - - appOrderGroupRouter := router.Group("/v1/app/order") - appOrderGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Checkout order - appOrderGroupRouter.POST("/checkout", appOrder.CheckoutOrderHandler(serverCtx)) - - // Close order - appOrderGroupRouter.POST("/close", appOrder.CloseOrderHandler(serverCtx)) - - // Get order - appOrderGroupRouter.GET("/detail", appOrder.QueryOrderDetailHandler(serverCtx)) - - // Get order list - appOrderGroupRouter.GET("/list", appOrder.QueryOrderListHandler(serverCtx)) - - // Pre create order - appOrderGroupRouter.POST("/pre", appOrder.PreCreateOrderHandler(serverCtx)) - - // purchase Subscription - appOrderGroupRouter.POST("/purchase", appOrder.PurchaseHandler(serverCtx)) - - // Recharge - appOrderGroupRouter.POST("/recharge", appOrder.RechargeHandler(serverCtx)) - - // Renewal Subscription - appOrderGroupRouter.POST("/renewal", appOrder.RenewalHandler(serverCtx)) - - // Reset traffic - appOrderGroupRouter.POST("/reset", appOrder.ResetTrafficHandler(serverCtx)) - } - - appPaymentGroupRouter := router.Group("/v1/app/payment") - appPaymentGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Get available payment methods - appPaymentGroupRouter.GET("/methods", appPayment.GetAvailablePaymentMethodsHandler(serverCtx)) - } - - appSubscribeGroupRouter := router.Group("/v1/app/subscribe") - appSubscribeGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Get application config - appSubscribeGroupRouter.GET("/application/config", appSubscribe.QueryApplicationConfigHandler(serverCtx)) - - // Get subscribe group list - appSubscribeGroupRouter.GET("/group/list", appSubscribe.QuerySubscribeGroupListHandler(serverCtx)) - - // Get subscribe list - appSubscribeGroupRouter.GET("/list", appSubscribe.QuerySubscribeListHandler(serverCtx)) - - // Reset user subscription period - appSubscribeGroupRouter.POST("/reset/period", appSubscribe.ResetUserSubscribePeriodHandler(serverCtx)) - - // Get Already subscribed to package - appSubscribeGroupRouter.GET("/user/already_subscribe", appSubscribe.QueryUserAlreadySubscribeHandler(serverCtx)) - - // Get Available subscriptions for users - appSubscribeGroupRouter.GET("/user/available_subscribe", appSubscribe.QueryUserAvailableUserSubscribeHandler(serverCtx)) - } - - appUserGroupRouter := router.Group("/v1/app/user") - appUserGroupRouter.Use(middleware.AppMiddleware(serverCtx), middleware.AuthMiddleware(serverCtx)) - - { - // Delete Account - appUserGroupRouter.DELETE("/account", appUser.DeleteAccountHandler(serverCtx)) - - // Query User Affiliate Count - appUserGroupRouter.GET("/affiliate/count", appUser.QueryUserAffiliateHandler(serverCtx)) - - // Query User Affiliate List - appUserGroupRouter.GET("/affiliate/list", appUser.QueryUserAffiliateListHandler(serverCtx)) - - // query user info - appUserGroupRouter.GET("/info", appUser.QueryUserInfoHandler(serverCtx)) - - // Get user online time total - appUserGroupRouter.GET("/online_time/statistics", appUser.GetUserOnlineTimeStatisticsHandler(serverCtx)) - - // Update Password - appUserGroupRouter.PUT("/password", appUser.UpdatePasswordHandler(serverCtx)) - - // Get user subcribe traffic logs - appUserGroupRouter.GET("/subscribe/traffic_logs", appUser.GetUserSubscribeTrafficLogsHandler(serverCtx)) - } - - appWsGroupRouter := router.Group("/v1/app/ws") - appWsGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) - - { - // App heartbeat - appWsGroupRouter.GET("/:userid/:identifier", appWs.AppWsHandler(serverCtx)) - } - authGroupRouter := router.Group("/v1/auth") + authGroupRouter.Use(middleware.DeviceMiddleware(serverCtx)) { // Check user is exist @@ -679,6 +590,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // User login authGroupRouter.POST("/login", auth.UserLoginHandler(serverCtx)) + // Device Login + authGroupRouter.POST("/login/device", auth.DeviceLoginHandler(serverCtx)) + // User Telephone login authGroupRouter.POST("/login/telephone", auth.TelephoneLoginHandler(serverCtx)) @@ -709,17 +623,18 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } commonGroupRouter := router.Group("/v1/common") + commonGroupRouter.Use(middleware.DeviceMiddleware(serverCtx)) { // Get Ads commonGroupRouter.GET("/ads", common.GetAdsHandler(serverCtx)) - // Get Tos Content - commonGroupRouter.GET("/application", common.GetApplicationHandler(serverCtx)) - // Check verification code commonGroupRouter.POST("/check_verification_code", common.CheckVerificationCodeHandler(serverCtx)) + // Get Client + commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx)) + // Get verification code commonGroupRouter.POST("/send_code", common.SendEmailCodeHandler(serverCtx)) @@ -740,7 +655,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicAnnouncementGroupRouter := router.Group("/v1/public/announcement") - publicAnnouncementGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicAnnouncementGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Query announcement @@ -748,7 +663,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicDocumentGroupRouter := router.Group("/v1/public/document") - publicDocumentGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicDocumentGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Get document detail @@ -759,7 +674,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicOrderGroupRouter := router.Group("/v1/public/order") - publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicOrderGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Close order @@ -788,7 +703,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicPaymentGroupRouter := router.Group("/v1/public/payment") - publicPaymentGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicPaymentGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Get available payment methods @@ -796,6 +711,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicPortalGroupRouter := router.Group("/v1/public/portal") + publicPortalGroupRouter.Use(middleware.DeviceMiddleware(serverCtx)) { // Purchase Checkout @@ -818,21 +734,15 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicSubscribeGroupRouter := router.Group("/v1/public/subscribe") - publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { - // Get application config - publicSubscribeGroupRouter.GET("/application/config", publicSubscribe.QueryApplicationConfigHandler(serverCtx)) - - // Get subscribe group list - publicSubscribeGroupRouter.GET("/group/list", publicSubscribe.QuerySubscribeGroupListHandler(serverCtx)) - // Get subscribe list publicSubscribeGroupRouter.GET("/list", publicSubscribe.QuerySubscribeListHandler(serverCtx)) } publicTicketGroupRouter := router.Group("/v1/public/ticket") - publicTicketGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicTicketGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Update ticket status @@ -852,7 +762,7 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { } publicUserGroupRouter := router.Group("/v1/public/user") - publicUserGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) + publicUserGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx)) { // Query User Affiliate Count @@ -882,6 +792,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Query User Commission Log publicUserGroupRouter.GET("/commission_log", publicUser.QueryUserCommissionLogHandler(serverCtx)) + // Get Device List + publicUserGroupRouter.GET("/devices", publicUser.GetDeviceListHandler(serverCtx)) + // Query User Info publicUserGroupRouter.GET("/info", publicUser.QueryUserInfoHandler(serverCtx)) @@ -906,6 +819,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Reset User Subscribe Token publicUserGroupRouter.PUT("/subscribe_token", publicUser.ResetUserSubscribeTokenHandler(serverCtx)) + // Unbind Device + publicUserGroupRouter.PUT("/unbind_device", publicUser.UnbindDeviceHandler(serverCtx)) + // Unbind OAuth publicUserGroupRouter.POST("/unbind_oauth", publicUser.UnbindOAuthHandler(serverCtx)) @@ -941,4 +857,11 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get user list serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx)) } + + serverV2GroupRouter := router.Group("/v2/server") + + { + // Get Server Protocol Config + serverV2GroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx)) + } } diff --git a/internal/handler/server/getServerConfigHandler.go b/internal/handler/server/getServerConfigHandler.go index e849633..5428340 100644 --- a/internal/handler/server/getServerConfigHandler.go +++ b/internal/handler/server/getServerConfigHandler.go @@ -2,11 +2,11 @@ package server import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/logic/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/handler/server/getServerUserListHandler.go b/internal/handler/server/getServerUserListHandler.go index cfa143c..eeb5e24 100644 --- a/internal/handler/server/getServerUserListHandler.go +++ b/internal/handler/server/getServerUserListHandler.go @@ -2,11 +2,11 @@ package server import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/logic/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/handler/server/pushOnlineUsersHandler.go b/internal/handler/server/pushOnlineUsersHandler.go index 0bf8258..bf09594 100644 --- a/internal/handler/server/pushOnlineUsersHandler.go +++ b/internal/handler/server/pushOnlineUsersHandler.go @@ -2,10 +2,10 @@ package server import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Push online users diff --git a/internal/handler/server/queryServerProtocolConfigHandler.go b/internal/handler/server/queryServerProtocolConfigHandler.go new file mode 100644 index 0000000..5d382a6 --- /dev/null +++ b/internal/handler/server/queryServerProtocolConfigHandler.go @@ -0,0 +1,43 @@ +package server + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/result" +) + +// QueryServerProtocolConfigHandler Get Server Protocol Config +func QueryServerProtocolConfigHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.QueryServerConfigRequest + + serverID, err := strconv.ParseInt(c.Param("server_id"), 10, 64) + if err != nil { + logger.Debugf("[QueryServerProtocolConfigHandler] - strconv.ParseInt(server_id) error: %v, Param: %s", err, c.Param("server_id")) + c.String(http.StatusBadRequest, "Invalid Params") + c.Abort() + return + } + req.ServerID = serverID + + if err = c.ShouldBindQuery(&req); err != nil { + logger.Debugf("[QueryServerProtocolConfigHandler] - ShouldBindQuery error: %v, Query: %v", err, c.Request.URL.Query()) + c.String(http.StatusBadRequest, "Invalid Params") + c.Abort() + return + } + + fmt.Printf("[QueryServerProtocolConfigHandler] - ShouldBindQuery request: %+v\n", req) + + l := server.NewQueryServerProtocolConfigLogic(c.Request.Context(), svcCtx) + resp, err := l.QueryServerProtocolConfig(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/server/serverPushStatusHandler.go b/internal/handler/server/serverPushStatusHandler.go index b690b21..c6e58ae 100644 --- a/internal/handler/server/serverPushStatusHandler.go +++ b/internal/handler/server/serverPushStatusHandler.go @@ -2,10 +2,10 @@ package server import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // Push server status diff --git a/internal/handler/server/serverPushUserTrafficHandler.go b/internal/handler/server/serverPushUserTrafficHandler.go index 156f29e..c115b4e 100644 --- a/internal/handler/server/serverPushUserTrafficHandler.go +++ b/internal/handler/server/serverPushUserTrafficHandler.go @@ -2,10 +2,10 @@ package server import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/result" + "github.com/perfect-panel/server/internal/logic/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" ) // ServerPushUserTrafficHandler Push user Traffic diff --git a/internal/handler/subscribe.go b/internal/handler/subscribe.go index 391fe38..bf72a19 100644 --- a/internal/handler/subscribe.go +++ b/internal/handler/subscribe.go @@ -1,10 +1,15 @@ package handler import ( + "net/http" + "strings" + "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/server/internal/logic/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" ) func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { @@ -15,11 +20,50 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { } else { req.Token = c.Query("token") } + ua := c.GetHeader("User-Agent") req.UA = c.Request.Header.Get("User-Agent") req.Flag = c.Query("flag") + + if svcCtx.Config.Subscribe.UserAgentLimit { + if ua == "" { + c.String(http.StatusForbidden, "Access denied") + c.Abort() + return + } + clientUserAgents := tool.RemoveDuplicateElements(strings.Split(svcCtx.Config.Subscribe.UserAgentList, "\n")...) + + // query client list + clients, err := svcCtx.ClientModel.List(c.Request.Context()) + if err != nil { + logger.Errorw("[PanDomainMiddleware] Query client list failed", logger.Field("error", err.Error())) + } + for _, item := range clients { + u := strings.ToLower(item.UserAgent) + u = strings.Trim(u, " ") + clientUserAgents = append(clientUserAgents, u) + } + + var allow = false + for _, keyword := range clientUserAgents { + keyword = strings.Trim(keyword, " ") + if keyword == "" { + continue + } + if strings.Contains(strings.ToLower(ua), strings.ToLower(keyword)) { + allow = true + } + } + if !allow { + c.String(http.StatusForbidden, "Access denied") + c.Abort() + return + } + } + l := subscribe.NewSubscribeLogic(c, svcCtx) - resp, err := l.Generate(&req) + resp, err := l.Handler(&req) if err != nil { + c.String(http.StatusInternalServerError, "Internal Server") return } c.Header("subscription-userinfo", resp.Header) @@ -30,7 +74,7 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { func RegisterSubscribeHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { path := serverCtx.Config.Subscribe.SubscribePath if path == "" { - path = "/api/subscribe" + path = "/v1/subscribe/config" } router.GET(path, SubscribeHandler(serverCtx)) } diff --git a/internal/handler/telegram.go b/internal/handler/telegram.go index 28fb83c..994f7ad 100644 --- a/internal/handler/telegram.go +++ b/internal/handler/telegram.go @@ -3,11 +3,11 @@ package handler import ( "github.com/gin-gonic/gin" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "github.com/perfect-panel/ppanel-server/internal/logic/telegram" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/internal/logic/telegram" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/result" + "github.com/perfect-panel/server/pkg/tool" ) func RegisterTelegramHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { diff --git a/internal/logic/admin/ads/createAdsLogic.go b/internal/logic/admin/ads/createAdsLogic.go index 1ee3435..c4b0df3 100644 --- a/internal/logic/admin/ads/createAdsLogic.go +++ b/internal/logic/admin/ads/createAdsLogic.go @@ -4,11 +4,11 @@ import ( "context" "time" - "github.com/perfect-panel/ppanel-server/internal/model/ads" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/ads" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/ads/deleteAdsLogic.go b/internal/logic/admin/ads/deleteAdsLogic.go index a9588c8..5a2f461 100644 --- a/internal/logic/admin/ads/deleteAdsLogic.go +++ b/internal/logic/admin/ads/deleteAdsLogic.go @@ -3,10 +3,10 @@ package ads import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/ads/getAdsDetailLogic.go b/internal/logic/admin/ads/getAdsDetailLogic.go index 2897ad3..ff8331f 100644 --- a/internal/logic/admin/ads/getAdsDetailLogic.go +++ b/internal/logic/admin/ads/getAdsDetailLogic.go @@ -3,11 +3,11 @@ package ads import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/ads/getAdsListLogic.go b/internal/logic/admin/ads/getAdsListLogic.go index 5c531fe..c23e519 100644 --- a/internal/logic/admin/ads/getAdsListLogic.go +++ b/internal/logic/admin/ads/getAdsListLogic.go @@ -3,12 +3,12 @@ package ads import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/ads" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/ads" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/ads/updateAdsLogic.go b/internal/logic/admin/ads/updateAdsLogic.go index bf99931..9939624 100644 --- a/internal/logic/admin/ads/updateAdsLogic.go +++ b/internal/logic/admin/ads/updateAdsLogic.go @@ -4,11 +4,11 @@ import ( "context" "time" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/announcement/createAnnouncementLogic.go b/internal/logic/admin/announcement/createAnnouncementLogic.go index d0ada9f..a1fe3b7 100644 --- a/internal/logic/admin/announcement/createAnnouncementLogic.go +++ b/internal/logic/admin/announcement/createAnnouncementLogic.go @@ -3,11 +3,11 @@ package announcement import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/announcement" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/announcement" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/announcement/deleteAnnouncementLogic.go b/internal/logic/admin/announcement/deleteAnnouncementLogic.go index edee3ac..62ebded 100644 --- a/internal/logic/admin/announcement/deleteAnnouncementLogic.go +++ b/internal/logic/admin/announcement/deleteAnnouncementLogic.go @@ -3,10 +3,10 @@ package announcement import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/announcement/getAnnouncementListLogic.go b/internal/logic/admin/announcement/getAnnouncementListLogic.go index 1b15b87..92a2147 100644 --- a/internal/logic/admin/announcement/getAnnouncementListLogic.go +++ b/internal/logic/admin/announcement/getAnnouncementListLogic.go @@ -3,14 +3,14 @@ package announcement import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/announcement" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/announcement" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type GetAnnouncementListLogic struct { diff --git a/internal/logic/admin/announcement/getAnnouncementLogic.go b/internal/logic/admin/announcement/getAnnouncementLogic.go index 9139d03..29ca1eb 100644 --- a/internal/logic/admin/announcement/getAnnouncementLogic.go +++ b/internal/logic/admin/announcement/getAnnouncementLogic.go @@ -3,11 +3,11 @@ package announcement import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/announcement/updateAnnouncementLogic.go b/internal/logic/admin/announcement/updateAnnouncementLogic.go index 50d217b..cf10d07 100644 --- a/internal/logic/admin/announcement/updateAnnouncementLogic.go +++ b/internal/logic/admin/announcement/updateAnnouncementLogic.go @@ -3,10 +3,10 @@ package announcement import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/application/createSubscribeApplicationLogic.go b/internal/logic/admin/application/createSubscribeApplicationLogic.go new file mode 100644 index 0000000..4fca252 --- /dev/null +++ b/internal/logic/admin/application/createSubscribeApplicationLogic.go @@ -0,0 +1,61 @@ +package application + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/client" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateSubscribeApplicationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCreateSubscribeApplicationLogic Create subscribe application +func NewCreateSubscribeApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateSubscribeApplicationLogic { + return &CreateSubscribeApplicationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateSubscribeApplicationLogic) CreateSubscribeApplication(req *types.CreateSubscribeApplicationRequest) (resp *types.SubscribeApplication, err error) { + var link client.DownloadLink + tool.DeepCopy(&link, req.DownloadLink) + linkData, err := link.Marshal() + if err != nil { + l.Errorf("Failed to marshal download link: %v", err) + return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), " Failed to marshal download link") + } + data := &client.SubscribeApplication{ + Name: req.Name, + Icon: req.Icon, + Description: req.Description, + Scheme: req.Scheme, + UserAgent: req.UserAgent, + IsDefault: req.IsDefault, + SubscribeTemplate: req.SubscribeTemplate, + OutputFormat: req.OutputFormat, + DownloadLink: string(linkData), + } + + err = l.svcCtx.ClientModel.Insert(l.ctx, data) + if err != nil { + l.Errorf("Failed to create subscribe application: %v", err) + return nil, errors.Wrap(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create subscribe application") + } + + resp = &types.SubscribeApplication{} + tool.DeepCopy(resp, data) + resp.DownloadLink = req.DownloadLink + + return +} diff --git a/internal/logic/admin/application/deleteSubscribeApplicationLogic.go b/internal/logic/admin/application/deleteSubscribeApplicationLogic.go new file mode 100644 index 0000000..57cdd70 --- /dev/null +++ b/internal/logic/admin/application/deleteSubscribeApplicationLogic.go @@ -0,0 +1,35 @@ +package application + +import ( + "context" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteSubscribeApplicationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewDeleteSubscribeApplicationLogic Delete subscribe application +func NewDeleteSubscribeApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteSubscribeApplicationLogic { + return &DeleteSubscribeApplicationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteSubscribeApplicationLogic) DeleteSubscribeApplication(req *types.DeleteSubscribeApplicationRequest) error { + err := l.svcCtx.ClientModel.Delete(l.ctx, req.Id) + if err != nil { + l.Errorf("Failed to delete subscribe application with ID %d: %v", req.Id, err) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), err.Error()) + } + return nil +} diff --git a/internal/logic/admin/application/getSubscribeApplicationListLogic.go b/internal/logic/admin/application/getSubscribeApplicationListLogic.go new file mode 100644 index 0000000..383b2b6 --- /dev/null +++ b/internal/logic/admin/application/getSubscribeApplicationListLogic.go @@ -0,0 +1,61 @@ +package application + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetSubscribeApplicationListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetSubscribeApplicationListLogic Get subscribe application list +func NewGetSubscribeApplicationListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscribeApplicationListLogic { + return &GetSubscribeApplicationListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSubscribeApplicationListLogic) GetSubscribeApplicationList(req *types.GetSubscribeApplicationListRequest) (resp *types.GetSubscribeApplicationListResponse, err error) { + data, err := l.svcCtx.ClientModel.List(l.ctx) + if err != nil { + l.Errorf("Failed to get subscribe application list: %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to get subscribe application list") + } + var list []types.SubscribeApplication + for _, item := range data { + var temp types.DownloadLink + if item.DownloadLink != "" { + _ = json.Unmarshal([]byte(item.DownloadLink), &temp) + } + list = append(list, types.SubscribeApplication{ + Id: item.Id, + Name: item.Name, + Description: item.Description, + Icon: item.Icon, + Scheme: item.Scheme, + UserAgent: item.UserAgent, + IsDefault: item.IsDefault, + SubscribeTemplate: item.SubscribeTemplate, + OutputFormat: item.OutputFormat, + DownloadLink: temp, + CreatedAt: item.CreatedAt.UnixMilli(), + UpdatedAt: item.UpdatedAt.UnixMilli(), + }) + } + resp = &types.GetSubscribeApplicationListResponse{ + Total: int64(len(list)), + List: list, + } + return +} diff --git a/internal/logic/admin/application/previewSubscribeTemplateLogic.go b/internal/logic/admin/application/previewSubscribeTemplateLogic.go new file mode 100644 index 0000000..8ca72e0 --- /dev/null +++ b/internal/logic/admin/application/previewSubscribeTemplateLogic.go @@ -0,0 +1,76 @@ +package application + +import ( + "context" + "time" + + "github.com/perfect-panel/server/adapter" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type PreviewSubscribeTemplateLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Preview Template +func NewPreviewSubscribeTemplateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreviewSubscribeTemplateLogic { + return &PreviewSubscribeTemplateLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PreviewSubscribeTemplateLogic) PreviewSubscribeTemplate(req *types.PreviewSubscribeTemplateRequest) (resp *types.PreviewSubscribeTemplateResponse, err error) { + enable := true + _, servers, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + Preload: true, + Enabled: &enable, + }) + if err != nil { + l.Errorf("[PreviewSubscribeTemplateLogic] FindAllServer error: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindAllServer error: %v", err.Error()) + } + + data, err := l.svcCtx.ClientModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Errorf("[PreviewSubscribeTemplateLogic] FindOne error: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneClient error: %v", err.Error()) + } + + sub := adapter.NewAdapter(data.SubscribeTemplate, adapter.WithServers(servers), + adapter.WithSiteName("PerfectPanel"), + adapter.WithSubscribeName("Test Subscribe"), + adapter.WithOutputFormat(data.OutputFormat), + adapter.WithUserInfo(adapter.User{ + Password: "test-password", + ExpiredAt: time.Now().AddDate(1, 0, 0), + Download: 0, + Upload: 0, + Traffic: 1000, + SubscribeURL: "https://example.com/subscribe", + })) + // Get client config + a, err := sub.Client() + if err != nil { + l.Errorf("[PreviewSubscribeTemplateLogic] Client error: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrMsg(err.Error()), "Client error: %v", err.Error()) + } + bytes, err := a.Build() + if err != nil { + l.Errorf("[PreviewSubscribeTemplateLogic] Build error: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrMsg(err.Error()), "Build error: %v", err.Error()) + } + return &types.PreviewSubscribeTemplateResponse{ + Template: string(bytes), + }, nil +} diff --git a/internal/logic/admin/application/updateSubscribeApplicationLogic.go b/internal/logic/admin/application/updateSubscribeApplicationLogic.go new file mode 100644 index 0000000..b209768 --- /dev/null +++ b/internal/logic/admin/application/updateSubscribeApplicationLogic.go @@ -0,0 +1,62 @@ +package application + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/client" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateSubscribeApplicationLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateSubscribeApplicationLogic Update subscribe application +func NewUpdateSubscribeApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateSubscribeApplicationLogic { + return &UpdateSubscribeApplicationLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateSubscribeApplicationLogic) UpdateSubscribeApplication(req *types.UpdateSubscribeApplicationRequest) (resp *types.SubscribeApplication, err error) { + data, err := l.svcCtx.ClientModel.FindOne(l.ctx, req.Id) + if err != nil { + l.Errorf("Failed to find subscribe application with ID %d: %v", req.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to find subscribe application with ID %d", req.Id) + } + var link client.DownloadLink + tool.DeepCopy(&link, req.DownloadLink) + linkData, err := link.Marshal() + if err != nil { + l.Errorf("Failed to marshal download link: %v", err) + return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), " Failed to marshal download link") + } + + data.Name = req.Name + data.Icon = req.Icon + data.Description = req.Description + data.Scheme = req.Scheme + data.UserAgent = req.UserAgent + data.IsDefault = req.IsDefault + data.SubscribeTemplate = req.SubscribeTemplate + data.OutputFormat = req.OutputFormat + data.DownloadLink = string(linkData) + err = l.svcCtx.ClientModel.Update(l.ctx, data) + if err != nil { + l.Errorf("Failed to update subscribe application with ID %d: %v", req.Id, err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to update subscribe application with ID %d", req.Id) + } + resp = &types.SubscribeApplication{} + tool.DeepCopy(&resp, data) + resp.DownloadLink = req.DownloadLink + return +} diff --git a/internal/logic/admin/authMethod/getAuthMethodConfigLogic.go b/internal/logic/admin/authMethod/getAuthMethodConfigLogic.go index c0ac121..febee66 100644 --- a/internal/logic/admin/authMethod/getAuthMethodConfigLogic.go +++ b/internal/logic/admin/authMethod/getAuthMethodConfigLogic.go @@ -4,11 +4,11 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -18,7 +18,7 @@ type GetAuthMethodConfigLogic struct { svcCtx *svc.ServiceContext } -// Get auth method config +// NewGetAuthMethodConfigLogic Get auth method config func NewGetAuthMethodConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAuthMethodConfigLogic { return &GetAuthMethodConfigLogic{ Logger: logger.WithContext(ctx), diff --git a/internal/logic/admin/authMethod/getAuthMethodListLogic.go b/internal/logic/admin/authMethod/getAuthMethodListLogic.go index 5265064..07883d9 100644 --- a/internal/logic/admin/authMethod/getAuthMethodListLogic.go +++ b/internal/logic/admin/authMethod/getAuthMethodListLogic.go @@ -4,11 +4,11 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/authMethod/getEmailPlatformLogic.go b/internal/logic/admin/authMethod/getEmailPlatformLogic.go index 6e6a607..cf9aa72 100644 --- a/internal/logic/admin/authMethod/getEmailPlatformLogic.go +++ b/internal/logic/admin/authMethod/getEmailPlatformLogic.go @@ -3,11 +3,11 @@ package authMethod import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/email" + "github.com/perfect-panel/server/pkg/email" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type GetEmailPlatformLogic struct { diff --git a/internal/logic/admin/authMethod/getSmsPlatformLogic.go b/internal/logic/admin/authMethod/getSmsPlatformLogic.go index a612a50..7b5b15d 100644 --- a/internal/logic/admin/authMethod/getSmsPlatformLogic.go +++ b/internal/logic/admin/authMethod/getSmsPlatformLogic.go @@ -3,11 +3,11 @@ package authMethod import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/sms" + "github.com/perfect-panel/server/pkg/sms" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type GetSmsPlatformLogic struct { diff --git a/internal/logic/admin/authMethod/testEmailSendLogic.go b/internal/logic/admin/authMethod/testEmailSendLogic.go index 8c071ba..3a5b636 100644 --- a/internal/logic/admin/authMethod/testEmailSendLogic.go +++ b/internal/logic/admin/authMethod/testEmailSendLogic.go @@ -4,11 +4,11 @@ import ( "context" "fmt" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/email" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/email" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/authMethod/testSmsSendLogic.go b/internal/logic/admin/authMethod/testSmsSendLogic.go index 75e9f4d..ea0cdb2 100644 --- a/internal/logic/admin/authMethod/testSmsSendLogic.go +++ b/internal/logic/admin/authMethod/testSmsSendLogic.go @@ -4,11 +4,11 @@ import ( "context" "fmt" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/sms" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/sms" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go b/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go index 57cb46b..d61e38f 100644 --- a/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go +++ b/internal/logic/admin/authMethod/updateAuthMethodConfigLogic.go @@ -4,15 +4,15 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/initialize" - "github.com/perfect-panel/ppanel-server/internal/model/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/email" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/sms" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/initialize" + "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/email" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/sms" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -40,34 +40,32 @@ func (l *UpdateAuthMethodConfigLogic) UpdateAuthMethodConfig(req *types.UpdateAu tool.DeepCopy(method, req) if req.Config != nil { - if value, ok := req.Config.(map[string]interface{}); ok { - if req.Method == "email" && value["verify_email_template"] == "" { - value["verify_email_template"] = email.DefaultEmailVerifyTemplate - } - if req.Method == "email" && value["expiration_email_template"] == "" { - value["expiration_email_template"] = email.DefaultExpirationEmailTemplate - } - if req.Method == "email" && value["maintenance_email_template"] == "" { - value["maintenance_email_template"] = email.DefaultMaintenanceEmailTemplate - } - if req.Method == "email" && value["traffic_exceed_email_template"] == "" { - value["traffic_exceed_email_template"] = email.DefaultTrafficExceedEmailTemplate - } - - if value["platform_config"] != nil { - platformConfig, err := validatePlatformConfig(value["platform"].(string), value["platform_config"].(map[string]interface{})) - if err != nil { - l.Errorw("validate platform config failed", logger.Field("config", req.Config), logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "validate platform config failed: %v", err.Error()) - } - req.Config.(map[string]interface{})["platform_config"] = platformConfig - } + _, exist := req.Config.(map[string]interface{}) + if !exist { + req.Config = initializePlatformConfig(req.Method).(string) } + if req.Method == "email" { + configs, _ := json.Marshal(req.Config) + emailConfig := new(auth.EmailAuthConfig) + emailConfig.Unmarshal(string(configs)) + req.Config = emailConfig + } + + if req.Method == "mobile" { + configs, _ := json.Marshal(req.Config) + mobileConfig := new(auth.MobileAuthConfig) + mobileConfig.Unmarshal(string(configs)) + req.Config = mobileConfig + } + bytes, err := json.Marshal(req.Config) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "marshal config failed: %v", err.Error()) } method.Config = string(bytes) + } else { + // initialize platform config + method.Config = initializePlatformConfig(req.Method).(string) } err = l.svcCtx.AuthModel.Update(l.ctx, method) if err != nil { @@ -94,6 +92,9 @@ func (l *UpdateAuthMethodConfigLogic) UpdateGlobal(method string) { if method == "mobile" { initialize.Mobile(l.svcCtx) } + if method == "device" { + initialize.Device(l.svcCtx) + } } func validatePlatformConfig(platform string, cfg map[string]interface{}) (interface{}, error) { @@ -124,3 +125,26 @@ func validatePlatformConfig(platform string, cfg map[string]interface{}) (interf } return config, nil } + +func initializePlatformConfig(platform string) interface{} { + var result interface{} + switch platform { + case "email": + result = new(auth.EmailAuthConfig).Marshal() + case "mobile": + result = new(auth.MobileAuthConfig).Marshal() + case "apple": + result = new(auth.AppleAuthConfig).Marshal() + case "google": + result = new(auth.GoogleAuthConfig).Marshal() + case "github": + result = new(auth.GithubAuthConfig).Marshal() + case "facebook": + result = new(auth.FacebookAuthConfig).Marshal() + case "telegram": + result = new(auth.TelegramAuthConfig).Marshal() + case "device": + result = new(auth.DeviceConfig).Marshal() + } + return result +} diff --git a/internal/logic/admin/authMethod/validate_test.go b/internal/logic/admin/authMethod/validate_test.go index 891eda2..7a2785e 100644 --- a/internal/logic/admin/authMethod/validate_test.go +++ b/internal/logic/admin/authMethod/validate_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "testing" - "github.com/perfect-panel/ppanel-server/pkg/sms" + "github.com/perfect-panel/server/pkg/sms" ) func TestValidate(t *testing.T) { diff --git a/internal/logic/admin/console/queryRevenueStatisticsLogic.go b/internal/logic/admin/console/queryRevenueStatisticsLogic.go index 70e5e78..f9dbb37 100644 --- a/internal/logic/admin/console/queryRevenueStatisticsLogic.go +++ b/internal/logic/admin/console/queryRevenueStatisticsLogic.go @@ -2,12 +2,14 @@ package console import ( "context" + "os" + "strings" "time" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -17,7 +19,7 @@ type QueryRevenueStatisticsLogic struct { svcCtx *svc.ServiceContext } -// Query revenue statistics +// NewQueryRevenueStatisticsLogic Query revenue statistics func NewQueryRevenueStatisticsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryRevenueStatisticsLogic { return &QueryRevenueStatisticsLogic{ Logger: logger.WithContext(ctx), @@ -27,6 +29,9 @@ func NewQueryRevenueStatisticsLogic(ctx context.Context, svcCtx *svc.ServiceCont } func (l *QueryRevenueStatisticsLogic) QueryRevenueStatistics() (resp *types.RevenueStatisticsResponse, err error) { + if strings.ToLower(os.Getenv("PPANEL_MODE")) == "demo" { + return l.mockRevenueStatistics(), nil + } var today, monthly, all types.OrdersStatistics now := time.Now() @@ -45,8 +50,8 @@ func (l *QueryRevenueStatisticsLogic) QueryRevenueStatistics() (resp *types.Reve // Get monthly's revenue statistics monthlyData, err := l.svcCtx.OrderModel.QueryMonthlyOrders(l.ctx, now) if err != nil { - l.Errorw("[QueryRevenueStatisticsLogic] QueryDateOrders error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryDateOrders error: %v", err) + l.Errorw("[QueryRevenueStatisticsLogic] QueryMonthlyOrders error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryMonthlyOrders error: %v", err) } else { monthly = types.OrdersStatistics{ AmountTotal: monthlyData.AmountTotal, @@ -56,6 +61,24 @@ func (l *QueryRevenueStatisticsLogic) QueryRevenueStatistics() (resp *types.Reve } } + // Get monthly daily list for the current month (from 1st to current date) + monthlyListData, err := l.svcCtx.OrderModel.QueryDailyOrdersList(l.ctx, now) + if err != nil { + l.Errorw("[QueryRevenueStatisticsLogic] QueryDailyOrdersList error", logger.Field("error", err.Error())) + // Don't return error, just log it and continue with empty list + } else { + monthlyList := make([]types.OrdersStatistics, len(monthlyListData)) + for i, data := range monthlyListData { + monthlyList[i] = types.OrdersStatistics{ + Date: data.Date, + AmountTotal: data.AmountTotal, + NewOrderAmount: data.NewOrderAmount, + RenewalOrderAmount: data.RenewalOrderAmount, + } + } + monthly.List = monthlyList + } + // Get all revenue statistics allData, err := l.svcCtx.OrderModel.QueryTotalOrders(l.ctx) if err != nil { @@ -69,9 +92,79 @@ func (l *QueryRevenueStatisticsLogic) QueryRevenueStatistics() (resp *types.Reve List: make([]types.OrdersStatistics, 0), } } + + // Get all monthly list for the past 6 months + allListData, err := l.svcCtx.OrderModel.QueryMonthlyOrdersList(l.ctx, now) + if err != nil { + l.Errorw("[QueryRevenueStatisticsLogic] QueryMonthlyOrdersList error", logger.Field("error", err.Error())) + // Don't return error, just log it and continue with empty list + } else { + allList := make([]types.OrdersStatistics, len(allListData)) + for i, data := range allListData { + allList[i] = types.OrdersStatistics{ + Date: data.Date, + AmountTotal: data.AmountTotal, + NewOrderAmount: data.NewOrderAmount, + RenewalOrderAmount: data.RenewalOrderAmount, + } + } + all.List = allList + } + return &types.RevenueStatisticsResponse{ Today: today, Monthly: monthly, All: all, }, nil } + +// mockRevenueStatistics is a mock function to simulate revenue statistics data. +func (l *QueryRevenueStatisticsLogic) mockRevenueStatistics() *types.RevenueStatisticsResponse { + now := time.Now() + + // Generate daily data for the current month (from 1st to current date) + monthlyList := make([]types.OrdersStatistics, 7) + for i := 0; i < 7; i++ { + dayDate := now.AddDate(0, 0, -(6 - i)) + baseAmount := int64(25000 + ((6 - i) * 3000) + ((6-i)%3)*8000) + monthlyList[i] = types.OrdersStatistics{ + Date: dayDate.Format("2006-01-02"), + AmountTotal: baseAmount, + NewOrderAmount: int64(float64(baseAmount) * 0.68), + RenewalOrderAmount: int64(float64(baseAmount) * 0.32), + } + } + + // Generate monthly data for the past 6 months (oldest first) + allList := make([]types.OrdersStatistics, 6) + for i := 0; i < 6; i++ { + monthDate := now.AddDate(0, -(5 - i), 0) + baseAmount := int64(1800000 + ((5 - i) * 200000) + ((5-i)%2)*500000) + allList[i] = types.OrdersStatistics{ + Date: monthDate.Format("2006-01"), + AmountTotal: baseAmount, + NewOrderAmount: int64(float64(baseAmount) * 0.68), + RenewalOrderAmount: int64(float64(baseAmount) * 0.32), + } + } + + return &types.RevenueStatisticsResponse{ + Today: types.OrdersStatistics{ + AmountTotal: 35888, + NewOrderAmount: 22888, + RenewalOrderAmount: 13000, + }, + Monthly: types.OrdersStatistics{ + AmountTotal: 888888, + NewOrderAmount: 588888, + RenewalOrderAmount: 300000, + List: monthlyList, + }, + All: types.OrdersStatistics{ + AmountTotal: 12888888, + NewOrderAmount: 8588888, + RenewalOrderAmount: 4300000, + List: allList, + }, + } +} diff --git a/internal/logic/admin/console/queryServerTotalDataLogic.go b/internal/logic/admin/console/queryServerTotalDataLogic.go index 6ec85b4..5bdafca 100644 --- a/internal/logic/admin/console/queryServerTotalDataLogic.go +++ b/internal/logic/admin/console/queryServerTotalDataLogic.go @@ -2,11 +2,19 @@ package console import ( "context" + "os" + "strings" + "time" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/traffic" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" + "gorm.io/gorm" ) type QueryServerTotalDataLogic struct { @@ -25,116 +33,269 @@ func NewQueryServerTotalDataLogic(ctx context.Context, svcCtx *svc.ServiceContex } func (l *QueryServerTotalDataLogic) QueryServerTotalData() (resp *types.ServerTotalDataResponse, err error) { - resp = &types.ServerTotalDataResponse{ - ServerTrafficRankingToday: make([]types.ServerTrafficData, 0), - ServerTrafficRankingYesterday: make([]types.ServerTrafficData, 0), - UserTrafficRankingToday: make([]types.UserTrafficData, 0), - UserTrafficRankingYesterday: make([]types.UserTrafficData, 0), + + if strings.ToLower(os.Getenv("PPANEL_MODE")) == "demo" { + return l.mockRevenueStatistics(), nil } - // Query node server status - servers, err := l.svcCtx.ServerModel.FindAllServer(l.ctx) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] FindAllServer error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(err, "FindAllServer error: %v", err) - } - onlineServers, err := l.svcCtx.NodeCache.GetOnlineNodeStatusCount(l.ctx) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] GetOnlineNodeStatusCount error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(err, "GetOnlineNodeStatusCount error: %v", err) - } - resp.OnlineServers = onlineServers - resp.OfflineServers = int64(len(servers) - int(onlineServers)) + now := time.Now() - // 获取所有节点在线用户 - allNodeOnlineUser, err := l.svcCtx.NodeCache.GetAllNodeOnlineUser(l.ctx) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get all node online user failed", logger.Field("error", err.Error())) - } - resp.OnlineUserIPs = int64(len(allNodeOnlineUser)) + todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + todayEnd := todayStart.Add(24 * time.Hour).Add(-time.Second) + query := l.svcCtx.DB.WithContext(l.ctx) + var todayTop10User []log.UserTraffic - // 获取所有节点今日上传下载流量 - allNodeUploadTraffic, err := l.svcCtx.NodeCache.GetAllNodeUploadTraffic(l.ctx) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get all node upload traffic failed", logger.Field("error", err.Error())) + err = query.Model(&traffic.TrafficLog{}). + Select("user_id, subscribe_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", todayStart, todayEnd). + Group("user_id, subscribe_id"). + Order("total DESC"). + Limit(10). + Scan(&todayTop10User).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Errorf("[Traffic Stat Queue] Query user traffic failed: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " Query user traffic failed: %v", err.Error()) } - resp.TodayUpload = allNodeUploadTraffic - allNodeDownloadTraffic, err := l.svcCtx.NodeCache.GetAllNodeDownloadTraffic(l.ctx) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get all node download traffic failed", logger.Field("error", err.Error())) + var userTodayTrafficRanking []types.UserTrafficData + for _, item := range todayTop10User { + userTodayTrafficRanking = append(userTodayTrafficRanking, types.UserTrafficData{ + SID: item.SubscribeId, + Upload: item.Upload, + Download: item.Download, + }) } - resp.TodayDownload = allNodeDownloadTraffic - // 获取节点流量排行榜 前10 - nodeTrafficRankingToday, err := l.svcCtx.NodeCache.GetNodeTodayTotalTrafficRank(l.ctx, 10) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get node today total traffic rank failed", logger.Field("error", err.Error())) + + // query yesterday user traffic rank log + yesterday := todayStart.Add(-24 * time.Hour).Format(time.DateOnly) + + var yesterdayLog log.SystemLog + err = query.Model(&log.SystemLog{}).Where("`date` = ? AND `type` = ?", yesterday, log.TypeUserTrafficRank).First(&yesterdayLog).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[QueryServerTotalDataLogic] Query yesterday user traffic rank log error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query yesterday user traffic rank log error: %v", err) } - if len(nodeTrafficRankingToday) > 0 { - var serverTrafficData []types.ServerTrafficData - for _, rank := range nodeTrafficRankingToday { - serverInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, rank.ID) + + var yesterdayUserRankData []types.UserTrafficData + if yesterdayLog.Id > 0 { + var rank log.UserTrafficRank + err = rank.Unmarshal([]byte(yesterdayLog.Content)) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] Unmarshal yesterday user traffic rank log error", logger.Field("error", err.Error())) + } + for _, v := range rank.Rank { + yesterdayUserRankData = append(yesterdayUserRankData, types.UserTrafficData{ + SID: v.SubscribeId, + Upload: v.Upload, + Download: v.Download, + }) + } + } + + // query server traffic rank today + var todayTop10Server []log.ServerTraffic + err = query.Model(&traffic.TrafficLog{}).Select("server_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", todayStart, todayEnd). + Group("server_id"). + Order("total DESC"). + Limit(10). + Scan(&todayTop10Server).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + logger.Errorf("[Traffic Stat Queue] Query server traffic failed: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " Query server traffic failed: %v", err.Error()) + } + + var todayServerRanking []types.ServerTrafficData + for _, item := range todayTop10Server { + info, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, item.ServerId) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] FindOneServer error", logger.Field("error", err.Error()), logger.Field("server_id", item.ServerId)) + continue + } + todayServerRanking = append(todayServerRanking, types.ServerTrafficData{ + ServerId: item.ServerId, + Name: info.Name, + Upload: item.Upload, + Download: item.Download, + }) + } + + // query server traffic rank yesterday + var yesterdayTop10Server []types.ServerTrafficData + var yesterdayServerTrafficLog log.SystemLog + err = query.Model(&log.SystemLog{}).Where("`date` = ? AND `type` = ?", yesterday, log.TypeServerTrafficRank).First(&yesterdayServerTrafficLog).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[QueryServerTotalDataLogic] Query yesterday server traffic rank log error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query yesterday server traffic rank log error: %v", err) + } + if yesterdayServerTrafficLog.Id > 0 { + var rank log.ServerTrafficRank + err = rank.Unmarshal([]byte(yesterdayServerTrafficLog.Content)) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] Unmarshal yesterday server traffic rank log error", logger.Field("error", err.Error())) + } + + for _, v := range rank.Rank { + info, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, v.ServerId) if err != nil { - l.Errorw("[QueryServerTotalDataLogic] FindOne error", logger.Field("error", err)) + l.Errorw("[QueryServerTotalDataLogic] FindOneServer error", logger.Field("error", err.Error()), logger.Field("server_id", v.ServerId)) continue } - serverTrafficData = append(serverTrafficData, types.ServerTrafficData{ - ServerId: rank.ID, - Name: serverInfo.Name, - Upload: rank.Upload, - Download: rank.Download, + yesterdayTop10Server = append(yesterdayTop10Server, types.ServerTrafficData{ + ServerId: v.ServerId, + Name: info.Name, + Upload: v.Upload, + Download: v.Download, }) } - resp.ServerTrafficRankingToday = serverTrafficData - } - // 获取用户流量排行榜 前10 - userTrafficRankingToday, err := l.svcCtx.NodeCache.GetUserTodayTotalTrafficRank(l.ctx, 10) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get user today total traffic rank failed", logger.Field("error", err.Error())) } - if len(userTrafficRankingToday) > 0 { - var userTrafficData []types.UserTrafficData - for _, rank := range userTrafficRankingToday { - userTrafficData = append(userTrafficData, types.UserTrafficData{ - SID: rank.SID, - Upload: rank.Upload, - Download: rank.Download, - }) - } - resp.UserTrafficRankingToday = userTrafficData - } - // 获取昨日节点流量排行榜 前10 - nodeTrafficRankingYesterday, err := l.svcCtx.NodeCache.GetYesterdayNodeTotalTrafficRank(l.ctx) + // query online user count + onlineUsers, err := l.svcCtx.NodeModel.OnlineUserSubscribeGlobal(l.ctx) if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get yesterday node total traffic rank failed", logger.Field("error", err.Error())) + l.Errorw("[QueryServerTotalDataLogic] OnlineUserSubscribeGlobal error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "OnlineUserSubscribeGlobal error: %v", err) } - if len(nodeTrafficRankingYesterday) > 0 { - var serverTrafficData []types.ServerTrafficData - for _, rank := range nodeTrafficRankingYesterday { - serverTrafficData = append(serverTrafficData, types.ServerTrafficData{ - ServerId: rank.ID, - Name: rank.Name, - Upload: rank.Upload, - Download: rank.Download, - }) + + // query online/offline server count + var onlineServers, offlineServers int64 + err = query.Model(&node.Server{}).Where("`last_reported_at` > ?", now.Add(-5*time.Minute)).Count(&onlineServers).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[QueryServerTotalDataLogic] Count online servers error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Count online servers error: %v", err) + } + + err = query.Model(&node.Server{}).Where("`last_reported_at` <= ? OR `last_reported_at` IS NULL", now.Add(-5*time.Minute)).Count(&offlineServers).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[QueryServerTotalDataLogic] Count offline servers error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Count offline servers error: %v", err) + } + // TodayUpload, TodayDownload, MonthlyUpload, MonthlyDownload + var todayUpload, todayDownload, monthlyUpload, monthlyDownload int64 + + type trafficSum struct { + Upload int64 + Download int64 + } + var todayTraffic trafficSum + // Today + err = query.Model(&traffic.TrafficLog{}).Select("SUM(upload) AS upload, SUM(download) AS download"). + Where("timestamp BETWEEN ? AND ?", todayStart, todayEnd). + Scan(&todayTraffic).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[QueryServerTotalDataLogic] Sum today traffic error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Sum today traffic error: %v", err) + } + todayUpload = todayTraffic.Upload + todayDownload = todayTraffic.Download + + // Monthly + monthlyUpload += todayUpload + monthlyDownload += todayDownload + + for i := now.Day() - 1; i >= 1; i-- { + var logInfo log.SystemLog + date := time.Date(now.Year(), now.Month(), i, 0, 0, 0, 0, now.Location()).Format(time.DateOnly) + err = query.Model(&log.SystemLog{}).Where("`date` = ? AND `type` = ?", date, log.TypeTrafficStat).First(&logInfo).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[QueryServerTotalDataLogic] Query daily traffic stat log error", logger.Field("error", err.Error()), logger.Field("date", date)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query daily traffic stat log error: %v", err) } - resp.ServerTrafficRankingYesterday = serverTrafficData - } - // 获取昨日用户流量排行榜 前10 - userTrafficRankingYesterday, err := l.svcCtx.NodeCache.GetYesterdayUserTotalTrafficRank(l.ctx) - if err != nil { - l.Errorw("[QueryServerTotalDataLogic] Get yesterday user total traffic rank failed", logger.Field("error", err.Error())) - } - if len(userTrafficRankingYesterday) > 0 { - var userTrafficData []types.UserTrafficData - for _, rank := range userTrafficRankingYesterday { - userTrafficData = append(userTrafficData, types.UserTrafficData{ - SID: rank.SID, - Upload: rank.Upload, - Download: rank.Download, - }) + if logInfo.Id > 0 { + var stat log.TrafficStat + err = stat.Unmarshal([]byte(logInfo.Content)) + if err != nil { + l.Errorw("[QueryServerTotalDataLogic] Unmarshal daily traffic stat log error", logger.Field("error", err.Error()), logger.Field("date", date)) + continue + } + monthlyUpload += stat.Upload + monthlyDownload += stat.Download } - resp.UserTrafficRankingYesterday = userTrafficData } + + resp = &types.ServerTotalDataResponse{ + OnlineUsers: onlineUsers, + OnlineServers: onlineServers, + OfflineServers: offlineServers, + TodayUpload: todayUpload, + TodayDownload: todayDownload, + MonthlyUpload: monthlyUpload, + MonthlyDownload: monthlyDownload, + UpdatedAt: now.Unix(), + ServerTrafficRankingToday: todayServerRanking, + ServerTrafficRankingYesterday: yesterdayTop10Server, + UserTrafficRankingToday: userTodayTrafficRanking, + UserTrafficRankingYesterday: yesterdayUserRankData, + } + return resp, nil } + +func (l *QueryServerTotalDataLogic) mockRevenueStatistics() *types.ServerTotalDataResponse { + now := time.Now() + + // Generate server traffic ranking data for today (top 10) + serverTrafficToday := make([]types.ServerTrafficData, 10) + serverNames := []string{"香港-01", "美国-洛杉矶", "日本-东京", "新加坡-01", "韩国-首尔", "台湾-01", "德国-法兰克福", "英国-伦敦", "加拿大-多伦多", "澳洲-悉尼"} + for i := 0; i < 10; i++ { + upload := int64(500000000 + (i * 100000000) + (i%3)*200000000) // 500MB - 1.5GB + download := int64(2000000000 + (i * 300000000) + (i%4)*500000000) // 2GB - 8GB + serverTrafficToday[i] = types.ServerTrafficData{ + ServerId: int64(i + 1), + Name: serverNames[i], + Upload: upload, + Download: download, + } + } + + // Generate server traffic ranking data for yesterday (top 10) + serverTrafficYesterday := make([]types.ServerTrafficData, 10) + for i := 0; i < 10; i++ { + upload := int64(480000000 + (i * 95000000) + (i%3)*180000000) + download := int64(1900000000 + (i * 280000000) + (i%4)*450000000) + serverTrafficYesterday[i] = types.ServerTrafficData{ + ServerId: int64(i + 1), + Name: serverNames[i], + Upload: upload, + Download: download, + } + } + + //// Generate user traffic ranking data for today (top 10) + //userTrafficToday := make([]types.UserTrafficData, 10) + //for i := 0; i < 10; i++ { + // upload := int64(100000000 + (i*20000000) + (i%5)*50000000) // 100MB - 400MB + // download := int64(800000000 + (i*150000000) + (i%3)*300000000) // 800MB - 3GB + // userTrafficToday[i] = types.UserTrafficData{ + // SID: int64(10001 + i), + // Upload: upload, + // Download: download, + // } + //} + + //// Generate user traffic ranking data for yesterday (top 10) + //userTrafficYesterday := make([]types.UserTrafficData, 10) + //for i := 0; i < 10; i++ { + // upload := int64(95000000 + (i*18000000) + (i%5)*45000000) + // download := int64(750000000 + (i*140000000) + (i%3)*280000000) + // userTrafficYesterday[i] = types.UserTrafficData{ + // SID: int64(10001 + i), + // Upload: upload, + // Download: download, + // } + //} + // + return &types.ServerTotalDataResponse{ + OnlineUsers: 1688, + OnlineServers: 8, + OfflineServers: 2, + TodayUpload: 8888888888, // ~8.3GB + TodayDownload: 28888888888, // ~26.9GB + MonthlyUpload: 288888888888, // ~269GB + MonthlyDownload: 888888888888, // ~828GB + UpdatedAt: now.Unix(), + ServerTrafficRankingToday: serverTrafficToday, + ServerTrafficRankingYesterday: serverTrafficYesterday, + //UserTrafficRankingToday: userTrafficToday, + //UserTrafficRankingYesterday: userTrafficYesterday, + } +} diff --git a/internal/logic/admin/console/queryTicketWaitReplyLogic.go b/internal/logic/admin/console/queryTicketWaitReplyLogic.go index e65984f..01c0eb3 100644 --- a/internal/logic/admin/console/queryTicketWaitReplyLogic.go +++ b/internal/logic/admin/console/queryTicketWaitReplyLogic.go @@ -3,9 +3,9 @@ package console import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type QueryTicketWaitReplyLogic struct { diff --git a/internal/logic/admin/console/queryUserStatisticsLogic.go b/internal/logic/admin/console/queryUserStatisticsLogic.go index 4e1d221..746176c 100644 --- a/internal/logic/admin/console/queryUserStatisticsLogic.go +++ b/internal/logic/admin/console/queryUserStatisticsLogic.go @@ -2,11 +2,13 @@ package console import ( "context" + "os" + "strings" "time" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type QueryUserStatisticsLogic struct { @@ -25,6 +27,9 @@ func NewQueryUserStatisticsLogic(ctx context.Context, svcCtx *svc.ServiceContext } func (l *QueryUserStatisticsLogic) QueryUserStatistics() (resp *types.UserStatisticsResponse, err error) { + if strings.ToLower(os.Getenv("PPANEL_MODE")) == "demo" { + return l.mockRevenueStatistics(), nil + } resp = &types.UserStatisticsResponse{} now := time.Now() // query today user register count @@ -56,8 +61,24 @@ func (l *QueryUserStatisticsLogic) QueryUserStatistics() (resp *types.UserStatis } else { resp.Monthly.NewOrderUsers = newMonth resp.Monthly.RenewalOrderUsers = renewalMonth - // TODO: Check the purchase status in the past seven days - resp.Monthly.List = make([]types.UserStatistics, 0) + } + + // Get monthly daily user statistics list for the current month (from 1st to current date) + monthlyListData, err := l.svcCtx.UserModel.QueryDailyUserStatisticsList(l.ctx, now) + if err != nil { + l.Errorw("[QueryUserStatisticsLogic] QueryDailyUserStatisticsList error", logger.Field("error", err.Error())) + // Don't return error, just log it and continue with empty list + } else { + monthlyList := make([]types.UserStatistics, len(monthlyListData)) + for i, data := range monthlyListData { + monthlyList[i] = types.UserStatistics{ + Date: data.Date, + Register: data.Register, + NewOrderUsers: data.NewOrderUsers, + RenewalOrderUsers: data.RenewalOrderUsers, + } + } + resp.Monthly.List = monthlyList } // query all user count @@ -67,5 +88,83 @@ func (l *QueryUserStatisticsLogic) QueryUserStatistics() (resp *types.UserStatis } else { resp.All.Register = allUserCount } + + // query all user order counts + allNewOrderUsers, allRenewalOrderUsers, err := l.svcCtx.OrderModel.QueryTotalUserCounts(l.ctx) + if err != nil { + l.Errorw("[QueryUserStatisticsLogic] QueryTotalUserCounts error", logger.Field("error", err.Error())) + } else { + resp.All.NewOrderUsers = allNewOrderUsers + resp.All.RenewalOrderUsers = allRenewalOrderUsers + } + + // Get all monthly user statistics list for the past 6 months + allListData, err := l.svcCtx.UserModel.QueryMonthlyUserStatisticsList(l.ctx, now) + if err != nil { + l.Errorw("[QueryUserStatisticsLogic] QueryMonthlyUserStatisticsList error", logger.Field("error", err.Error())) + // Don't return error, just log it and continue with empty list + } else { + allList := make([]types.UserStatistics, len(allListData)) + for i, data := range allListData { + allList[i] = types.UserStatistics{ + Date: data.Date, + Register: data.Register, + NewOrderUsers: data.NewOrderUsers, + RenewalOrderUsers: data.RenewalOrderUsers, + } + } + resp.All.List = allList + } + return } + +func (l *QueryUserStatisticsLogic) mockRevenueStatistics() *types.UserStatisticsResponse { + now := time.Now() + + // Generate daily user statistics for the current month (from 1st to current date) + monthlyList := make([]types.UserStatistics, 7) + for i := 0; i < 7; i++ { + dayDate := now.AddDate(0, 0, -(6 - i)) + baseRegister := int64(18 + ((6 - i) * 3) + ((6-i)%3)*8) + monthlyList[i] = types.UserStatistics{ + Date: dayDate.Format("2006-01-02"), + Register: baseRegister, + NewOrderUsers: int64(float64(baseRegister) * 0.65), + RenewalOrderUsers: int64(float64(baseRegister) * 0.35), + } + } + + // Generate monthly user statistics for the past 6 months (oldest first) + allList := make([]types.UserStatistics, 6) + for i := 0; i < 6; i++ { + monthDate := now.AddDate(0, -(5 - i), 0) + baseRegister := int64(1800 + ((5 - i) * 200) + ((5-i)%2)*500) + allList[i] = types.UserStatistics{ + Date: monthDate.Format("2006-01"), + Register: baseRegister, + NewOrderUsers: int64(float64(baseRegister) * 0.65), + RenewalOrderUsers: int64(float64(baseRegister) * 0.35), + } + } + + return &types.UserStatisticsResponse{ + Today: types.UserStatistics{ + Register: 28, + NewOrderUsers: 18, + RenewalOrderUsers: 10, + }, + Monthly: types.UserStatistics{ + Register: 888, + NewOrderUsers: 588, + RenewalOrderUsers: 300, + List: monthlyList, + }, + All: types.UserStatistics{ + Register: 18888, + NewOrderUsers: 0, // This field is not used in All statistics + RenewalOrderUsers: 0, // This field is not used in All statistics + List: allList, + }, + } +} diff --git a/internal/logic/admin/coupon/batchDeleteCouponLogic.go b/internal/logic/admin/coupon/batchDeleteCouponLogic.go index 9e9da2c..afc229f 100644 --- a/internal/logic/admin/coupon/batchDeleteCouponLogic.go +++ b/internal/logic/admin/coupon/batchDeleteCouponLogic.go @@ -3,10 +3,10 @@ package coupon import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/coupon/createCouponLogic.go b/internal/logic/admin/coupon/createCouponLogic.go index 26ff184..d83f28a 100644 --- a/internal/logic/admin/coupon/createCouponLogic.go +++ b/internal/logic/admin/coupon/createCouponLogic.go @@ -5,14 +5,14 @@ import ( "math/rand" "time" - "github.com/perfect-panel/ppanel-server/internal/model/coupon" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/random" - "github.com/perfect-panel/ppanel-server/pkg/snowflake" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/coupon" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/random" + "github.com/perfect-panel/server/pkg/snowflake" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/coupon/deleteCouponLogic.go b/internal/logic/admin/coupon/deleteCouponLogic.go index b041ecd..f50da63 100644 --- a/internal/logic/admin/coupon/deleteCouponLogic.go +++ b/internal/logic/admin/coupon/deleteCouponLogic.go @@ -3,10 +3,10 @@ package coupon import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/coupon/getCouponListLogic.go b/internal/logic/admin/coupon/getCouponListLogic.go index c58e015..125608d 100644 --- a/internal/logic/admin/coupon/getCouponListLogic.go +++ b/internal/logic/admin/coupon/getCouponListLogic.go @@ -3,11 +3,11 @@ package coupon import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/coupon/updateCouponLogic.go b/internal/logic/admin/coupon/updateCouponLogic.go index ee3136a..e4b3712 100644 --- a/internal/logic/admin/coupon/updateCouponLogic.go +++ b/internal/logic/admin/coupon/updateCouponLogic.go @@ -4,12 +4,12 @@ import ( "context" "fmt" - "github.com/perfect-panel/ppanel-server/internal/model/coupon" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/coupon" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/document/batchDeleteDocumentLogic.go b/internal/logic/admin/document/batchDeleteDocumentLogic.go index 3a53f46..2514a3b 100644 --- a/internal/logic/admin/document/batchDeleteDocumentLogic.go +++ b/internal/logic/admin/document/batchDeleteDocumentLogic.go @@ -3,10 +3,10 @@ package document import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/document/createDocumentLogic.go b/internal/logic/admin/document/createDocumentLogic.go index f30a0cd..88ae718 100644 --- a/internal/logic/admin/document/createDocumentLogic.go +++ b/internal/logic/admin/document/createDocumentLogic.go @@ -4,11 +4,11 @@ import ( "context" "strings" - "github.com/perfect-panel/ppanel-server/internal/model/document" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/document" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/document/deleteDocumentLogic.go b/internal/logic/admin/document/deleteDocumentLogic.go index f264d05..a01eb04 100644 --- a/internal/logic/admin/document/deleteDocumentLogic.go +++ b/internal/logic/admin/document/deleteDocumentLogic.go @@ -3,10 +3,10 @@ package document import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/document/getDocumentDetailLogic.go b/internal/logic/admin/document/getDocumentDetailLogic.go index 7b94a61..55e2a76 100644 --- a/internal/logic/admin/document/getDocumentDetailLogic.go +++ b/internal/logic/admin/document/getDocumentDetailLogic.go @@ -3,11 +3,11 @@ package document import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/document/getDocumentListLogic.go b/internal/logic/admin/document/getDocumentListLogic.go index 1aa88db..87294d7 100644 --- a/internal/logic/admin/document/getDocumentListLogic.go +++ b/internal/logic/admin/document/getDocumentListLogic.go @@ -3,11 +3,11 @@ package document import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/document/updateDocumentLogic.go b/internal/logic/admin/document/updateDocumentLogic.go index e741028..5f1d191 100644 --- a/internal/logic/admin/document/updateDocumentLogic.go +++ b/internal/logic/admin/document/updateDocumentLogic.go @@ -4,11 +4,11 @@ import ( "context" "strings" - "github.com/perfect-panel/ppanel-server/internal/model/document" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/document" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/log/filterBalanceLogLogic.go b/internal/logic/admin/log/filterBalanceLogLogic.go new file mode 100644 index 0000000..6393d66 --- /dev/null +++ b/internal/logic/admin/log/filterBalanceLogLogic.go @@ -0,0 +1,64 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterBalanceLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterBalanceLogLogic Filter balance log +func NewFilterBalanceLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterBalanceLogLogic { + return &FilterBalanceLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterBalanceLogLogic) FilterBalanceLog(req *types.FilterBalanceLogRequest) (resp *types.FilterBalanceLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeBalance.Uint8(), + Data: req.Date, + ObjectID: req.UserId, + }) + + if err != nil { + l.Errorw("[FilterBalanceLog] Query User Balance Log Error:", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Balance Log Error") + } + + list := make([]types.BalanceLog, 0) + for _, datum := range data { + var content log.Balance + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("[QueryUserBalanceLog] unmarshal balance log content failed: %v", err.Error()) + continue + } + list = append(list, types.BalanceLog{ + UserId: datum.ObjectID, + Amount: content.Amount, + Type: content.Type, + OrderNo: content.OrderNo, + Balance: content.Balance, + Timestamp: content.Timestamp, + }) + } + + return &types.FilterBalanceLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterCommissionLogLogic.go b/internal/logic/admin/log/filterCommissionLogLogic.go new file mode 100644 index 0000000..6e4020d --- /dev/null +++ b/internal/logic/admin/log/filterCommissionLogLogic.go @@ -0,0 +1,61 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterCommissionLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterCommissionLogLogic Filter commission log +func NewFilterCommissionLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterCommissionLogLogic { + return &FilterCommissionLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterCommissionLogLogic) FilterCommissionLog(req *types.FilterCommissionLogRequest) (resp *types.FilterCommissionLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Data: req.Date, + Type: log.TypeCommission.Uint8(), + ObjectID: req.UserId, + }) + if err != nil { + l.Errorw("Query User Commission Log failed", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Commission Log failed") + } + var list []types.CommissionLog + + for _, datum := range data { + var content log.Commission + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("unmarshal commission log content failed: %v", err.Error()) + continue + } + list = append(list, types.CommissionLog{ + UserId: datum.ObjectID, + Type: content.Type, + Amount: content.Amount, + OrderNo: content.OrderNo, + Timestamp: content.Timestamp, + }) + } + return &types.FilterCommissionLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterEmailLogLogic.go b/internal/logic/admin/log/filterEmailLogLogic.go new file mode 100644 index 0000000..21ce204 --- /dev/null +++ b/internal/logic/admin/log/filterEmailLogLogic.go @@ -0,0 +1,68 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterEmailLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterEmailLogLogic Filter email log +func NewFilterEmailLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterEmailLogLogic { + return &FilterEmailLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterEmailLogLogic) FilterEmailLog(req *types.FilterLogParams) (resp *types.FilterEmailLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeEmailMessage.Uint8(), + Data: req.Date, + Search: req.Search, + }) + + if err != nil { + l.Errorf("[FilterEmailLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) + } + + var list []types.MessageLog + + for _, datum := range data { + var content log.Message + err = content.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[FilterEmailLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.MessageLog{ + Id: datum.Id, + Type: datum.Type, + Platform: content.Platform, + To: content.To, + Subject: content.Subject, + Content: content.Content, + Status: content.Status, + CreatedAt: datum.CreatedAt.UnixMilli(), + }) + } + + return &types.FilterEmailLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterGiftLogLogic.go b/internal/logic/admin/log/filterGiftLogLogic.go new file mode 100644 index 0000000..5b23119 --- /dev/null +++ b/internal/logic/admin/log/filterGiftLogLogic.go @@ -0,0 +1,68 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterGiftLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Filter gift log +func NewFilterGiftLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterGiftLogLogic { + return &FilterGiftLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterGiftLogLogic) FilterGiftLog(req *types.FilterGiftLogRequest) (resp *types.FilterGiftLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeGift.Uint8(), + ObjectID: req.UserId, + Data: req.Date, + Search: req.Search, + }) + + if err != nil { + l.Errorf("[FilterGiftLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) + } + + var list []types.GiftLog + for _, datum := range data { + var content log.Gift + err = content.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[FilterGiftLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.GiftLog{ + Type: content.Type, + UserId: datum.ObjectID, + OrderNo: content.OrderNo, + SubscribeId: content.SubscribeId, + Amount: content.Amount, + Balance: content.Balance, + Remark: content.Remark, + Timestamp: content.Timestamp, + }) + } + + return &types.FilterGiftLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterLoginLogLogic.go b/internal/logic/admin/log/filterLoginLogLogic.go new file mode 100644 index 0000000..3a43941 --- /dev/null +++ b/internal/logic/admin/log/filterLoginLogLogic.go @@ -0,0 +1,65 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterLoginLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterLoginLogLogic Filter login log +func NewFilterLoginLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterLoginLogLogic { + return &FilterLoginLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterLoginLogLogic) FilterLoginLog(req *types.FilterLoginLogRequest) (resp *types.FilterLoginLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeLogin.Uint8(), + ObjectID: req.UserId, + Data: req.Date, + Search: req.Search, + }) + + if err != nil { + l.Errorf("[FilterLoginLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) + } + var list []types.LoginLog + for _, datum := range data { + var item log.Login + err = item.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[FilterLoginLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.LoginLog{ + UserId: datum.ObjectID, + Method: item.Method, + LoginIP: item.LoginIP, + UserAgent: item.UserAgent, + Success: item.Success, + Timestamp: datum.CreatedAt.UnixMilli(), + }) + } + + return &types.FilterLoginLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterMobileLogLogic.go b/internal/logic/admin/log/filterMobileLogLogic.go new file mode 100644 index 0000000..f5f0f4c --- /dev/null +++ b/internal/logic/admin/log/filterMobileLogLogic.go @@ -0,0 +1,68 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterMobileLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Filter mobile log +func NewFilterMobileLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterMobileLogLogic { + return &FilterMobileLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterMobileLogLogic) FilterMobileLog(req *types.FilterLogParams) (resp *types.FilterMobileLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeMobileMessage.Uint8(), + Data: req.Date, + Search: req.Search, + }) + + if err != nil { + l.Errorf("[FilterMobileLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) + } + + var list []types.MessageLog + + for _, datum := range data { + var content log.Message + err = content.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[FilterMobileLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.MessageLog{ + Id: datum.Id, + Type: datum.Type, + Platform: content.Platform, + To: content.To, + Subject: content.Subject, + Content: content.Content, + Status: content.Status, + CreatedAt: datum.CreatedAt.UnixMilli(), + }) + } + + return &types.FilterMobileLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterRegisterLogLogic.go b/internal/logic/admin/log/filterRegisterLogLogic.go new file mode 100644 index 0000000..81c9684 --- /dev/null +++ b/internal/logic/admin/log/filterRegisterLogLogic.go @@ -0,0 +1,66 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterRegisterLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Filter register log +func NewFilterRegisterLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterRegisterLogLogic { + return &FilterRegisterLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterRegisterLogLogic) FilterRegisterLog(req *types.FilterRegisterLogRequest) (resp *types.FilterRegisterLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeRegister.Uint8(), + ObjectID: req.UserId, + Data: req.Date, + Search: req.Search, + }) + + if err != nil { + l.Errorf("[FilterRegisterLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) + } + + var list []types.RegisterLog + for _, datum := range data { + var item log.Register + err = item.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[FilterLoginLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.RegisterLog{ + UserId: datum.ObjectID, + AuthMethod: item.AuthMethod, + Identifier: item.Identifier, + RegisterIP: item.RegisterIP, + UserAgent: item.UserAgent, + Timestamp: item.Timestamp, + }) + } + + return &types.FilterRegisterLogResponse{ + List: list, + Total: total, + }, nil +} diff --git a/internal/logic/admin/log/filterResetSubscribeLogLogic.go b/internal/logic/admin/log/filterResetSubscribeLogLogic.go new file mode 100644 index 0000000..31e2d2a --- /dev/null +++ b/internal/logic/admin/log/filterResetSubscribeLogLogic.go @@ -0,0 +1,66 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterResetSubscribeLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterResetSubscribeLogLogic Filter reset subscribe log +func NewFilterResetSubscribeLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterResetSubscribeLogLogic { + return &FilterResetSubscribeLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterResetSubscribeLogLogic) FilterResetSubscribeLog(req *types.FilterResetSubscribeLogRequest) (resp *types.FilterResetSubscribeLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeResetSubscribe.Uint8(), + ObjectID: req.UserSubscribeId, + Data: req.Date, + Search: req.Search, + }) + + if err != nil { + l.Errorf("[FilterResetSubscribeLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) + } + + var list []types.ResetSubscribeLog + + for _, item := range data { + var content log.ResetSubscribe + err = content.Unmarshal([]byte(item.Content)) + if err != nil { + l.Errorf("[FilterResetSubscribeLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.ResetSubscribeLog{ + Type: content.Type, + UserId: content.UserId, + UserSubscribeId: item.ObjectID, + OrderNo: content.OrderNo, + Timestamp: content.Timestamp, + }) + } + + return &types.FilterResetSubscribeLogResponse{ + List: list, + Total: total, + }, nil +} diff --git a/internal/logic/admin/log/filterServerTrafficLogLogic.go b/internal/logic/admin/log/filterServerTrafficLogLogic.go new file mode 100644 index 0000000..df5ce41 --- /dev/null +++ b/internal/logic/admin/log/filterServerTrafficLogLogic.go @@ -0,0 +1,166 @@ +package log + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/traffic" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterServerTrafficLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterServerTrafficLogLogic Filter server traffic log +func NewFilterServerTrafficLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterServerTrafficLogLogic { + return &FilterServerTrafficLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} +func (l *FilterServerTrafficLogLogic) FilterServerTrafficLog(req *types.FilterServerTrafficLogRequest) (resp *types.FilterServerTrafficLogResponse, err error) { + today := time.Now().Format("2006-01-02") + var list []types.ServerTrafficLog + var total int64 + + if req.Date == today || req.Date == "" { + now := time.Now() + start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) + end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + + var serverTraffic []log.ServerTraffic + err = l.svcCtx.DB.WithContext(l.ctx). + Model(&traffic.TrafficLog{}). + Select("server_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Group("server_id"). + Order("SUM(download + upload) DESC"). + Scan(&serverTraffic).Error + if err != nil { + l.Errorw("[FilterServerTrafficLog] Query Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "today traffic query error: %s", err.Error()) + } + + for _, v := range serverTraffic { + list = append(list, types.ServerTrafficLog{ + ServerId: v.ServerId, + Upload: v.Upload, + Download: v.Download, + Total: v.Total, + Date: today, + Details: true, + }) + } + + todayTotal := len(list) + + startIdx := (req.Page - 1) * req.Size + endIdx := startIdx + req.Size + + if startIdx < todayTotal { + if endIdx > todayTotal { + endIdx = todayTotal + } + pageData := list[startIdx:endIdx] + return &types.FilterServerTrafficLogResponse{ + List: pageData, + Total: int64(todayTotal), + }, nil + } + + need := endIdx - todayTotal + historyPage := (need + req.Size - 1) / req.Size // 算出需要的历史页数 + historyData, historyTotal, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: historyPage, + Size: need, + Type: log.TypeServerTraffic.Uint8(), + }) + if err != nil { + l.Errorw("[FilterServerTrafficLog] Query History Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "history query error: %s", err.Error()) + } + + for _, item := range historyData { + var content log.ServerTraffic + if err = content.Unmarshal([]byte(item.Content)); err != nil { + l.Errorw("[FilterServerTrafficLog] Unmarshal Error", logger.Field("error", err.Error()), logger.Field("content", item.Content)) + continue + } + + hasDetails := true + if l.svcCtx.Config.Log.AutoClear { + last := now.AddDate(0, 0, int(-l.svcCtx.Config.Log.ClearDays)) + dataTime, err := time.Parse(time.DateOnly, item.Date) + if err != nil { + l.Errorw("[FilterServerTrafficLog] Parse Date Error", logger.Field("error", err.Error()), logger.Field("date", item.Date)) + } else { + if dataTime.Before(last) { + hasDetails = false + } else { + hasDetails = true + } + } + } + + list = append(list, types.ServerTrafficLog{ + ServerId: item.ObjectID, + Upload: content.Upload, + Download: content.Download, + Total: content.Total, + Date: item.Date, + Details: hasDetails, + }) + } + + // 返回最终分页数据 + if endIdx > len(list) { + endIdx = len(list) + } + pageData := list[startIdx:endIdx] + + return &types.FilterServerTrafficLogResponse{ + List: pageData, + Total: int64(todayTotal) + historyTotal, + }, nil + } + + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeServerTraffic.Uint8(), + }) + if err != nil { + l.Errorw("[FilterServerTrafficLog] Query Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "history query error: %s", err.Error()) + } + + for _, item := range data { + var content log.ServerTraffic + if err = content.Unmarshal([]byte(item.Content)); err != nil { + l.Errorw("[FilterServerTrafficLog] Unmarshal Error", logger.Field("error", err.Error()), logger.Field("content", item.Content)) + continue + } + list = append(list, types.ServerTrafficLog{ + ServerId: item.ObjectID, + Upload: content.Upload, + Download: content.Download, + Total: content.Total, + Date: item.Date, + Details: false, + }) + } + + return &types.FilterServerTrafficLogResponse{ + List: list, + Total: total, + }, nil +} diff --git a/internal/logic/admin/log/filterSubscribeLogLogic.go b/internal/logic/admin/log/filterSubscribeLogLogic.go new file mode 100644 index 0000000..560b145 --- /dev/null +++ b/internal/logic/admin/log/filterSubscribeLogLogic.go @@ -0,0 +1,71 @@ +package log + +import ( + "context" + "strconv" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterSubscribeLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterSubscribeLogLogic Filter subscribe log +func NewFilterSubscribeLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterSubscribeLogLogic { + return &FilterSubscribeLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterSubscribeLogLogic) FilterSubscribeLog(req *types.FilterSubscribeLogRequest) (resp *types.FilterSubscribeLogResponse, err error) { + params := &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeSubscribe.Uint8(), + Data: req.Date, + ObjectID: req.UserId, + } + + if req.UserSubscribeId != 0 { + params.Search = `"user_subscribe_id":` + strconv.FormatInt(req.UserSubscribeId, 10) + } + + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, params) + if err != nil { + l.Errorf("[FilterSubscribeLog] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log") + } + + var list []types.SubscribeLog + for _, datum := range data { + var content log.Subscribe + err = content.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[FilterSubscribeLog] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.SubscribeLog{ + UserId: datum.ObjectID, + Token: content.Token, + UserAgent: content.UserAgent, + ClientIP: content.ClientIP, + UserSubscribeId: content.UserSubscribeId, + Timestamp: datum.CreatedAt.UnixMilli(), + }) + } + + return &types.FilterSubscribeLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/log/filterTrafficLogDetailsLogic.go b/internal/logic/admin/log/filterTrafficLogDetailsLogic.go new file mode 100644 index 0000000..0dea661 --- /dev/null +++ b/internal/logic/admin/log/filterTrafficLogDetailsLogic.go @@ -0,0 +1,84 @@ +package log + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/traffic" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterTrafficLogDetailsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterTrafficLogDetailsLogic Filter traffic log details +func NewFilterTrafficLogDetailsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterTrafficLogDetailsLogic { + return &FilterTrafficLogDetailsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterTrafficLogDetailsLogic) FilterTrafficLogDetails(req *types.FilterTrafficLogDetailsRequest) (resp *types.FilterTrafficLogDetailsResponse, err error) { + var start, end time.Time + if req.Date != "" { + day, err := time.ParseInLocation("2006-01-02", req.Date, time.Local) + if err != nil { + l.Errorw("[FilterTrafficLogDetails] Date Parse Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), " date parse error: %s", err.Error()) + } + start = day + end = day.Add(24*time.Hour - time.Nanosecond) + } else { + // query today + now := time.Now() + start = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + end = start.Add(24*time.Hour - time.Nanosecond) + } + var data []*traffic.TrafficLog + tx := l.svcCtx.DB.WithContext(l.ctx).Model(&traffic.TrafficLog{}) + if req.ServerId != 0 { + tx = tx.Where("server_id = ?", req.ServerId) + } + if !start.IsZero() && !end.IsZero() { + tx = tx.Where("timestamp BETWEEN ? AND ?", start, end) + } + if req.UserId != 0 { + tx = tx.Where("user_id = ?", req.UserId) + } + if req.SubscribeId != 0 { + tx = tx.Where("subscribe_id = ?", req.SubscribeId) + } + var total int64 + err = tx.Count(&total).Limit(req.Size).Offset((req.Page - 1) * req.Size).Find(&data).Error + if err != nil { + l.Errorw("[FilterTrafficLogDetails] Query Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " database query error: %s", err.Error()) + } + + var logs []types.TrafficLogDetails + for _, v := range data { + logs = append(logs, types.TrafficLogDetails{ + Id: v.Id, + UserId: v.UserId, + ServerId: v.ServerId, + SubscribeId: v.SubscribeId, + Download: v.Download, + Upload: v.Upload, + Timestamp: v.Timestamp.UnixMilli(), + }) + } + + return &types.FilterTrafficLogDetailsResponse{ + List: logs, + Total: total, + }, nil +} diff --git a/internal/logic/admin/log/filterUserSubscribeTrafficLogLogic.go b/internal/logic/admin/log/filterUserSubscribeTrafficLogLogic.go new file mode 100644 index 0000000..f6c5a46 --- /dev/null +++ b/internal/logic/admin/log/filterUserSubscribeTrafficLogLogic.go @@ -0,0 +1,160 @@ +package log + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/traffic" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterUserSubscribeTrafficLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterUserSubscribeTrafficLogLogic Filter user subscribe traffic log +func NewFilterUserSubscribeTrafficLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterUserSubscribeTrafficLogLogic { + return &FilterUserSubscribeTrafficLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterUserSubscribeTrafficLogLogic) FilterUserSubscribeTrafficLog(req *types.FilterSubscribeTrafficRequest) (resp *types.FilterSubscribeTrafficResponse, err error) { + if req.Size <= 0 { + req.Size = 10 + } + if req.Page <= 0 { + req.Page = 1 + } + + today := time.Now().Format("2006-01-02") + var list []types.UserSubscribeTrafficLog + var total int64 + + if req.Date == today || req.Date == "" { + now := time.Now() + start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) + end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + + var userTraffic []types.UserSubscribeTrafficLog + err = l.svcCtx.DB.WithContext(l.ctx). + Model(&traffic.TrafficLog{}). + Select("user_id, subscribe_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Group("user_id, subscribe_id"). + Order("SUM(download + upload) DESC"). + Scan(&userTraffic).Error + if err != nil { + l.Errorw("[FilterUserSubscribeTrafficLog] Query Database Error", logger.Field("error", err.Error())) + return nil, err + } + + for _, v := range userTraffic { + list = append(list, types.UserSubscribeTrafficLog{ + UserId: v.UserId, + SubscribeId: v.SubscribeId, + Upload: v.Upload, + Download: v.Download, + Total: v.Total, + Date: today, + Details: true, + }) + } + todayTotal := len(list) + + startIdx := (req.Page - 1) * req.Size + endIdx := startIdx + req.Size + if startIdx < todayTotal { + if endIdx > todayTotal { + endIdx = todayTotal + } + pageData := list[startIdx:endIdx] + return &types.FilterSubscribeTrafficResponse{ + List: pageData, + Total: int64(todayTotal), + }, nil + } + + need := endIdx - todayTotal + historyPage := (need + req.Size - 1) / req.Size // 算出需要的历史页数 + historyData, historyTotal, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: historyPage, + Size: need, + Type: log.TypeSubscribeTraffic.Uint8(), + }) + + if err != nil { + l.Errorw("[FilterUserSubscribeTrafficLog] Query Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[FilterUserSubscribeTrafficLog] Query Database Error") + } + + for _, datum := range historyData { + var item log.UserTraffic + err = item.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorw("[FilterUserSubscribeTrafficLog] Unmarshal Content Error", logger.Field("error", err.Error())) + continue + } + list = append(list, types.UserSubscribeTrafficLog{ + UserId: item.UserId, + SubscribeId: item.SubscribeId, + Upload: item.Upload, + Download: item.Download, + Total: item.Total, + Date: datum.Date, + Details: false, + }) + } + // 返回最终分页数据 + if endIdx > len(list) { + endIdx = len(list) + } + pageData := list[startIdx:endIdx] + + return &types.FilterSubscribeTrafficResponse{ + List: pageData, + Total: int64(todayTotal) + historyTotal, + }, nil + } + var data []*log.SystemLog + data, total, err = l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeSubscribeTraffic.Uint8(), + Data: req.Date, + }) + if err != nil { + l.Errorw("[FilterUserSubscribeTrafficLog] Query Database Error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[FilterUserSubscribeTrafficLog] Query Database Error") + } + for _, datum := range data { + var item log.UserTraffic + err = item.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorw("[FilterUserSubscribeTrafficLog] Unmarshal Content Error", logger.Field("error", err.Error())) + continue + } + list = append(list, types.UserSubscribeTrafficLog{ + UserId: item.UserId, + SubscribeId: item.SubscribeId, + Upload: item.Upload, + Download: item.Download, + Total: item.Total, + Date: datum.Date, + Details: false, + }) + } + return &types.FilterSubscribeTrafficResponse{ + List: list, + Total: total, + }, nil +} diff --git a/internal/logic/admin/log/getLogSettingLogic.go b/internal/logic/admin/log/getLogSettingLogic.go new file mode 100644 index 0000000..568d7e0 --- /dev/null +++ b/internal/logic/admin/log/getLogSettingLogic.go @@ -0,0 +1,37 @@ +package log + +import ( + "context" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" +) + +type GetLogSettingLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get log setting +func NewGetLogSettingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLogSettingLogic { + return &GetLogSettingLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetLogSettingLogic) GetLogSetting() (resp *types.LogSetting, err error) { + configs, err := l.svcCtx.SystemModel.GetLogConfig(l.ctx) + if err != nil { + l.Errorw("[GetLogSetting] Database query error", logger.Field("error", err.Error())) + return nil, err + } + resp = &types.LogSetting{} + // reflect to response + tool.SystemConfigSliceReflectToStruct(configs, resp) + return +} diff --git a/internal/logic/admin/log/getMessageLogListLogic.go b/internal/logic/admin/log/getMessageLogListLogic.go index f8be0cb..252028b 100644 --- a/internal/logic/admin/log/getMessageLogListLogic.go +++ b/internal/logic/admin/log/getMessageLogListLogic.go @@ -3,12 +3,11 @@ package log import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/log" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -28,20 +27,39 @@ func NewGetMessageLogListLogic(ctx context.Context, svcCtx *svc.ServiceContext) } func (l *GetMessageLogListLogic) GetMessageLogList(req *types.GetMessageLogListRequest) (resp *types.GetMessageLogListResponse, err error) { - total, data, err := l.svcCtx.LogModel.FindMessageLogList(l.ctx, req.Page, req.Size, log.MessageLogFilterParams{ - Type: req.Type, - Platform: req.Platform, - To: req.To, - Subject: req.Subject, - Content: req.Content, - Status: req.Status, + + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: req.Type, + Search: req.Search, }) + if err != nil { - l.Errorw("[GetMessageLogList] Database Error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[GetMessageLogList] Database Error: %s", err.Error()) + l.Errorf("[GetMessageLogList] failed to filter system log: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to filter system log: %v", err.Error()) } + var list []types.MessageLog - tool.DeepCopy(&list, data) + + for _, datum := range data { + var content log.Message + err = content.Unmarshal([]byte(datum.Content)) + if err != nil { + l.Errorf("[GetMessageLogList] failed to unmarshal content: %v", err.Error()) + continue + } + list = append(list, types.MessageLog{ + Id: datum.Id, + Type: datum.Type, + Platform: content.Platform, + To: content.To, + Subject: content.Subject, + Content: content.Content, + Status: content.Status, + CreatedAt: datum.CreatedAt.UnixMilli(), + }) + } return &types.GetMessageLogListResponse{ Total: total, diff --git a/internal/logic/admin/log/updateLogSettingLogic.go b/internal/logic/admin/log/updateLogSettingLogic.go new file mode 100644 index 0000000..39e5846 --- /dev/null +++ b/internal/logic/admin/log/updateLogSettingLogic.go @@ -0,0 +1,63 @@ +package log + +import ( + "context" + "reflect" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type UpdateLogSettingLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateLogSettingLogic Update log setting +func NewUpdateLogSettingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateLogSettingLogic { + return &UpdateLogSettingLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateLogSettingLogic) UpdateLogSetting(req *types.LogSetting) error { + v := reflect.ValueOf(*req) + // Get the reflection type of the structure + t := v.Type() + err := l.svcCtx.SystemModel.Transaction(l.ctx, func(db *gorm.DB) error { + var err error + for i := 0; i < v.NumField(); i++ { + // Get the field name + fieldName := t.Field(i).Name + // Get the field value to string + fieldValue := tool.ConvertValueToString(v.Field(i)) + // Update the server config + err = db.Model(&system.System{}).Where("`category` = 'log' and `key` = ?", fieldName).Update("value", fieldValue).Error + if err != nil { + break + } + } + return err + }) + if err != nil { + l.Errorw("[UpdateLogSetting] update log setting error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), " update log setting error: %v", err) + } + + l.svcCtx.Config.Log = config.Log{ + AutoClear: *req.AutoClear, + ClearDays: req.ClearDays, + } + + return nil +} diff --git a/internal/logic/admin/marketing/createBatchSendEmailTaskLogic.go b/internal/logic/admin/marketing/createBatchSendEmailTaskLogic.go new file mode 100644 index 0000000..d2f181b --- /dev/null +++ b/internal/logic/admin/marketing/createBatchSendEmailTaskLogic.go @@ -0,0 +1,170 @@ +package marketing + +import ( + "context" + "strconv" + "strings" + "time" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + types2 "github.com/perfect-panel/server/queue/types" + "gorm.io/gorm" +) + +type CreateBatchSendEmailTaskLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCreateBatchSendEmailTaskLogic Create a batch send email task +func NewCreateBatchSendEmailTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateBatchSendEmailTaskLogic { + return &CreateBatchSendEmailTaskLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} +func (l *CreateBatchSendEmailTaskLogic) CreateBatchSendEmailTask(req *types.CreateBatchSendEmailTaskRequest) (err error) { + tx := l.svcCtx.DB + + var emails []string + + // 通用查询器(含 user JOIN + 注册时间范围过滤) + baseQuery := func() *gorm.DB { + query := tx.Model(&user.AuthMethods{}). + Select("auth_identifier"). + Joins("JOIN user ON user.id = user_auth_methods.user_id"). + Where("auth_type = ?", "email") + + if req.RegisterStartTime != 0 { + query = query.Where("user.created_at >= ?", time.UnixMilli(req.RegisterStartTime)) + } + if req.RegisterEndTime != 0 { + query = query.Where("user.created_at <= ?", time.UnixMilli(req.RegisterEndTime)) + } + return query + } + + var query *gorm.DB + + scope := task.ParseScopeType(req.Scope) + + switch scope { + case task.ScopeAll: + query = baseQuery() + + case task.ScopeActive: + query = baseQuery(). + Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.status IN ?", []int64{1, 2}) + + case task.ScopeExpired: + query = baseQuery(). + Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.status = ?", 3) + + case task.ScopeNone: + query = baseQuery(). + Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.user_id IS NULL") + default: + + } + if query != nil { + // 执行查询 + err = query.Pluck("auth_identifier", &emails).Error + if err != nil { + l.Errorf("[CreateBatchSendEmailTask] Failed to fetch email addresses: %v", err.Error()) + return xerr.NewErrCode(xerr.DatabaseQueryError) + } + } + + // 邮箱列表为空,返回错误 + if len(emails) == 0 && scope != task.ScopeSkip { + l.Errorf("[CreateBatchSendEmailTask] No email addresses found for the specified scope") + return xerr.NewErrMsg("No email addresses found for the specified scope") + } + + // 邮箱地址去重 + emails = tool.RemoveDuplicateElements(emails...) + + var additionalEmails []string + // 追加额外的邮箱地址(不覆盖) + if req.Additional != "" { + additionalEmails = tool.RemoveDuplicateElements(strings.Split(req.Additional, "\n")...) + } + if len(additionalEmails) == 0 && scope == task.ScopeSkip { + l.Errorf("[CreateBatchSendEmailTask] No additional email addresses provided for skip scope") + return xerr.NewErrMsg("No additional email addresses provided for skip scope") + } + + scheduledAt := time.Now().Add(10 * time.Second) // 默认延迟10秒执行,防止任务创建和执行时间过于接近 + if req.Scheduled != 0 { + scheduledAt = time.Unix(req.Scheduled, 0) + if scheduledAt.Before(time.Now()) { + scheduledAt = time.Now() + } + } + + scopeInfo := task.EmailScope{ + Type: scope.Int8(), + RegisterStartTime: req.RegisterStartTime, + RegisterEndTime: req.RegisterEndTime, + Recipients: emails, + Additional: additionalEmails, + Scheduled: req.Scheduled, + Interval: req.Interval, + Limit: req.Limit, + } + scopeBytes, _ := scopeInfo.Marshal() + + taskContent := task.EmailContent{ + Subject: req.Subject, + Content: req.Content, + } + + contentBytes, _ := taskContent.Marshal() + + var total uint64 + if additionalEmails != nil { + list := append(emails, additionalEmails...) + total = uint64(len(tool.RemoveDuplicateElements(list...))) + } else { + total = uint64(len(emails)) + } + + taskInfo := &task.Task{ + Type: task.TypeEmail, + Scope: string(scopeBytes), + Content: string(contentBytes), + Status: 0, + Errors: "", + Total: total, + Current: 0, + } + + if err = l.svcCtx.DB.Model(&task.Task{}).Create(taskInfo).Error; err != nil { + l.Errorf("[CreateBatchSendEmailTask] Failed to create email task: %v", err.Error()) + return xerr.NewErrCode(xerr.DatabaseInsertError) + } + // create task + l.Infof("[CreateBatchSendEmailTask] Successfully created email task with ID: %d", taskInfo.Id) + + t := asynq.NewTask(types2.ScheduledBatchSendEmail, []byte(strconv.FormatInt(taskInfo.Id, 10))) + info, err := l.svcCtx.Queue.EnqueueContext(l.ctx, t, asynq.ProcessAt(scheduledAt)) + if err != nil { + l.Errorf("[CreateBatchSendEmailTask] Failed to enqueue email task: %v", err.Error()) + return xerr.NewErrCode(xerr.QueueEnqueueError) + } + l.Infof("[CreateBatchSendEmailTask] Successfully enqueued email task with ID: %s, scheduled at: %s", info.ID, scheduledAt.Format(time.DateTime)) + + return nil +} diff --git a/internal/logic/admin/marketing/createQuotaTaskLogic.go b/internal/logic/admin/marketing/createQuotaTaskLogic.go new file mode 100644 index 0000000..606435f --- /dev/null +++ b/internal/logic/admin/marketing/createQuotaTaskLogic.go @@ -0,0 +1,104 @@ +package marketing + +import ( + "context" + "strconv" + "time" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + queueType "github.com/perfect-panel/server/queue/types" + "github.com/pkg/errors" +) + +type CreateQuotaTaskLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCreateQuotaTaskLogic Create a quota task +func NewCreateQuotaTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateQuotaTaskLogic { + return &CreateQuotaTaskLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateQuotaTaskLogic) CreateQuotaTask(req *types.CreateQuotaTaskRequest) error { + var subs []*user.Subscribe + query := l.svcCtx.DB.WithContext(l.ctx).Model(&user.Subscribe{}) + if len(req.Subscribers) > 0 { + query = query.Where("`subscribe_id` IN ?", req.Subscribers) + } + + if req.IsActive != nil && *req.IsActive { + query = query.Where("`status` IN ?", []int64{0, 1, 2}) // 0: Pending 1: Active 2: Finished + } + if req.StartTime != 0 { + start := time.UnixMilli(req.StartTime) + query = query.Where("`start_time` <= ?", start) + } + if req.EndTime != 0 { + end := time.UnixMilli(req.EndTime) + query = query.Where("`expire_time` >= ?", end) + } + + if err := query.Find(&subs).Error; err != nil { + l.Errorf("[CreateQuotaTask] find subscribers error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribers error") + } + if len(subs) == 0 { + return errors.Wrapf(xerr.NewErrMsg("No subscribers found"), "no subscribers found") + } + var subIds []int64 + for _, sub := range subs { + subIds = append(subIds, sub.Id) + } + + scopeInfo := task.QuotaScope{ + Subscribers: req.Subscribers, + IsActive: req.IsActive, + StartTime: req.StartTime, + EndTime: req.EndTime, + Objects: subIds, + } + scopeBytes, _ := scopeInfo.Marshal() + contentInfo := task.QuotaContent{ + ResetTraffic: req.ResetTraffic, + Days: req.Days, + GiftType: req.GiftType, + GiftValue: req.GiftValue, + } + contentBytes, _ := contentInfo.Marshal() + // create task + newTask := &task.Task{ + Type: task.TypeQuota, + Status: 0, + Scope: string(scopeBytes), + Content: string(contentBytes), + Total: uint64(len(subIds)), + Current: 0, + Errors: "", + } + + if err := l.svcCtx.DB.WithContext(l.ctx).Model(&task.Task{}).Create(newTask).Error; err != nil { + l.Errorf("[CreateQuotaTask] create task error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create task error") + } + + // enqueue task + queueTask := asynq.NewTask(queueType.ForthwithQuotaTask, []byte(strconv.FormatInt(newTask.Id, 10))) + if _, err := l.svcCtx.Queue.EnqueueContext(l.ctx, queueTask); err != nil { + l.Errorf("[CreateQuotaTask] enqueue task error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.QueueEnqueueError), "enqueue task error") + } + logger.Infof("[CreateQuotaTask] Successfully created task with ID: %d", newTask.Id) + return nil +} diff --git a/internal/logic/admin/marketing/getBatchSendEmailTaskListLogic.go b/internal/logic/admin/marketing/getBatchSendEmailTaskListLogic.go new file mode 100644 index 0000000..e3a3c7b --- /dev/null +++ b/internal/logic/admin/marketing/getBatchSendEmailTaskListLogic.go @@ -0,0 +1,89 @@ +package marketing + +import ( + "context" + "strings" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" +) + +type GetBatchSendEmailTaskListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetBatchSendEmailTaskListLogic Get batch send email task list +func NewGetBatchSendEmailTaskListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetBatchSendEmailTaskListLogic { + return &GetBatchSendEmailTaskListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetBatchSendEmailTaskListLogic) GetBatchSendEmailTaskList(req *types.GetBatchSendEmailTaskListRequest) (resp *types.GetBatchSendEmailTaskListResponse, err error) { + + var tasks []*task.Task + tx := l.svcCtx.DB.Model(&task.Task{}).Where("`type` = ?", task.TypeEmail) + if req.Status != nil { + tx = tx.Where("status = ?", *req.Status) + } + if req.Scope != nil { + tx = tx.Where("scope = ?", req.Scope) + } + if req.Page == 0 { + req.Page = 1 + } + if req.Size == 0 { + req.Size = 10 + } + err = tx.Offset((req.Page - 1) * req.Size).Limit(req.Size).Order("created_at DESC").Find(&tasks).Error + if err != nil { + l.Errorf("failed to get email tasks: %v", err) + return nil, xerr.NewErrCode(xerr.DatabaseQueryError) + } + + list := make([]types.BatchSendEmailTask, 0) + + for _, t := range tasks { + var scopeInfo task.EmailScope + if err = scopeInfo.Unmarshal([]byte(t.Scope)); err != nil { + l.Errorf("[GetBatchSendEmailTaskList] failed to unmarshal email task scope: %v", err.Error()) + continue + } + var contentInfo task.EmailContent + if err = contentInfo.Unmarshal([]byte(t.Content)); err != nil { + l.Errorf("[GetBatchSendEmailTaskList] failed to unmarshal email task content: %v", err.Error()) + continue + } + + list = append(list, types.BatchSendEmailTask{ + Id: t.Id, + Subject: contentInfo.Subject, + Content: contentInfo.Content, + Recipients: strings.Join(scopeInfo.Recipients, "\n"), + Scope: scopeInfo.Type, + RegisterStartTime: scopeInfo.RegisterStartTime, + RegisterEndTime: scopeInfo.RegisterEndTime, + Additional: strings.Join(scopeInfo.Additional, "\n"), + Scheduled: scopeInfo.Scheduled, + Interval: scopeInfo.Interval, + Limit: scopeInfo.Limit, + Status: uint8(t.Status), + Errors: t.Errors, + Total: t.Total, + Current: t.Current, + CreatedAt: t.CreatedAt.UnixMilli(), + UpdatedAt: t.UpdatedAt.UnixMilli(), + }) + } + + return &types.GetBatchSendEmailTaskListResponse{ + List: list, + }, nil +} diff --git a/internal/logic/admin/marketing/getBatchSendEmailTaskStatusLogic.go b/internal/logic/admin/marketing/getBatchSendEmailTaskStatusLogic.go new file mode 100644 index 0000000..e21a7c5 --- /dev/null +++ b/internal/logic/admin/marketing/getBatchSendEmailTaskStatusLogic.go @@ -0,0 +1,44 @@ +package marketing + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" +) + +type GetBatchSendEmailTaskStatusLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetBatchSendEmailTaskStatusLogic Get batch send email task status +func NewGetBatchSendEmailTaskStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetBatchSendEmailTaskStatusLogic { + return &GetBatchSendEmailTaskStatusLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetBatchSendEmailTaskStatusLogic) GetBatchSendEmailTaskStatus(req *types.GetBatchSendEmailTaskStatusRequest) (resp *types.GetBatchSendEmailTaskStatusResponse, err error) { + tx := l.svcCtx.DB + + var taskInfo *task.Task + err = tx.Model(&task.Task{}).Where("id = ?", req.Id).First(&taskInfo).Error + if err != nil { + l.Errorf("failed to get email task status, error: %v", err) + return nil, xerr.NewErrCode(xerr.DatabaseQueryError) + } + + return &types.GetBatchSendEmailTaskStatusResponse{ + Status: uint8(taskInfo.Status), + Total: int64(taskInfo.Total), + Current: int64(taskInfo.Current), + Errors: taskInfo.Errors, + }, nil +} diff --git a/internal/logic/admin/marketing/getPreSendEmailCountLogic.go b/internal/logic/admin/marketing/getPreSendEmailCountLogic.go new file mode 100644 index 0000000..9fbdbe4 --- /dev/null +++ b/internal/logic/admin/marketing/getPreSendEmailCountLogic.go @@ -0,0 +1,93 @@ +package marketing + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "gorm.io/gorm" +) + +type GetPreSendEmailCountLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetPreSendEmailCountLogic Get pre-send email count +func NewGetPreSendEmailCountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPreSendEmailCountLogic { + return &GetPreSendEmailCountLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPreSendEmailCountLogic) GetPreSendEmailCount(req *types.GetPreSendEmailCountRequest) (resp *types.GetPreSendEmailCountResponse, err error) { + tx := l.svcCtx.DB + var count int64 + // 通用查询器(含 user JOIN + 注册时间范围过滤) + baseQuery := func() *gorm.DB { + query := tx.Model(&user.AuthMethods{}). + Select("auth_identifier"). + Joins("JOIN user ON user.id = user_auth_methods.user_id"). + Where("auth_type = ?", "email") + + if req.RegisterStartTime != 0 { + + registerStartTime := time.UnixMilli(req.RegisterStartTime) + + query = query.Where("user.created_at >= ?", registerStartTime) + } + if req.RegisterEndTime != 0 { + registerEndTime := time.UnixMilli(req.RegisterEndTime) + query = query.Where("user.created_at <= ?", registerEndTime) + } + return query + } + var query *gorm.DB + scope := task.ParseScopeType(req.Scope) + + switch scope { + case task.ScopeAll: + query = baseQuery() + + case task.ScopeActive: + query = baseQuery(). + Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.status IN ?", []int64{1, 2}) + + case task.ScopeExpired: + query = baseQuery(). + Joins("JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.status = ?", 3) + + case task.ScopeNone: + query = baseQuery(). + Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id"). + Where("user_subscribe.user_id IS NULL") + case task.ScopeSkip: + // Skip scope does not require a count + query = nil + default: + l.Errorf("[CreateBatchSendEmailTask] Invalid scope: %v", req.Scope) + return nil, xerr.NewErrMsg("Invalid email scope") + + } + + if query != nil { + if err = query.Count(&count).Error; err != nil { + l.Errorf("[GetPreSendEmailCount] Count error: %v", err) + return nil, xerr.NewErrMsg("Failed to count emails") + } + } + + return &types.GetPreSendEmailCountResponse{ + Count: count, + }, nil +} diff --git a/internal/logic/admin/marketing/queryQuotaTaskListLogic.go b/internal/logic/admin/marketing/queryQuotaTaskListLogic.go new file mode 100644 index 0000000..50cfd9f --- /dev/null +++ b/internal/logic/admin/marketing/queryQuotaTaskListLogic.go @@ -0,0 +1,83 @@ +package marketing + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type QueryQuotaTaskListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryQuotaTaskListLogic Query quota task list +func NewQueryQuotaTaskListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryQuotaTaskListLogic { + return &QueryQuotaTaskListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryQuotaTaskListLogic) QueryQuotaTaskList(req *types.QueryQuotaTaskListRequest) (resp *types.QueryQuotaTaskListResponse, err error) { + var data []*task.Task + var count int64 + query := l.svcCtx.DB.Model(&task.Task{}).Where("`type` = ?", task.TypeQuota) + if req.Page == 0 { + req.Page = 1 + } + if req.Size == 0 { + req.Size = 20 + } + + if req.Status != nil { + query = query.Where("`status` = ?", *req.Status) + } + err = query.Count(&count).Offset((req.Page - 1) * req.Size).Limit(req.Size).Order("created_at DESC").Find(&data).Error + if err != nil { + l.Errorf("[QueryQuotaTaskList] failed to get quota tasks: %v", err) + return nil, err + } + + var list []types.QuotaTask + for _, item := range data { + var scopeInfo task.QuotaScope + if err = scopeInfo.Unmarshal([]byte(item.Scope)); err != nil { + l.Errorf("[QueryQuotaTaskList] failed to unmarshal quota task scope: %v", err.Error()) + continue + } + var contentInfo task.QuotaContent + if err = contentInfo.Unmarshal([]byte(item.Content)); err != nil { + l.Errorf("[QueryQuotaTaskList] failed to unmarshal quota task content: %v", err.Error()) + continue + } + list = append(list, types.QuotaTask{ + Id: item.Id, + Subscribers: scopeInfo.Subscribers, + IsActive: scopeInfo.IsActive, + StartTime: scopeInfo.StartTime, + EndTime: scopeInfo.EndTime, + ResetTraffic: contentInfo.ResetTraffic, + Days: contentInfo.Days, + GiftType: contentInfo.GiftType, + GiftValue: contentInfo.GiftValue, + Objects: scopeInfo.Objects, + Status: uint8(item.Status), + Total: int64(item.Total), + Current: int64(item.Current), + Errors: item.Errors, + CreatedAt: item.CreatedAt.UnixMilli(), + UpdatedAt: item.UpdatedAt.UnixMilli(), + }) + } + + return &types.QueryQuotaTaskListResponse{ + Total: count, + List: list, + }, nil +} diff --git a/internal/logic/admin/marketing/queryQuotaTaskPreCountLogic.go b/internal/logic/admin/marketing/queryQuotaTaskPreCountLogic.go new file mode 100644 index 0000000..21b0cb4 --- /dev/null +++ b/internal/logic/admin/marketing/queryQuotaTaskPreCountLogic.go @@ -0,0 +1,55 @@ +package marketing + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type QueryQuotaTaskPreCountLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryQuotaTaskPreCountLogic Query quota task pre-count +func NewQueryQuotaTaskPreCountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryQuotaTaskPreCountLogic { + return &QueryQuotaTaskPreCountLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryQuotaTaskPreCountLogic) QueryQuotaTaskPreCount(req *types.QueryQuotaTaskPreCountRequest) (resp *types.QueryQuotaTaskPreCountResponse, err error) { + tx := l.svcCtx.DB.WithContext(l.ctx).Model(&user.Subscribe{}) + var count int64 + + if len(req.Subscribers) > 0 { + tx = tx.Where("`subscribe_id` IN ?", req.Subscribers) + } + + if req.IsActive != nil && *req.IsActive { + tx = tx.Where("`status` IN ?", []int64{0, 1, 2}) // 0: Pending 1: Active 2: Finished + } + if req.StartTime != 0 { + start := time.UnixMilli(req.StartTime) + tx = tx.Where("`start_time` <= ?", start) + } + if req.EndTime != 0 { + end := time.UnixMilli(req.EndTime) + tx = tx.Where("`expire_time` >= ?", end) + } + if err = tx.Count(&count).Error; err != nil { + l.Errorf("[QueryQuotaTaskPreCount] count error: %v", err.Error()) + return nil, err + } + + return &types.QueryQuotaTaskPreCountResponse{ + Count: count, + }, nil +} diff --git a/internal/logic/admin/marketing/queryQuotaTaskStatusLogic.go b/internal/logic/admin/marketing/queryQuotaTaskStatusLogic.go new file mode 100644 index 0000000..70599fe --- /dev/null +++ b/internal/logic/admin/marketing/queryQuotaTaskStatusLogic.go @@ -0,0 +1,42 @@ +package marketing + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryQuotaTaskStatusLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryQuotaTaskStatusLogic Query quota task status +func NewQueryQuotaTaskStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryQuotaTaskStatusLogic { + return &QueryQuotaTaskStatusLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryQuotaTaskStatusLogic) QueryQuotaTaskStatus(req *types.QueryQuotaTaskStatusRequest) (resp *types.QueryQuotaTaskStatusResponse, err error) { + var data *task.Task + err = l.svcCtx.DB.Model(&task.Task{}).Where("id = ? AND `type` = ?", req.Id, task.TypeQuota).First(&data).Error + if err != nil { + l.Errorf("[QueryQuotaTaskStatus] failed to get quota task: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), " failed to get quota task: %v", err.Error()) + } + return &types.QueryQuotaTaskStatusResponse{ + Status: uint8(data.Status), + Current: int64(data.Current), + Total: int64(data.Total), + Errors: data.Errors, + }, nil +} diff --git a/internal/logic/admin/marketing/stopBatchSendEmailTaskLogic.go b/internal/logic/admin/marketing/stopBatchSendEmailTaskLogic.go new file mode 100644 index 0000000..da3949f --- /dev/null +++ b/internal/logic/admin/marketing/stopBatchSendEmailTaskLogic.go @@ -0,0 +1,42 @@ +package marketing + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/email" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" +) + +type StopBatchSendEmailTaskLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewStopBatchSendEmailTaskLogic Stop a batch send email task +func NewStopBatchSendEmailTaskLogic(ctx context.Context, svcCtx *svc.ServiceContext) *StopBatchSendEmailTaskLogic { + return &StopBatchSendEmailTaskLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *StopBatchSendEmailTaskLogic) StopBatchSendEmailTask(req *types.StopBatchSendEmailTaskRequest) (err error) { + if email.Manager != nil { + email.Manager.RemoveWorker(req.Id) + } else { + logger.Error("[StopBatchSendEmailTaskLogic] email.Manager is nil, cannot stop task") + } + err = l.svcCtx.DB.Model(&task.Task{}).Where("id = ?", req.Id).Update("status", 2).Error + + if err != nil { + l.Errorf("failed to stop email task, error: %v", err) + return xerr.NewErrCode(xerr.DatabaseUpdateError) + } + return +} diff --git a/internal/logic/admin/order/createOrderLogic.go b/internal/logic/admin/order/createOrderLogic.go index cc9036c..32abeaf 100644 --- a/internal/logic/admin/order/createOrderLogic.go +++ b/internal/logic/admin/order/createOrderLogic.go @@ -3,12 +3,12 @@ package order import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/order/getOrderListLogic.go b/internal/logic/admin/order/getOrderListLogic.go index e272b15..cdf9da6 100644 --- a/internal/logic/admin/order/getOrderListLogic.go +++ b/internal/logic/admin/order/getOrderListLogic.go @@ -3,11 +3,11 @@ package order import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/order/updateOrderStatusLogic.go b/internal/logic/admin/order/updateOrderStatusLogic.go index c82a8a0..17df64d 100644 --- a/internal/logic/admin/order/updateOrderStatusLogic.go +++ b/internal/logic/admin/order/updateOrderStatusLogic.go @@ -5,14 +5,14 @@ import ( "encoding/json" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + queue "github.com/perfect-panel/server/queue/types" ) type UpdateOrderStatusLogic struct { diff --git a/internal/logic/admin/payment/createPaymentMethodLogic.go b/internal/logic/admin/payment/createPaymentMethodLogic.go index 1a132cf..23cd48d 100644 --- a/internal/logic/admin/payment/createPaymentMethodLogic.go +++ b/internal/logic/admin/payment/createPaymentMethodLogic.go @@ -3,16 +3,20 @@ package payment import ( "context" "encoding/json" + "fmt" - "github.com/perfect-panel/ppanel-server/pkg/random" + "github.com/perfect-panel/server/pkg/payment/stripe" + "gorm.io/gorm" - paymentModel "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/payment" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/random" + + paymentModel "github.com/perfect-panel/server/internal/model/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/payment" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -50,9 +54,42 @@ func (l *CreatePaymentMethodLogic) CreatePaymentMethod(req *types.CreatePaymentM Enable: req.Enable, Token: random.KeyNew(8, 1), } - if err := l.svcCtx.PaymentModel.Insert(l.ctx, paymentMethod); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert payment method error: %s", err.Error()) + err = l.svcCtx.PaymentModel.Transaction(l.ctx, func(tx *gorm.DB) error { + if req.Platform == "Stripe" { + var cfg paymentModel.StripeConfig + if err = cfg.Unmarshal([]byte(paymentMethod.Config)); err != nil { + l.Errorf("[CreatePaymentMethod] unmarshal stripe config error: %s", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal stripe config error: %s", err.Error()) + } + if cfg.SecretKey == "" { + l.Error("[CreatePaymentMethod] stripe secret key is empty") + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "stripe secret key is empty") + } + + // Create Stripe webhook endpoint + client := stripe.NewClient(stripe.Config{ + SecretKey: cfg.SecretKey, + PublicKey: cfg.PublicKey, + }) + url := fmt.Sprintf("%s/v1/notify/Stripe/%s", req.Domain, paymentMethod.Token) + endpoint, err := client.CreateWebhookEndpoint(url) + if err != nil { + l.Errorw("[CreatePaymentMethod] create stripe webhook endpoint error", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "create stripe webhook endpoint error: %s", err.Error()) + } + cfg.WebhookSecret = endpoint.Secret + content, _ := cfg.Marshal() + paymentMethod.Config = string(content) + } + if err = tx.Model(&paymentModel.Payment{}).Create(paymentMethod).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert payment method error: %s", err.Error()) + } + return nil + }) + if err != nil { + return nil, err } + resp = &types.PaymentConfig{} tool.DeepCopy(resp, paymentMethod) var configMap map[string]interface{} @@ -64,33 +101,36 @@ func (l *CreatePaymentMethodLogic) CreatePaymentMethod(req *types.CreatePaymentM func parsePaymentPlatformConfig(ctx context.Context, platform payment.Platform, config interface{}) string { data, err := json.Marshal(config) if err != nil { - logger.WithContext(ctx).Errorw("parse payment platform config error", logger.Field("platform", platform), logger.Field("config", config), logger.Field("error", err.Error())) + logger.WithContext(ctx).Errorw("marshal config error", logger.Field("platform", platform), logger.Field("config", config), logger.Field("error", err.Error())) + return "" } + + // 通用处理函数 + handleConfig := func(name string, target interface { + Unmarshal([]byte) error + Marshal() ([]byte, error) + }) string { + if err = target.Unmarshal(data); err != nil { + logger.WithContext(ctx).Errorw("parse "+name+" config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) + return "" + } + content, err := target.Marshal() + if err != nil { + logger.WithContext(ctx).Errorw("marshal "+name+" config error", logger.Field("error", err.Error())) + return "" + } + return string(content) + } + switch platform { case payment.Stripe: - stripe := &paymentModel.StripeConfig{} - if err := stripe.Unmarshal(string(data)); err != nil { - logger.WithContext(ctx).Errorw("parse stripe config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) - } - return stripe.Marshal() + return handleConfig("Stripe", &paymentModel.StripeConfig{}) case payment.AlipayF2F: - alipay := &paymentModel.AlipayF2FConfig{} - if err := alipay.Unmarshal(string(data)); err != nil { - logger.WithContext(ctx).Errorw("parse alipay config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) - } - return alipay.Marshal() + return handleConfig("Alipay", &paymentModel.AlipayF2FConfig{}) case payment.EPay: - epay := &paymentModel.EPayConfig{} - if err := epay.Unmarshal(string(data)); err != nil { - logger.WithContext(ctx).Errorw("parse epay config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) - } - return epay.Marshal() - case payment.Payssion: - payssion := &paymentModel.PayssionConfig{} - if err := payssion.Unmarshal(string(data)); err != nil { - logger.WithContext(ctx).Errorw("parse payssion config error", logger.Field("config", string(data)), logger.Field("error", err.Error())) - } - return payssion.Marshal() + return handleConfig("Epay", &paymentModel.EPayConfig{}) + case payment.CryptoSaaS: + return handleConfig("CryptoSaaS", &paymentModel.CryptoSaaSConfig{}) default: return "" } diff --git a/internal/logic/admin/payment/deletePaymentMethodLogic.go b/internal/logic/admin/payment/deletePaymentMethodLogic.go index 72b4075..2508c58 100644 --- a/internal/logic/admin/payment/deletePaymentMethodLogic.go +++ b/internal/logic/admin/payment/deletePaymentMethodLogic.go @@ -3,10 +3,10 @@ package payment import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/payment/getPaymentMethodListLogic.go b/internal/logic/admin/payment/getPaymentMethodListLogic.go index 2b9cbf5..77c7e40 100644 --- a/internal/logic/admin/payment/getPaymentMethodListLogic.go +++ b/internal/logic/admin/payment/getPaymentMethodListLogic.go @@ -4,13 +4,13 @@ import ( "context" "encoding/json" - paymentPlatform "github.com/perfect-panel/ppanel-server/pkg/payment" + paymentPlatform "github.com/perfect-panel/server/pkg/payment" - "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -55,17 +55,18 @@ func (l *GetPaymentMethodListLogic) GetPaymentMethodList(req *types.GetPaymentMe } } resp.List[i] = types.PaymentMethodDetail{ - Id: v.Id, - Name: v.Name, - Platform: v.Platform, - Icon: v.Icon, - Domain: v.Domain, - Config: config, - FeeMode: v.FeeMode, - FeePercent: v.FeePercent, - FeeAmount: v.FeeAmount, - Enable: *v.Enable, - NotifyURL: notifyUrl, + Id: v.Id, + Name: v.Name, + Platform: v.Platform, + Icon: v.Icon, + Domain: v.Domain, + Config: config, + FeeMode: v.FeeMode, + FeePercent: v.FeePercent, + FeeAmount: v.FeeAmount, + Enable: *v.Enable, + NotifyURL: notifyUrl, + Description: v.Description, } } return diff --git a/internal/logic/admin/payment/getPaymentPlatformLogic.go b/internal/logic/admin/payment/getPaymentPlatformLogic.go index e04cac8..0b87b78 100644 --- a/internal/logic/admin/payment/getPaymentPlatformLogic.go +++ b/internal/logic/admin/payment/getPaymentPlatformLogic.go @@ -3,10 +3,10 @@ package payment import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/payment" ) type GetPaymentPlatformLogic struct { diff --git a/internal/logic/admin/payment/updatePaymentMethodLogic.go b/internal/logic/admin/payment/updatePaymentMethodLogic.go index 25ff600..7c2dda2 100644 --- a/internal/logic/admin/payment/updatePaymentMethodLogic.go +++ b/internal/logic/admin/payment/updatePaymentMethodLogic.go @@ -4,12 +4,12 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/payment" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/payment" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -19,7 +19,7 @@ type UpdatePaymentMethodLogic struct { svcCtx *svc.ServiceContext } -// Update Payment Method +// NewUpdatePaymentMethodLogic Update Payment Method func NewUpdatePaymentMethodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePaymentMethodLogic { return &UpdatePaymentMethodLogic{ Logger: logger.WithContext(ctx), @@ -39,7 +39,7 @@ func (l *UpdatePaymentMethodLogic) UpdatePaymentMethod(req *types.UpdatePaymentM return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %s", err.Error()) } config := parsePaymentPlatformConfig(l.ctx, payment.ParsePlatform(req.Platform), req.Config) - tool.DeepCopy(method, req) + tool.DeepCopy(method, req, tool.CopyWithIgnoreEmpty(false)) method.Config = config if err := l.svcCtx.PaymentModel.Update(l.ctx, method); err != nil { l.Errorw("update payment method error", logger.Field("id", req.Id), logger.Field("error", err.Error())) diff --git a/internal/logic/admin/server/batchDeleteNodeGroupLogic.go b/internal/logic/admin/server/batchDeleteNodeGroupLogic.go deleted file mode 100644 index 83f1201..0000000 --- a/internal/logic/admin/server/batchDeleteNodeGroupLogic.go +++ /dev/null @@ -1,44 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type BatchDeleteNodeGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewBatchDeleteNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteNodeGroupLogic { - return &BatchDeleteNodeGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *BatchDeleteNodeGroupLogic) BatchDeleteNodeGroup(req *types.BatchDeleteNodeGroupRequest) error { - // Check if the group is empty - count, err := l.svcCtx.ServerModel.QueryServerCountByServerGroups(l.ctx, req.Ids) - if err != nil { - l.Errorw("[BatchDeleteNodeGroup] Query Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query server error: %v", err) - } - if count > 0 { - return errors.Wrapf(xerr.NewErrCode(xerr.NodeGroupNotEmpty), "group is not empty") - } - // Delete the group - err = l.svcCtx.ServerModel.BatchDeleteNodeGroup(l.ctx, req.Ids) - if err != nil { - l.Errorw("[BatchDeleteNodeGroup] Delete Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), err.Error()) - } - return nil -} diff --git a/internal/logic/admin/server/batchDeleteNodeLogic.go b/internal/logic/admin/server/batchDeleteNodeLogic.go deleted file mode 100644 index 4cfcb95..0000000 --- a/internal/logic/admin/server/batchDeleteNodeLogic.go +++ /dev/null @@ -1,43 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type BatchDeleteNodeLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewBatchDeleteNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteNodeLogic { - return &BatchDeleteNodeLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *BatchDeleteNodeLogic) BatchDeleteNode(req *types.BatchDeleteNodeRequest) error { - err := l.svcCtx.DB.Transaction(func(db *gorm.DB) error { - for _, id := range req.Ids { - err := l.svcCtx.ServerModel.Delete(l.ctx, id) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - l.Errorw("[BatchDeleteNode] Delete Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), err.Error()) - } - return nil -} diff --git a/internal/logic/admin/server/constant.go b/internal/logic/admin/server/constant.go new file mode 100644 index 0000000..ae06fea --- /dev/null +++ b/internal/logic/admin/server/constant.go @@ -0,0 +1,11 @@ +package server + +const ( + ShadowSocks = "shadowsocks" + Vmess = "vmess" + Vless = "vless" + Trojan = "trojan" + AnyTLS = "anytls" + Tuic = "tuic" + Hysteria2 = "hysteria2" +) diff --git a/internal/logic/admin/server/createNodeGroupLogic.go b/internal/logic/admin/server/createNodeGroupLogic.go deleted file mode 100644 index 83a8d71..0000000 --- a/internal/logic/admin/server/createNodeGroupLogic.go +++ /dev/null @@ -1,40 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - - "github.com/pkg/errors" -) - -type CreateNodeGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewCreateNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateNodeGroupLogic { - return &CreateNodeGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CreateNodeGroupLogic) CreateNodeGroup(req *types.CreateNodeGroupRequest) error { - groupInfo := &server.Group{ - Name: req.Name, - Description: req.Description, - } - err := l.svcCtx.ServerModel.InsertGroup(l.ctx, groupInfo) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), err.Error()) - } - return nil -} diff --git a/internal/logic/admin/server/createNodeLogic.go b/internal/logic/admin/server/createNodeLogic.go index 3e87ce9..f635f85 100644 --- a/internal/logic/admin/server/createNodeLogic.go +++ b/internal/logic/admin/server/createNodeLogic.go @@ -2,18 +2,13 @@ package server import ( "context" - "encoding/json" - "strings" - "time" - "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -23,6 +18,7 @@ type CreateNodeLogic struct { svcCtx *svc.ServiceContext } +// NewCreateNodeLogic Create Node func NewCreateNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateNodeLogic { return &CreateNodeLogic{ Logger: logger.WithContext(ctx), @@ -32,75 +28,19 @@ func NewCreateNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Create } func (l *CreateNodeLogic) CreateNode(req *types.CreateNodeRequest) error { - config, err := json.Marshal(req.Config) - if err != nil { - return err + data := node.Node{ + Name: req.Name, + Tags: tool.StringSliceToString(req.Tags), + Port: req.Port, + Address: req.Address, + ServerId: req.ServerId, + Protocol: req.Protocol, } - var serverInfo server.Server - tool.DeepCopy(&serverInfo, req) - serverInfo.Config = string(config) - nodeRelay, err := json.Marshal(req.RelayNode) - if err != nil { - l.Errorw("[UpdateNode] Marshal RelayNode Error: ", logger.Field("error", err.Error())) - return err - } - if len(req.Tags) > 0 { - serverInfo.Tags = strings.Join(req.Tags, ",") - } - - serverInfo.LastReportedAt = time.UnixMicro(1218124800) - - serverInfo.City = req.City - serverInfo.Country = req.Country - - serverInfo.RelayNode = string(nodeRelay) - if req.Protocol == "vless" { - var cfg types.Vless - if err := json.Unmarshal(config, &cfg); err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json.Unmarshal error: %v", err.Error()) - } - if cfg.Security == "reality" && cfg.SecurityConfig.RealityPublicKey == "" { - public, private, err := tool.Curve25519Genkey(false, "") - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "generate curve25519 key error") - } - cfg.SecurityConfig.RealityPublicKey = public - cfg.SecurityConfig.RealityPrivateKey = private - cfg.SecurityConfig.RealityShortId = tool.GenerateShortID(private) - } - if cfg.SecurityConfig.RealityServerAddr == "" { - cfg.SecurityConfig.RealityServerAddr = cfg.SecurityConfig.SNI - } - if cfg.SecurityConfig.RealityServerPort == 0 { - cfg.SecurityConfig.RealityServerPort = 443 - } - config, _ = json.Marshal(cfg) - serverInfo.Config = string(config) - } - - err = l.svcCtx.ServerModel.Insert(l.ctx, &serverInfo) + err := l.svcCtx.NodeModel.InsertNode(l.ctx, &data) if err != nil { l.Errorw("[CreateNode] Insert Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create server error: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "[CreateNode] Insert Database Error") } - // Marshal the task payload - payload, err := json.Marshal(queue.GetNodeCountry{ - Protocol: serverInfo.Protocol, - ServerAddr: serverInfo.ServerAddr, - }) - if err != nil { - l.Errorw("[GetNodeCountry]: Marshal Error", logger.Field("error", err.Error())) - return errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to marshal task payload") - } - // Create a queue task - task := asynq.NewTask(queue.ForthwithGetCountry, payload) - // Enqueue the task - taskInfo, err := l.svcCtx.Queue.Enqueue(task) - if err != nil { - l.Errorw("[GetNodeCountry]: Enqueue Error", logger.Field("error", err.Error()), logger.Field("payload", string(payload))) - return errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to enqueue task") - } - l.Infow("[GetNodeCountry]: Enqueue Success", logger.Field("taskID", taskInfo.ID), logger.Field("payload", string(payload))) return nil } diff --git a/internal/logic/admin/server/createRuleGroupLogic.go b/internal/logic/admin/server/createRuleGroupLogic.go deleted file mode 100644 index d8bb8bb..0000000 --- a/internal/logic/admin/server/createRuleGroupLogic.go +++ /dev/null @@ -1,69 +0,0 @@ -package server - -import ( - "context" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/rules" - - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type CreateRuleGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Create rule group -func NewCreateRuleGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateRuleGroupLogic { - return &CreateRuleGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} -func parseAndValidateRules(ruleText, ruleName string) ([]string, error) { - var rs []string - ruleArr := strings.Split(ruleText, "\n") - if len(ruleArr) == 0 { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "rules is empty") - } - - for _, s := range ruleArr { - r := rules.NewRule(s, ruleName) - if r == nil { - continue - } - if err := r.Validate(); err != nil { - continue - } - rs = append(rs, r.String()) - } - return rs, nil -} -func (l *CreateRuleGroupLogic) CreateRuleGroup(req *types.CreateRuleGroupRequest) error { - rs, err := parseAndValidateRules(req.Rules, req.Name) - if err != nil { - return err - } - - err = l.svcCtx.ServerModel.InsertRuleGroup(l.ctx, &server.RuleGroup{ - Name: req.Name, - Icon: req.Icon, - Tags: tool.StringSliceToString(req.Tags), - Rules: strings.Join(rs, "\n"), - Enable: req.Enable, - }) - if err != nil { - l.Errorw("[CreateRuleGroup] Insert Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create server rule group error: %v", err) - } - return nil -} diff --git a/internal/logic/admin/server/createServerLogic.go b/internal/logic/admin/server/createServerLogic.go new file mode 100644 index 0000000..2ab1993 --- /dev/null +++ b/internal/logic/admin/server/createServerLogic.go @@ -0,0 +1,110 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/ip" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type CreateServerLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewCreateServerLogic Create Server +func NewCreateServerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateServerLogic { + return &CreateServerLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateServerLogic) CreateServer(req *types.CreateServerRequest) error { + data := node.Server{ + Name: req.Name, + Country: req.Country, + City: req.City, + Address: req.Address, + Sort: req.Sort, + Protocols: "", + } + protocols := make([]node.Protocol, 0) + for _, item := range req.Protocols { + if item.Type == "" { + return errors.Wrapf(xerr.NewErrCodeMsg(xerr.InvalidParams, "protocols type is empty"), "protocols type is empty") + } + var protocol node.Protocol + tool.DeepCopy(&protocol, item) + + // VLESS Reality Key Generation + if protocol.Type == "vless" { + if protocol.Security == "reality" { + if protocol.RealityPublicKey == "" { + public, private, err := tool.Curve25519Genkey(false, "") + if err != nil { + l.Errorf("[CreateServer] Generate Reality Key Error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "generate reality key error: %v", err) + } + protocol.RealityPublicKey = public + protocol.RealityPrivateKey = private + protocol.RealityShortId = tool.GenerateShortID(private) + } + if protocol.RealityServerAddr == "" { + protocol.RealityServerAddr = protocol.SNI + } + if protocol.RealityServerPort == 0 { + protocol.RealityServerPort = 443 + } + } + + } + // ShadowSocks 2022 Key Generation + if protocol.Type == "shadowsocks" { + if strings.Contains(protocol.Cipher, "2022") { + var length int + switch protocol.Cipher { + case "2022-blake3-aes-128-gcm": + length = 16 + default: + length = 32 + } + if len(protocol.ServerKey) != length { + protocol.ServerKey = tool.GenerateCipher(protocol.ServerKey, length) + } + } + } + protocols = append(protocols, protocol) + } + + err := data.MarshalProtocols(protocols) + if err != nil { + l.Errorf("[CreateServer] Marshal Protocols Error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCodeMsg(xerr.InvalidParams, "protocols marshal error"), "protocols marshal error: %v", err) + } + if data.City == "" && data.Country == "" { + // query server ip location + result, err := ip.GetRegionByIp(req.Address) + if err != nil { + l.Errorf("[CreateServer] GetRegionByIp Error: %v", err.Error()) + } else { + data.City = result.City + data.Country = result.Country + } + } + err = l.svcCtx.NodeModel.InsertServer(l.ctx, &data) + if err != nil { + l.Errorf("[CreateServer] Insert Server error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert server error: %v", err) + } + return nil +} diff --git a/internal/logic/admin/server/deleteNodeGroupLogic.go b/internal/logic/admin/server/deleteNodeGroupLogic.go deleted file mode 100644 index dc1bac3..0000000 --- a/internal/logic/admin/server/deleteNodeGroupLogic.go +++ /dev/null @@ -1,44 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type DeleteNodeGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewDeleteNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteNodeGroupLogic { - return &DeleteNodeGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *DeleteNodeGroupLogic) DeleteNodeGroup(req *types.DeleteNodeGroupRequest) error { - // Check if the group is empty - count, err := l.svcCtx.ServerModel.QueryServerCountByServerGroups(l.ctx, []int64{req.Id}) - if err != nil { - l.Errorw("[DeleteNodeGroup] Query Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query server error: %v", err) - } - if count > 0 { - return errors.Wrapf(xerr.NewErrCode(xerr.NodeGroupNotEmpty), "group is not empty") - } - // Delete the group - err = l.svcCtx.ServerModel.DeleteGroup(l.ctx, req.Id) - if err != nil { - l.Errorw("[DeleteNodeGroup] Delete Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), err.Error()) - } - return nil -} diff --git a/internal/logic/admin/server/deleteNodeLogic.go b/internal/logic/admin/server/deleteNodeLogic.go index 5c5655c..8a5f6a0 100644 --- a/internal/logic/admin/server/deleteNodeLogic.go +++ b/internal/logic/admin/server/deleteNodeLogic.go @@ -2,14 +2,14 @@ package server import ( "context" + "strings" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "gorm.io/gorm" ) type DeleteNodeLogic struct { @@ -18,6 +18,7 @@ type DeleteNodeLogic struct { svcCtx *svc.ServiceContext } +// NewDeleteNodeLogic Delete Node func NewDeleteNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteNodeLogic { return &DeleteNodeLogic{ Logger: logger.WithContext(ctx), @@ -27,30 +28,20 @@ func NewDeleteNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Delete } func (l *DeleteNodeLogic) DeleteNode(req *types.DeleteNodeRequest) error { - err := l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { - // Delete server - err := l.svcCtx.ServerModel.Delete(l.ctx, req.Id) - if err != nil { - return err - } - // Delete server to subscribe - subs, err := l.svcCtx.SubscribeModel.QuerySubscribeIdsByServerIdAndServerGroupId(l.ctx, req.Id, 0) - if err != nil { - return err - } - for _, sub := range subs { - servers := tool.StringToInt64Slice(sub.Server) - newServers := tool.RemoveElementBySlice(servers, req.Id) - sub.Server = tool.Int64SliceToString(newServers) - if err = l.svcCtx.SubscribeModel.Update(l.ctx, sub); err != nil { - return err - } - } - return nil - }) + data, err := l.svcCtx.NodeModel.FindOneNode(l.ctx, req.Id) + + err = l.svcCtx.NodeModel.DeleteNode(l.ctx, req.Id) if err != nil { l.Errorw("[DeleteNode] Delete Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete server error: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "[DeleteNode] Delete Database Error") } - return nil + + return l.svcCtx.NodeModel.ClearNodeCache(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: []int64{data.ServerId}, + Tag: strings.Split(data.Tags, ","), + Search: "", + Protocol: data.Protocol, + }) } diff --git a/internal/logic/admin/server/deleteRuleGroupLogic.go b/internal/logic/admin/server/deleteRuleGroupLogic.go deleted file mode 100644 index 63c7acb..0000000 --- a/internal/logic/admin/server/deleteRuleGroupLogic.go +++ /dev/null @@ -1,35 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type DeleteRuleGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Delete rule group -func NewDeleteRuleGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteRuleGroupLogic { - return &DeleteRuleGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *DeleteRuleGroupLogic) DeleteRuleGroup(req *types.DeleteRuleGroupRequest) error { - err := l.svcCtx.ServerModel.DeleteRuleGroup(l.ctx, req.Id) - if err != nil { - l.Errorw("[DeleteRuleGroup] Delete Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete server rule group error: %v", err) - } - return nil -} diff --git a/internal/logic/admin/server/deleteServerLogic.go b/internal/logic/admin/server/deleteServerLogic.go new file mode 100644 index 0000000..186d14a --- /dev/null +++ b/internal/logic/admin/server/deleteServerLogic.go @@ -0,0 +1,41 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type DeleteServerLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewDeleteServerLogic Delete Server +func NewDeleteServerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteServerLogic { + return &DeleteServerLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteServerLogic) DeleteServer(req *types.DeleteServerRequest) error { + err := l.svcCtx.NodeModel.DeleteServer(l.ctx, req.Id) + if err != nil { + l.Errorw("[DeleteServer] Delete Server Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "[DeleteServer] Delete Server Error") + } + return l.svcCtx.NodeModel.ClearNodeCache(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: []int64{req.Id}, + Search: "", + }) +} diff --git a/internal/logic/admin/server/filterNodeListLogic.go b/internal/logic/admin/server/filterNodeListLogic.go new file mode 100644 index 0000000..2e41cec --- /dev/null +++ b/internal/logic/admin/server/filterNodeListLogic.go @@ -0,0 +1,64 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterNodeListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterNodeListLogic Filter Node List +func NewFilterNodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterNodeListLogic { + return &FilterNodeListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterNodeListLogic) FilterNodeList(req *types.FilterNodeListRequest) (resp *types.FilterNodeListResponse, err error) { + total, data, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ + Page: req.Page, + Size: req.Size, + Search: req.Search, + }) + + if err != nil { + l.Errorw("[FilterNodeList] Query Database Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[FilterNodeList] Query Database Error") + } + + list := make([]types.Node, 0) + for _, datum := range data { + list = append(list, types.Node{ + Id: datum.Id, + Name: datum.Name, + Tags: tool.RemoveDuplicateElements(strings.Split(datum.Tags, ",")...), + Port: datum.Port, + Address: datum.Address, + ServerId: datum.ServerId, + Protocol: datum.Protocol, + Enabled: datum.Enabled, + Sort: datum.Sort, + CreatedAt: datum.CreatedAt.UnixMilli(), + UpdatedAt: datum.UpdatedAt.UnixMilli(), + }) + } + + return &types.FilterNodeListResponse{ + List: list, + Total: total, + }, nil +} diff --git a/internal/logic/admin/server/filterServerListLogic.go b/internal/logic/admin/server/filterServerListLogic.go new file mode 100644 index 0000000..fb47ffa --- /dev/null +++ b/internal/logic/admin/server/filterServerListLogic.go @@ -0,0 +1,164 @@ +package server + +import ( + "context" + "time" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +type FilterServerListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewFilterServerListLogic Filter Server List +func NewFilterServerListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterServerListLogic { + return &FilterServerListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterServerListLogic) FilterServerList(req *types.FilterServerListRequest) (resp *types.FilterServerListResponse, err error) { + total, data, err := l.svcCtx.NodeModel.FilterServerList(l.ctx, &node.FilterParams{ + Page: req.Page, + Size: req.Size, + Search: req.Search, + }) + if err != nil { + l.Errorw("[FilterServerList] Query Database Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[FilterServerList] Query Database Error") + } + + list := make([]types.Server, 0) + + for _, datum := range data { + var server types.Server + tool.DeepCopy(&server, datum) + + // handler protocols + var protocols []types.Protocol + dst, err := datum.UnmarshalProtocols() + if err != nil { + l.Errorf("[FilterServerList] UnmarshalProtocols Error: %s", err.Error()) + continue + } + tool.DeepCopy(&protocols, dst) + server.Protocols = protocols + + nodeStatus, err := l.svcCtx.NodeModel.StatusCache(l.ctx, datum.Id) + if err != nil { + if !errors.Is(err, redis.Nil) { + l.Errorw("[handlerServerStatus] GetNodeStatus Error: ", logger.Field("error", err.Error()), logger.Field("node_id", datum.Id)) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetNodeStatus Error") + } + server.Status = types.ServerStatus{ + Mem: nodeStatus.Mem, + Cpu: nodeStatus.Cpu, + Disk: nodeStatus.Disk, + Online: l.handlerServerStatus(datum.Id, protocols), + Status: l.handlerServerStaus(datum.LastReportedAt), + } + list = append(list, server) + } + + return &types.FilterServerListResponse{ + List: list, + Total: total, + }, nil +} + +func (l *FilterServerListLogic) handlerServerStatus(id int64, protocols []types.Protocol) []types.ServerOnlineUser { + result := make([]types.ServerOnlineUser, 0) + + for _, protocol := range protocols { + // query online user + data, err := l.svcCtx.NodeModel.OnlineUserSubscribe(l.ctx, id, protocol.Type) + if err != nil { + if !errors.Is(err, redis.Nil) { + l.Errorw("[handlerServerStatus] OnlineUserSubscribe Error: ", logger.Field("error", err.Error()), logger.Field("node_id", id), logger.Field("protocol", protocol.Type)) + } + continue + } + if len(data) > 0 { + for sub, online := range data { + var ips []types.ServerOnlineIP + for _, ip := range online { + ips = append(ips, types.ServerOnlineIP{ + IP: ip, + Protocol: protocol.Type, + }) + } + + result = append(result, types.ServerOnlineUser{ + IP: ips, + SubscribeId: sub, + }) + } + } + } + // merge same subscribe + var mapResult = make(map[int64]types.ServerOnlineUser) + for _, item := range result { + if exist, ok := mapResult[item.SubscribeId]; ok { + // merge + exist.Traffic += item.Traffic + exist.IP = append(exist.IP, item.IP...) + mapResult[item.SubscribeId] = exist + } else { + // get subscribe info + info, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, item.SubscribeId) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("[handlerServerStatus] FindOneSubscribe Error: ", logger.Field("error", err.Error()), logger.Field("subscribe_id", item.SubscribeId)) + } + continue + } + data := types.ServerOnlineUser{ + IP: item.IP, + UserId: info.UserId, + Subscribe: "", + SubscribeId: item.SubscribeId, + Traffic: info.Download + info.Upload, + ExpiredAt: info.ExpireTime.UnixMilli(), + } + if info.Subscribe != nil { + data.Subscribe = info.Subscribe.Name + } + // add new + mapResult[item.SubscribeId] = data + } + } + // convert map to slice + result = make([]types.ServerOnlineUser, 0, len(mapResult)) + for _, item := range mapResult { + result = append(result, item) + } + return result +} + +func (l *FilterServerListLogic) handlerServerStaus(last *time.Time) string { + if last == nil { + return "offline" + } + if time.Since(*last) > time.Minute*5 { + return "offline" + } + if time.Since(*last) > time.Minute*3 { + return "warning" + } + return "online" + +} diff --git a/internal/logic/admin/server/getNodeDetailLogic.go b/internal/logic/admin/server/getNodeDetailLogic.go deleted file mode 100644 index c71d0be..0000000 --- a/internal/logic/admin/server/getNodeDetailLogic.go +++ /dev/null @@ -1,43 +0,0 @@ -package server - -import ( - "context" - "encoding/json" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetNodeDetailLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGetNodeDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeDetailLogic { - return &GetNodeDetailLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetNodeDetailLogic) GetNodeDetail(req *types.GetDetailRequest) (resp *types.Server, err error) { - detail, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.Id) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get server detail error: %v", err.Error()) - } - resp = &types.Server{} - tool.DeepCopy(resp, detail) - var cfg map[string]interface{} - err = json.Unmarshal([]byte(detail.Config), &cfg) - if err != nil { - cfg = make(map[string]interface{}) - } - resp.Config = cfg - return -} diff --git a/internal/logic/admin/server/getNodeGroupListLogic.go b/internal/logic/admin/server/getNodeGroupListLogic.go deleted file mode 100644 index 4a5cc32..0000000 --- a/internal/logic/admin/server/getNodeGroupListLogic.go +++ /dev/null @@ -1,39 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetNodeGroupListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGetNodeGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeGroupListLogic { - return &GetNodeGroupListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetNodeGroupListLogic) GetNodeGroupList() (resp *types.GetNodeGroupListResponse, err error) { - nodeGroupList, err := l.svcCtx.ServerModel.QueryAllGroup(l.ctx) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) - } - nodeGroups := make([]types.ServerGroup, 0) - tool.DeepCopy(&nodeGroups, nodeGroupList) - return &types.GetNodeGroupListResponse{ - Total: int64(len(nodeGroups)), - List: nodeGroups, - }, nil -} diff --git a/internal/logic/admin/server/getNodeListLogic.go b/internal/logic/admin/server/getNodeListLogic.go deleted file mode 100644 index 1e04be6..0000000 --- a/internal/logic/admin/server/getNodeListLogic.go +++ /dev/null @@ -1,100 +0,0 @@ -package server - -import ( - "context" - "encoding/json" - "strings" - - "github.com/perfect-panel/ppanel-server/internal/model/server" - - "github.com/redis/go-redis/v9" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetNodeListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewGetNodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeListLogic { - return &GetNodeListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetNodeListLogic) GetNodeList(req *types.GetNodeServerListRequest) (resp *types.GetNodeServerListResponse, err error) { - total, list, err := l.svcCtx.ServerModel.FindServerListByFilter(l.ctx, &server.ServerFilter{ - Page: req.Page, - Size: req.Size, - Search: req.Search, - Tag: req.Tag, - Group: req.GroupId, - }) - if err != nil { - l.Errorw("[GetNodeList] Query Database Error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) - } - nodes := make([]types.Server, 0) - for _, v := range list { - node := types.Server{} - tool.DeepCopy(&node, v) - // default relay mode - if node.RelayMode == "" { - node.RelayMode = "none" - } - if len(v.Tags) > 0 { - if strings.Contains(v.Tags, ",") { - node.Tags = strings.Split(v.Tags, ",") - } else { - node.Tags = []string{v.Tags} - } - } - // parse config - var cfg map[string]interface{} - err = json.Unmarshal([]byte(v.Config), &cfg) - if err != nil { - cfg = make(map[string]interface{}) - } - node.Config = cfg - relayNode := make([]types.NodeRelay, 0) - err = json.Unmarshal([]byte(v.RelayNode), &relayNode) - if err != nil { - l.Errorw("[GetNodeList] Unmarshal RelayNode Error: ", logger.Field("error", err.Error()), logger.Field("relayNode", v.RelayNode)) - } - node.RelayNode = relayNode - var status types.NodeStatus - nodeStatus, err := l.svcCtx.NodeCache.GetNodeStatus(l.ctx, v.Id) - if err != nil { - // redis nil is not a Error - if !errors.Is(err, redis.Nil) { - l.Errorw("[GetNodeList] Get Node Status Error: ", logger.Field("error", err.Error())) - } - } else { - onlineUser, err := l.svcCtx.NodeCache.GetNodeOnlineUser(l.ctx, v.Id) - if err != nil { - l.Errorw("[GetNodeList] Get Node Online User Error: ", logger.Field("error", err.Error())) - } else { - status.Online = onlineUser - } - status.Cpu = nodeStatus.Cpu - status.Mem = nodeStatus.Mem - status.Disk = nodeStatus.Disk - status.UpdatedAt = nodeStatus.UpdatedAt - } - node.Status = &status - nodes = append(nodes, node) - } - return &types.GetNodeServerListResponse{ - Total: total, - List: nodes, - }, nil -} diff --git a/internal/logic/admin/server/getNodeTagListLogic.go b/internal/logic/admin/server/getNodeTagListLogic.go deleted file mode 100644 index 722fd6d..0000000 --- a/internal/logic/admin/server/getNodeTagListLogic.go +++ /dev/null @@ -1,53 +0,0 @@ -package server - -import ( - "context" - "strings" - - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type GetNodeTagListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get node tag list -func NewGetNodeTagListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeTagListLogic { - return &GetNodeTagListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetNodeTagListLogic) GetNodeTagList() (resp *types.GetNodeTagListResponse, err error) { - var nodeTags, tags []string - err = l.svcCtx.ServerModel.Transaction(l.ctx, func(db *gorm.DB) error { - - return db.Model(&server.Server{}).Select("tags").Pluck("tags", &nodeTags).Error - }) - - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get node tag list failed, %s", err.Error()) - } - - for _, tag := range nodeTags { - tags = append(tags, strings.Split(tag, ",")...) - } - - // Remove duplicate tags - tags = tool.RemoveDuplicateElements(tags...) - - return &types.GetNodeTagListResponse{ - Tags: tags, - }, nil -} diff --git a/internal/logic/admin/server/getRuleGroupListLogic.go b/internal/logic/admin/server/getRuleGroupListLogic.go deleted file mode 100644 index a22766d..0000000 --- a/internal/logic/admin/server/getRuleGroupListLogic.go +++ /dev/null @@ -1,52 +0,0 @@ -package server - -import ( - "context" - "strings" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetRuleGroupListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get rule group list -func NewGetRuleGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRuleGroupListLogic { - return &GetRuleGroupListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetRuleGroupListLogic) GetRuleGroupList() (resp *types.GetRuleGroupResponse, err error) { - nodeRuleGroupList, err := l.svcCtx.ServerModel.QueryAllRuleGroup(l.ctx) - if err != nil { - l.Errorw("[GetRuleGroupList] Query Database Error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) - } - nodeRuleGroups := make([]types.ServerRuleGroup, len(nodeRuleGroupList)) - for i, v := range nodeRuleGroupList { - nodeRuleGroups[i] = types.ServerRuleGroup{ - Id: v.Id, - Icon: v.Icon, - Name: v.Name, - Tags: strings.Split(v.Tags, ","), - Rules: v.Rules, - Enable: v.Enable, - CreatedAt: v.CreatedAt.UnixMilli(), - UpdatedAt: v.UpdatedAt.UnixMilli(), - } - } - return &types.GetRuleGroupResponse{ - Total: int64(len(nodeRuleGroups)), - List: nodeRuleGroups, - }, nil -} diff --git a/internal/logic/admin/server/getServerProtocolsLogic.go b/internal/logic/admin/server/getServerProtocolsLogic.go new file mode 100644 index 0000000..66519d9 --- /dev/null +++ b/internal/logic/admin/server/getServerProtocolsLogic.go @@ -0,0 +1,49 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetServerProtocolsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Server Protocols +func NewGetServerProtocolsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetServerProtocolsLogic { + return &GetServerProtocolsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetServerProtocolsLogic) GetServerProtocols(req *types.GetServerProtocolsRequest) (resp *types.GetServerProtocolsResponse, err error) { + // find server + data, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.Id) + if err != nil { + l.Errorf("[GetServerProtocols] FindOneServer Error: %s", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[GetServerProtocols] FindOneServer Error: %s", err.Error()) + } + + // handler protocols + var protocols []types.Protocol + dst, err := data.UnmarshalProtocols() + if err != nil { + l.Errorf("[FilterServerList] UnmarshalProtocols Error: %s", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[FilterServerList] UnmarshalProtocols Error: %s", err.Error()) + } + tool.DeepCopy(&protocols, dst) + + return &types.GetServerProtocolsResponse{ + Protocols: protocols, + }, nil +} diff --git a/internal/logic/admin/server/hasMigrateSeverNodeLogic.go b/internal/logic/admin/server/hasMigrateSeverNodeLogic.go new file mode 100644 index 0000000..128b7f6 --- /dev/null +++ b/internal/logic/admin/server/hasMigrateSeverNodeLogic.go @@ -0,0 +1,52 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type HasMigrateSeverNodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewHasMigrateSeverNodeLogic Check if there is any server or node to migrate +func NewHasMigrateSeverNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HasMigrateSeverNodeLogic { + return &HasMigrateSeverNodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *HasMigrateSeverNodeLogic) HasMigrateSeverNode() (resp *types.HasMigrateSeverNodeResponse, err error) { + var oldCount, newCount int64 + query := l.svcCtx.DB.WithContext(l.ctx) + + err = query.Model(&server.Server{}).Count(&oldCount).Error + if err != nil { + l.Errorw("[HasMigrateSeverNode] Query Old Server Count Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[HasMigrateSeverNode] Query Old Server Count Error") + } + err = query.Model(&node.Server{}).Count(&newCount).Error + if err != nil { + l.Errorw("[HasMigrateSeverNode] Query New Server Count Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[HasMigrateSeverNode] Query New Server Count Error") + } + var shouldMigrate bool + if oldCount != 0 && newCount == 0 { + shouldMigrate = true + } + + return &types.HasMigrateSeverNodeResponse{ + HasMigrate: shouldMigrate, + }, nil +} diff --git a/internal/logic/admin/server/migrateServerNodeLogic.go b/internal/logic/admin/server/migrateServerNodeLogic.go new file mode 100644 index 0000000..4f0a497 --- /dev/null +++ b/internal/logic/admin/server/migrateServerNodeLogic.go @@ -0,0 +1,338 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/server" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" +) + +type MigrateServerNodeLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewMigrateServerNodeLogic Migrate server and node data to new database +func NewMigrateServerNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MigrateServerNodeLogic { + return &MigrateServerNodeLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *MigrateServerNodeLogic) MigrateServerNode() (resp *types.MigrateServerNodeResponse, err error) { + tx := l.svcCtx.DB.WithContext(l.ctx).Begin() + var oldServers []*server.Server + var newServers []*node.Server + var newNodes []*node.Node + + err = tx.Model(&server.Server{}).Find(&oldServers).Error + if err != nil { + l.Errorw("[MigrateServerNode] Query Old Server List Error: ", logger.Field("error", err.Error())) + return &types.MigrateServerNodeResponse{ + Succee: 0, + Fail: 0, + Message: fmt.Sprintf("Query Old Server List Error: %s", err.Error()), + }, nil + } + for _, oldServer := range oldServers { + data, err := l.adapterServer(oldServer) + if err != nil { + l.Errorw("[MigrateServerNode] Adapter Server Error: ", logger.Field("error", err.Error())) + if resp == nil { + resp = &types.MigrateServerNodeResponse{} + } + resp.Fail++ + if resp.Message == "" { + resp.Message = fmt.Sprintf("Adapter Server Error: %s", err.Error()) + } else { + resp.Message = fmt.Sprintf("%s; Adapter Server Error: %s", resp.Message, err.Error()) + } + continue + } + newServers = append(newServers, data) + + newNode, err := l.adapterNode(oldServer) + if err != nil { + l.Errorw("[MigrateServerNode] Adapter Node Error: ", logger.Field("error", err.Error())) + if resp == nil { + resp = &types.MigrateServerNodeResponse{} + } + resp.Fail++ + if resp.Message == "" { + resp.Message = fmt.Sprintf("Adapter Node Error: %s", err.Error()) + } else { + resp.Message = fmt.Sprintf("%s; Adapter Node Error: %s", resp.Message, err.Error()) + } + continue + } + for _, item := range newNode { + if item.Port == 0 { + protocols, _ := data.UnmarshalProtocols() + if len(protocols) > 0 { + item.Port = protocols[0].Port + } + } + newNodes = append(newNodes, item) + } + } + + if len(newServers) > 0 { + err = tx.Model(&node.Server{}).CreateInBatches(newServers, 20).Error + if err != nil { + tx.Rollback() + l.Errorw("[MigrateServerNode] Insert New Server List Error: ", logger.Field("error", err.Error())) + return &types.MigrateServerNodeResponse{ + Succee: 0, + Fail: uint64(len(newServers)), + Message: fmt.Sprintf("Insert New Server List Error: %s", err.Error()), + }, nil + } + } + if len(newNodes) > 0 { + err = tx.Model(&node.Node{}).CreateInBatches(newNodes, 20).Error + if err != nil { + tx.Rollback() + l.Errorw("[MigrateServerNode] Insert New Node List Error: ", logger.Field("error", err.Error())) + return &types.MigrateServerNodeResponse{ + Succee: uint64(len(newServers)), + Fail: uint64(len(newNodes)), + Message: fmt.Sprintf("Insert New Node List Error: %s", err.Error()), + }, nil + } + } + tx.Commit() + + return &types.MigrateServerNodeResponse{ + Succee: uint64(len(newServers)), + Fail: 0, + Message: fmt.Sprintf("Migrate Success: %d servers and %d nodes", len(newServers), len(newNodes)), + }, nil +} + +func (l *MigrateServerNodeLogic) adapterServer(info *server.Server) (*node.Server, error) { + result := &node.Server{ + Id: info.Id, + Name: info.Name, + Country: info.Country, + City: info.City, + //Ratio: info.TrafficRatio, + Address: info.ServerAddr, + Sort: int(info.Sort), + Protocols: "", + } + var protocols []node.Protocol + + switch info.Protocol { + case ShadowSocks: + var src server.Shadowsocks + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocols = append(protocols, node.Protocol{ + Type: "shadowsocks", + Cipher: src.Method, + Port: uint16(src.Port), + ServerKey: src.ServerKey, + Ratio: float64(info.TrafficRatio), + }) + case Vmess: + var src server.Vmess + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "vmess", + Port: uint16(src.Port), + Security: src.Security, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Transport: src.Transport, + Host: src.TransportConfig.Host, + Path: src.TransportConfig.Path, + ServiceName: src.TransportConfig.ServiceName, + Flow: src.Flow, + Ratio: float64(info.TrafficRatio), + } + protocols = append(protocols, protocol) + protocols = append(protocols, protocol) + case Vless: + var src server.Vless + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "vless", + Port: uint16(src.Port), + Security: src.Security, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Transport: src.Transport, + Host: src.TransportConfig.Host, + Path: src.TransportConfig.Path, + ServiceName: src.TransportConfig.ServiceName, + Flow: src.Flow, + Ratio: float64(info.TrafficRatio), + } + protocols = append(protocols, protocol) + case Trojan: + var src server.Trojan + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "trojan", + Port: uint16(src.Port), + Security: src.Security, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Transport: src.Transport, + Host: src.TransportConfig.Host, + Path: src.TransportConfig.Path, + ServiceName: src.TransportConfig.ServiceName, + Flow: src.Flow, + Ratio: float64(info.TrafficRatio), + } + protocols = append(protocols, protocol) + case Hysteria2: + var src server.Hysteria2 + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "hysteria", + Port: uint16(src.Port), + HopPorts: src.HopPorts, + HopInterval: src.HopInterval, + ObfsPassword: src.ObfsPassword, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Ratio: float64(info.TrafficRatio), + } + protocols = append(protocols, protocol) + case Tuic: + var src server.Tuic + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "tuic", + Port: uint16(src.Port), + DisableSNI: src.DisableSNI, + ReduceRtt: src.ReduceRtt, + UDPRelayMode: src.UDPRelayMode, + CongestionController: src.CongestionController, + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Ratio: float64(info.TrafficRatio), + } + protocols = append(protocols, protocol) + case AnyTLS: + var src server.AnyTLS + err := json.Unmarshal([]byte(info.Config), &src) + if err != nil { + return nil, err + } + protocol := node.Protocol{ + Type: "anytls", + Port: uint16(src.Port), + SNI: src.SecurityConfig.SNI, + AllowInsecure: src.SecurityConfig.AllowInsecure, + Fingerprint: src.SecurityConfig.Fingerprint, + RealityServerAddr: src.SecurityConfig.RealityServerAddr, + RealityServerPort: src.SecurityConfig.RealityServerPort, + RealityPrivateKey: src.SecurityConfig.RealityPrivateKey, + RealityPublicKey: src.SecurityConfig.RealityPublicKey, + RealityShortId: src.SecurityConfig.RealityShortId, + Ratio: float64(info.TrafficRatio), + } + protocols = append(protocols, protocol) + } + if len(protocols) > 0 { + err := result.MarshalProtocols(protocols) + if err != nil { + return nil, err + } + } + + return result, nil +} + +func (l *MigrateServerNodeLogic) adapterNode(info *server.Server) ([]*node.Node, error) { + var nodes []*node.Node + enable := true + switch info.RelayMode { + case server.RelayModeNone: + nodes = append(nodes, &node.Node{ + Name: info.Name, + Tags: "", + Port: 0, + Address: info.ServerAddr, + ServerId: info.Id, + Protocol: info.Protocol, + Enabled: &enable, + }) + default: + var relays []server.NodeRelay + err := json.Unmarshal([]byte(info.RelayNode), &relays) + if err != nil { + return nil, err + } + for _, relay := range relays { + nodes = append(nodes, &node.Node{ + Name: relay.Prefix + info.Name, + Tags: "", + Port: uint16(relay.Port), + Address: relay.Host, + ServerId: info.Id, + Protocol: info.Protocol, + Enabled: &enable, + }) + } + } + + return nodes, nil +} diff --git a/internal/logic/admin/server/queryNodeTagLogic.go b/internal/logic/admin/server/queryNodeTagLogic.go new file mode 100644 index 0000000..47e0daf --- /dev/null +++ b/internal/logic/admin/server/queryNodeTagLogic.go @@ -0,0 +1,46 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type QueryNodeTagLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryNodeTagLogic Query all node tags +func NewQueryNodeTagLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryNodeTagLogic { + return &QueryNodeTagLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryNodeTagLogic) QueryNodeTag() (resp *types.QueryNodeTagResponse, err error) { + + var nodes []*node.Node + if err = l.svcCtx.DB.WithContext(l.ctx).Model(&node.Node{}).Find(&nodes).Error; err != nil { + l.Errorw("[QueryNodeTag] Query Database Error: ", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[QueryNodeTag] Query Database Error") + } + var tags []string + for _, item := range nodes { + tags = append(tags, strings.Split(item.Tags, ",")...) + } + + return &types.QueryNodeTagResponse{ + Tags: tool.RemoveDuplicateElements(tags...), + }, nil +} diff --git a/internal/logic/admin/server/nodeSortLogic.go b/internal/logic/admin/server/resetSortWithNodeLogic.go similarity index 59% rename from internal/logic/admin/server/nodeSortLogic.go rename to internal/logic/admin/server/resetSortWithNodeLogic.go index f6c8842..3866f54 100644 --- a/internal/logic/admin/server/nodeSortLogic.go +++ b/internal/logic/admin/server/resetSortWithNodeLogic.go @@ -3,36 +3,35 @@ package server import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" ) -type NodeSortLogic struct { +type ResetSortWithNodeLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } -// Node sort -func NewNodeSortLogic(ctx context.Context, svcCtx *svc.ServiceContext) *NodeSortLogic { - return &NodeSortLogic{ +// NewResetSortWithNodeLogic Reset node sort +func NewResetSortWithNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetSortWithNodeLogic { + return &ResetSortWithNodeLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } -func (l *NodeSortLogic) NodeSort(req *types.NodeSortRequest) error { - err := l.svcCtx.ServerModel.Transaction(l.ctx, func(db *gorm.DB) error { +func (l *ResetSortWithNodeLogic) ResetSortWithNode(req *types.ResetSortRequest) error { + err := l.svcCtx.NodeModel.Transaction(l.ctx, func(db *gorm.DB) error { // find all servers id var existingIDs []int64 - db.Model(&server.Server{}).Select("id").Find(&existingIDs) + db.Model(&node.Node{}).Select("id").Find(&existingIDs) // check if the id is valid validIDMap := make(map[int64]bool) for _, id := range existingIDs { @@ -46,12 +45,12 @@ func (l *NodeSortLogic) NodeSort(req *types.NodeSortRequest) error { } } // query all servers - var servers []*server.Server - db.Model(&server.Server{}).Order("sort ASC").Find(&servers) + var servers []*node.Node + db.Model(&node.Node{}).Order("sort ASC").Find(&servers) // create a map of the current sort currentSortMap := make(map[int64]int64) for _, item := range servers { - currentSortMap[item.Id] = item.Sort + currentSortMap[item.Id] = int64(item.Sort) } // new sort map @@ -67,7 +66,12 @@ func (l *NodeSortLogic) NodeSort(req *types.NodeSortRequest) error { } } for _, item := range itemsToUpdate { - if err := db.Model(&server.Server{}).Where("id = ?", item.Id).Update("sort", item.Sort).Error; err != nil { + s, err := l.svcCtx.NodeModel.FindOneNode(l.ctx, item.Id) + if err != nil { + return err + } + s.Sort = int(item.Sort) + if err = l.svcCtx.NodeModel.UpdateNode(l.ctx, s, db); err != nil { l.Errorw("[NodeSort] Update Database Error: ", logger.Field("error", err.Error()), logger.Field("id", item.Id), logger.Field("sort", item.Sort)) return err } diff --git a/internal/logic/admin/server/resetSortWithServerLogic.go b/internal/logic/admin/server/resetSortWithServerLogic.go new file mode 100644 index 0000000..3fbe237 --- /dev/null +++ b/internal/logic/admin/server/resetSortWithServerLogic.go @@ -0,0 +1,86 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type ResetSortWithServerLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewResetSortWithServerLogic Reset server sort +func NewResetSortWithServerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetSortWithServerLogic { + return &ResetSortWithServerLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ResetSortWithServerLogic) ResetSortWithServer(req *types.ResetSortRequest) error { + err := l.svcCtx.NodeModel.Transaction(l.ctx, func(db *gorm.DB) error { + // find all servers id + var existingIDs []int64 + db.Model(&node.Server{}).Select("id").Find(&existingIDs) + // check if the id is valid + validIDMap := make(map[int64]bool) + for _, id := range existingIDs { + validIDMap[id] = true + } + // check if the sort is valid + var validItems []types.SortItem + for _, item := range req.Sort { + if validIDMap[item.Id] { + validItems = append(validItems, item) + } + } + // query all servers + var servers []*node.Server + db.Model(&node.Server{}).Order("sort ASC").Find(&servers) + // create a map of the current sort + currentSortMap := make(map[int64]int64) + for _, item := range servers { + currentSortMap[item.Id] = int64(item.Sort) + } + + // new sort map + newSortMap := make(map[int64]int64) + for _, item := range validItems { + newSortMap[item.Id] = item.Sort + } + + var itemsToUpdate []types.SortItem + for _, item := range validItems { + if oldSort, exists := currentSortMap[item.Id]; exists && oldSort != item.Sort { + itemsToUpdate = append(itemsToUpdate, item) + } + } + for _, item := range itemsToUpdate { + s, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, item.Id) + if err != nil { + return err + } + s.Sort = int(item.Sort) + if err = l.svcCtx.NodeModel.UpdateServer(l.ctx, s, db); err != nil { + l.Errorw("[NodeSort] Update Database Error: ", logger.Field("error", err.Error()), logger.Field("id", item.Id), logger.Field("sort", item.Sort)) + return err + } + } + return nil + }) + if err != nil { + l.Errorw("[NodeSort] Update Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error()) + } + return nil +} diff --git a/internal/logic/admin/server/toggleNodeStatusLogic.go b/internal/logic/admin/server/toggleNodeStatusLogic.go new file mode 100644 index 0000000..f71048f --- /dev/null +++ b/internal/logic/admin/server/toggleNodeStatusLogic.go @@ -0,0 +1,51 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type ToggleNodeStatusLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewToggleNodeStatusLogic Toggle Node Status +func NewToggleNodeStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ToggleNodeStatusLogic { + return &ToggleNodeStatusLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ToggleNodeStatusLogic) ToggleNodeStatus(req *types.ToggleNodeStatusRequest) error { + data, err := l.svcCtx.NodeModel.FindOneNode(l.ctx, req.Id) + if err != nil { + l.Errorw("[ToggleNodeStatus] Query Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "[ToggleNodeStatus] Query Database Error") + } + data.Enabled = req.Enable + + err = l.svcCtx.NodeModel.UpdateNode(l.ctx, data) + if err != nil { + l.Errorw("[ToggleNodeStatus] Update Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "[ToggleNodeStatus] Update Database Error") + } + + return l.svcCtx.NodeModel.ClearNodeCache(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: []int64{data.ServerId}, + Tag: strings.Split(data.Tags, ","), + Search: "", + }) +} diff --git a/internal/logic/admin/server/updateNodeGroupLogic.go b/internal/logic/admin/server/updateNodeGroupLogic.go deleted file mode 100644 index 6774c0b..0000000 --- a/internal/logic/admin/server/updateNodeGroupLogic.go +++ /dev/null @@ -1,40 +0,0 @@ -package server - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type UpdateNodeGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewUpdateNodeGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateNodeGroupLogic { - return &UpdateNodeGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpdateNodeGroupLogic) UpdateNodeGroup(req *types.UpdateNodeGroupRequest) error { - // check server group exist - nodeGroup, err := l.svcCtx.ServerModel.FindOneGroup(l.ctx, req.Id) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) - } - nodeGroup.Name = req.Name - nodeGroup.Description = req.Description - err = l.svcCtx.ServerModel.UpdateGroup(l.ctx, nodeGroup) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error()) - } - return nil -} diff --git a/internal/logic/admin/server/updateNodeLogic.go b/internal/logic/admin/server/updateNodeLogic.go index c5dc4ff..2af8a4d 100644 --- a/internal/logic/admin/server/updateNodeLogic.go +++ b/internal/logic/admin/server/updateNodeLogic.go @@ -2,18 +2,13 @@ package server import ( "context" - "encoding/json" - "strings" - "github.com/perfect-panel/ppanel-server/pkg/device" - - "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -23,6 +18,7 @@ type UpdateNodeLogic struct { svcCtx *svc.ServiceContext } +// NewUpdateNodeLogic Update Node func NewUpdateNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateNodeLogic { return &UpdateNodeLogic{ Logger: logger.WithContext(ctx), @@ -32,79 +28,27 @@ func NewUpdateNodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Update } func (l *UpdateNodeLogic) UpdateNode(req *types.UpdateNodeRequest) error { - // Check server exist - nodeInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.Id) + data, err := l.svcCtx.NodeModel.FindOneNode(l.ctx, req.Id) if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server error: %v", err) + l.Errorw("[UpdateNode] Query Database Error: ", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "[UpdateNode] Query Database Error") } - tool.DeepCopy(nodeInfo, req) - config, err := json.Marshal(req.Config) - if err != nil { - return err - } - - nodeInfo.Config = string(config) - nodeRelay, err := json.Marshal(req.RelayNode) - if err != nil { - l.Errorw("[UpdateNode] Marshal RelayNode Error: ", logger.Field("error", err.Error())) - return err - } - - if len(req.Tags) > 0 { - nodeInfo.Tags = strings.Join(req.Tags, ",") - } - - nodeInfo.City = req.City - nodeInfo.Country = req.Country - - nodeInfo.RelayNode = string(nodeRelay) - if req.Protocol == "vless" { - var cfg types.Vless - if err := json.Unmarshal(config, &cfg); err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json.Unmarshal error: %v", err.Error()) - } - if cfg.Security == "reality" && cfg.SecurityConfig.RealityPublicKey == "" { - public, private, err := tool.Curve25519Genkey(false, "") - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "generate curve25519 key error") - } - cfg.SecurityConfig.RealityPublicKey = public - cfg.SecurityConfig.RealityPrivateKey = private - cfg.SecurityConfig.RealityShortId = tool.GenerateShortID(private) - } - if cfg.SecurityConfig.RealityServerAddr == "" { - cfg.SecurityConfig.RealityServerAddr = cfg.SecurityConfig.SNI - } - if cfg.SecurityConfig.RealityServerPort == 0 { - cfg.SecurityConfig.RealityServerPort = 443 - } - config, _ = json.Marshal(cfg) - nodeInfo.Config = string(config) - } - err = l.svcCtx.ServerModel.Update(l.ctx, nodeInfo) + data.Name = req.Name + data.Tags = tool.StringSliceToString(req.Tags) + data.ServerId = req.ServerId + data.Port = req.Port + data.Address = req.Address + data.Protocol = req.Protocol + data.Enabled = req.Enabled + err = l.svcCtx.NodeModel.UpdateNode(l.ctx, data) if err != nil { l.Errorw("[UpdateNode] Update Database Error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create server error: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "[UpdateNode] Update Database Error") } - - // Marshal the task payload - payload, err := json.Marshal(queue.GetNodeCountry{ - Protocol: nodeInfo.Protocol, - ServerAddr: nodeInfo.ServerAddr, + return l.svcCtx.NodeModel.ClearNodeCache(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: []int64{data.ServerId}, + Search: "", }) - if err != nil { - l.Errorw("[GetNodeCountry]: Marshal Error", logger.Field("error", err.Error())) - return errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to marshal task payload") - } - // Create a queue task - task := asynq.NewTask(queue.ForthwithGetCountry, payload) - // Enqueue the task - taskInfo, err := l.svcCtx.Queue.Enqueue(task) - if err != nil { - l.Errorw("[GetNodeCountry]: Enqueue Error", logger.Field("error", err.Error()), logger.Field("payload", string(payload))) - return errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to enqueue task") - } - l.Infow("[GetNodeCountry]: Enqueue Success", logger.Field("taskID", taskInfo.ID), logger.Field("payload", string(payload))) - l.svcCtx.DeviceManager.Broadcast(device.SubscribeUpdate) - return nil } diff --git a/internal/logic/admin/server/updateRuleGroupLogic.go b/internal/logic/admin/server/updateRuleGroupLogic.go deleted file mode 100644 index 33193e1..0000000 --- a/internal/logic/admin/server/updateRuleGroupLogic.go +++ /dev/null @@ -1,50 +0,0 @@ -package server - -import ( - "context" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/tool" - - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type UpdateRuleGroupLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// NewUpdateRuleGroupLogic Update rule group -func NewUpdateRuleGroupLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateRuleGroupLogic { - return &UpdateRuleGroupLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpdateRuleGroupLogic) UpdateRuleGroup(req *types.UpdateRuleGroupRequest) error { - rs, err := parseAndValidateRules(req.Rules, req.Name) - if err != nil { - return err - } - err = l.svcCtx.ServerModel.UpdateRuleGroup(l.ctx, &server.RuleGroup{ - Id: req.Id, - Icon: req.Icon, - Name: req.Name, - Tags: tool.StringSliceToString(req.Tags), - Rules: strings.Join(rs, "\n"), - Enable: req.Enable, - }) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), err.Error()) - } - return nil -} diff --git a/internal/logic/admin/server/updateServerLogic.go b/internal/logic/admin/server/updateServerLogic.go new file mode 100644 index 0000000..f419130 --- /dev/null +++ b/internal/logic/admin/server/updateServerLogic.go @@ -0,0 +1,119 @@ +package server + +import ( + "context" + "strings" + + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/ip" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type UpdateServerLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewUpdateServerLogic Update Server +func NewUpdateServerLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateServerLogic { + return &UpdateServerLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateServerLogic) UpdateServer(req *types.UpdateServerRequest) error { + data, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.Id) + if err != nil { + l.Errorf("[UpdateServer] FindOneServer Error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server error: %v", err.Error()) + } + data.Name = req.Name + data.Country = req.Country + data.City = req.City + // only update address when it's different + if req.Address != data.Address { + // query server ip location + result, err := ip.GetRegionByIp(req.Address) + if err != nil { + l.Errorf("[UpdateServer] GetRegionByIp Error: %v", err.Error()) + } else { + data.City = result.City + data.Country = result.Country + } + // update address + data.Address = req.Address + } + protocols := make([]node.Protocol, 0) + for _, item := range req.Protocols { + if item.Type == "" { + return errors.Wrapf(xerr.NewErrCodeMsg(xerr.InvalidParams, "protocols type is empty"), "protocols type is empty") + } + var protocol node.Protocol + tool.DeepCopy(&protocol, item) + + // VLESS Reality Key Generation + if protocol.Type == "vless" { + if protocol.Security == "reality" { + if protocol.RealityPublicKey == "" { + public, private, err := tool.Curve25519Genkey(false, "") + if err != nil { + l.Errorf("[CreateServer] Generate Reality Key Error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "generate reality key error: %v", err) + } + protocol.RealityPublicKey = public + protocol.RealityPrivateKey = private + protocol.RealityShortId = tool.GenerateShortID(private) + } + if protocol.RealityServerAddr == "" { + protocol.RealityServerAddr = protocol.SNI + } + if protocol.RealityServerPort == 0 { + protocol.RealityServerPort = 443 + } + } + + } + // ShadowSocks 2022 Key Generation + if protocol.Type == "shadowsocks" { + if strings.Contains(protocol.Cipher, "2022") { + var length int + switch protocol.Cipher { + case "2022-blake3-aes-128-gcm": + length = 16 + default: + length = 32 + } + if len(protocol.ServerKey) != length { + protocol.ServerKey = tool.GenerateCipher(protocol.ServerKey, length) + } + } + } + protocols = append(protocols, protocol) + } + err = data.MarshalProtocols(protocols) + if err != nil { + l.Errorf("[UpdateServer] Marshal Protocols Error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCodeMsg(xerr.InvalidParams, "protocols marshal error"), "protocols marshal error: %v", err) + } + + err = l.svcCtx.NodeModel.UpdateServer(l.ctx, data) + if err != nil { + l.Errorf("[UpdateServer] UpdateServer Error: %v", err.Error()) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update server error: %v", err.Error()) + } + + return l.svcCtx.NodeModel.ClearNodeCache(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: []int64{req.Id}, + Search: "", + }) +} diff --git a/internal/logic/admin/subscribe/batchDeleteSubscribeGroupLogic.go b/internal/logic/admin/subscribe/batchDeleteSubscribeGroupLogic.go index 7bc9189..0dbe21c 100644 --- a/internal/logic/admin/subscribe/batchDeleteSubscribeGroupLogic.go +++ b/internal/logic/admin/subscribe/batchDeleteSubscribeGroupLogic.go @@ -3,11 +3,11 @@ package subscribe import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/subscribe/batchDeleteSubscribeLogic.go b/internal/logic/admin/subscribe/batchDeleteSubscribeLogic.go index eaee569..c5ddba4 100644 --- a/internal/logic/admin/subscribe/batchDeleteSubscribeLogic.go +++ b/internal/logic/admin/subscribe/batchDeleteSubscribeLogic.go @@ -3,14 +3,14 @@ package subscribe import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type BatchDeleteSubscribeLogic struct { diff --git a/internal/logic/admin/subscribe/createSubscribeGroupLogic.go b/internal/logic/admin/subscribe/createSubscribeGroupLogic.go index e21d2ea..dd6d7a9 100644 --- a/internal/logic/admin/subscribe/createSubscribeGroupLogic.go +++ b/internal/logic/admin/subscribe/createSubscribeGroupLogic.go @@ -3,11 +3,11 @@ package subscribe import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/subscribe/createSubscribeLogic.go b/internal/logic/admin/subscribe/createSubscribeLogic.go index 1c8c912..bf50d6a 100644 --- a/internal/logic/admin/subscribe/createSubscribeLogic.go +++ b/internal/logic/admin/subscribe/createSubscribeLogic.go @@ -4,12 +4,12 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/internal/model/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -37,6 +37,7 @@ func (l *CreateSubscribeLogic) CreateSubscribe(req *types.CreateSubscribeRequest sub := &subscribe.Subscribe{ Id: 0, Name: req.Name, + Language: req.Language, Description: req.Description, UnitPrice: req.UnitPrice, UnitTime: req.UnitTime, @@ -47,9 +48,8 @@ func (l *CreateSubscribeLogic) CreateSubscribe(req *types.CreateSubscribeRequest SpeedLimit: req.SpeedLimit, DeviceLimit: req.DeviceLimit, Quota: req.Quota, - GroupId: req.GroupId, - ServerGroup: tool.Int64SliceToString(req.ServerGroup), - Server: tool.Int64SliceToString(req.Server), + Nodes: tool.Int64SliceToString(req.Nodes), + NodeTags: tool.StringSliceToString(req.NodeTags), Show: req.Show, Sell: req.Sell, Sort: 0, diff --git a/internal/logic/admin/subscribe/deleteSubscribeGroupLogic.go b/internal/logic/admin/subscribe/deleteSubscribeGroupLogic.go index 655febc..72c3a08 100644 --- a/internal/logic/admin/subscribe/deleteSubscribeGroupLogic.go +++ b/internal/logic/admin/subscribe/deleteSubscribeGroupLogic.go @@ -3,11 +3,11 @@ package subscribe import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/subscribe/deleteSubscribeLogic.go b/internal/logic/admin/subscribe/deleteSubscribeLogic.go index 44d22fc..6ba4434 100644 --- a/internal/logic/admin/subscribe/deleteSubscribeLogic.go +++ b/internal/logic/admin/subscribe/deleteSubscribeLogic.go @@ -3,13 +3,13 @@ package subscribe import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/server/internal/model/user" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go b/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go index 26f4be4..6defdf1 100644 --- a/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go +++ b/internal/logic/admin/subscribe/getSubscribeDetailsLogic.go @@ -3,12 +3,13 @@ package subscribe import ( "context" "encoding/json" + "strings" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -41,7 +42,7 @@ func (l *GetSubscribeDetailsLogic) GetSubscribeDetails(req *types.GetSubscribeDe l.Logger.Error("[GetSubscribeDetailsLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", sub.Discount)) } } - resp.Server = tool.StringToInt64Slice(sub.Server) - resp.ServerGroup = tool.StringToInt64Slice(sub.ServerGroup) + resp.Nodes = tool.StringToInt64Slice(sub.Nodes) + resp.NodeTags = strings.Split(sub.NodeTags, ",") return resp, nil } diff --git a/internal/logic/admin/subscribe/getSubscribeGroupListLogic.go b/internal/logic/admin/subscribe/getSubscribeGroupListLogic.go index 81c65b7..7df5b55 100644 --- a/internal/logic/admin/subscribe/getSubscribeGroupListLogic.go +++ b/internal/logic/admin/subscribe/getSubscribeGroupListLogic.go @@ -3,12 +3,12 @@ package subscribe import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/subscribe/getSubscribeListLogic.go b/internal/logic/admin/subscribe/getSubscribeListLogic.go index 20d5455..e8c7866 100644 --- a/internal/logic/admin/subscribe/getSubscribeListLogic.go +++ b/internal/logic/admin/subscribe/getSubscribeListLogic.go @@ -3,12 +3,14 @@ package subscribe import ( "context" "encoding/json" + "strings" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -28,7 +30,12 @@ func NewGetSubscribeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) * } func (l *GetSubscribeListLogic) GetSubscribeList(req *types.GetSubscribeListRequest) (resp *types.GetSubscribeListResponse, err error) { - total, list, err := l.svcCtx.SubscribeModel.QuerySubscribeListByPage(l.ctx, int(req.Page), int(req.Size), req.GroupId, req.Search) + total, list, err := l.svcCtx.SubscribeModel.FilterList(l.ctx, &subscribe.FilterParams{ + Page: int(req.Page), + Size: int(req.Size), + Language: req.Language, + Search: req.Search, + }) if err != nil { l.Logger.Error("[GetSubscribeListLogic] get subscribe list failed: ", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe list failed: %v", err.Error()) @@ -47,8 +54,8 @@ func (l *GetSubscribeListLogic) GetSubscribeList(req *types.GetSubscribeListRequ l.Logger.Error("[GetSubscribeListLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", item.Discount)) } } - sub.Server = tool.StringToInt64Slice(item.Server) - sub.ServerGroup = tool.StringToInt64Slice(item.ServerGroup) + sub.Nodes = tool.StringToInt64Slice(item.Nodes) + sub.NodeTags = strings.Split(item.NodeTags, ",") resultList = append(resultList, sub) } @@ -59,8 +66,8 @@ func (l *GetSubscribeListLogic) GetSubscribeList(req *types.GetSubscribeListRequ } for i, item := range resultList { - if subscribe, ok := subscribeMaps[item.Id]; ok { - resultList[i].Sold = subscribe + if sub, ok := subscribeMaps[item.Id]; ok { + resultList[i].Sold = sub } } diff --git a/internal/logic/admin/subscribe/subscribeSortLogic.go b/internal/logic/admin/subscribe/subscribeSortLogic.go index f4d7b08..9a5cb5b 100644 --- a/internal/logic/admin/subscribe/subscribeSortLogic.go +++ b/internal/logic/admin/subscribe/subscribeSortLogic.go @@ -3,13 +3,14 @@ package subscribe import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type SubscribeSortLogic struct { @@ -40,7 +41,11 @@ func (l *SubscribeSortLogic) SubscribeSort(req *types.SubscribeSortRequest) erro l.Logger.Error("[SubscribeSortLogic] query subscribe list by ids error: ", logger.Field("error", err.Error())) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query subscribe list by ids error: %v", err.Error()) } - subs, err := l.svcCtx.SubscribeModel.QuerySubscribeListByIds(l.ctx, ids) + _, subs, err := l.svcCtx.SubscribeModel.FilterList(l.ctx, &subscribe.FilterParams{ + Page: 1, + Size: 9999, + Ids: ids, + }) if err != nil { l.Logger.Error("[SubscribeSortLogic] query subscribe list by ids error: ", logger.Field("error", err.Error())) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query subscribe list by ids error: %v", err.Error()) diff --git a/internal/logic/admin/subscribe/updateSubscribeGroupLogic.go b/internal/logic/admin/subscribe/updateSubscribeGroupLogic.go index 5eb4119..2f8871b 100644 --- a/internal/logic/admin/subscribe/updateSubscribeGroupLogic.go +++ b/internal/logic/admin/subscribe/updateSubscribeGroupLogic.go @@ -3,11 +3,11 @@ package subscribe import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/subscribe/updateSubscribeLogic.go b/internal/logic/admin/subscribe/updateSubscribeLogic.go index e61df40..060af5a 100644 --- a/internal/logic/admin/subscribe/updateSubscribeLogic.go +++ b/internal/logic/admin/subscribe/updateSubscribeLogic.go @@ -4,14 +4,14 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/pkg/device" + "github.com/perfect-panel/server/pkg/device" - "github.com/perfect-panel/ppanel-server/internal/model/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -45,6 +45,7 @@ func (l *UpdateSubscribeLogic) UpdateSubscribe(req *types.UpdateSubscribeRequest sub := &subscribe.Subscribe{ Id: req.Id, Name: req.Name, + Language: req.Language, Description: req.Description, UnitPrice: req.UnitPrice, UnitTime: req.UnitTime, @@ -55,9 +56,8 @@ func (l *UpdateSubscribeLogic) UpdateSubscribe(req *types.UpdateSubscribeRequest SpeedLimit: req.SpeedLimit, DeviceLimit: req.DeviceLimit, Quota: req.Quota, - GroupId: req.GroupId, - ServerGroup: tool.Int64SliceToString(req.ServerGroup), - Server: tool.Int64SliceToString(req.Server), + Nodes: tool.Int64SliceToString(req.Nodes), + NodeTags: tool.StringSliceToString(req.NodeTags), Show: req.Show, Sell: req.Sell, Sort: req.Sort, diff --git a/internal/logic/admin/system/createApplicationLogic.go b/internal/logic/admin/system/createApplicationLogic.go deleted file mode 100644 index 89aeecb..0000000 --- a/internal/logic/admin/system/createApplicationLogic.go +++ /dev/null @@ -1,125 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/model/application" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type CreateApplicationLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewCreateApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateApplicationLogic { - return &CreateApplicationLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CreateApplicationLogic) CreateApplication(req *types.CreateApplicationRequest) error { - var ios []application.ApplicationVersion - if len(req.Platform.IOS) > 0 { - for _, ios_ := range req.Platform.IOS { - ios = append(ios, application.ApplicationVersion{ - Url: ios_.Url, - Version: ios_.Version, - Platform: "ios", - IsDefault: ios_.IsDefault, - Description: ios_.Description, - }) - } - } - - var mac []application.ApplicationVersion - if len(req.Platform.MacOS) > 0 { - for _, mac_ := range req.Platform.MacOS { - mac = append(mac, application.ApplicationVersion{ - Url: mac_.Url, - Version: mac_.Version, - Platform: "macos", - IsDefault: mac_.IsDefault, - Description: mac_.Description, - }) - } - } - - var linux []application.ApplicationVersion - if len(req.Platform.Linux) > 0 { - for _, linux_ := range req.Platform.Linux { - linux = append(linux, application.ApplicationVersion{ - Url: linux_.Url, - Version: linux_.Version, - Platform: "linux", - IsDefault: linux_.IsDefault, - Description: linux_.Description, - }) - } - } - - var android []application.ApplicationVersion - if len(req.Platform.Android) > 0 { - for _, android_ := range req.Platform.Android { - android = append(android, application.ApplicationVersion{ - Url: android_.Url, - Version: android_.Version, - Platform: "android", - IsDefault: android_.IsDefault, - Description: android_.Description, - }) - } - } - - var windows []application.ApplicationVersion - if len(req.Platform.Windows) > 0 { - for _, windows_ := range req.Platform.Windows { - windows = append(windows, application.ApplicationVersion{ - Url: windows_.Url, - Version: windows_.Version, - Platform: "windows", - IsDefault: windows_.IsDefault, - Description: windows_.Description, - }) - } - } - - var harmony []application.ApplicationVersion - if len(req.Platform.Harmony) > 0 { - for _, harmony_ := range req.Platform.Harmony { - harmony = append(harmony, application.ApplicationVersion{ - Url: harmony_.Url, - Version: harmony_.Version, - Platform: "harmony", - IsDefault: harmony_.IsDefault, - Description: harmony_.Description, - }) - } - } - var applicationVersions []application.ApplicationVersion - applicationVersions = append(applicationVersions, ios...) - applicationVersions = append(applicationVersions, mac...) - applicationVersions = append(applicationVersions, linux...) - applicationVersions = append(applicationVersions, android...) - applicationVersions = append(applicationVersions, windows...) - applicationVersions = append(applicationVersions, harmony...) - app := application.Application{ - Name: req.Name, - Icon: req.Icon, - SubscribeType: req.SubscribeType, - ApplicationVersions: applicationVersions, - } - err := l.svcCtx.ApplicationModel.Insert(l.ctx, &app) - if err != nil { - l.Errorw("[CreateApplicationLogic] create application error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create application error: %v", err) - } - return nil -} diff --git a/internal/logic/admin/system/createApplicationVersionLogic.go b/internal/logic/admin/system/createApplicationVersionLogic.go deleted file mode 100644 index 7ee2033..0000000 --- a/internal/logic/admin/system/createApplicationVersionLogic.go +++ /dev/null @@ -1,44 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/model/application" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type CreateApplicationVersionLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Create application version -func NewCreateApplicationVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateApplicationVersionLogic { - return &CreateApplicationVersionLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CreateApplicationVersionLogic) CreateApplicationVersion(req *types.CreateApplicationVersionRequest) error { - create := &application.ApplicationVersion{ - Url: req.Url, - Platform: req.Platform, - Version: req.Version, - Description: req.Description, - IsDefault: req.IsDefault, - ApplicationId: req.ApplicationId, - } - err := l.svcCtx.ApplicationModel.InsertVersion(l.ctx, create) - if err != nil { - l.Errorw("[CreateApplicationVersion] create application version error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create application version error: %v", err) - } - return nil -} diff --git a/internal/logic/admin/system/deleteApplicationLogic.go b/internal/logic/admin/system/deleteApplicationLogic.go deleted file mode 100644 index 4e7e489..0000000 --- a/internal/logic/admin/system/deleteApplicationLogic.go +++ /dev/null @@ -1,35 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type DeleteApplicationLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewDeleteApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteApplicationLogic { - return &DeleteApplicationLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *DeleteApplicationLogic) DeleteApplication(req *types.DeleteApplicationRequest) error { - // delete application - err := l.svcCtx.ApplicationModel.Delete(l.ctx, req.Id) - if err != nil { - l.Errorw("[DeleteApplicationLogic] delete application error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete application error: %v", err.Error()) - } - return nil -} diff --git a/internal/logic/admin/system/deleteApplicationVersionLogic.go b/internal/logic/admin/system/deleteApplicationVersionLogic.go deleted file mode 100644 index f2235e5..0000000 --- a/internal/logic/admin/system/deleteApplicationVersionLogic.go +++ /dev/null @@ -1,36 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type DeleteApplicationVersionLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Delete application -func NewDeleteApplicationVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteApplicationVersionLogic { - return &DeleteApplicationVersionLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *DeleteApplicationVersionLogic) DeleteApplicationVersion(req *types.DeleteApplicationVersionRequest) error { - // delete application - err := l.svcCtx.ApplicationModel.DeleteVersion(l.ctx, req.Id) - if err != nil { - l.Errorw("[DeleteApplicationVersion] delete application version error: ", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete application version error: %v", err.Error()) - } - return nil -} diff --git a/internal/logic/admin/system/getApplicationConfigLogic.go b/internal/logic/admin/system/getApplicationConfigLogic.go deleted file mode 100644 index 39a853f..0000000 --- a/internal/logic/admin/system/getApplicationConfigLogic.go +++ /dev/null @@ -1,49 +0,0 @@ -package system - -import ( - "context" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type GetApplicationConfigLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// get application config -func NewGetApplicationConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetApplicationConfigLogic { - return &GetApplicationConfigLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetApplicationConfigLogic) GetApplicationConfig() (resp *types.ApplicationConfig, err error) { - resp = &types.ApplicationConfig{} - appConfig, err := l.svcCtx.ApplicationModel.FindOneConfig(l.ctx, 1) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - err = nil - return - } - l.Errorw("[GetApplicationConfig] Database Error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get app config error: %v", err.Error()) - } - resp.AppId = appConfig.AppId - resp.EncryptionKey = appConfig.EncryptionKey - resp.EncryptionMethod = appConfig.EncryptionMethod - resp.Domains = strings.Split(appConfig.Domains, ";") - resp.StartupPicture = appConfig.StartupPicture - resp.StartupPictureSkipTime = appConfig.StartupPictureSkipTime - return -} diff --git a/internal/logic/admin/system/getApplicationLogic.go b/internal/logic/admin/system/getApplicationLogic.go deleted file mode 100644 index 7c94d5c..0000000 --- a/internal/logic/admin/system/getApplicationLogic.go +++ /dev/null @@ -1,113 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/model/application" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type GetApplicationLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get application -func NewGetApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetApplicationLogic { - return &GetApplicationLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetApplicationLogic) GetApplication() (resp *types.ApplicationResponse, err error) { - resp = &types.ApplicationResponse{} - var applications []*application.Application - err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { - return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error - }) - if err != nil { - l.Errorw("[GetApplicationLogic] get application error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) - } - - if len(applications) == 0 { - return resp, nil - } - - for _, app := range applications { - applicationResponse := types.ApplicationResponseInfo{ - Id: app.Id, - Name: app.Name, - Icon: app.Icon, - Description: app.Description, - SubscribeType: app.SubscribeType, - } - applicationVersions := app.ApplicationVersions - if len(applicationVersions) != 0 { - for _, applicationVersion := range applicationVersions { - switch applicationVersion.Platform { - case "ios": - applicationResponse.Platform.IOS = append(applicationResponse.Platform.IOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "macos": - applicationResponse.Platform.MacOS = append(applicationResponse.Platform.MacOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "linux": - applicationResponse.Platform.Linux = append(applicationResponse.Platform.Linux, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "android": - applicationResponse.Platform.Android = append(applicationResponse.Platform.Android, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "windows": - applicationResponse.Platform.Windows = append(applicationResponse.Platform.Windows, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "harmony": - applicationResponse.Platform.Harmony = append(applicationResponse.Platform.Harmony, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - } - } - } - resp.Applications = append(resp.Applications, applicationResponse) - } - - return -} diff --git a/internal/logic/admin/system/getCurrencyConfigLogic.go b/internal/logic/admin/system/getCurrencyConfigLogic.go index 42286b3..32239a0 100644 --- a/internal/logic/admin/system/getCurrencyConfigLogic.go +++ b/internal/logic/admin/system/getCurrencyConfigLogic.go @@ -3,11 +3,11 @@ package system import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/system/getInviteConfigLogic.go b/internal/logic/admin/system/getInviteConfigLogic.go index b12ac8f..e5786e8 100644 --- a/internal/logic/admin/system/getInviteConfigLogic.go +++ b/internal/logic/admin/system/getInviteConfigLogic.go @@ -3,11 +3,11 @@ package system import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/system/getNodeConfigLogic.go b/internal/logic/admin/system/getNodeConfigLogic.go index caa647a..1212b1a 100644 --- a/internal/logic/admin/system/getNodeConfigLogic.go +++ b/internal/logic/admin/system/getNodeConfigLogic.go @@ -2,12 +2,13 @@ package system import ( "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "encoding/json" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -26,15 +27,45 @@ func NewGetNodeConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get } func (l *GetNodeConfigLogic) GetNodeConfig() (*types.NodeConfig, error) { - resp := &types.NodeConfig{} - // get server config from db configs, err := l.svcCtx.SystemModel.GetNodeConfig(l.ctx) if err != nil { l.Errorw("[GetNodeConfigLogic] GetNodeConfig get server config error: ", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetNodeConfig get server config error: %v", err.Error()) } - // reflect to response - tool.SystemConfigSliceReflectToStruct(configs, resp) - return resp, nil + var dbConfig config.NodeDBConfig + tool.SystemConfigSliceReflectToStruct(configs, &dbConfig) + c := &types.NodeConfig{ + NodeSecret: dbConfig.NodeSecret, + NodePullInterval: dbConfig.NodePullInterval, + NodePushInterval: dbConfig.NodePushInterval, + IPStrategy: dbConfig.IPStrategy, + TrafficReportThreshold: dbConfig.TrafficReportThreshold, + } + + if dbConfig.DNS != "" { + var dns []types.NodeDNS + err = json.Unmarshal([]byte(dbConfig.DNS), &dns) + if err != nil { + logger.Errorf("[Node] Unmarshal DNS config error: %s", err.Error()) + panic(err) + } + c.DNS = dns + } + if dbConfig.Block != "" { + var block []string + _ = json.Unmarshal([]byte(dbConfig.Block), &block) + c.Block = tool.RemoveDuplicateElements(block...) + } + if dbConfig.Outbound != "" { + var outbound []types.NodeOutbound + err = json.Unmarshal([]byte(dbConfig.Outbound), &outbound) + if err != nil { + logger.Errorf("[Node] Unmarshal Outbound config error: %s", err.Error()) + panic(err) + } + c.Outbound = outbound + } + + return c, nil } diff --git a/internal/logic/admin/system/getNodeMultiplierLogic.go b/internal/logic/admin/system/getNodeMultiplierLogic.go index cc330e4..e580202 100644 --- a/internal/logic/admin/system/getNodeMultiplierLogic.go +++ b/internal/logic/admin/system/getNodeMultiplierLogic.go @@ -4,10 +4,10 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/system/getPrivacyPolicyConfigLogic.go b/internal/logic/admin/system/getPrivacyPolicyConfigLogic.go index 8c8d15d..b0c6844 100644 --- a/internal/logic/admin/system/getPrivacyPolicyConfigLogic.go +++ b/internal/logic/admin/system/getPrivacyPolicyConfigLogic.go @@ -3,11 +3,11 @@ package system import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/system/getRegisterConfigLogic.go b/internal/logic/admin/system/getRegisterConfigLogic.go index 7fcdf3a..7ba002f 100644 --- a/internal/logic/admin/system/getRegisterConfigLogic.go +++ b/internal/logic/admin/system/getRegisterConfigLogic.go @@ -3,11 +3,11 @@ package system import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/system/getSiteConfigLogic.go b/internal/logic/admin/system/getSiteConfigLogic.go index ec7890d..7787ecc 100644 --- a/internal/logic/admin/system/getSiteConfigLogic.go +++ b/internal/logic/admin/system/getSiteConfigLogic.go @@ -3,11 +3,11 @@ package system import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/system/getSubscribeConfigLogic.go b/internal/logic/admin/system/getSubscribeConfigLogic.go index 4e36c81..71ecd6f 100644 --- a/internal/logic/admin/system/getSubscribeConfigLogic.go +++ b/internal/logic/admin/system/getSubscribeConfigLogic.go @@ -3,11 +3,11 @@ package system import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/system/getSubscribeTypeLogic.go b/internal/logic/admin/system/getSubscribeTypeLogic.go deleted file mode 100644 index 41ab1f9..0000000 --- a/internal/logic/admin/system/getSubscribeTypeLogic.go +++ /dev/null @@ -1,42 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/model/subscribeType" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetSubscribeTypeLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logger.Logger -} - -func NewGetSubscribeTypeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscribeTypeLogic { - return &GetSubscribeTypeLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logger.WithContext(ctx), - } -} - -func (l *GetSubscribeTypeLogic) GetSubscribeType() (resp *types.SubscribeType, err error) { - var list []*subscribeType.SubscribeType - err = l.svcCtx.DB.Model(&subscribeType.SubscribeType{}).Find(&list).Error - if err != nil { - l.Errorw("[GetSubscribeType] get subscribe type failed", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe type failed: %v", err) - } - typeList := make([]string, 0) - for _, item := range list { - typeList = append(typeList, item.Name) - } - return &types.SubscribeType{ - SubscribeTypes: typeList, - }, nil -} diff --git a/internal/logic/admin/system/getTosConfigLogic.go b/internal/logic/admin/system/getTosConfigLogic.go index be2e9db..80a70fb 100644 --- a/internal/logic/admin/system/getTosConfigLogic.go +++ b/internal/logic/admin/system/getTosConfigLogic.go @@ -3,11 +3,11 @@ package system import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/system/getVerifyCodeConfigLogic.go b/internal/logic/admin/system/getVerifyCodeConfigLogic.go index 52db722..13c931d 100644 --- a/internal/logic/admin/system/getVerifyCodeConfigLogic.go +++ b/internal/logic/admin/system/getVerifyCodeConfigLogic.go @@ -3,11 +3,11 @@ package system import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/system/getVerifyConfigLogic.go b/internal/logic/admin/system/getVerifyConfigLogic.go index 5568430..044ba22 100644 --- a/internal/logic/admin/system/getVerifyConfigLogic.go +++ b/internal/logic/admin/system/getVerifyConfigLogic.go @@ -3,12 +3,12 @@ package system import ( "context" - "github.com/perfect-panel/ppanel-server/initialize" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/initialize" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/system/preViewNodeMultiplierLogic.go b/internal/logic/admin/system/preViewNodeMultiplierLogic.go new file mode 100644 index 0000000..58d1dd5 --- /dev/null +++ b/internal/logic/admin/system/preViewNodeMultiplierLogic.go @@ -0,0 +1,33 @@ +package system + +import ( + "context" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "time" +) + +type PreViewNodeMultiplierLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// PreView Node Multiplier +func NewPreViewNodeMultiplierLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreViewNodeMultiplierLogic { + return &PreViewNodeMultiplierLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PreViewNodeMultiplierLogic) PreViewNodeMultiplier() (resp *types.PreViewNodeMultiplierResponse, err error) { + now := time.Now() + ratio := l.svcCtx.NodeMultiplierManager.GetMultiplier(now) + return &types.PreViewNodeMultiplierResponse{ + Ratio: ratio, + CurrentTime: now.Format("2006-01-02 15:04:05"), + }, nil +} diff --git a/internal/logic/admin/system/setNodeMultiplierLogic.go b/internal/logic/admin/system/setNodeMultiplierLogic.go index 4f4b6ae..78edf50 100644 --- a/internal/logic/admin/system/setNodeMultiplierLogic.go +++ b/internal/logic/admin/system/setNodeMultiplierLogic.go @@ -4,10 +4,10 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/system/settingTelegramBotLogic.go b/internal/logic/admin/system/settingTelegramBotLogic.go index c919743..2986860 100644 --- a/internal/logic/admin/system/settingTelegramBotLogic.go +++ b/internal/logic/admin/system/settingTelegramBotLogic.go @@ -3,10 +3,10 @@ package system import ( "context" - "github.com/perfect-panel/ppanel-server/initialize" + "github.com/perfect-panel/server/initialize" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" ) type SettingTelegramBotLogic struct { diff --git a/internal/logic/admin/system/updateApplicationConfigLogic.go b/internal/logic/admin/system/updateApplicationConfigLogic.go deleted file mode 100644 index 2b5c936..0000000 --- a/internal/logic/admin/system/updateApplicationConfigLogic.go +++ /dev/null @@ -1,45 +0,0 @@ -package system - -import ( - "context" - "strings" - - "github.com/perfect-panel/ppanel-server/internal/model/application" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type UpdateApplicationConfigLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// update application config -func NewUpdateApplicationConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateApplicationConfigLogic { - return &UpdateApplicationConfigLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpdateApplicationConfigLogic) UpdateApplicationConfig(req *types.ApplicationConfig) error { - err := l.svcCtx.ApplicationModel.UpdateConfig(l.ctx, &application.ApplicationConfig{ - Id: 1, - AppId: req.AppId, - EncryptionKey: req.EncryptionKey, - EncryptionMethod: req.EncryptionMethod, - Domains: strings.Join(req.Domains, ";"), - StartupPicture: req.StartupPicture, - StartupPictureSkipTime: req.StartupPictureSkipTime, - }) - if err != nil { - l.Errorw("[UpdateApplicationConfig] Database Error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update app config error: %v", err.Error()) - } - return nil -} diff --git a/internal/logic/admin/system/updateApplicationLogic.go b/internal/logic/admin/system/updateApplicationLogic.go deleted file mode 100644 index dd12e22..0000000 --- a/internal/logic/admin/system/updateApplicationLogic.go +++ /dev/null @@ -1,149 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/model/application" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" -) - -type UpdateApplicationLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewUpdateApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateApplicationLogic { - return &UpdateApplicationLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpdateApplicationLogic) UpdateApplication(req *types.UpdateApplicationRequest) error { - - // find application - app, err := l.svcCtx.ApplicationModel.FindOne(l.ctx, req.Id) - if err != nil { - l.Errorw("[UpdateApplication] find application error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find application error: %v", err.Error()) - } - app.Name = req.Name - app.Icon = req.Icon - app.SubscribeType = req.SubscribeType - app.Description = req.Description - - var ios []application.ApplicationVersion - if len(req.Platform.IOS) > 0 { - for _, ios_ := range req.Platform.IOS { - ios = append(ios, application.ApplicationVersion{ - Url: ios_.Url, - Version: ios_.Version, - Platform: "ios", - IsDefault: ios_.IsDefault, - Description: ios_.Description, - ApplicationId: app.Id, - }) - } - } - - var mac []application.ApplicationVersion - if len(req.Platform.MacOS) > 0 { - for _, mac_ := range req.Platform.MacOS { - mac = append(mac, application.ApplicationVersion{ - Url: mac_.Url, - Version: mac_.Version, - Platform: "macos", - IsDefault: mac_.IsDefault, - Description: mac_.Description, - ApplicationId: app.Id, - }) - } - } - - var linux []application.ApplicationVersion - if len(req.Platform.Linux) > 0 { - for _, linux_ := range req.Platform.Linux { - linux = append(linux, application.ApplicationVersion{ - Url: linux_.Url, - Version: linux_.Version, - Platform: "linux", - IsDefault: linux_.IsDefault, - Description: linux_.Description, - ApplicationId: app.Id, - }) - } - } - - var android []application.ApplicationVersion - if len(req.Platform.Android) > 0 { - for _, android_ := range req.Platform.Android { - android = append(android, application.ApplicationVersion{ - Url: android_.Url, - Version: android_.Version, - Platform: "android", - IsDefault: android_.IsDefault, - Description: android_.Description, - ApplicationId: app.Id, - }) - } - } - - var windows []application.ApplicationVersion - if len(req.Platform.Windows) > 0 { - for _, windows_ := range req.Platform.Windows { - windows = append(windows, application.ApplicationVersion{ - Url: windows_.Url, - Version: windows_.Version, - Platform: "windows", - IsDefault: windows_.IsDefault, - Description: windows_.Description, - ApplicationId: app.Id, - }) - } - } - - var harmony []application.ApplicationVersion - if len(req.Platform.Harmony) > 0 { - for _, harmony_ := range req.Platform.Harmony { - harmony = append(harmony, application.ApplicationVersion{ - Url: harmony_.Url, - Version: harmony_.Version, - Platform: "harmony", - IsDefault: harmony_.IsDefault, - Description: harmony_.Description, - ApplicationId: app.Id, - }) - } - } - var applicationVersions []application.ApplicationVersion - applicationVersions = append(applicationVersions, ios...) - applicationVersions = append(applicationVersions, mac...) - applicationVersions = append(applicationVersions, linux...) - applicationVersions = append(applicationVersions, android...) - applicationVersions = append(applicationVersions, windows...) - applicationVersions = append(applicationVersions, harmony...) - app.ApplicationVersions = applicationVersions - err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(db *gorm.DB) error { - - if err = db.Where("application_id = ?", app.Id).Delete(&application.ApplicationVersion{}).Error; err != nil { - return err - } - if err = db.Create(&applicationVersions).Error; err != nil { - return err - } - return db.Save(app).Error - }) - if err != nil { - l.Errorw("[UpdateApplication] update application error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update application error: %v", err.Error()) - } - return nil -} diff --git a/internal/logic/admin/system/updateApplicationVersionLogic.go b/internal/logic/admin/system/updateApplicationVersionLogic.go deleted file mode 100644 index ce33db0..0000000 --- a/internal/logic/admin/system/updateApplicationVersionLogic.go +++ /dev/null @@ -1,45 +0,0 @@ -package system - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type UpdateApplicationVersionLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Update application version -func NewUpdateApplicationVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateApplicationVersionLogic { - return &UpdateApplicationVersionLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpdateApplicationVersionLogic) UpdateApplicationVersion(req *types.UpdateApplicationVersionRequest) error { - // find application - app, err := l.svcCtx.ApplicationModel.FindOneVersion(l.ctx, req.Id) - if err != nil { - l.Errorw("[UpdateApplicationVersion] find application version error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find application error: %v", err.Error()) - } - app.Url = req.Url - app.Version = req.Version - app.Description = req.Description - app.IsDefault = req.IsDefault - err = l.svcCtx.ApplicationModel.UpdateVersion(l.ctx, app) - if err != nil { - l.Errorw("[UpdateApplicationVersion] update application version error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update application version error: %v", err.Error()) - } - return nil -} diff --git a/internal/logic/admin/system/updateCurrencyConfigLogic.go b/internal/logic/admin/system/updateCurrencyConfigLogic.go index 8bd2326..0104331 100644 --- a/internal/logic/admin/system/updateCurrencyConfigLogic.go +++ b/internal/logic/admin/system/updateCurrencyConfigLogic.go @@ -4,16 +4,16 @@ import ( "context" "reflect" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type UpdateCurrencyConfigLogic struct { diff --git a/internal/logic/admin/system/updateInviteConfigLogic.go b/internal/logic/admin/system/updateInviteConfigLogic.go index 8ebc555..8e2201f 100644 --- a/internal/logic/admin/system/updateInviteConfigLogic.go +++ b/internal/logic/admin/system/updateInviteConfigLogic.go @@ -4,18 +4,18 @@ import ( "context" "reflect" - "github.com/perfect-panel/ppanel-server/initialize" + "github.com/perfect-panel/server/initialize" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" ) type UpdateInviteConfigLogic struct { diff --git a/internal/logic/admin/system/updateNodeConfigLogic.go b/internal/logic/admin/system/updateNodeConfigLogic.go index c7523de..dbbba84 100644 --- a/internal/logic/admin/system/updateNodeConfigLogic.go +++ b/internal/logic/admin/system/updateNodeConfigLogic.go @@ -4,17 +4,17 @@ import ( "context" "reflect" - "github.com/perfect-panel/ppanel-server/initialize" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/initialize" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" ) type UpdateNodeConfigLogic struct { @@ -41,7 +41,9 @@ func (l *UpdateNodeConfigLogic) UpdateNodeConfig(req *types.NodeConfig) error { // Get the field name fieldName := t.Field(i).Name // Get the field value to string - fieldValue := tool.ConvertValueToString(v.Field(i)) + var fieldValue string + + fieldValue = tool.ConvertValueToString(v.Field(i)) // Update the server config err = db.Model(&system.System{}).Where("`category` = 'server' and `key` = ?", fieldName).Update("value", fieldValue).Error if err != nil { diff --git a/internal/logic/admin/system/updatePrivacyPolicyConfigLogic.go b/internal/logic/admin/system/updatePrivacyPolicyConfigLogic.go index 49f5122..bd8cdf8 100644 --- a/internal/logic/admin/system/updatePrivacyPolicyConfigLogic.go +++ b/internal/logic/admin/system/updatePrivacyPolicyConfigLogic.go @@ -4,16 +4,16 @@ import ( "context" "reflect" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type UpdatePrivacyPolicyConfigLogic struct { diff --git a/internal/logic/admin/system/updateRegisterConfigLogic.go b/internal/logic/admin/system/updateRegisterConfigLogic.go index b990d4c..d5bf8c3 100644 --- a/internal/logic/admin/system/updateRegisterConfigLogic.go +++ b/internal/logic/admin/system/updateRegisterConfigLogic.go @@ -5,18 +5,18 @@ import ( "reflect" - "github.com/perfect-panel/ppanel-server/initialize" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/initialize" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" ) type UpdateRegisterConfigLogic struct { diff --git a/internal/logic/admin/system/updateSiteConfigLogic.go b/internal/logic/admin/system/updateSiteConfigLogic.go index 3e8a3cc..2fc1e0a 100644 --- a/internal/logic/admin/system/updateSiteConfigLogic.go +++ b/internal/logic/admin/system/updateSiteConfigLogic.go @@ -4,13 +4,13 @@ import ( "context" "reflect" - "github.com/perfect-panel/ppanel-server/initialize" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/initialize" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) diff --git a/internal/logic/admin/system/updateSubscribeConfigLogic.go b/internal/logic/admin/system/updateSubscribeConfigLogic.go index 69b1436..f4e79b4 100644 --- a/internal/logic/admin/system/updateSubscribeConfigLogic.go +++ b/internal/logic/admin/system/updateSubscribeConfigLogic.go @@ -4,14 +4,14 @@ import ( "context" "reflect" - "github.com/perfect-panel/ppanel-server/initialize" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/initialize" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) diff --git a/internal/logic/admin/system/updateTosConfigLogic.go b/internal/logic/admin/system/updateTosConfigLogic.go index 39d3b5e..94a99f2 100644 --- a/internal/logic/admin/system/updateTosConfigLogic.go +++ b/internal/logic/admin/system/updateTosConfigLogic.go @@ -4,13 +4,13 @@ import ( "context" "reflect" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) diff --git a/internal/logic/admin/system/updateVerifyCodeConfigLogic.go b/internal/logic/admin/system/updateVerifyCodeConfigLogic.go index 2838b24..e089dd6 100644 --- a/internal/logic/admin/system/updateVerifyCodeConfigLogic.go +++ b/internal/logic/admin/system/updateVerifyCodeConfigLogic.go @@ -4,17 +4,17 @@ import ( "context" "reflect" - "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type UpdateVerifyCodeConfigLogic struct { diff --git a/internal/logic/admin/system/updateVerifyConfigLogic.go b/internal/logic/admin/system/updateVerifyConfigLogic.go index 5ba7543..ddb3e55 100644 --- a/internal/logic/admin/system/updateVerifyConfigLogic.go +++ b/internal/logic/admin/system/updateVerifyConfigLogic.go @@ -4,14 +4,14 @@ import ( "context" "reflect" - "github.com/perfect-panel/ppanel-server/initialize" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/initialize" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) diff --git a/internal/logic/admin/ticket/createTicketFollowLogic.go b/internal/logic/admin/ticket/createTicketFollowLogic.go index 93b3373..c01cd27 100644 --- a/internal/logic/admin/ticket/createTicketFollowLogic.go +++ b/internal/logic/admin/ticket/createTicketFollowLogic.go @@ -3,11 +3,11 @@ package ticket import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/ticket" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/ticket" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/ticket/getTicketListLogic.go b/internal/logic/admin/ticket/getTicketListLogic.go index 0a16273..c03932a 100644 --- a/internal/logic/admin/ticket/getTicketListLogic.go +++ b/internal/logic/admin/ticket/getTicketListLogic.go @@ -3,11 +3,11 @@ package ticket import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/ticket/getTicketLogic.go b/internal/logic/admin/ticket/getTicketLogic.go index 7aaa826..87a0e2a 100644 --- a/internal/logic/admin/ticket/getTicketLogic.go +++ b/internal/logic/admin/ticket/getTicketLogic.go @@ -3,11 +3,11 @@ package ticket import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/ticket/updateTicketStatusLogic.go b/internal/logic/admin/ticket/updateTicketStatusLogic.go index c205057..7897564 100644 --- a/internal/logic/admin/ticket/updateTicketStatusLogic.go +++ b/internal/logic/admin/ticket/updateTicketStatusLogic.go @@ -3,10 +3,10 @@ package ticket import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/tool/getSystemLogLogic.go b/internal/logic/admin/tool/getSystemLogLogic.go index e2d4660..760a60e 100644 --- a/internal/logic/admin/tool/getSystemLogLogic.go +++ b/internal/logic/admin/tool/getSystemLogLogic.go @@ -2,10 +2,14 @@ package tool import ( "context" + "encoding/json" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type GetSystemLogLogic struct { @@ -14,7 +18,7 @@ type GetSystemLogLogic struct { svcCtx *svc.ServiceContext } -// Get System Log +// NewGetSystemLogLogic Get System Log func NewGetSystemLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSystemLogLogic { return &GetSystemLogLogic{ Logger: logger.WithContext(ctx), @@ -24,17 +28,22 @@ func NewGetSystemLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetS } func (l *GetSystemLogLogic) GetSystemLog() (resp *types.LogResponse, err error) { - //if l.svcCtx.Config.Debug { - // return nil, errors.Wrapf(xerr.NewErrCode(xerr.DebugModeError), "debug mode is enabled") - //} - //lines, err := logger.ReadLastNLines(l.svcCtx.Config.Logger.FilePath, 100) - //if err != nil { - // l.Errorw("[GetSystemLog]", logger.Field("error", "ReadLastNLines"), logger.Field(err)) - // return nil, err - //} - //logs := logger.ParseLog(lines) - //return &types.LogResponse{ - // List: logs, - //}, nil - return nil, nil + lines, err := logger.ReadLastNLines(l.svcCtx.Config.Logger.Path, 50) + if err != nil { + l.Error(err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get system log error: %v", err.Error()) + } + var list []map[string]interface{} + for _, line := range lines { + var log map[string]interface{} + if err = json.Unmarshal([]byte(line), &log); err != nil { + l.Error(err) + continue + } + list = append(list, log) + } + + return &types.LogResponse{ + List: list, + }, nil } diff --git a/internal/logic/admin/tool/getVersionLogic.go b/internal/logic/admin/tool/getVersionLogic.go new file mode 100644 index 0000000..a1a9acc --- /dev/null +++ b/internal/logic/admin/tool/getVersionLogic.go @@ -0,0 +1,51 @@ +package tool + +import ( + "context" + "fmt" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" +) + +type GetVersionLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewGetVersionLogic Get Version +func NewGetVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetVersionLogic { + return &GetVersionLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetVersionLogic) GetVersion() (resp *types.VersionResponse, err error) { + version := constant.Version + buildTime := constant.BuildTime + + // Normalize unknown values + if version == "unknown version" { + version = "unknown" + } + if buildTime == "unknown time" { + buildTime = "unknown" + } + + // Format version based on whether it starts with 'v' + var formattedVersion string + if len(version) > 0 && version[0] == 'v' { + formattedVersion = fmt.Sprintf("%s(%s)", version[1:], buildTime) + } else { + formattedVersion = fmt.Sprintf("%s(%s) Develop", version, buildTime) + } + + return &types.VersionResponse{ + Version: formattedVersion, + }, nil +} diff --git a/internal/logic/admin/tool/restartSystemLogic.go b/internal/logic/admin/tool/restartSystemLogic.go index 929ae70..f5b0a8e 100644 --- a/internal/logic/admin/tool/restartSystemLogic.go +++ b/internal/logic/admin/tool/restartSystemLogic.go @@ -3,8 +3,8 @@ package tool import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" ) type RestartSystemLogic struct { diff --git a/internal/logic/admin/user/batchDeleteUserLogic.go b/internal/logic/admin/user/batchDeleteUserLogic.go index c7781a1..d78376e 100644 --- a/internal/logic/admin/user/batchDeleteUserLogic.go +++ b/internal/logic/admin/user/batchDeleteUserLogic.go @@ -2,11 +2,14 @@ package user import ( "context" + "os" + "strings" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -25,6 +28,12 @@ func NewBatchDeleteUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *B } func (l *BatchDeleteUserLogic) BatchDeleteUser(req *types.BatchDeleteUserRequest) error { + isDemo := strings.ToLower(os.Getenv("PPANEL_MODE")) == "demo" + + if tool.Contains(req.Ids, 2) && isDemo { + return errors.Wrapf(xerr.NewErrCodeMsg(503, "Demo mode does not allow deletion of the admin user"), "BatchDeleteUser failed: cannot delete admin user in demo mode") + } + err := l.svcCtx.UserModel.BatchDeleteUser(l.ctx, req.Ids) if err != nil { l.Logger.Error("[BatchDeleteUserLogic] BatchDeleteUser failed: ", logger.Field("error", err.Error())) diff --git a/internal/logic/admin/user/createUserAuthMethodLogic.go b/internal/logic/admin/user/createUserAuthMethodLogic.go index a9bd1e1..d57a4f2 100644 --- a/internal/logic/admin/user/createUserAuthMethodLogic.go +++ b/internal/logic/admin/user/createUserAuthMethodLogic.go @@ -3,14 +3,14 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type CreateUserAuthMethodLogic struct { diff --git a/internal/logic/admin/user/createUserLogic.go b/internal/logic/admin/user/createUserLogic.go index 3cd97a4..5f6c858 100644 --- a/internal/logic/admin/user/createUserLogic.go +++ b/internal/logic/admin/user/createUserLogic.go @@ -5,13 +5,13 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) @@ -39,10 +39,12 @@ func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) error { } pwd := tool.EncodePassWord(req.Password) newUser := &user.User{ - Password: pwd, - ReferCode: req.ReferCode, - Balance: req.Balance, - IsAdmin: &req.IsAdmin, + Password: pwd, + ReferralPercentage: req.ReferralPercentage, + OnlyFirstPurchase: &req.OnlyFirstPurchase, + ReferCode: req.ReferCode, + Balance: req.Balance, + IsAdmin: &req.IsAdmin, } var ams []user.AuthMethods diff --git a/internal/logic/admin/user/createUserSubscribeLogic.go b/internal/logic/admin/user/createUserSubscribeLogic.go index dbaff74..08876f8 100644 --- a/internal/logic/admin/user/createUserSubscribeLogic.go +++ b/internal/logic/admin/user/createUserSubscribeLogic.go @@ -6,12 +6,12 @@ import ( "time" "github.com/google/uuid" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -77,5 +77,9 @@ func (l *CreateUserSubscribeLogic) CreateUserSubscribe(req *types.CreateUserSubs return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "UpdateUserCache error: %v", err.Error()) } + err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId) + if err != nil { + logger.Errorw("ClearSubscribe error", logger.Field("error", err.Error())) + } return nil } diff --git a/internal/logic/admin/user/currentUserLogic.go b/internal/logic/admin/user/currentUserLogic.go index 28b2727..adad6d6 100644 --- a/internal/logic/admin/user/currentUserLogic.go +++ b/internal/logic/admin/user/currentUserLogic.go @@ -3,15 +3,15 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) diff --git a/internal/logic/admin/user/deleteUserAuthMethodLogic.go b/internal/logic/admin/user/deleteUserAuthMethodLogic.go index 38692ea..29dfcfe 100644 --- a/internal/logic/admin/user/deleteUserAuthMethodLogic.go +++ b/internal/logic/admin/user/deleteUserAuthMethodLogic.go @@ -3,10 +3,10 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/user/deleteUserDeviceLogic.go b/internal/logic/admin/user/deleteUserDeviceLogic.go index 101b33b..c92f79e 100644 --- a/internal/logic/admin/user/deleteUserDeviceLogic.go +++ b/internal/logic/admin/user/deleteUserDeviceLogic.go @@ -3,12 +3,12 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type DeleteUserDeviceLogic struct { diff --git a/internal/logic/admin/user/deleteUserLogic.go b/internal/logic/admin/user/deleteUserLogic.go index d283058..5253d09 100644 --- a/internal/logic/admin/user/deleteUserLogic.go +++ b/internal/logic/admin/user/deleteUserLogic.go @@ -2,11 +2,13 @@ package user import ( "context" + "os" + "strings" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -25,6 +27,11 @@ func NewDeleteUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Delete } func (l *DeleteUserLogic) DeleteUser(req *types.GetDetailRequest) error { + isDemo := strings.ToLower(os.Getenv("PPANEL_MODE")) == "demo" + + if req.Id == 2 && isDemo { + return errors.Wrapf(xerr.NewErrCodeMsg(503, "Demo mode does not allow deletion of the admin user"), "delete user failed: cannot delete admin user in demo mode") + } err := l.svcCtx.UserModel.Delete(l.ctx, req.Id) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete user error: %v", err.Error()) diff --git a/internal/logic/admin/user/deleteUserSubscribeLogic.go b/internal/logic/admin/user/deleteUserSubscribeLogic.go index 463638e..397299d 100644 --- a/internal/logic/admin/user/deleteUserSubscribeLogic.go +++ b/internal/logic/admin/user/deleteUserSubscribeLogic.go @@ -3,10 +3,10 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -26,10 +26,27 @@ func NewDeleteUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext } func (l *DeleteUserSubscribeLogic) DeleteUserSubscribe(req *types.DeleteUserSubscribeRequest) error { - err := l.svcCtx.UserModel.DeleteSubscribeById(l.ctx, req.UserSubscribeId) + // find user subscribe by ID + userSubscribe, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId) + if err != nil { + l.Errorw("failed to find user subscribe", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to find user subscribe: %v", err.Error()) + } + + err = l.svcCtx.UserModel.DeleteSubscribeById(l.ctx, req.UserSubscribeId) if err != nil { l.Errorw("failed to delete user subscribe", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "failed to delete user subscribe: %v", err.Error()) } + // Clear user subscribe cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSubscribe); err != nil { + l.Errorw("failed to clear user subscribe cache", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear user subscribe cache: %v", err.Error()) + } + // Clear subscribe cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSubscribe.SubscribeId); err != nil { + l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSubscribe.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error()) + } return nil } diff --git a/internal/logic/admin/user/getUserAuthMethodLogic.go b/internal/logic/admin/user/getUserAuthMethodLogic.go index a7f8740..aba4dfc 100644 --- a/internal/logic/admin/user/getUserAuthMethodLogic.go +++ b/internal/logic/admin/user/getUserAuthMethodLogic.go @@ -3,11 +3,11 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/user/getUserDetailLogic.go b/internal/logic/admin/user/getUserDetailLogic.go index ab4bfd7..11597d5 100644 --- a/internal/logic/admin/user/getUserDetailLogic.go +++ b/internal/logic/admin/user/getUserDetailLogic.go @@ -3,11 +3,11 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/user/getUserListLogic.go b/internal/logic/admin/user/getUserListLogic.go index 145cba9..3859f76 100644 --- a/internal/logic/admin/user/getUserListLogic.go +++ b/internal/logic/admin/user/getUserListLogic.go @@ -3,13 +3,13 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/phone" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/phone" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -32,6 +32,7 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge Search: req.Search, SubscribeId: req.SubscribeId, UserSubscribeId: req.UserSubscribeId, + Order: "DESC", }) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetUserListLogic failed: %v", err.Error()) @@ -40,20 +41,20 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge userRespList := make([]types.User, 0, len(list)) for _, item := range list { - var user types.User - tool.DeepCopy(&user, item) + var u types.User + tool.DeepCopy(&u, item) // 处理 AuthMethods - authMethods := make([]types.UserAuthMethod, len(user.AuthMethods)) // 直接创建目标 slice - for i, method := range user.AuthMethods { + authMethods := make([]types.UserAuthMethod, len(u.AuthMethods)) // 直接创建目标 slice + for i, method := range u.AuthMethods { tool.DeepCopy(&authMethods[i], method) if method.AuthType == "mobile" { authMethods[i].AuthIdentifier = phone.FormatToInternational(method.AuthIdentifier) } } - user.AuthMethods = authMethods + u.AuthMethods = authMethods - userRespList = append(userRespList, user) + userRespList = append(userRespList, u) } return &types.GetUserListResponse{ diff --git a/internal/logic/admin/user/getUserLoginLogsLogic.go b/internal/logic/admin/user/getUserLoginLogsLogic.go index 46ccc94..afaba61 100644 --- a/internal/logic/admin/user/getUserLoginLogsLogic.go +++ b/internal/logic/admin/user/getUserLoginLogsLogic.go @@ -3,12 +3,11 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -28,15 +27,34 @@ func NewGetUserLoginLogsLogic(ctx context.Context, svcCtx *svc.ServiceContext) * } func (l *GetUserLoginLogsLogic) GetUserLoginLogs(req *types.GetUserLoginLogsRequest) (resp *types.GetUserLoginLogsResponse, err error) { - data, total, err := l.svcCtx.UserModel.FilterLoginLogList(l.ctx, req.Page, req.Size, &user.LoginLogFilterParams{ - UserId: req.UserId, + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeLogin.Uint8(), + ObjectID: req.UserId, }) if err != nil { l.Errorw("[GetUserLoginLogs] get user login logs failed", logger.Field("error", err.Error()), logger.Field("request", req)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get user login logs failed: %v", err.Error()) } var list []types.UserLoginLog - tool.DeepCopy(&list, data) + + for _, datum := range data { + var content log.Login + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("[GetUserLoginLogs] unmarshal login log content failed: %v", err.Error()) + continue + } + list = append(list, types.UserLoginLog{ + Id: datum.Id, + UserId: datum.ObjectID, + LoginIP: content.LoginIP, + UserAgent: content.UserAgent, + Success: content.Success, + Timestamp: datum.CreatedAt.UnixMilli(), + }) + } + return &types.GetUserLoginLogsResponse{ Total: total, List: list, diff --git a/internal/logic/admin/user/getUserSubscribeByIdLogic.go b/internal/logic/admin/user/getUserSubscribeByIdLogic.go index 6a4a454..2f9af88 100644 --- a/internal/logic/admin/user/getUserSubscribeByIdLogic.go +++ b/internal/logic/admin/user/getUserSubscribeByIdLogic.go @@ -3,11 +3,11 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/user/getUserSubscribeDevicesLogic.go b/internal/logic/admin/user/getUserSubscribeDevicesLogic.go index 349152f..6d84f65 100644 --- a/internal/logic/admin/user/getUserSubscribeDevicesLogic.go +++ b/internal/logic/admin/user/getUserSubscribeDevicesLogic.go @@ -3,13 +3,13 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type GetUserSubscribeDevicesLogic struct { diff --git a/internal/logic/admin/user/getUserSubscribeLogic.go b/internal/logic/admin/user/getUserSubscribeLogic.go index abf8dfc..2deb3ac 100644 --- a/internal/logic/admin/user/getUserSubscribeLogic.go +++ b/internal/logic/admin/user/getUserSubscribeLogic.go @@ -3,11 +3,11 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/user/getUserSubscribeLogsLogic.go b/internal/logic/admin/user/getUserSubscribeLogsLogic.go index 01b8135..ca33355 100644 --- a/internal/logic/admin/user/getUserSubscribeLogsLogic.go +++ b/internal/logic/admin/user/getUserSubscribeLogsLogic.go @@ -3,12 +3,12 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -28,10 +28,7 @@ func NewGetUserSubscribeLogsLogic(ctx context.Context, svcCtx *svc.ServiceContex } func (l *GetUserSubscribeLogsLogic) GetUserSubscribeLogs(req *types.GetUserSubscribeLogsRequest) (resp *types.GetUserSubscribeLogsResponse, err error) { - data, total, err := l.svcCtx.UserModel.FilterSubscribeLogList(l.ctx, req.Page, req.Size, &user.SubscribeLogFilterParams{ - UserSubscribeId: req.SubscribeId, - UserId: req.UserId, - }) + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{}) if err != nil { l.Errorw("[GetUserSubscribeLogs] Get User Subscribe Logs Error:", logger.Field("err", err.Error())) diff --git a/internal/logic/admin/user/getUserSubscribeResetTrafficLogsLogic.go b/internal/logic/admin/user/getUserSubscribeResetTrafficLogsLogic.go new file mode 100644 index 0000000..fb01d01 --- /dev/null +++ b/internal/logic/admin/user/getUserSubscribeResetTrafficLogsLogic.go @@ -0,0 +1,62 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetUserSubscribeResetTrafficLogsLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get user subcribe reset traffic logs +func NewGetUserSubscribeResetTrafficLogsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserSubscribeResetTrafficLogsLogic { + return &GetUserSubscribeResetTrafficLogsLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserSubscribeResetTrafficLogsLogic) GetUserSubscribeResetTrafficLogs(req *types.GetUserSubscribeResetTrafficLogsRequest) (resp *types.GetUserSubscribeResetTrafficLogsResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeResetSubscribe.Uint8(), + ObjectID: req.UserSubscribeId, + }) + if err != nil { + l.Errorf("[ResetSubscribeTrafficLog] failed to filter system log: %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FilterSystemLog failed, err: %v", err) + } + + var list []types.ResetSubscribeTrafficLog + + for _, item := range data { + var content log.ResetSubscribe + if err = content.Unmarshal([]byte(item.Content)); err != nil { + l.Errorf("[ResetSubscribeTrafficLog] failed to unmarshal log: %v", err) + continue + } + list = append(list, types.ResetSubscribeTrafficLog{ + Id: item.Id, + Type: content.Type, + OrderNo: content.OrderNo, + Timestamp: content.Timestamp, + UserSubscribeId: item.ObjectID, + }) + } + + return &types.GetUserSubscribeResetTrafficLogsResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/user/getUserSubscribeTrafficLogsLogic.go b/internal/logic/admin/user/getUserSubscribeTrafficLogsLogic.go index 782b96f..07f0055 100644 --- a/internal/logic/admin/user/getUserSubscribeTrafficLogsLogic.go +++ b/internal/logic/admin/user/getUserSubscribeTrafficLogsLogic.go @@ -3,13 +3,13 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type GetUserSubscribeTrafficLogsLogic struct { diff --git a/internal/logic/admin/user/kickOfflineByUserDeviceLogic.go b/internal/logic/admin/user/kickOfflineByUserDeviceLogic.go index dafec89..9c4ec82 100644 --- a/internal/logic/admin/user/kickOfflineByUserDeviceLogic.go +++ b/internal/logic/admin/user/kickOfflineByUserDeviceLogic.go @@ -3,10 +3,10 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/user/updateUserAuthMethodLogic.go b/internal/logic/admin/user/updateUserAuthMethodLogic.go index c97d466..6b87523 100644 --- a/internal/logic/admin/user/updateUserAuthMethodLogic.go +++ b/internal/logic/admin/user/updateUserAuthMethodLogic.go @@ -3,10 +3,10 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -31,11 +31,21 @@ func (l *UpdateUserAuthMethodLogic) UpdateUserAuthMethod(req *types.UpdateUserAu l.Errorw("Get user auth method error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId), logger.Field("authType", req.AuthType)) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Get user auth method error: %v", err.Error()) } + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, req.UserId) + if err != nil { + l.Errorw("Get user info error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Get user info error: %v", err.Error()) + } + method.AuthType = req.AuthType method.AuthIdentifier = req.AuthIdentifier if err = l.svcCtx.UserModel.UpdateUserAuthMethods(l.ctx, method); err != nil { l.Errorw("Update user auth method error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId), logger.Field("authType", req.AuthType)) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Update user auth method error: %v", err.Error()) } + if err = l.svcCtx.UserModel.UpdateUserCache(l.ctx, userInfo); err != nil { + l.Errorw("Update user cache error", logger.Field("error", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Update user cache error: %v", err.Error()) + } return nil } diff --git a/internal/logic/admin/user/updateUserBasicInfoLogic.go b/internal/logic/admin/user/updateUserBasicInfoLogic.go index 5f3d8a9..9f57f75 100644 --- a/internal/logic/admin/user/updateUserBasicInfoLogic.go +++ b/internal/logic/admin/user/updateUserBasicInfoLogic.go @@ -2,12 +2,16 @@ package user import ( "context" + "os" + "strings" + "time" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -33,12 +37,97 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Find User Error") } - tool.DeepCopy(userInfo, req) + isDemo := strings.ToLower(os.Getenv("PPANEL_MODE")) == "demo" + if req.Avatar != "" && !tool.IsValidImageSize(req.Avatar, 1024) { return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Invalid Image Size") } + + if userInfo.Balance != req.Balance { + change := req.Balance - userInfo.Balance + balanceLog := log.Balance{ + Type: log.BalanceTypeAdjust, + Amount: change, + OrderNo: "", + Balance: req.Balance, + Timestamp: time.Now().UnixMilli(), + } + content, _ := balanceLog.Marshal() + + err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeBalance.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: userInfo.Id, + Content: string(content), + }) + if err != nil { + l.Errorw("[UpdateUserBasicInfoLogic] Insert Balance Log Error:", logger.Field("err", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Insert Balance Log Error") + } + userInfo.Balance = req.Balance + } + + if userInfo.GiftAmount != req.GiftAmount { + change := req.GiftAmount - userInfo.GiftAmount + if change != 0 { + var changeType uint16 + if userInfo.GiftAmount < req.GiftAmount { + changeType = log.GiftTypeIncrease + } else { + changeType = log.GiftTypeReduce + } + giftLog := log.Gift{ + Type: changeType, + Amount: change, + Balance: req.GiftAmount, + Remark: "Admin adjustment", + Timestamp: time.Now().UnixMilli(), + } + content, _ := giftLog.Marshal() + // Add gift amount change log + err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeGift.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: userInfo.Id, + Content: string(content), + }) + if err != nil { + l.Errorw("[UpdateUserBasicInfoLogic] Insert Balance Log Error:", logger.Field("err", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Insert Balance Log Error") + } + userInfo.GiftAmount = req.GiftAmount + } + } + + if req.Commission != userInfo.Commission { + + commentLog := log.Commission{ + Type: log.CommissionTypeAdjust, + Amount: req.Commission - userInfo.Commission, + Timestamp: time.Now().UnixMilli(), + } + + content, _ := commentLog.Marshal() + err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeCommission.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: userInfo.Id, + Content: string(content), + }) + if err != nil { + l.Errorw("[UpdateUserBasicInfoLogic] Insert Commission Log Error:", logger.Field("err", err.Error()), logger.Field("userId", req.UserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Insert Commission Log Error") + } + userInfo.Commission = req.Commission + } + tool.DeepCopy(userInfo, req) + userInfo.OnlyFirstPurchase = &req.OnlyFirstPurchase + userInfo.ReferralPercentage = req.ReferralPercentage + if req.Password != "" { - l.Infow("[UpdateUserBasicInfoLogic] Update User Password:", logger.Field("userId", req.UserId), logger.Field("password", req.Password)) + if userInfo.Id == 2 && isDemo { + return errors.Wrapf(xerr.NewErrCodeMsg(503, "Demo mode does not allow modification of the admin user password"), "UpdateUserBasicInfo failed: cannot update admin user password in demo mode") + } userInfo.Password = tool.EncodePassWord(req.Password) } diff --git a/internal/logic/admin/user/updateUserDeviceLogic.go b/internal/logic/admin/user/updateUserDeviceLogic.go index ecffd39..40ff9b2 100644 --- a/internal/logic/admin/user/updateUserDeviceLogic.go +++ b/internal/logic/admin/user/updateUserDeviceLogic.go @@ -3,10 +3,10 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/user/updateUserNotifySettingLogic.go b/internal/logic/admin/user/updateUserNotifySettingLogic.go index fc0142b..18673e1 100644 --- a/internal/logic/admin/user/updateUserNotifySettingLogic.go +++ b/internal/logic/admin/user/updateUserNotifySettingLogic.go @@ -3,11 +3,11 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/admin/user/updateUserSubscribeLogic.go b/internal/logic/admin/user/updateUserSubscribeLogic.go index 00b7020..9d92ce5 100644 --- a/internal/logic/admin/user/updateUserSubscribeLogic.go +++ b/internal/logic/admin/user/updateUserSubscribeLogic.go @@ -4,11 +4,11 @@ import ( "context" "time" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -28,13 +28,20 @@ func NewUpdateUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext } func (l *UpdateUserSubscribeLogic) UpdateUserSubscribe(req *types.UpdateUserSubscribeRequest) error { - userSub, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeId) + userSub, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId) if err != nil { l.Errorw("FindOneUserSubscribe failed:", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId)) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneUserSubscribe failed: %v", err.Error()) } + expiredAt := time.UnixMilli(req.ExpiredAt) + if time.Since(expiredAt).Minutes() > 0 { + userSub.Status = 3 + } else { + userSub.Status = 1 + } + err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &user.Subscribe{ - Id: req.UserSubscribeId, + Id: userSub.Id, UserId: userSub.UserId, OrderId: userSub.OrderId, SubscribeId: req.SubscribeId, @@ -52,6 +59,15 @@ func (l *UpdateUserSubscribeLogic) UpdateUserSubscribe(req *types.UpdateUserSubs l.Errorw("UpdateSubscribe failed:", logger.Field("error", err.Error())) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateSubscribe failed: %v", err.Error()) } - + // Clear user subscribe cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSub); err != nil { + l.Errorw("ClearSubscribeCache failed:", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + // Clear subscribe cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil { + l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error()) + } return nil } diff --git a/internal/logic/app/announcement/queryAnnouncementLogic.go b/internal/logic/app/announcement/queryAnnouncementLogic.go deleted file mode 100644 index 01c771c..0000000 --- a/internal/logic/app/announcement/queryAnnouncementLogic.go +++ /dev/null @@ -1,47 +0,0 @@ -package announcement - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/model/announcement" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type QueryAnnouncementLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// NewQueryAnnouncementLogic Query announcement -func NewQueryAnnouncementLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryAnnouncementLogic { - return &QueryAnnouncementLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryAnnouncementLogic) QueryAnnouncement(req *types.QueryAnnouncementRequest) (resp *types.QueryAnnouncementResponse, err error) { - enable := true - total, list, err := l.svcCtx.AnnouncementModel.GetAnnouncementListByPage(l.ctx, req.Page, req.Size, announcement.Filter{ - Show: &enable, - Pinned: req.Pinned, - Popup: req.Popup, - }) - if err != nil { - l.Error("[QueryAnnouncementLogic] GetAnnouncementListByPage error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAnnouncementListByPage error: %v", err.Error()) - } - resp = &types.QueryAnnouncementResponse{} - resp.Total = total - resp.List = make([]types.Announcement, 0) - tool.DeepCopy(&resp.List, list) - return -} diff --git a/internal/logic/app/auth/checkLogic.go b/internal/logic/app/auth/checkLogic.go deleted file mode 100644 index 3247084..0000000 --- a/internal/logic/app/auth/checkLogic.go +++ /dev/null @@ -1,41 +0,0 @@ -package auth - -import ( - "context" - - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type CheckLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Check Account -func NewCheckLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckLogic { - return &CheckLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CheckLogic) Check(req *types.AppAuthCheckRequest) (resp *types.AppAuthCheckResponse, err error) { - resp = &types.AppAuthCheckResponse{} - _, err = findUserByMethod(l.ctx, l.svcCtx, req.Method, req.Identifier, req.Account, req.AreaCode) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - resp.Status = false - return resp, nil - } - return resp, err - } - resp.Status = true - return -} diff --git a/internal/logic/app/auth/findUserByMethod.go b/internal/logic/app/auth/findUserByMethod.go deleted file mode 100644 index b5a0b6c..0000000 --- a/internal/logic/app/auth/findUserByMethod.go +++ /dev/null @@ -1,59 +0,0 @@ -package auth - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/pkg/authmethod" - - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/phone" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -func findUserByMethod(ctx context.Context, svcCtx *svc.ServiceContext, method, identifier, account, areaCode string) (userInfo *user.User, err error) { - var authMethods *user.AuthMethods - switch method { - case authmethod.Email: - authMethods, err = svcCtx.UserModel.FindUserAuthMethodByOpenID(ctx, authmethod.Email, account) - case authmethod.Mobile: - phoneNumber, err := phone.FormatToE164(areaCode, account) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") - } - authMethods, err = svcCtx.UserModel.FindUserAuthMethodByOpenID(ctx, authmethod.Mobile, phoneNumber) - if err != nil { - return nil, err - } - case authmethod.Device: - userDevice, err := svcCtx.UserModel.FindOneDeviceByIdentifier(ctx, identifier) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, err - } - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user device imei error") - } - return svcCtx.UserModel.FindOne(ctx, userDevice.UserId) - default: - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "unknown method") - } - if err != nil { - return nil, err - } - return svcCtx.UserModel.FindOne(ctx, authMethods.UserId) -} - -func existError(method string) error { - switch method { - case authmethod.Email: - return errors.Wrapf(xerr.NewErrCode(xerr.EmailExist), "") - case authmethod.Mobile: - return errors.Wrapf(xerr.NewErrCode(xerr.TelephoneExist), "") - case authmethod.Device: - return errors.Wrapf(xerr.NewErrCode(xerr.DeviceExist), "") - default: - return errors.New("unknown method") - } -} diff --git a/internal/logic/app/auth/getAppConfigLogic.go b/internal/logic/app/auth/getAppConfigLogic.go deleted file mode 100644 index d5434d3..0000000 --- a/internal/logic/app/auth/getAppConfigLogic.go +++ /dev/null @@ -1,136 +0,0 @@ -package auth - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - "github.com/perfect-panel/ppanel-server/internal/model/application" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type GetAppConfigLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// GetAppConfig -func NewGetAppConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAppConfigLogic { - return &GetAppConfigLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetAppConfigLogic) GetAppConfig(req *types.AppConfigRequest) (resp *types.AppConfigResponse, err error) { - resp = &types.AppConfigResponse{} - systems, err := l.svcCtx.SystemModel.GetSiteConfig(l.ctx) - if err != nil { - l.Errorw("[QueryApplicationConfig] GetSiteConfig error: ", logger.Field("error", err.Error())) - } - for _, sysVal := range systems { - if sysVal.Key == "CustomData" { - jsonStr := strings.ReplaceAll(sysVal.Value, "\\", "") - customData := make(map[string]interface{}) - if err = json.Unmarshal([]byte(jsonStr), &customData); err != nil { - break - } - - website := customData["website"] - if website != nil { - resp.OfficialWebsite = fmt.Sprintf("%v", website) - } - krWebsiteId := customData["kr_website_id"] - if krWebsiteId != nil { - resp.KrWebsiteId = fmt.Sprintf("%v", krWebsiteId) - } - invitationLink := customData["invitation_link"] - if krWebsiteId != nil { - resp.InvitationLink = fmt.Sprintf("%v", invitationLink) - } - - versionReview := customData["version_review"] - if versionReview != nil && req.UserAgent == "ios" { - resp.Application.VersionReview = fmt.Sprintf("%v", versionReview) - } - - contacts := customData["contacts"] - if contacts != nil { - contactsJson, err := json.Marshal(contacts) - if err == nil { - contactsMap := make(map[string]string) - err = json.Unmarshal(contactsJson, &contactsMap) - if err == nil { - resp.OfficialEmail = fmt.Sprintf("%v", contactsMap["email"]) - resp.OfficialTelegram = fmt.Sprintf("%v", contactsMap["telegram"]) - resp.OfficialTelephone = fmt.Sprintf("%v", contactsMap["telephone"]) - } - } - } - break - } - } - - var applications []*application.Application - err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { - return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error - }) - if err != nil { - l.Errorw("[QueryApplicationConfig] get application error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) - } - - if len(applications) == 0 { - return resp, nil - } - - isOk := false - for _, app := range applications { - if isOk { - break - } - resp.Application.Name = app.Name - resp.Application.Description = app.Description - applicationVersions := app.ApplicationVersions - if len(applicationVersions) != 0 { - for _, applicationVersion := range applicationVersions { - if applicationVersion.Platform == req.UserAgent { - resp.Application.Id = applicationVersion.ApplicationId - resp.Application.Url = applicationVersion.Url - resp.Application.Version = applicationVersion.Version - resp.Application.VersionDescription = applicationVersion.Description - resp.Application.IsDefault = applicationVersion.IsDefault - isOk = true - break - } - } - } - } - - configs, err := l.svcCtx.ApplicationModel.FindOneConfig(l.ctx, 1) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - l.Logger.Error("[GetAppInfo] FindOneAppConfig error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAppInfo FindOneAppConfig error: %v", err.Error()) - } - resp.EncryptionKey = configs.EncryptionKey - resp.EncryptionMethod = configs.EncryptionMethod - resp.Domains = strings.Split(configs.Domains, ";") - resp.StartupPicture = configs.StartupPicture - resp.StartupPictureSkipTime = configs.StartupPictureSkipTime - if configs.InvitationLink != "" { - resp.InvitationLink = configs.InvitationLink - } - if configs.KrWebsiteId != "" { - resp.KrWebsiteId = configs.KrWebsiteId - } - return -} diff --git a/internal/logic/app/auth/loginLogic.go b/internal/logic/app/auth/loginLogic.go deleted file mode 100644 index e70905d..0000000 --- a/internal/logic/app/auth/loginLogic.go +++ /dev/null @@ -1,194 +0,0 @@ -package auth - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/authmethod" - - "github.com/gin-gonic/gin" - - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/phone" - - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/jwt" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type LoginLogic struct { - logger.Logger - ctx *gin.Context - svcCtx *svc.ServiceContext -} - -// Login -func NewLoginLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *LoginLogic { - return &LoginLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *LoginLogic) Login(req *types.AppAuthRequest) (resp *types.AppAuthRespone, err error) { - - loginStatus := false - var userInfo *user.User - // Record login status - defer func(svcCtx *svc.ServiceContext) { - if userInfo != nil && userInfo.Id != 0 { - if err := svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ - UserId: userInfo.Id, - LoginIP: l.ctx.ClientIP(), - UserAgent: l.ctx.Request.UserAgent(), - Success: &loginStatus, - }); err != nil { - l.Errorw("InsertLoginLog Error", logger.Field("error", err.Error())) - } - } - }(l.svcCtx) - - resp = &types.AppAuthRespone{} - //query user - userInfo, err = findUserByMethod(l.ctx, l.svcCtx, req.Method, req.Identifier, req.Account, req.AreaCode) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") - } - return resp, err - } - - switch req.Method { - case authmethod.Email: - - if !l.svcCtx.Config.Email.Enable { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.EmailNotEnabled), "Email function is not enabled yet") - } - - if req.Code != "" { - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security.String(), req.Account) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload common.CacheKeyPayload - err = json.Unmarshal([]byte(value), &payload) - if err != nil { - l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if payload.Code != req.Code { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - l.svcCtx.Redis.Del(l.ctx, cacheKey) - } else { - // Verify password - if !tool.VerifyPassWord(req.Password, userInfo.Password) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") - } - } - case authmethod.Mobile: - if !l.svcCtx.Config.Mobile.Enable { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled") - } - phoneNumber, err := phone.FormatToE164(req.AreaCode, req.Account) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") - } - - if req.Code != "" { - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Security, phoneNumber) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if value == "" { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload common.CacheKeyPayload - if err := json.Unmarshal([]byte(value), &payload); err != nil { - l.Errorw("[SendSmsCode]: Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - } - if payload.Code != req.Code { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - l.svcCtx.Redis.Del(l.ctx, cacheKey) - } else { - // Verify password - if !tool.VerifyPassWord(req.Password, userInfo.Password) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") - } - } - case authmethod.Device: - default: - return nil, existError(req.Method) - } - - device, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - if req.Method == authmethod.Device { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "device not exist") - } - //Add User Device - userInfo.UserDevices = append(userInfo.UserDevices, user.Device{ - UserAgent: req.UserAgent, - Identifier: req.Identifier, - Ip: l.ctx.ClientIP(), - }) - err = l.svcCtx.UserModel.Update(l.ctx, userInfo) - if err != nil { - l.Errorw("[UpdateUserBindDevice] Fail", logger.Field("error", err.Error())) - } - } - } else { - //Change the user who owns the device - if device.UserId != userInfo.Id { - device.UserId = userInfo.Id - } - device.Ip = l.ctx.ClientIP() - err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device) - if err != nil { - l.Errorw("[UpdateUserBindDevice] Fail", logger.Field("error", err.Error())) - } - } - - // Generate session id - sessionId := uuidx.NewUUID().String() - // Generate token - token, err := jwt.NewJwtToken( - l.svcCtx.Config.JwtAuth.AccessSecret, - time.Now().Unix(), - l.svcCtx.Config.JwtAuth.AccessExpire, - jwt.WithOption("UserId", userInfo.Id), - jwt.WithOption("SessionId", sessionId), - ) - if err != nil { - l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) - } - sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) - if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) - } - - resp.Token = token - return -} diff --git a/internal/logic/app/auth/registerLogic.go b/internal/logic/app/auth/registerLogic.go deleted file mode 100644 index cb047b3..0000000 --- a/internal/logic/app/auth/registerLogic.go +++ /dev/null @@ -1,249 +0,0 @@ -package auth - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/authmethod" - - "github.com/gin-gonic/gin" - - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/jwt" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/phone" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type CacheKeyPayload struct { - Code string `json:"code"` - LastAt int64 `json:"lastAt"` -} -type RegisterLogic struct { - logger.Logger - ctx *gin.Context - svcCtx *svc.ServiceContext -} - -// Register -func NewRegisterLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *RegisterLogic { - return &RegisterLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *RegisterLogic) Register(req *types.AppAuthRequest) (resp *types.AppAuthRespone, err error) { - resp = &types.AppAuthRespone{} - var referer *user.User - c := l.svcCtx.Config.Register - // Check if the registration is stopped - if c.StopRegister { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register") - } - - if req.Invite == "" { - if l.svcCtx.Config.Invite.ForcedInvite { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is required") - } - } else { - // Check if the invite code is valid - referer, err = l.svcCtx.UserModel.FindOneByReferCode(l.ctx, req.Invite) - if err != nil { - l.Errorw("FindOneByReferCode Error", logger.Field("error", err)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is invalid") - } - } - - if req.Password == "" { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.PasswordIsEmpty), "Password required") - } - - userInfo, err := findUserByMethod(l.ctx, l.svcCtx, req.Method, req.Identifier, req.Account, req.AreaCode) - if err == nil && userInfo != nil { - return nil, existError(req.Method) - } - if !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, err - } - // Generate password - pwd := tool.EncodePassWord(req.Password) - userInfo = &user.User{ - Password: pwd, - } - if referer != nil { - userInfo.RefererId = referer.Id - } - switch req.Method { - case authmethod.Email: - if !l.svcCtx.Config.Email.Enable { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.EmailNotEnabled), "Email function is not enabled yet") - } - if l.svcCtx.Config.Email.EnableVerify { - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Register.String(), req.Account) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload common.CacheKeyPayload - err = json.Unmarshal([]byte(value), &payload) - if err != nil { - l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if payload.Code != req.Code { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - } - userInfo.AuthMethods = []user.AuthMethods{{ - AuthType: authmethod.Email, - AuthIdentifier: req.Account, - }} - - case authmethod.Mobile: - if req.AreaCode == "" { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneAreaCodeIsEmpty), "area code required") - } - - if !l.svcCtx.Config.Mobile.Enable { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled") - } - phoneNumber, err := phone.FormatToE164(req.AreaCode, req.Account) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") - } - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Register, phoneNumber) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload CacheKeyPayload - _ = json.Unmarshal([]byte(value), &payload) - if payload.Code != req.Code { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - userInfo.AuthMethods = []user.AuthMethods{{ - AuthType: authmethod.Mobile, - AuthIdentifier: phoneNumber, - Verified: true, - }} - case authmethod.Device: - oneDevice, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) - if err == nil && oneDevice != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DeviceExist), "device exist") - } - default: - return nil, existError(req.Method) - } - - device, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - //Add User Device - userInfo.UserDevices = append(userInfo.UserDevices, user.Device{ - Ip: l.ctx.ClientIP(), - Identifier: req.Identifier, - UserAgent: req.UserAgent, - }) - } else { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) - } - } else { - //Delete Other User Device - err = l.svcCtx.UserModel.DeleteDevice(l.ctx, device.Id) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "delete old user device failed: %v", err.Error()) - } else { - //User Add Device - userInfo.UserDevices = append(userInfo.UserDevices, user.Device{ - Ip: l.ctx.ClientIP(), - Identifier: req.Identifier, - UserAgent: req.UserAgent, - }) - } - } - - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - // Save user information - if err := db.Create(userInfo).Error; err != nil { - return err - } - // Generate ReferCode - userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) - // Update ReferCode - if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { - return err - } - if l.svcCtx.Config.Register.EnableTrial { - // Active trial - if err = l.activeTrial(userInfo.Id); err != nil { - return err - } - } - return nil - }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert user info failed: %v", err.Error()) - } - - // Generate session id - sessionId := uuidx.NewUUID().String() - // Generate token - token, err := jwt.NewJwtToken( - l.svcCtx.Config.JwtAuth.AccessSecret, - time.Now().Unix(), - l.svcCtx.Config.JwtAuth.AccessExpire, - jwt.WithOption("UserId", userInfo.Id), - jwt.WithOption("SessionId", sessionId), - ) - if err != nil { - l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) - } - - sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) - if err := l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) - } - - resp.Token = token - return -} - -func (l *RegisterLogic) activeTrial(uid int64) error { - sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe) - if err != nil { - return err - } - userSub := &user.Subscribe{ - Id: 0, - UserId: uid, - OrderId: 0, - SubscribeId: sub.Id, - StartTime: time.Now(), - ExpireTime: tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, time.Now()), - Traffic: sub.Traffic, - Download: 0, - Upload: 0, - Token: uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", uid)), - UUID: uuidx.NewUUID().String(), - Status: 1, - } - return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub) -} diff --git a/internal/logic/app/auth/resetPasswordLogic.go b/internal/logic/app/auth/resetPasswordLogic.go deleted file mode 100644 index 21a7a5b..0000000 --- a/internal/logic/app/auth/resetPasswordLogic.go +++ /dev/null @@ -1,161 +0,0 @@ -package auth - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/authmethod" - - "github.com/gin-gonic/gin" - - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/phone" - - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/jwt" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type ResetPasswordLogic struct { - logger.Logger - ctx *gin.Context - svcCtx *svc.ServiceContext -} - -// Reset Password -func NewResetPasswordLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *ResetPasswordLogic { - return &ResetPasswordLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *ResetPasswordLogic) ResetPassword(req *types.AppAuthRequest) (resp *types.AppAuthRespone, err error) { - resp = &types.AppAuthRespone{} - userInfo, err := findUserByMethod(l.ctx, l.svcCtx, req.Method, req.Identifier, req.Account, req.AreaCode) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "query user info failed") - } - l.Errorw("FindOneByEmail Error", logger.Field("error", err)) - return nil, err - } - - switch req.Method { - case authmethod.Mobile: - if !l.svcCtx.Config.Mobile.Enable { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled") - } - phoneNumber, err := phone.FormatToE164(req.AreaCode, req.Account) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") - } - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Security, phoneNumber) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload common.CacheKeyPayload - err = json.Unmarshal([]byte(value), &payload) - if err != nil { - l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if payload.Code != req.Code { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - case authmethod.Email: - if !l.svcCtx.Config.Email.Enable { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.EmailNotEnabled), "Email function is not enabled yet") - } - - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security.String(), req.Account) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload CacheKeyPayload - err = json.Unmarshal([]byte(value), &payload) - if err != nil { - l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if payload.Code != req.Code { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - default: - return nil, errors.New("unknown method") - } - - userInfo.Password = tool.EncodePassWord(req.Password) - err = l.svcCtx.UserModel.Update(l.ctx, userInfo) - if err != nil { - l.Errorw("UpdateUser Error", logger.Field("error", err)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user info failed: %v", err.Error()) - } - - device, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - //Add User Device - userInfo.UserDevices = append(userInfo.UserDevices, user.Device{ - Ip: l.ctx.ClientIP(), - Identifier: req.Identifier, - UserAgent: req.UserAgent, - }) - } else { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) - } - } else { - if device.UserId != userInfo.Id { - //Change the user who owns the device - if device.UserId != userInfo.Id { - device.UserId = userInfo.Id - } - device.Ip = l.ctx.ClientIP() - err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device) - if err != nil { - l.Errorw("[UpdateUserBindDevice] Fail", logger.Field("error", err.Error())) - } - } - } - - // Generate session id - sessionId := uuidx.NewUUID().String() - // Generate token - token, err := jwt.NewJwtToken( - l.svcCtx.Config.JwtAuth.AccessSecret, - time.Now().Unix(), - l.svcCtx.Config.JwtAuth.AccessExpire, - jwt.WithOption("UserId", userInfo.Id), - jwt.WithOption("SessionId", sessionId), - ) - if err != nil { - l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) - } - - sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) - if err := l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) - } - resp.Token = token - return -} diff --git a/internal/logic/app/document/queryDocumentDetailLogic.go b/internal/logic/app/document/queryDocumentDetailLogic.go deleted file mode 100644 index f049042..0000000 --- a/internal/logic/app/document/queryDocumentDetailLogic.go +++ /dev/null @@ -1,39 +0,0 @@ -package document - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type QueryDocumentDetailLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// NewQueryDocumentDetailLogic Get document detail -func NewQueryDocumentDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryDocumentDetailLogic { - return &QueryDocumentDetailLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryDocumentDetailLogic) QueryDocumentDetail(req *types.QueryDocumentDetailRequest) (resp *types.Document, err error) { - // find document - data, err := l.svcCtx.DocumentModel.FindOne(l.ctx, req.Id) - if err != nil { - l.Error("[QueryDocumentDetailLogic] FindOne error", logger.Field("id", req.Id), logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOne error: %s", err.Error()) - } - resp = &types.Document{} - tool.DeepCopy(resp, data) - return -} diff --git a/internal/logic/app/document/queryDocumentListLogic.go b/internal/logic/app/document/queryDocumentListLogic.go deleted file mode 100644 index 447e7ed..0000000 --- a/internal/logic/app/document/queryDocumentListLogic.go +++ /dev/null @@ -1,48 +0,0 @@ -package document - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type QueryDocumentListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get document list -func NewQueryDocumentListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryDocumentListLogic { - return &QueryDocumentListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryDocumentListLogic) QueryDocumentList() (resp *types.QueryDocumentListResponse, err error) { - total, data, err := l.svcCtx.DocumentModel.GetDocumentListByAll(l.ctx) - if err != nil { - l.Error("[QueryDocumentList] error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryDocumentList error: %v", err.Error()) - } - resp = &types.QueryDocumentListResponse{ - Total: total, - List: make([]types.Document, 0), - } - for _, item := range data { - resp.List = append(resp.List, types.Document{ - Id: item.Id, - Title: item.Title, - Tags: tool.StringMergeAndRemoveDuplicates(item.Tags), - UpdatedAt: item.UpdatedAt.UnixMilli(), - }) - } - return -} diff --git a/internal/logic/app/node/getNodeListLogic.go b/internal/logic/app/node/getNodeListLogic.go deleted file mode 100644 index d37b4ed..0000000 --- a/internal/logic/app/node/getNodeListLogic.go +++ /dev/null @@ -1,91 +0,0 @@ -package node - -import ( - "context" - "strconv" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/countryCenter" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type GetNodeListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get Node list -func NewGetNodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNodeListLogic { - return &GetNodeListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetNodeListLogic) GetNodeList(req *types.AppUserSubscbribeNodeRequest) (resp *types.AppUserSubscbribeNodeResponse, err error) { - resp = &types.AppUserSubscbribeNodeResponse{List: make([]types.AppUserSubscbribeNode, 0)} - userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) - userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.Id) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscribe: %v", err.Error()) - } - - if userInfo.Id != userSubscribe.UserId { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "find user subscribe: %v", err.Error()) - } - - //拿到所有订阅下的服务组id - var ids []int64 - for _, idStr := range strings.Split(userSubscribe.Subscribe.ServerGroup, ",") { - id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - continue - } - ids = append(ids, id) - } - - //根据服务组id拿到所有节点 - servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, ids) - if err != nil { - return nil, err - } - for _, server := range servers { - latitude, longitude, found := countryCenter.GetCountryCenterByCountryOrCity(server.Country, server.City) - if !found { - latitude = server.Latitude - longitude = server.Longitude - } - - resp.List = append(resp.List, types.AppUserSubscbribeNode{ - Id: server.Id, - Uuid: userSubscribe.UUID, - Traffic: userSubscribe.Traffic, - Upload: userSubscribe.Upload, - Download: userSubscribe.Download, - RelayNode: server.RelayNode, - RelayMode: server.RelayMode, - Longitude: server.Longitude, - Latitude: server.Latitude, - LatitudeCountry: latitude, - LongitudeCountry: longitude, - Tags: strings.Split(server.Tags, ","), - Config: server.Config, - ServerAddr: server.ServerAddr, - Protocol: server.Protocol, - SpeedLimit: server.SpeedLimit, - City: server.City, - Country: server.Country, - Name: server.Name, - }) - } - return -} diff --git a/internal/logic/app/node/getRuleGroupListLogic.go b/internal/logic/app/node/getRuleGroupListLogic.go deleted file mode 100644 index 70350cd..0000000 --- a/internal/logic/app/node/getRuleGroupListLogic.go +++ /dev/null @@ -1,41 +0,0 @@ -package node - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetRuleGroupListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get rule group list -func NewGetRuleGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRuleGroupListLogic { - return &GetRuleGroupListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetRuleGroupListLogic) GetRuleGroupList() (resp *types.AppRuleGroupListResponse, err error) { - nodeRuleGroupList, err := l.svcCtx.ServerModel.QueryAllRuleGroup(l.ctx) - if err != nil { - l.Logger.Error("[GetRuleGroupList] get subscribe rule group list failed: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe rule group list failed: %v", err.Error()) - } - nodeRuleGroups := make([]types.ServerRuleGroup, 0) - tool.DeepCopy(&nodeRuleGroups, nodeRuleGroupList) - return &types.AppRuleGroupListResponse{ - Total: int64(len(nodeRuleGroups)), - List: nodeRuleGroups, - }, nil -} diff --git a/internal/logic/app/order/calculateCoupon.go b/internal/logic/app/order/calculateCoupon.go deleted file mode 100644 index e9150cb..0000000 --- a/internal/logic/app/order/calculateCoupon.go +++ /dev/null @@ -1,13 +0,0 @@ -package order - -import ( - "github.com/perfect-panel/ppanel-server/internal/model/coupon" -) - -func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 { - if couponInfo.Type == 1 { - return int64(float64(amount) * (float64(couponInfo.Discount) / float64(100))) - } else { - return min(couponInfo.Discount, amount) - } -} diff --git a/internal/logic/app/order/calculateFee.go b/internal/logic/app/order/calculateFee.go deleted file mode 100644 index 432ad27..0000000 --- a/internal/logic/app/order/calculateFee.go +++ /dev/null @@ -1,20 +0,0 @@ -package order - -import "github.com/perfect-panel/ppanel-server/internal/model/payment" - -func calculateFee(amount int64, config *payment.Payment) int64 { - var fee float64 - switch config.FeeMode { - case 0: - return 0 - case 1: - fee = float64(amount) * (float64(config.FeePercent) / float64(100)) - case 2: - if amount > 0 { - fee = float64(config.FeeAmount) - } - case 3: - fee = float64(amount)*(float64(config.FeePercent)/float64(100)) + float64(config.FeeAmount) - } - return int64(fee) -} diff --git a/internal/logic/app/order/checkoutOrderLogic.go b/internal/logic/app/order/checkoutOrderLogic.go deleted file mode 100644 index 1852943..0000000 --- a/internal/logic/app/order/checkoutOrderLogic.go +++ /dev/null @@ -1,373 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - - order2 "github.com/perfect-panel/ppanel-server/internal/logic/public/order" - "github.com/perfect-panel/ppanel-server/pkg/payment/payssion" - - paymentPlatform "github.com/perfect-panel/ppanel-server/pkg/payment" - - "github.com/perfect-panel/ppanel-server/pkg/constant" - - "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/exchangeRate" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/payment/alipay" - "github.com/perfect-panel/ppanel-server/pkg/payment/epay" - "github.com/perfect-panel/ppanel-server/pkg/payment/stripe" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - queueType "github.com/perfect-panel/ppanel-server/queue/types" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type CheckoutOrderLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -type CurrencyConfig struct { - CurrencyUnit string - CurrencySymbol string - AccessKey string -} - -const ( - Stripe = "Stripe" - QR = "qr" - Link = "link" -) - -// NewCheckoutOrderLogic Checkout order -func NewCheckoutOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CheckoutOrderLogic { - return &CheckoutOrderLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CheckoutOrderLogic) CheckoutOrder(req *types.CheckoutOrderRequest, requestHost string) (resp *types.CheckoutOrderResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - l.Error("[CheckoutOrderLogic] Invalid access") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid access") - } - // find order - orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) - if err != nil { - l.Error("[CheckoutOrderLogic] FindOneByOrderNo error", logger.Field("orderNo", req.OrderNo), logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneByOrderNo error: %s", err.Error()) - } - - if orderInfo.Status != 1 { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Order status error") - } - - paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, orderInfo.PaymentId) - if err != nil { - l.Error("[CheckoutOrderLogic] FindOneByPaymentMark error", logger.Field("paymentMark", orderInfo.Method), logger.Field("PaymentID", orderInfo.PaymentId), logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneByPaymentMark error: %s", err.Error()) - } - var stripePayment *types.StripePayment = nil - var url, t string - - // switch payment method - switch paymentPlatform.ParsePlatform(paymentConfig.Platform) { - case paymentPlatform.Stripe: - result, err := l.stripePayment(paymentConfig.Config, orderInfo, u) - if err != nil { - l.Error("[CheckoutOrderLogic] stripePayment error", logger.Field("error", err.Error())) - return nil, err - } - stripePayment = result - t = Stripe - case paymentPlatform.EPay: - // epay - url, err = l.epayPayment(paymentConfig, orderInfo, req.ReturnUrl, requestHost) - if err != nil { - l.Error("[CheckoutOrderLogic] epayPayment error", logger.Field("error", err.Error())) - return nil, err - } - t = Link - case paymentPlatform.AlipayF2F: - // alipay f2f - url, err = l.alipayF2fPayment(paymentConfig, orderInfo, requestHost) - if err != nil { - return nil, err - } - t = QR - case paymentPlatform.Payssion: - logger.Infof("匹配配置类型: %v", order2.Payssion) - url, err = l.payssionPayment(paymentConfig, orderInfo, requestHost) - if err != nil { - l.Error("[CheckoutOrderLogic] epayPayment error", logger.Field("error", err.Error())) - return nil, err - } - t = Link - case paymentPlatform.Balance: - // balance - if err = l.balancePayment(u, orderInfo); err != nil { - return nil, err - } - t = paymentPlatform.Balance.String() - default: - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Payment method not supported") - } - return &types.CheckoutOrderResponse{ - Type: t, - CheckoutUrl: url, - Stripe: stripePayment, - }, nil -} - -// Query exchange rate -func (l *CheckoutOrderLogic) queryExchangeRate(to string, src int64) (amount float64, err error) { - amount = float64(src) / float64(100) - // query system currency - currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx) - if err != nil { - l.Error("[CheckoutOrderLogic] GetCurrencyConfig error", logger.Field("error", err.Error())) - return 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetCurrencyConfig error: %s", err.Error()) - } - configs := &CurrencyConfig{} - tool.SystemConfigSliceReflectToStruct(currency, configs) - if configs.AccessKey == "" { - return amount, nil - } - if configs.CurrencyUnit != to { - // query exchange rate - result, err := exchangeRate.GetExchangeRete(configs.CurrencyUnit, to, configs.AccessKey, 1) - if err != nil { - return 0, err - } - amount = result * amount - } - return amount, nil -} - -// Stripe Payment -func (l *CheckoutOrderLogic) stripePayment(config string, info *order.Order, u *user.User) (*types.StripePayment, error) { - // stripe WeChat pay or stripe alipay - stripeConfig := payment.StripeConfig{} - if err := json.Unmarshal([]byte(config), &stripeConfig); err != nil { - l.Error("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) - } - client := stripe.NewClient(stripe.Config{ - SecretKey: stripeConfig.SecretKey, - PublicKey: stripeConfig.PublicKey, - WebhookSecret: stripeConfig.WebhookSecret, - }) - // Calculate the amount with exchange rate - amount, err := l.queryExchangeRate("CNY", info.Amount) - if err != nil { - l.Error("[CheckoutOrderLogic] queryExchangeRate error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error()) - } - convertAmount := int64(amount * 100) - // create payment - result, err := client.CreatePaymentSheet(&stripe.Order{ - OrderNo: info.OrderNo, - Subscribe: strconv.FormatInt(info.SubscribeId, 10), - Amount: convertAmount, - Currency: "cny", - Payment: stripeConfig.Payment, - }, - &stripe.User{ - UserId: u.Id, - }) - if err != nil { - l.Error("[CheckoutOrderLogic] CreatePaymentSheet error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "CreatePaymentSheet error: %s", err.Error()) - } - tradeNo := result.TradeNo - stripePayment := &types.StripePayment{ - PublishableKey: stripeConfig.PublicKey, - ClientSecret: result.ClientSecret, - Method: stripeConfig.Payment, - } - // save payment - info.TradeNo = tradeNo - err = l.svcCtx.OrderModel.Update(l.ctx, info) - if err != nil { - l.Error("[CheckoutOrderLogic] Update error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update error: %s", err.Error()) - } - return stripePayment, nil -} - -// epay payment -func (l *CheckoutOrderLogic) epayPayment(config *payment.Payment, info *order.Order, returnUrl, requestHost string) (string, error) { - epayConfig := payment.EPayConfig{} - if err := json.Unmarshal([]byte(config.Config), &epayConfig); err != nil { - l.Errorw("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) - return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) - } - client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key) - // Calculate the amount with exchange rate - amount, err := l.queryExchangeRate("CNY", info.Amount) - if err != nil { - return "", err - } - notifyUrl := "" - if config.Domain != "" { - notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token - } else { - host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) - if !ok { - host = l.svcCtx.Config.Host - } - notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token - } - // create payment - url := client.CreatePayUrl(epay.Order{ - Name: l.svcCtx.Config.Site.SiteName, - Amount: amount, - OrderNo: info.OrderNo, - SignType: "MD5", - NotifyUrl: notifyUrl, - ReturnUrl: returnUrl, - }) - return url, nil -} - -// alipay f2f payment -func (l *CheckoutOrderLogic) alipayF2fPayment(pay *payment.Payment, info *order.Order, requestHost string) (string, error) { - f2FConfig := payment.AlipayF2FConfig{} - if err := json.Unmarshal([]byte(pay.Config), &f2FConfig); err != nil { - l.Error("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) - return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) - } - var domain string - if pay.Domain != "" { - domain = pay.Domain - } else { - domain = fmt.Sprintf("http://%s", requestHost) - } - client := alipay.NewClient(alipay.Config{ - AppId: f2FConfig.AppId, - PrivateKey: f2FConfig.PrivateKey, - PublicKey: f2FConfig.PublicKey, - InvoiceName: f2FConfig.InvoiceName, - NotifyURL: domain + "/notify/alipay", - }) - // Calculate the amount with exchange rate - amount, err := l.queryExchangeRate("CNY", info.Amount) - if err != nil { - l.Error("[CheckoutOrderLogic] queryExchangeRate error", logger.Field("error", err.Error())) - return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error()) - } - convertAmount := int64(amount * 100) - // create payment - QRCode, err := client.PreCreateTrade(l.ctx, alipay.Order{ - OrderNo: info.OrderNo, - Amount: convertAmount, - }) - if err != nil { - l.Error("[CheckoutOrderLogic] PreCreateTrade error", logger.Field("error", err.Error())) - return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "PreCreateTrade error: %s", err.Error()) - } - return QRCode, nil -} - -// Balance payment -func (l *CheckoutOrderLogic) balancePayment(u *user.User, o *order.Order) error { - var userInfo user.User - err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - err := db.Model(&user.User{}).Where("id = ?", u.Id).First(&userInfo).Error - if err != nil { - return err - } - - if userInfo.Balance < o.Amount { - return errors.Wrapf(xerr.NewErrCode(xerr.InsufficientBalance), "Insufficient balance") - } - // deduct balance - userInfo.Balance -= o.Amount - err = l.svcCtx.UserModel.Update(l.ctx, &userInfo) - if err != nil { - return err - } - // create balance log - balanceLog := &user.BalanceLog{ - Id: 0, - UserId: u.Id, - Amount: o.Amount, - Type: 3, - OrderId: o.Id, - Balance: userInfo.Balance, - } - err = db.Create(balanceLog).Error - if err != nil { - return err - } - return l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, o.OrderNo, 2) - }) - if err != nil { - l.Error("[CheckoutOrderLogic] Transaction error", logger.Field("error", err.Error()), logger.Field("orderNo", o.OrderNo)) - return err - } - // create activity order task - payload := queueType.ForthwithActivateOrderPayload{ - OrderNo: o.OrderNo, - } - bytes, err := json.Marshal(payload) - if err != nil { - l.Error("[CheckoutOrderLogic] Marshal error", logger.Field("error", err.Error())) - return err - } - - task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes) - _, err = l.svcCtx.Queue.EnqueueContext(l.ctx, task) - if err != nil { - l.Error("[CheckoutOrderLogic] Enqueue error", logger.Field("error", err.Error())) - return err - } - l.Logger.Info("[CheckoutOrderLogic] Enqueue success", logger.Field("orderNo", o.OrderNo)) - return nil -} - -func (l *CheckoutOrderLogic) payssionPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) { - payssionConfig := payment.PayssionConfig{} - if err := json.Unmarshal([]byte(config.Config), &payssionConfig); err != nil { - l.Errorw("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) - return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) - } - client := payssion.NewClient(payssionConfig.ApiKey, payssionConfig.SecretKey, payssionConfig.PmId, payssionConfig.Currency, payssionConfig.QueryUrl, payssionConfig.CreateUrl) - // Calculate the amount with exchange rate - amount, err := l.queryExchangeRate("CNY", info.Amount) - if err != nil { - return "", err - } - notifyUrl := "" - if config.Domain != "" { - notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token - } else { - host, ok := l.ctx.Value(constant.CtxKeyRequestHost).(string) - if !ok { - host = l.svcCtx.Config.Host - } - notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token - } - // create payment - url, err := client.CreateOrder(payssion.Order{ - Name: l.svcCtx.Config.Site.SiteName, - Amount: amount, - OrderNo: info.OrderNo, - NotifyUrl: notifyUrl, - ReturnUrl: returnUrl, - }) - return url, err -} diff --git a/internal/logic/app/order/closeOrderLogic.go b/internal/logic/app/order/closeOrderLogic.go deleted file mode 100644 index b98abc9..0000000 --- a/internal/logic/app/order/closeOrderLogic.go +++ /dev/null @@ -1,205 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - "github.com/perfect-panel/ppanel-server/pkg/payment/payssion" - - paymentPlatform "github.com/perfect-panel/ppanel-server/pkg/payment" - - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/payment/stripe" - "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/payment/alipay" -) - -type CloseOrderLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// NewCloseOrderLogic Close order -func NewCloseOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CloseOrderLogic { - return &CloseOrderLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error { - // Find order information by order number - orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) - if err != nil { - l.Error("[CloseOrder] Find order info failed", - logger.Field("error", err.Error()), - logger.Field("orderNo", req.OrderNo), - ) - return nil - } - // If the order status is not 1, it means that the order has been closed or paid - if orderInfo.Status != 1 { - l.Info("[CloseOrder] Order status is not 1", - logger.Field("orderNo", req.OrderNo), - logger.Field("status", orderInfo.Status), - ) - return nil - } - - err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { - // update order status - err := tx.Model(&order.Order{}).Where("order_no = ?", req.OrderNo).Update("status", 3).Error - if err != nil { - l.Error("[CloseOrder] Update order status failed", - logger.Field("error", err.Error()), - logger.Field("orderNo", req.OrderNo), - ) - return err - } - // refund deduction amount to user deduction balance - if orderInfo.GiftAmount > 0 { - userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, orderInfo.UserId) - if err != nil { - l.Error("[CloseOrder] Find user info failed", - logger.Field("error", err.Error()), - logger.Field("user_id", orderInfo.UserId), - ) - return err - } - deduction := userInfo.GiftAmount + orderInfo.GiftAmount - err = tx.Model(&user.User{}).Where("id = ?", orderInfo.UserId).Update("deduction", deduction).Error - if err != nil { - l.Error("[CloseOrder] Refund deduction amount failed", - logger.Field("error", err.Error()), - logger.Field("uid", orderInfo.UserId), - logger.Field("deduction", orderInfo.GiftAmount), - ) - return err - } - // Record the deduction refund log - giftAmountLog := &user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 1, - Balance: deduction, - Remark: "Order cancellation refund", - } - err = tx.Model(&user.GiftAmountLog{}).Create(giftAmountLog).Error - if err != nil { - l.Error("[CloseOrder] Record cancellation refund log failed", - logger.Field("error", err.Error()), - logger.Field("uid", orderInfo.UserId), - logger.Field("deduction", orderInfo.GiftAmount), - ) - return err - } - // update user cache - return l.svcCtx.UserModel.UpdateUserCache(l.ctx, userInfo) - } - return nil - }) - if err != nil { - return err - } - return nil -} - -// confirmationPayment Determine whether the payment is successful -// -//nolint:unused -func (l *CloseOrderLogic) confirmationPayment(order *order.Order) bool { - paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, order.PaymentId) - if err != nil { - l.Error("[CloseOrder] Find payment config failed", logger.Field("error", err.Error()), logger.Field("paymentMark", order.Method)) - return false - } - switch paymentPlatform.ParsePlatform(order.Method) { - case paymentPlatform.AlipayF2F: - if l.queryAlipay(paymentConfig, order.TradeNo) { - return true - } - case paymentPlatform.Payssion: - if l.queryPayssion(paymentConfig, order.TradeNo) { - return true - } - case paymentPlatform.Stripe: - if l.queryStripe(paymentConfig, order.TradeNo) { - return true - } - default: - l.Info("[CloseOrder] Unsupported payment method", logger.Field("paymentMethod", order.Method)) - } - return false -} - -// queryAlipay Query Alipay payment status -func (l *CloseOrderLogic) queryAlipay(paymentConfig *payment.Payment, TradeNo string) bool { - config := payment.AlipayF2FConfig{} - if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil { - l.Error("[CloseOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config)) - return false - } - client := alipay.NewClient(alipay.Config{ - AppId: config.AppId, - PrivateKey: config.PrivateKey, - PublicKey: config.PublicKey, - InvoiceName: config.InvoiceName, - }) - status, err := client.QueryTrade(l.ctx, TradeNo) - if err != nil { - l.Error("[CloseOrder] Query trade failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) - return false - } - if status == alipay.Success || status == alipay.Finished { - return true - } - return false -} - -// queryStripe Query Stripe payment status -func (l *CloseOrderLogic) queryStripe(paymentConfig *payment.Payment, TradeNo string) bool { - config := payment.StripeConfig{} - if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil { - l.Error("[CloseOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config)) - return false - } - client := stripe.NewClient(stripe.Config{ - PublicKey: config.PublicKey, - SecretKey: config.SecretKey, - WebhookSecret: config.WebhookSecret, - }) - status, err := client.QueryOrderStatus(TradeNo) - if err != nil { - l.Error("[CloseOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) - return false - } - return status -} - -//nolint:unused -func (l *CloseOrderLogic) queryPayssion(paymentConfig *payment.Payment, TradeNo string) bool { - l.Info("[CloseOrder]1 Query Payssion", logger.Field("TradeNo", TradeNo)) - payssionConfig := payment.PayssionConfig{} - if err := json.Unmarshal([]byte(paymentConfig.Config), &payssionConfig); err != nil { - l.Errorw("[CloseOrder] Unmarshal error", logger.Field("error", err.Error())) - return false - } - client := payssion.NewClient(payssionConfig.ApiKey, payssionConfig.SecretKey, payssionConfig.PmId, payssionConfig.Currency, payssionConfig.QueryUrl, payssionConfig.CreateUrl) - l.Infof("[CloseOrder]2 Query Payssion", logger.Field("TradeNo", TradeNo)) - // create payment - result, err := client.QueryOrder(TradeNo) - if err != nil { - l.Errorw("[CloseOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) - return false - } - l.Infof("[CloseOrder]3 Query Payssion", logger.Field("TradeNo", TradeNo)) - return result.Transaction.State == "completed" -} diff --git a/internal/logic/app/order/getDiscount.go b/internal/logic/app/order/getDiscount.go deleted file mode 100644 index 4d896f9..0000000 --- a/internal/logic/app/order/getDiscount.go +++ /dev/null @@ -1,14 +0,0 @@ -package order - -import "github.com/perfect-panel/ppanel-server/internal/types" - -func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 { - var finalDiscount int64 = 100 - - for _, discount := range discounts { - if inputMonths >= discount.Quantity && discount.Discount < finalDiscount { - finalDiscount = discount.Discount - } - } - return float64(finalDiscount) / float64(100) -} diff --git a/internal/logic/app/order/preCreateOrderLogic.go b/internal/logic/app/order/preCreateOrderLogic.go deleted file mode 100644 index d36cd10..0000000 --- a/internal/logic/app/order/preCreateOrderLogic.go +++ /dev/null @@ -1,104 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - - "github.com/perfect-panel/ppanel-server/pkg/constant" - - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type PreCreateOrderLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Pre create order -func NewPreCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreCreateOrderLogic { - return &PreCreateOrderLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (resp *types.PreOrderResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - // find subscribe plan - sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId) - if err != nil { - l.Error("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) - } - var discount float64 = 1 - if sub.Discount != "" { - var dis []types.SubscribeDiscount - _ = json.Unmarshal([]byte(sub.Discount), &dis) - discount = getDiscount(dis, req.Quantity) - } - price := sub.UnitPrice * req.Quantity - amount := int64(float64(price) * discount) - discountAmount := price - amount - var coupon int64 - if req.Coupon != "" { - couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found") - } - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) - } - if couponInfo.Count <= couponInfo.UsedCount { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") - } - coupon = calculateCoupon(amount, couponInfo) - } - amount -= coupon - - var deductionAmount int64 - // Check user deduction amount - if u.GiftAmount > 0 { - if u.GiftAmount >= amount { - deductionAmount = amount - amount = 0 - } else { - deductionAmount = u.GiftAmount - amount -= u.GiftAmount - } - } - - payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) - if err != nil { - l.Logger.Error("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) - } - var feeAmount int64 - // Calculate the handling fee - if amount > 0 { - feeAmount = calculateFee(amount, payment) - } - amount += feeAmount - - resp = &types.PreOrderResponse{ - Price: price, - Amount: amount, - Discount: discountAmount, - GiftAmount: deductionAmount, - Coupon: req.Coupon, - CouponDiscount: coupon, - FeeAmount: feeAmount, - } - return -} diff --git a/internal/logic/app/order/purchaseLogic.go b/internal/logic/app/order/purchaseLogic.go deleted file mode 100644 index 7934cc6..0000000 --- a/internal/logic/app/order/purchaseLogic.go +++ /dev/null @@ -1,204 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/constant" - - "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - queue "github.com/perfect-panel/ppanel-server/queue/types" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type PurchaseLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -const CloseOrderTimeMinutes = 15 - -// purchase Subscription -func NewPurchaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PurchaseLogic { - return &PurchaseLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.PurchaseOrderResponse, err error) { - - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - // find user subscription - - if l.svcCtx.Config.Subscribe.SingleModel { - userSubs, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id) - if err != nil { - l.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscription error: %v", err.Error()) - } - if (len(userSubs) == 1 && //订阅数等于1 - //启用试用 - l.svcCtx.Config.Register.EnableTrial && - //试用订阅ID不等于0 - l.svcCtx.Config.Register.TrialSubscribe != 0 && - //使用者订阅ID不等于试用订阅ID - userSubs[0].SubscribeId != l.svcCtx.Config.Register.TrialSubscribe) || len(userSubs) > 1 { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserSubscribeExist), "user has subscription") - } - } - - // find subscribe plan - sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId) - - if err != nil { - l.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) - } - // check subscribe plan status - if !*sub.Sell { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell") - } - var discount float64 = 1 - if sub.Discount != "" { - var dis []types.SubscribeDiscount - _ = json.Unmarshal([]byte(sub.Discount), &dis) - discount = getDiscount(dis, req.Quantity) - } - price := sub.UnitPrice * req.Quantity - // discount amount - amount := int64(float64(price) * discount) - discountAmount := price - amount - var coupon int64 = 0 - // Calculate the coupon deduction - if req.Coupon != "" { - couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotExist), "coupon not found") - } - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) - } - if couponInfo.Count <= couponInfo.UsedCount { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") - } - coupon = calculateCoupon(amount, couponInfo) - } - // Calculate the handling fee - amount -= coupon - var deductionAmount int64 - // Check user deduction amount - if u.GiftAmount > 0 { - if u.GiftAmount >= amount { - deductionAmount = amount - amount = 0 - u.GiftAmount -= amount - } else { - deductionAmount = u.GiftAmount - amount -= u.GiftAmount - u.GiftAmount = 0 - } - } - // find payment method - payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) - if err != nil { - l.Logger.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) - } - var feeAmount int64 - // Calculate the handling fee - if amount > 0 { - feeAmount = calculateFee(amount, payment) - } - // query user is new purchase or renewal - isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id) - if err != nil { - l.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user order error: %v", err.Error()) - } - // create order - orderInfo := &order.Order{ - UserId: u.Id, - OrderNo: tool.GenerateTradeNo(), - Type: 1, - Quantity: req.Quantity, - Price: price, - Amount: amount, - Discount: discountAmount, - GiftAmount: deductionAmount, - Coupon: req.Coupon, - CouponDiscount: coupon, - PaymentId: req.Payment, - Method: payment.Platform, - FeeAmount: feeAmount, - Status: 1, - IsNew: isNew, - SubscribeId: req.SubscribeId, - } - // Database transaction - err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error { - // update user deduction && Pre deduction ,Return after canceling the order - if orderInfo.GiftAmount > 0 { - // update user deduction && Pre deduction ,Return after canceling the order - if e := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { - l.Error("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) - return e - } - // create deduction record - giftAmountLog := user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 2, - Balance: u.GiftAmount, - Remark: "Purchase order deduction", - } - if e := db.Model(&user.GiftAmountLog{}).Create(&giftAmountLog).Error; e != nil { - l.Error("[Purchase] Database insert error", - logger.Field("error", err.Error()), - logger.Field("deductionLog", giftAmountLog), - ) - return e - } - } - // insert order - return db.Model(&order.Order{}).Create(&orderInfo).Error - }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert order error: %v", err.Error()) - } - // Deferred task - payload := queue.DeferCloseOrderPayload{ - OrderNo: orderInfo.OrderNo, - } - val, err := json.Marshal(payload) - if err != nil { - l.Error("[CreateOrder] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) - } - task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) - taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) - if err != nil { - l.Error("[CreateOrder] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) - } else { - l.Info("[CreateOrder] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) - } - - return &types.PurchaseOrderResponse{ - OrderNo: orderInfo.OrderNo, - }, nil -} diff --git a/internal/logic/app/order/queryOrderDetailLogic.go b/internal/logic/app/order/queryOrderDetailLogic.go deleted file mode 100644 index 5cc6b55..0000000 --- a/internal/logic/app/order/queryOrderDetailLogic.go +++ /dev/null @@ -1,40 +0,0 @@ -package order - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type QueryOrderDetailLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get order -func NewQueryOrderDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryOrderDetailLogic { - return &QueryOrderDetailLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryOrderDetailLogic) QueryOrderDetail(req *types.QueryOrderDetailRequest) (resp *types.OrderDetail, err error) { - orderInfo, err := l.svcCtx.OrderModel.FindOneDetailsByOrderNo(l.ctx, req.OrderNo) - if err != nil { - l.Error("[QueryOrderDetail] Database query error", logger.Field("error", err.Error()), logger.Field("order_no", req.OrderNo)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", err.Error()) - } - resp = &types.OrderDetail{} - tool.DeepCopy(resp, orderInfo) - // Prevent commission amount leakage - resp.Commission = 0 - return -} diff --git a/internal/logic/app/order/queryOrderListLogic.go b/internal/logic/app/order/queryOrderListLogic.go deleted file mode 100644 index 245e5d6..0000000 --- a/internal/logic/app/order/queryOrderListLogic.go +++ /dev/null @@ -1,56 +0,0 @@ -package order - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/pkg/constant" - - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type QueryOrderListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get order list -func NewQueryOrderListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryOrderListLogic { - return &QueryOrderListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryOrderListLogic) QueryOrderList(req *types.QueryOrderListRequest) (resp *types.QueryOrderListResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - total, data, err := l.svcCtx.OrderModel.QueryOrderListByPage(l.ctx, req.Page, req.Size, 0, u.Id, 0, "") - if err != nil { - l.Error("[QueryOrderListLogic] Query order list failed", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query order list failed") - } - resp = &types.QueryOrderListResponse{ - Total: total, - List: make([]types.OrderDetail, 0), - } - for _, item := range data { - var orderInfo types.OrderDetail - tool.DeepCopy(&orderInfo, item) - // Prevent commission amount leakage - orderInfo.Commission = 0 - resp.List = append(resp.List, orderInfo) - } - - return -} diff --git a/internal/logic/app/order/rechargeLogic.go b/internal/logic/app/order/rechargeLogic.go deleted file mode 100644 index 2957c54..0000000 --- a/internal/logic/app/order/rechargeLogic.go +++ /dev/null @@ -1,92 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - - "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - queue "github.com/perfect-panel/ppanel-server/queue/types" - "github.com/pkg/errors" -) - -type RechargeLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// NewRechargeLogic Recharge -func NewRechargeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RechargeLogic { - return &RechargeLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *RechargeLogic) Recharge(req *types.RechargeOrderRequest) (resp *types.RechargeOrderResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - // find payment method - payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) - if err != nil { - l.Error("[Recharge] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) - return nil, errors.Wrapf(err, "find payment error: %v", err.Error()) - } - // Calculate the handling fee - feeAmount := calculateFee(req.Amount, payment) - // query user is new purchase or renewal - isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id) - if err != nil { - l.Error("[Recharge] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) - return nil, errors.Wrapf(err, "query user error: %v", err.Error()) - } - orderInfo := order.Order{ - UserId: u.Id, - OrderNo: tool.GenerateTradeNo(), - Type: 4, - Price: req.Amount, - Amount: req.Amount + feeAmount, - FeeAmount: feeAmount, - PaymentId: req.Payment, - Method: payment.Platform, - Status: 1, - IsNew: isNew, - } - err = l.svcCtx.OrderModel.Insert(l.ctx, &orderInfo) - if err != nil { - l.Error("[Recharge] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo)) - return nil, errors.Wrapf(err, "insert order error: %v", err.Error()) - } - // Deferred task - payload := queue.DeferCloseOrderPayload{ - OrderNo: orderInfo.OrderNo, - } - val, err := json.Marshal(payload) - if err != nil { - l.Error("[Recharge] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) - } - task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) - taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) - if err != nil { - l.Error("[Recharge] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) - } else { - l.Info("[Recharge] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) - } - return &types.RechargeOrderResponse{ - OrderNo: orderInfo.OrderNo, - }, nil -} diff --git a/internal/logic/app/order/renewalLogic.go b/internal/logic/app/order/renewalLogic.go deleted file mode 100644 index 4878160..0000000 --- a/internal/logic/app/order/renewalLogic.go +++ /dev/null @@ -1,178 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/constant" - - "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - queue "github.com/perfect-panel/ppanel-server/queue/types" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type RenewalLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Renewal Subscription -func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLogic { - return &RenewalLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.RenewalOrderResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - orderNo := tool.GenerateTradeNo() - // find user subscribe - userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscribe error: %v", err.Error()) - } - // find subscription - sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, userSubscribe.SubscribeId) - if err != nil { - l.Error("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", userSubscribe.SubscribeId)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) - } - // check subscribe plan status - if !*sub.Sell { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell") - } - var discount float64 = 1 - if sub.Discount != "" { - var dis []types.SubscribeDiscount - _ = json.Unmarshal([]byte(sub.Discount), &dis) - discount = getDiscount(dis, req.Quantity) - } - price := sub.UnitPrice * req.Quantity - amount := int64(float64(price) * discount) - discountAmount := price - amount - var coupon int64 = 0 - if req.Coupon != "" { - couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) - if err != nil { - l.Error("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("coupon", req.Coupon)) - return nil, errors.Wrapf(err, "find coupon error: %v", err.Error()) - } - if couponInfo.Count <= couponInfo.UsedCount { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") - } - coupon = calculateCoupon(amount, couponInfo) - } - payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) - if err != nil { - l.Error("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) - return nil, errors.Wrapf(err, "find payment error: %v", err.Error()) - } - amount -= coupon - - var deductionAmount int64 - // Check user deduction amount - if u.GiftAmount > 0 { - if u.GiftAmount >= amount { - deductionAmount = amount - amount = 0 - u.GiftAmount -= amount - } else { - deductionAmount = u.GiftAmount - amount -= u.GiftAmount - u.GiftAmount = 0 - } - } - - var feeAmount int64 - // Calculate the handling fee - if amount > 0 { - feeAmount = calculateFee(amount, payment) - } - - amount += feeAmount - - // create order - orderInfo := order.Order{ - UserId: u.Id, - ParentId: userSubscribe.OrderId, - OrderNo: orderNo, - Type: 2, - Quantity: req.Quantity, - Price: price, - Amount: amount, - GiftAmount: deductionAmount, - Discount: discountAmount, - Coupon: req.Coupon, - CouponDiscount: coupon, - PaymentId: payment.Id, - Method: payment.Platform, - FeeAmount: feeAmount, - Status: 1, - SubscribeId: userSubscribe.SubscribeId, - SubscribeToken: userSubscribe.Token, - } - // Database transaction - err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error { - // update user deduction && Pre deduction ,Return after canceling the order - if orderInfo.GiftAmount > 0 { - // update user deduction && Pre deduction ,Return after canceling the order - if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { - l.Error("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) - return err - } - // create deduction record - deductionLog := user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 2, - Balance: u.GiftAmount, - Remark: "Renewal order deduction", - } - if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil { - l.Error("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog)) - return err - } - } - // insert order - return db.Model(&order.Order{}).Create(&orderInfo).Error - }) - if err != nil { - l.Error("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo)) - return nil, errors.Wrapf(err, "insert order error: %v", err.Error()) - } - // Deferred task - payload := queue.DeferCloseOrderPayload{ - OrderNo: orderInfo.OrderNo, - } - val, err := json.Marshal(payload) - if err != nil { - l.Error("[Renewal] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) - } - task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) - taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) - if err != nil { - l.Error("[Renewal] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) - } else { - l.Info("[Renewal] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) - } - return &types.RenewalOrderResponse{ - OrderNo: orderInfo.OrderNo, - }, nil -} diff --git a/internal/logic/app/order/resetTrafficLogic.go b/internal/logic/app/order/resetTrafficLogic.go deleted file mode 100644 index 1ec2f22..0000000 --- a/internal/logic/app/order/resetTrafficLogic.go +++ /dev/null @@ -1,146 +0,0 @@ -package order - -import ( - "context" - "encoding/json" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - - "gorm.io/gorm" - - "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/tool" - queue "github.com/perfect-panel/ppanel-server/queue/types" - "github.com/pkg/errors" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type ResetTrafficLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Reset traffic -func NewResetTrafficLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetTrafficLogic { - return &ResetTrafficLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *ResetTrafficLogic) ResetTraffic(req *types.ResetTrafficOrderRequest) (resp *types.ResetTrafficOrderResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - // find user subscription - userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID) - if err != nil { - l.Error("[ResetTraffic] Database query error", logger.Field("error", err.Error()), logger.Field("UserSubscribeID", req.UserSubscribeID)) - return nil, errors.Wrapf(err, "find user subscribe error: %v", err.Error()) - } - if userSubscribe.Subscribe == nil { - l.Error("[ResetTraffic] subscribe not found", logger.Field("UserSubscribeID", req.UserSubscribeID)) - return nil, errors.New("subscribe not found") - } - amount := userSubscribe.Subscribe.Replacement - var deductionAmount int64 - // Check user deduction amount - if u.GiftAmount > 0 { - if u.GiftAmount >= amount { - deductionAmount = amount - amount = 0 - u.GiftAmount -= amount - } else { - deductionAmount = u.GiftAmount - amount -= u.GiftAmount - u.GiftAmount = 0 - } - } - // find payment method - payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) - if err != nil { - l.Error("[ResetTraffic] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) - return nil, errors.Wrapf(err, "find payment error: %v", err.Error()) - } - var feeAmount int64 - // Calculate the handling fee - if amount > 0 { - feeAmount = calculateFee(amount, payment) - } - // create order - orderInfo := order.Order{ - Id: 0, - ParentId: userSubscribe.OrderId, - UserId: u.Id, - OrderNo: tool.GenerateTradeNo(), - Type: 3, - Price: userSubscribe.Subscribe.Replacement, - Amount: amount + feeAmount, - GiftAmount: deductionAmount, - FeeAmount: feeAmount, - PaymentId: req.Payment, - Method: payment.Platform, - Status: 1, - SubscribeId: userSubscribe.SubscribeId, - SubscribeToken: userSubscribe.Token, - } - // Database transaction - err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error { - // update user deduction && Pre deduction ,Return after canceling the order - if orderInfo.GiftAmount > 0 { - // update user deduction && Pre deduction ,Return after canceling the order - if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { - l.Error("[ResetTraffic] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) - return err - } - // create deduction record - deductionLog := user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 2, - Balance: u.GiftAmount, - Remark: "ResetTraffic order deduction", - } - if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil { - l.Error("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog)) - return err - } - } - // insert order - return db.Model(&order.Order{}).Create(&orderInfo).Error - }) - if err != nil { - l.Error("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo)) - return nil, errors.Wrapf(err, "insert order error: %v", err.Error()) - } - // Deferred task - payload := queue.DeferCloseOrderPayload{ - OrderNo: orderInfo.OrderNo, - } - val, err := json.Marshal(payload) - if err != nil { - l.Error("[ResetTraffic] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) - } - task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) - taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) - if err != nil { - l.Error("[ResetTraffic] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) - } else { - l.Info("[ResetTraffic] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) - } - return &types.ResetTrafficOrderResponse{ - OrderNo: orderInfo.OrderNo, - }, nil -} diff --git a/internal/logic/app/payment/getAvailablePaymentMethodsLogic.go b/internal/logic/app/payment/getAvailablePaymentMethodsLogic.go deleted file mode 100644 index 8d8a6d5..0000000 --- a/internal/logic/app/payment/getAvailablePaymentMethodsLogic.go +++ /dev/null @@ -1,40 +0,0 @@ -package payment - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetAvailablePaymentMethodsLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// NewGetAvailablePaymentMethodsLogic Get available payment methods -func NewGetAvailablePaymentMethodsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAvailablePaymentMethodsLogic { - return &GetAvailablePaymentMethodsLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetAvailablePaymentMethodsLogic) GetAvailablePaymentMethods() (resp *types.GetAvailablePaymentMethodsResponse, err error) { - data, err := l.svcCtx.PaymentModel.FindAvailableMethods(l.ctx) - if err != nil { - l.Error("[GetAvailablePaymentMethods] database error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAvailablePaymentMethods: %v", err.Error()) - } - resp = &types.GetAvailablePaymentMethodsResponse{ - List: make([]types.PaymentMethod, 0), - } - tool.DeepCopy(&resp.List, data) - return -} diff --git a/internal/logic/app/subscribe/queryApplicationConfigLogic.go b/internal/logic/app/subscribe/queryApplicationConfigLogic.go deleted file mode 100644 index 4f091f4..0000000 --- a/internal/logic/app/subscribe/queryApplicationConfigLogic.go +++ /dev/null @@ -1,115 +0,0 @@ -package subscribe - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/model/application" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type QueryApplicationConfigLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get application config -func NewQueryApplicationConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryApplicationConfigLogic { - return &QueryApplicationConfigLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryApplicationConfigLogic) QueryApplicationConfig() (resp *types.ApplicationResponse, err error) { - resp = &types.ApplicationResponse{} - var applications []*application.Application - err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { - return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error - }) - if err != nil { - l.Errorw("[QueryApplicationConfig] get application error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) - } - - if len(applications) == 0 { - return resp, nil - } - - for _, app := range applications { - applicationResponse := types.ApplicationResponseInfo{ - Id: app.Id, - Name: app.Name, - Icon: app.Icon, - Description: app.Description, - SubscribeType: app.SubscribeType, - } - applicationVersions := app.ApplicationVersions - if len(applicationVersions) != 0 { - for _, applicationVersion := range applicationVersions { - /*if !applicationVersion.IsDefault { - continue - }*/ - switch applicationVersion.Platform { - case "ios": - applicationResponse.Platform.IOS = append(applicationResponse.Platform.IOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "macos": - applicationResponse.Platform.MacOS = append(applicationResponse.Platform.MacOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "linux": - applicationResponse.Platform.Linux = append(applicationResponse.Platform.Linux, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "android": - applicationResponse.Platform.Android = append(applicationResponse.Platform.Android, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "windows": - applicationResponse.Platform.Windows = append(applicationResponse.Platform.Windows, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "harmony": - applicationResponse.Platform.Harmony = append(applicationResponse.Platform.Harmony, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - } - } - } - resp.Applications = append(resp.Applications, applicationResponse) - } - return -} diff --git a/internal/logic/app/subscribe/querySubscribeGroupListLogic.go b/internal/logic/app/subscribe/querySubscribeGroupListLogic.go deleted file mode 100644 index d0f08ac..0000000 --- a/internal/logic/app/subscribe/querySubscribeGroupListLogic.go +++ /dev/null @@ -1,44 +0,0 @@ -package subscribe - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/model/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type QuerySubscribeGroupListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get subscribe group list -func NewQuerySubscribeGroupListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QuerySubscribeGroupListLogic { - return &QuerySubscribeGroupListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QuerySubscribeGroupListLogic) QuerySubscribeGroupList() (resp *types.QuerySubscribeGroupListResponse, err error) { - var list []*subscribe.Group - var total int64 - err = l.svcCtx.DB.Model(&subscribe.Group{}).Count(&total).Find(&list).Error - if err != nil { - l.Logger.Error("[QuerySubscribeGroupListLogic] get subscribe group list failed: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscribe group list failed: %v", err.Error()) - } - groupList := make([]types.SubscribeGroup, 0) - tool.DeepCopy(&groupList, list) - return &types.QuerySubscribeGroupListResponse{ - Total: total, - List: groupList, - }, nil -} diff --git a/internal/logic/app/subscribe/querySubscribeListLogic.go b/internal/logic/app/subscribe/querySubscribeListLogic.go deleted file mode 100644 index 22475d0..0000000 --- a/internal/logic/app/subscribe/querySubscribeListLogic.go +++ /dev/null @@ -1,55 +0,0 @@ -package subscribe - -import ( - "context" - "encoding/json" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type QuerySubscribeListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get subscribe list -func NewQuerySubscribeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QuerySubscribeListLogic { - return &QuerySubscribeListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QuerySubscribeListLogic) QuerySubscribeList() (resp *types.QuerySubscribeListResponse, err error) { - - data, err := l.svcCtx.SubscribeModel.QuerySubscribeList(l.ctx) - if err != nil { - l.Errorw("[QuerySubscribeListLogic] Database Error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QuerySubscribeList error: %v", err.Error()) - } - resp = &types.QuerySubscribeListResponse{ - List: make([]types.Subscribe, 0), - Total: int64(len(data)), - } - for _, v := range data { - var sub types.Subscribe - tool.DeepCopy(&sub, v) - if v.Discount != "" { - if err = json.Unmarshal([]byte(v.Discount), &sub.Discount); err != nil { - l.Errorw("[QuerySubscribeListLogic] json.Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", v.Discount)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "json.Unmarshal error: %v", err.Error()) - } - } else { - sub.Discount = make([]types.SubscribeDiscount, 0) - } - resp.List = append(resp.List, sub) - } - return -} diff --git a/internal/logic/app/subscribe/queryUserAlreadySubscribeLogic.go b/internal/logic/app/subscribe/queryUserAlreadySubscribeLogic.go deleted file mode 100644 index 330213d..0000000 --- a/internal/logic/app/subscribe/queryUserAlreadySubscribeLogic.go +++ /dev/null @@ -1,67 +0,0 @@ -package subscribe - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" -) - -type QueryUserAlreadySubscribeLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get Already subscribed to package -func NewQueryUserAlreadySubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAlreadySubscribeLogic { - return &QueryUserAlreadySubscribeLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryUserAlreadySubscribeLogic) QueryUserAlreadySubscribe() (resp *types.QueryUserSubscribeResp, err error) { - resp = &types.QueryUserSubscribeResp{ - Data: make([]types.UserSubscribeData, 0), - } - userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) - var orderIds []int64 - var subscribes []user.Subscribe - err = l.svcCtx.OrderModel.Transaction(context.Background(), func(tx *gorm.DB) error { - if err := tx.Model(&order.Order{}).Where("user_id = ? AND status in ?", userInfo.Id, []int64{2, 5}).Select("id").Find(&orderIds).Error; err != nil { - return err - } - if len(orderIds) == 0 { - return nil - } - return tx.Model(&user.Subscribe{}).Where("user_id = ? AND order_id in ?", userInfo.Id, orderIds).Order("created_at desc").Find(&subscribes).Error - }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", err.Error()) - } - if len(subscribes) == 0 { - return - } - - userAlreadySubscribe := make(map[int64]int64) - for _, subscribe := range subscribes { - userAlreadySubscribe[subscribe.SubscribeId] = subscribe.Id - } - - for k, v := range userAlreadySubscribe { - resp.Data = append(resp.Data, types.UserSubscribeData{ - SubscribeId: k, - UserSubscribeId: v, - }) - } - return -} diff --git a/internal/logic/app/subscribe/queryUserAvailableUserSubscribeLogic.go b/internal/logic/app/subscribe/queryUserAvailableUserSubscribeLogic.go deleted file mode 100644 index a41513a..0000000 --- a/internal/logic/app/subscribe/queryUserAvailableUserSubscribeLogic.go +++ /dev/null @@ -1,118 +0,0 @@ -package subscribe - -import ( - "context" - "encoding/json" - "strconv" - "strings" - "time" - - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/countryCenter" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type QueryUserAvailableUserSubscribeLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get Available subscriptions for users -func NewQueryUserAvailableUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAvailableUserSubscribeLogic { - return &QueryUserAvailableUserSubscribeLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryUserAvailableUserSubscribeLogic) QueryUserAvailableUserSubscribe(req *types.AppUserSubscribeRequest) (resp *types.AppUserSubscbribeResponse, err error) { - resp = &types.AppUserSubscbribeResponse{List: make([]types.AppUserSubcbribe, 0)} - userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) - //查询用户订阅 - subscribeDetails, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userInfo.Id, 1, 2) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get query user subscribe error: %v", err.Error()) - } - - userSubscribeMap := make(map[int64]types.AppUserSubcbribe) - for _, sd := range subscribeDetails { - userSubscribeInfo := types.AppUserSubcbribe{ - Id: sd.Id, - Name: sd.Subscribe.Name, - Traffic: sd.Traffic, - Upload: sd.Upload, - Download: sd.Download, - ExpireTime: sd.ExpireTime.Format(time.DateTime), - StartTime: sd.StartTime.Format(time.DateTime), - DeviceLimit: sd.Subscribe.DeviceLimit, - } - - //不需要查询节点 - if req.ContainsNodes == nil || !*req.ContainsNodes { - resp.List = append(resp.List, userSubscribeInfo) - continue - } - - //拿到所有订阅下的服务组id - var ids []int64 - for _, idStr := range strings.Split(sd.Subscribe.ServerGroup, ",") { - id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - continue - } - ids = append(ids, id) - } - //根据服务组id拿到所有节点 - servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, ids) - if err != nil { - l.Logger.Errorf("FindServerListByGroupIds error: %v", err.Error()) - continue - } - - for _, server := range servers { - latitude, longitude, found := countryCenter.GetCountryCenterByCountryOrCity(server.Country, server.City) - if !found { - latitude = server.Latitude - longitude = server.Longitude - } - userSubscribeInfo.List = append(userSubscribeInfo.List, types.AppUserSubscbribeNode{ - Id: server.Id, - Uuid: sd.UUID, - Traffic: sd.Traffic, - Upload: sd.Upload, - Download: sd.Download, - RelayNode: server.RelayNode, - RelayMode: server.RelayMode, - Longitude: server.Longitude, - Latitude: server.Latitude, - LatitudeCountry: latitude, - LongitudeCountry: longitude, - Tags: strings.Split(server.Tags, ","), - Config: server.Config, - ServerAddr: server.ServerAddr, - Protocol: server.Protocol, - SpeedLimit: server.SpeedLimit, - City: server.City, - Country: server.Country, - Name: server.Name, - }) - } - resp.List = append(resp.List, userSubscribeInfo) - userSubscribeMap[userSubscribeInfo.Id] = userSubscribeInfo - } - - for _, userSubscribeInfo := range userSubscribeMap { - resp.List = append(resp.List, userSubscribeInfo) - } - data, _ := json.Marshal(resp) - l.Logger.Infof("QueryUserAvailableUserSubscribe resp: %s", string(data)) - return resp, nil - -} diff --git a/internal/logic/app/subscribe/resetUserSubscribePeriodLogic.go b/internal/logic/app/subscribe/resetUserSubscribePeriodLogic.go deleted file mode 100644 index 1b5e8f5..0000000 --- a/internal/logic/app/subscribe/resetUserSubscribePeriodLogic.go +++ /dev/null @@ -1,60 +0,0 @@ -package subscribe - -import ( - "context" - "time" - - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type ResetUserSubscribePeriodLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewResetUserSubscribePeriodLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ResetUserSubscribePeriodLogic { - return &ResetUserSubscribePeriodLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *ResetUserSubscribePeriodLogic) ResetUserSubscribePeriod(req *types.UserSubscribeResetPeriodRequest) (resp *types.UserSubscribeResetPeriodResponse, err error) { - resp = &types.UserSubscribeResetPeriodResponse{} - userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) - subscribe, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", err.Error()) - } - if userInfo.Id != subscribe.UserId { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "user not authorized,subscribe not available") - } - - if time.Now().After(subscribe.ExpireTime) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeExpired), "subscribe expired") - } - - if subscribe.Traffic < 1 { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ExistAvailableTraffic), "Unlimited data plan.") - } - - if (subscribe.Download + subscribe.Upload + 10240) < subscribe.Traffic { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ExistAvailableTraffic), "There is still available traffic.") - } - - subscribe.ExpireTime = subscribe.ExpireTime.AddDate(0, -1, 0) - err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, subscribe) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update subscribe error: %v", err.Error()) - } - resp.Status = true - return -} diff --git a/internal/logic/app/user/deleteAccountLogic.go b/internal/logic/app/user/deleteAccountLogic.go deleted file mode 100644 index fdc04e3..0000000 --- a/internal/logic/app/user/deleteAccountLogic.go +++ /dev/null @@ -1,103 +0,0 @@ -package user - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type DeleteAccountLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Delete Account -func NewDeleteAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteAccountLogic { - return &DeleteAccountLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *DeleteAccountLogic) DeleteAccount(req *types.DeleteAccountRequest) error { - userInfo, exists := l.ctx.Value(constant.CtxKeyUser).(user.User) - if !exists { - return nil - } - - var account string - for _, authMethod := range userInfo.AuthMethods { - if authMethod.AuthType == req.Method { - account = authMethod.AuthIdentifier - break - } - } - if account == "" { - return nil - } - - if req.Method == "email" { - emailConfig := l.svcCtx.Config.Email - - if !emailConfig.Enable { - return errors.Wrapf(xerr.NewErrCode(xerr.EmailNotEnabled), "Email function is not enabled yet") - } - - if emailConfig.EnableVerify { - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, account) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload common.CacheKeyPayload - err = json.Unmarshal([]byte(value), &payload) - if err != nil { - l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if payload.Code != req.Code { - return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - } - } else { - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Security, account) - value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err != nil { - l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) - return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - if value == "" { - return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - - var payload common.CacheKeyPayload - if err := json.Unmarshal([]byte(value), &payload); err != nil { - l.Errorw("[SendSmsCode]: Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) - } - if payload.Code != req.Code { - return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") - } - } - err := l.svcCtx.UserModel.Delete(l.ctx, userInfo.Id) - if err != nil { - l.Errorw("update user password error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user password") - } - return nil -} diff --git a/internal/logic/app/user/getuseronlinetimestatisticslogic.go b/internal/logic/app/user/getuseronlinetimestatisticslogic.go deleted file mode 100644 index e875dfd..0000000 --- a/internal/logic/app/user/getuseronlinetimestatisticslogic.go +++ /dev/null @@ -1,115 +0,0 @@ -package user - -import ( - "context" - "sort" - "time" - - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type GetUserOnlineTimeStatisticsLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get user online time total -func NewGetUserOnlineTimeStatisticsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserOnlineTimeStatisticsLogic { - return &GetUserOnlineTimeStatisticsLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetUserOnlineTimeStatisticsLogic) GetUserOnlineTimeStatistics() (resp *types.GetUserOnlineTimeStatisticsResponse, err error) { - u := l.ctx.Value(constant.CtxKeyUser).(*user.User) - //获取历史最长在线时间 - var OnlineSeconds int64 - if err := l.svcCtx.DB.Model(user.DeviceOnlineRecord{}).Where("user_id = ?", u.Id).Select("online_seconds").Order("online_seconds desc").Limit(1).Scan(&OnlineSeconds).Error; err != nil { - l.Logger.Error(err) - } - - //获取历史连续最长在线天数 - var DurationDays int64 - if err := l.svcCtx.DB.Model(user.DeviceOnlineRecord{}).Where("user_id = ?", u.Id).Select("duration_days").Order("duration_days desc").Limit(1).Scan(&DurationDays).Error; err != nil { - l.Logger.Error(err) - } - - //获取近七天在线情况 - var userOnlineRecord []user.DeviceOnlineRecord - if err := l.svcCtx.DB.Model(&userOnlineRecord).Where("user_id = ? and created_at >= ?", u.Id, time.Now().AddDate(0, 0, -7).Format(time.DateTime)).Order("created_at desc").Find(&userOnlineRecord).Error; err != nil { - l.Logger.Error(err) - } - - //获取当前连续在线天数 - var currentContinuousDays int64 - if len(userOnlineRecord) > 0 { - currentContinuousDays = userOnlineRecord[0].DurationDays - } else { - currentContinuousDays = 1 - } - - var dates []string - for i := 0; i < 7; i++ { - date := time.Now().AddDate(0, 0, -i).Format(time.DateOnly) - dates = append(dates, date) - } - - onlineDays := make(map[string]types.WeeklyStat) - for _, record := range userOnlineRecord { - //获取近七天在线情况 - onlineTime := record.OnlineTime.Format(time.DateOnly) - if weeklyStat, ok := onlineDays[onlineTime]; ok { - weeklyStat.Hours += float64(record.OnlineSeconds) - onlineDays[onlineTime] = weeklyStat - } else { - onlineDays[onlineTime] = types.WeeklyStat{ - Hours: float64(record.OnlineSeconds), - //根据日期获取周几 - DayName: record.OnlineTime.Weekday().String(), - } - } - } - - //补全不存在的日期 - for _, date := range dates { - if _, ok := onlineDays[date]; !ok { - onlineTime, _ := time.Parse(time.DateOnly, date) - onlineDays[date] = types.WeeklyStat{ - DayName: onlineTime.Weekday().String(), - } - } - } - - var keys []string - for key := range onlineDays { - keys = append(keys, key) - } - - //排序 - sort.Strings(keys) - - var weeklyStats []types.WeeklyStat - for index, key := range keys { - weeklyStat := onlineDays[key] - weeklyStat.Day = index + 1 - weeklyStat.Hours = weeklyStat.Hours / float64(3600) - weeklyStats = append(weeklyStats, weeklyStat) - } - - resp = &types.GetUserOnlineTimeStatisticsResponse{ - WeeklyStats: weeklyStats, - ConnectionRecords: types.ConnectionRecords{ - CurrentContinuousDays: currentContinuousDays, - HistoryContinuousDays: DurationDays, - LongestSingleConnection: OnlineSeconds / 60, - }, - } - return -} diff --git a/internal/logic/app/user/getusersubscribetrafficlogslogic.go b/internal/logic/app/user/getusersubscribetrafficlogslogic.go deleted file mode 100644 index 4c8a196..0000000 --- a/internal/logic/app/user/getusersubscribetrafficlogslogic.go +++ /dev/null @@ -1,85 +0,0 @@ -package user - -import ( - "context" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - - "github.com/perfect-panel/ppanel-server/internal/model/traffic" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "gorm.io/gorm" -) - -type GetUserSubscribeTrafficLogsLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get user subcribe traffic logs -func NewGetUserSubscribeTrafficLogsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserSubscribeTrafficLogsLogic { - return &GetUserSubscribeTrafficLogsLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetUserSubscribeTrafficLogsLogic) GetUserSubscribeTrafficLogs(req *types.GetUserSubscribeTrafficLogsRequest) (resp *types.GetUserSubscribeTrafficLogsResponse, err error) { - resp = &types.GetUserSubscribeTrafficLogsResponse{} - u := l.ctx.Value(constant.CtxKeyUser).(*user.User) - var traffics []traffic.TrafficLog - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Model(traffic.TrafficLog{}).Where("user_id = ? and `timestamp` >= ? and `timestamp` < ?", u.Id, time.UnixMilli(req.StartTime), time.UnixMilli(req.EndTime)).Find(&traffics).Error - }) - - if err != nil { - l.Errorw("get user subscribe traffic logs failed", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error()) - } - - //合并多条记录为以天为单位 - trafficMap := make(map[string]*traffic.TrafficLog) - for _, traf := range traffics { - key := traf.Timestamp.Format(time.DateOnly) - existTraf := trafficMap[key] - if existTraf == nil { - trafficMap[key] = &traf - } else { - existTraf.Upload = existTraf.Download + traf.Upload - existTraf.Download = existTraf.Download + traf.Download - trafficMap[key] = existTraf - } - } - - startTime := time.UnixMilli(req.StartTime) - EndTime := time.UnixMilli(req.EndTime) - res := make(map[string]traffic.TrafficLog) - - // 循环遍历每一天 - for current := startTime; !current.After(EndTime); current = current.AddDate(0, 0, 1) { - dateStr := current.Format(time.DateOnly) // 格式化为日期字符串 - if trafficMap[dateStr] == nil { - res[dateStr] = traffic.TrafficLog{ - Timestamp: current, - } - } else { - res[dateStr] = *trafficMap[dateStr] - } - resp.List = append(resp.List, types.TrafficLog{ - Id: res[dateStr].Id, - ServerId: res[dateStr].ServerId, - Upload: res[dateStr].Upload, - Download: res[dateStr].Download, - Timestamp: res[dateStr].Timestamp.UnixMilli(), - }) - } - - return -} diff --git a/internal/logic/app/user/queryUserAffiliateListLogic.go b/internal/logic/app/user/queryUserAffiliateListLogic.go deleted file mode 100644 index 4ef7a41..0000000 --- a/internal/logic/app/user/queryUserAffiliateListLogic.go +++ /dev/null @@ -1,62 +0,0 @@ -package user - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/pkg/constant" - - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type QueryUserAffiliateListLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Query User Affiliate List -func NewQueryUserAffiliateListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAffiliateListLogic { - return &QueryUserAffiliateListLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryUserAffiliateListLogic) QueryUserAffiliateList(req *types.QueryUserAffiliateListRequest) (resp *types.QueryUserAffiliateListResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - var data []*user.User - var total int64 - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Model(&user.User{}).Order("id desc").Where("referer_id = ?", u.Id).Count(&total).Limit(req.Size).Offset((req.Page - 1) * req.Size).Find(&data).Error - }) - if err != nil { - l.Errorw("Query User Affiliate List failed: %v", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate List failed: %v", err.Error()) - } - - list := make([]types.UserAffiliate, 0) - for _, item := range data { - list = append(list, types.UserAffiliate{ - //Email: tool.MaskEmail(item.Email), - Avatar: item.Avatar, - RegisteredAt: item.CreatedAt.UnixMilli(), - Enable: *item.Enable, - }) - } - return &types.QueryUserAffiliateListResponse{ - Total: total, - List: list, - }, nil -} diff --git a/internal/logic/app/user/queryUserInfoLogic.go b/internal/logic/app/user/queryUserInfoLogic.go deleted file mode 100644 index 76fea78..0000000 --- a/internal/logic/app/user/queryUserInfoLogic.go +++ /dev/null @@ -1,63 +0,0 @@ -package user - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/pkg/constant" - - "github.com/perfect-panel/ppanel-server/internal/model/user" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type QueryUserInfoLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// query user info -func NewQueryUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserInfoLogic { - return &QueryUserInfoLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.UserInfoResponse, err error) { - u := l.ctx.Value(constant.CtxKeyUser).(*user.User) - var devices []types.UserDevice - if len(u.UserDevices) != 0 { - for _, device := range u.UserDevices { - devices = append(devices, types.UserDevice{ - Id: device.Id, - Identifier: device.Identifier, - Online: device.Online, - }) - } - } - var authMeths []types.UserAuthMethod - authMethods, err := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, u.Id) - if err == nil && len(authMeths) != 0 { - for _, as := range authMethods { - authMeths = append(authMeths, types.UserAuthMethod{ - AuthType: as.AuthType, - AuthIdentifier: as.AuthIdentifier, - }) - } - } - - resp = &types.UserInfoResponse{ - Id: u.Id, - Balance: u.Balance, - Avatar: u.Avatar, - ReferCode: u.ReferCode, - RefererId: u.RefererId, - Devices: devices, - AuthMethods: authMeths, - } - return -} diff --git a/internal/logic/app/user/queryuseraffiliatelogic.go b/internal/logic/app/user/queryuseraffiliatelogic.go deleted file mode 100644 index 839a81d..0000000 --- a/internal/logic/app/user/queryuseraffiliatelogic.go +++ /dev/null @@ -1,60 +0,0 @@ -package user - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type QueryUserAffiliateLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Query User Affiliate Count -func NewQueryUserAffiliateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserAffiliateLogic { - return &QueryUserAffiliateLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryUserAffiliateLogic) QueryUserAffiliate() (resp *types.QueryUserAffiliateCountResponse, err error) { - u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) - if !ok { - logger.Error("current user is not found in context") - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") - } - var sum int64 - var total int64 - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Model(&user.User{}).Where("referer_id = ?", u.Id).Count(&total).Find(&user.User{}).Error - }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate failed: %v", err) - } - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Model(&user.CommissionLog{}). - Where("user_id = ?", u.Id). - Select("COALESCE(SUM(amount), 0)"). - Scan(&sum).Error - }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate failed: %v", err) - } - - return &types.QueryUserAffiliateCountResponse{ - Registers: total, - TotalCommission: sum, - }, nil -} diff --git a/internal/logic/app/user/updatePasswordLogic.go b/internal/logic/app/user/updatePasswordLogic.go deleted file mode 100644 index 073600b..0000000 --- a/internal/logic/app/user/updatePasswordLogic.go +++ /dev/null @@ -1,46 +0,0 @@ -package user - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/pkg/constant" - - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type UpdatePasswordLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Update Password -func NewUpdatePasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePasswordLogic { - return &UpdatePasswordLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *UpdatePasswordLogic) UpdatePassword(req *types.UpdatePasswordRequeset) error { - userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) - - // Verify password - if !tool.VerifyPassWord(req.Password, userInfo.Password) { - return errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") - } - userInfo.Password = tool.EncodePassWord(req.NewPassword) - err := l.svcCtx.UserModel.Update(l.ctx, userInfo) - if err != nil { - l.Errorw("update user password error", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user password") - } - return err -} diff --git a/internal/logic/app/ws/appWsLogic.go b/internal/logic/app/ws/appWsLogic.go deleted file mode 100644 index 8f474da..0000000 --- a/internal/logic/app/ws/appWsLogic.go +++ /dev/null @@ -1,81 +0,0 @@ -package ws - -import ( - "context" - "net/http" - "strconv" - "time" - - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type AppWsLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// App heartbeat -func NewAppWsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AppWsLogic { - return &AppWsLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *AppWsLogic) AppWs(w http.ResponseWriter, r *http.Request, userid, identifier string) error { - //获取设备号 - if identifier == "" { - return xerr.NewErrCode(xerr.DeviceNotExist) - } - //获取用户id - userID, err := strconv.ParseInt(userid, 10, 64) - if err != nil { - return xerr.NewErrCode(xerr.UseridNotMatch) - } - - ////获取session - value := l.ctx.Value(constant.CtxKeySessionID) - if value == nil { - return xerr.NewErrCode(xerr.ErrorTokenInvalid) - } - session := value.(string) - - //获取用户 - userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) - - if userID != userInfo.Id { - return xerr.NewErrCode(xerr.UseridNotMatch) - } - - _, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier) - if err != nil { - return xerr.NewErrCode(xerr.DeviceNotExist) - } - - //if device.UserId != userInfo.Id { - // return xerr.NewErrCode(xerr.DeviceNotExist) - //} - - //默认在线设备1 - maxDevice := 3 - subscribe, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userInfo.Id, 1, 2) - if err == nil { - for _, sub := range subscribe { - if time.Now().Before(sub.ExpireTime) { - deviceLimit := int(sub.Subscribe.DeviceLimit) - if deviceLimit > maxDevice { - maxDevice = deviceLimit - } - } - } - } - l.svcCtx.DeviceManager.AddDevice(w, r, session, userID, identifier, maxDevice) - return nil -} diff --git a/internal/logic/auth/bindDeviceLogic.go b/internal/logic/auth/bindDeviceLogic.go new file mode 100644 index 0000000..19b0666 --- /dev/null +++ b/internal/logic/auth/bindDeviceLogic.go @@ -0,0 +1,234 @@ +package auth + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type BindDeviceLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewBindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindDeviceLogic { + return &BindDeviceLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// BindDeviceToUser binds a device to a user +// If the device is already bound to another user, it will disable that user and bind the device to the current user +func (l *BindDeviceLogic) BindDeviceToUser(identifier, ip, userAgent string, currentUserId int64) error { + if identifier == "" { + // No device identifier provided, skip binding + return nil + } + + l.Infow("binding device to user", + logger.Field("identifier", identifier), + logger.Field("user_id", currentUserId), + logger.Field("ip", ip), + ) + + // Check if device exists + deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Device not found, create new device record + return l.createDeviceForUser(identifier, ip, userAgent, currentUserId) + } + l.Errorw("failed to query device", + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device failed: %v", err.Error()) + } + + // Device exists, check if it's bound to current user + if deviceInfo.UserId == currentUserId { + // Already bound to current user, just update IP and UserAgent + l.Infow("device already bound to current user, updating info", + logger.Field("identifier", identifier), + logger.Field("user_id", currentUserId), + ) + deviceInfo.Ip = ip + deviceInfo.UserAgent = userAgent + if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil { + l.Errorw("failed to update device", + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error()) + } + return nil + } + + // Device is bound to another user, need to disable old user and rebind + l.Infow("device bound to another user, rebinding", + logger.Field("identifier", identifier), + logger.Field("old_user_id", deviceInfo.UserId), + logger.Field("new_user_id", currentUserId), + ) + + return l.rebindDeviceToNewUser(deviceInfo, ip, userAgent, currentUserId) +} + +func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string, userId int64) error { + l.Infow("creating new device for user", + logger.Field("identifier", identifier), + logger.Field("user_id", userId), + ) + + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Create device auth method + authMethod := &user.AuthMethods{ + UserId: userId, + AuthType: "device", + AuthIdentifier: identifier, + Verified: true, + } + if err := db.Create(authMethod).Error; err != nil { + l.Errorw("failed to create device auth method", + logger.Field("user_id", userId), + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err) + } + + // Create device record + deviceInfo := &user.Device{ + Ip: ip, + UserId: userId, + UserAgent: userAgent, + Identifier: identifier, + Enabled: true, + Online: false, + } + if err := db.Create(deviceInfo).Error; err != nil { + l.Errorw("failed to create device", + logger.Field("user_id", userId), + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device failed: %v", err) + } + + return nil + }) + + if err != nil { + l.Errorw("device creation failed", + logger.Field("identifier", identifier), + logger.Field("user_id", userId), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("device created successfully", + logger.Field("identifier", identifier), + logger.Field("user_id", userId), + ) + + return nil +} + +func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, userAgent string, newUserId int64) error { + oldUserId := deviceInfo.UserId + + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Check if old user has other auth methods besides device + var authMethods []user.AuthMethods + if err := db.Where("user_id = ?", oldUserId).Find(&authMethods).Error; err != nil { + l.Errorw("failed to query auth methods for old user", + logger.Field("old_user_id", oldUserId), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query auth methods failed: %v", err) + } + + // Count non-device auth methods + nonDeviceAuthCount := 0 + for _, auth := range authMethods { + if auth.AuthType != "device" { + nonDeviceAuthCount++ + } + } + + // Only disable old user if they have no other auth methods + if nonDeviceAuthCount == 0 { + falseVal := false + if err := db.Model(&user.User{}).Where("id = ?", oldUserId).Update("enable", &falseVal).Error; err != nil { + l.Errorw("failed to disable old user", + logger.Field("old_user_id", oldUserId), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable old user failed: %v", err) + } + + l.Infow("disabled old user (no other auth methods)", + logger.Field("old_user_id", oldUserId), + ) + } else { + l.Infow("old user has other auth methods, not disabling", + logger.Field("old_user_id", oldUserId), + logger.Field("non_device_auth_count", nonDeviceAuthCount), + ) + } + + // Update device auth method to new user + if err := db.Model(&user.AuthMethods{}). + Where("auth_type = ? AND auth_identifier = ?", "device", deviceInfo.Identifier). + Update("user_id", newUserId).Error; err != nil { + l.Errorw("failed to update device auth method", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device auth method failed: %v", err) + } + + // Update device record + deviceInfo.UserId = newUserId + deviceInfo.Ip = ip + deviceInfo.UserAgent = userAgent + deviceInfo.Enabled = true + + if err := db.Save(deviceInfo).Error; err != nil { + l.Errorw("failed to update device", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err) + } + + return nil + }) + + if err != nil { + l.Errorw("device rebinding failed", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("device rebound successfully", + logger.Field("identifier", deviceInfo.Identifier), + logger.Field("old_user_id", oldUserId), + logger.Field("new_user_id", newUserId), + ) + + return nil +} diff --git a/internal/logic/auth/checkUserLogic.go b/internal/logic/auth/checkUserLogic.go index c1503b8..a3727d2 100644 --- a/internal/logic/auth/checkUserLogic.go +++ b/internal/logic/auth/checkUserLogic.go @@ -3,10 +3,10 @@ package auth import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) diff --git a/internal/logic/auth/checkUserTelephoneLogic.go b/internal/logic/auth/checkUserTelephoneLogic.go index 9c42fc6..92aab10 100644 --- a/internal/logic/auth/checkUserTelephoneLogic.go +++ b/internal/logic/auth/checkUserTelephoneLogic.go @@ -3,14 +3,14 @@ package auth import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/phone" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/phone" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type CheckUserTelephoneLogic struct { diff --git a/internal/logic/auth/deviceLoginLogic.go b/internal/logic/auth/deviceLoginLogic.go new file mode 100644 index 0000000..8f7807a --- /dev/null +++ b/internal/logic/auth/deviceLoginLogic.go @@ -0,0 +1,295 @@ +package auth + +import ( + "context" + "fmt" + "time" + + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type DeviceLoginLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Device Login +func NewDeviceLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceLoginLogic { + return &DeviceLoginLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *types.LoginResponse, err error) { + //TODO : check device login rate limit + // Check if device login is enabled + //if !l.svcCtx.Config.Register.EnableDevice { + // return nil, xerr.NewErrMsg("Device login is disabled") + //} + + loginStatus := false + var userInfo *user.User + // Record login status + defer func() { + if userInfo != nil && userInfo.Id != 0 { + loginLog := log.Login{ + Method: "device", + LoginIP: req.IP, + UserAgent: req.UserAgent, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + } + }() + + // Check if device exists by identifier + deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Device not found, create new user and device + userInfo, err = l.registerUserAndDevice(req) + if err != nil { + return nil, err + } + } else { + l.Errorw("query device failed", + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device failed: %v", err.Error()) + } + } else { + // Device found, get user info + userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, deviceInfo.UserId) + if err != nil { + l.Errorw("query user failed", + logger.Field("user_id", deviceInfo.UserId), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user failed: %v", err.Error()) + } + } + + // Generate session id + sessionId := uuidx.NewUUID().String() + + // Generate token + token, err := jwt.NewJwtToken( + l.svcCtx.Config.JwtAuth.AccessSecret, + time.Now().Unix(), + l.svcCtx.Config.JwtAuth.AccessExpire, + jwt.WithOption("UserId", userInfo.Id), + jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", "device"), + ) + if err != nil { + l.Errorw("token generate error", + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + } + + // Store session id in redis + sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { + l.Errorw("set session id error", + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) + } + + loginStatus = true + return &types.LoginResponse{ + Token: token, + }, nil +} + +func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest) (*user.User, error) { + l.Infow("device not found, creating new user and device", + logger.Field("identifier", req.Identifier), + logger.Field("ip", req.IP), + ) + + var userInfo *user.User + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Create new user + userInfo = &user.User{ + OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, + } + if err := db.Create(userInfo).Error; err != nil { + l.Errorw("failed to create user", + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user failed: %v", err) + } + + // Update refer code + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + l.Errorw("failed to update refer code", + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err) + } + + // Create device auth method + authMethod := &user.AuthMethods{ + UserId: userInfo.Id, + AuthType: "device", + AuthIdentifier: req.Identifier, + Verified: true, + } + if err := db.Create(authMethod).Error; err != nil { + l.Errorw("failed to create device auth method", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err) + } + + // Insert device record + deviceInfo := &user.Device{ + Ip: req.IP, + UserId: userInfo.Id, + UserAgent: req.UserAgent, + Identifier: req.Identifier, + Enabled: true, + Online: false, + } + if err := db.Create(deviceInfo).Error; err != nil { + l.Errorw("failed to insert device", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert device failed: %v", err) + } + + // Activate trial if enabled + if l.svcCtx.Config.Register.EnableTrial { + if err := l.activeTrial(userInfo.Id, db); err != nil { + return err + } + } + + return nil + }) + + if err != nil { + l.Errorw("device registration failed", + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + return nil, err + } + + l.Infow("device registration completed successfully", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("refer_code", userInfo.ReferCode), + ) + + // Register log + registerLog := log.Register{ + AuthMethod: "device", + Identifier: req.Identifier, + RegisterIP: req.IP, + UserAgent: req.UserAgent, + Timestamp: time.Now().UnixMilli(), + } + content, _ := registerLog.Marshal() + + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeRegister.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert register log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + + return userInfo, nil +} + +func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) error { + sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe) + if err != nil { + l.Errorw("failed to find trial subscription template", + logger.Field("user_id", userId), + logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe), + logger.Field("error", err.Error()), + ) + return err + } + + startTime := time.Now() + expireTime := tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, startTime) + subscribeToken := uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", userId)) + subscribeUUID := uuidx.NewUUID().String() + + userSub := &user.Subscribe{ + UserId: userId, + OrderId: 0, + SubscribeId: sub.Id, + StartTime: startTime, + ExpireTime: expireTime, + Traffic: sub.Traffic, + Download: 0, + Upload: 0, + Token: subscribeToken, + UUID: subscribeUUID, + Status: 1, + } + + if err := db.Create(userSub).Error; err != nil { + l.Errorw("failed to insert trial subscription", + logger.Field("user_id", userId), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("trial subscription activated successfully", + logger.Field("user_id", userId), + logger.Field("subscribe_id", sub.Id), + logger.Field("expire_time", expireTime), + logger.Field("traffic", sub.Traffic), + ) + + return nil +} diff --git a/internal/logic/auth/oauth/appleLoginCallbackLogic.go b/internal/logic/auth/oauth/appleLoginCallbackLogic.go index 38c825f..333c9cd 100644 --- a/internal/logic/auth/oauth/appleLoginCallbackLogic.go +++ b/internal/logic/auth/oauth/appleLoginCallbackLogic.go @@ -6,9 +6,9 @@ import ( "net/http" "net/url" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type AppleLoginCallbackLogic struct { diff --git a/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go b/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go index 4fd9833..4e12d2f 100644 --- a/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go +++ b/internal/logic/auth/oauth/oAuthLoginGetTokenLogic.go @@ -6,24 +6,34 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/auth" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/jwt" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/oauth/apple" - "github.com/perfect-panel/ppanel-server/pkg/oauth/google" - "github.com/perfect-panel/ppanel-server/pkg/oauth/telegram" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/oauth/apple" + "github.com/perfect-panel/server/pkg/oauth/google" + "github.com/perfect-panel/server/pkg/oauth/telegram" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) -type googleRequest struct { +const ( + OAuthGoogle = "google" + OAuthApple = "apple" + OAuthTelegram = "telegram" + AuthEmail = "email" + AuthExpire = 86400 + TelegramDomain = "ppanel.com" +) + +type oauthRequest struct { Code string `json:"code"` State string `json:"state"` } @@ -43,38 +53,524 @@ func NewOAuthLoginGetTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) } func (l *OAuthLoginGetTokenLogic) OAuthLoginGetToken(req *types.OAuthLoginGetTokenRequest, ip, userAgent string) (resp *types.LoginResponse, err error) { + requestID := uuidx.NewUUID().String() loginStatus := false var userInfo *user.User - // Record login status - defer func(svcCtx *svc.ServiceContext) { - if userInfo != nil && userInfo.Id != 0 { - if err := svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ - UserId: userInfo.Id, - LoginIP: ip, - UserAgent: userAgent, - Success: &loginStatus, - }); err != nil { - l.Errorw("error insert login log: %v", logger.Field("error", err.Error())) - } - } - }(l.svcCtx) - switch req.Method { - case "google": - userInfo, err = l.google(req) - case "apple": - userInfo, err = l.apple(req) - case "telegram": - userInfo, err = l.telegram(req) - default: - l.Errorw("oauth login method not support: %v", logger.Field("method", req.Method)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "oauth login method not support: %v", req.Method) - } + + l.Infow("oauth login request started", + logger.Field("request_id", requestID), + logger.Field("method", req.Method), + logger.Field("ip", ip), + logger.Field("user_agent", userAgent), + ) + + defer func() { + l.recordLoginStatus(loginStatus, userInfo, ip, userAgent, requestID, req.Method) + }() + + userInfo, err = l.handleOAuthProvider(req, requestID, ip, userAgent) if err != nil { return nil, err } - // Generate session id + + token, err := l.generateToken(userInfo, requestID) + if err != nil { + return nil, err + } + + loginStatus = true + return &types.LoginResponse{Token: token}, nil +} + +func (l *OAuthLoginGetTokenLogic) google(req *types.OAuthLoginGetTokenRequest, requestID, ip, userAgent string) (*user.User, error) { + startTime := time.Now() + l.Infow("google oauth processing started", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + ) + + var request oauthRequest + if err := tool.CloneMapToStruct(req.Callback.(map[string]interface{}), &request); err != nil { + l.Errorw("failed to parse google callback data", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "parse callback data failed: %v", err) + } + + l.Debugw("google oauth state validation started", + logger.Field("request_id", requestID), + logger.Field("state", request.State), + ) + + redirect, err := l.validateStateCode(OAuthGoogle, request.State, requestID) + if err != nil { + return nil, err + } + + cfg, err := l.getGoogleConfig(requestID) + if err != nil { + return nil, err + } + + client := google.New(&google.Config{ + ClientID: cfg.ClientId, + ClientSecret: cfg.ClientSecret, + RedirectURL: redirect, + }) + + l.Debugw("exchanging google authorization code for token", + logger.Field("request_id", requestID), + logger.Field("redirect_url", redirect), + ) + + token, err := client.Exchange(l.ctx, request.Code) + if err != nil { + l.Errorw("failed to exchange google authorization code", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "exchange token failed: %v", err) + } + + l.Debugw("fetching google user information", + logger.Field("request_id", requestID), + ) + + googleUserInfo, err := client.GetUserInfo(token.AccessToken) + if err != nil { + l.Errorw("failed to get google user info", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get user info failed: %v", err) + } + + l.Infow("google oauth processing completed", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("openid", googleUserInfo.OpenID), + logger.Field("email", googleUserInfo.Email), + logger.Field("duration_ms", time.Since(startTime).Milliseconds()), + ) + + return l.findOrRegisterUser(OAuthGoogle, googleUserInfo.OpenID, googleUserInfo.Email, googleUserInfo.Picture, requestID, ip, userAgent) +} + +func (l *OAuthLoginGetTokenLogic) apple(req *types.OAuthLoginGetTokenRequest, requestID, ip, userAgent string) (*user.User, error) { + startTime := time.Now() + l.Infow("apple oauth processing started", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + ) + + callback := req.Callback.(map[string]interface{}) + state, _ := callback["state"].(string) + code, _ := callback["code"].(string) + + l.Debugw("apple oauth state validation started", + logger.Field("request_id", requestID), + logger.Field("state", state), + ) + + if _, err := l.validateStateCode(OAuthApple, state, requestID); err != nil { + return nil, err + } + + cfg, err := l.getAppleConfig(requestID) + if err != nil { + return nil, err + } + + client, err := apple.New(apple.Config{ + ClientID: cfg.ClientId, + TeamID: cfg.TeamID, + KeyID: cfg.KeyID, + ClientSecret: cfg.ClientSecret, + RedirectURI: cfg.RedirectURL, + }) + if err != nil { + l.Errorw("failed to create apple client", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "new apple client failed: %v", err) + } + + l.Debugw("verifying apple web token", + logger.Field("request_id", requestID), + ) + + resp, err := client.VerifyWebToken(l.ctx, code) + if err != nil { + l.Errorw("failed to verify apple web token", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "verify web token failed: %v", err) + } + + if resp.Error != "" { + l.Errorw("apple web token verification returned error", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("apple_error", resp.Error), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "verify web token failed: %v", resp.Error) + } + + appleUnique, err := apple.GetUniqueID(resp.IDToken) + if err != nil { + l.Errorw("failed to get apple unique id", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple unique id failed: %v", err) + } + + appleUserInfo, err := apple.GetClaims(resp.AccessToken) + if err != nil { + l.Errorw("failed to get apple user claims", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple user info failed: %v", err) + } + + email := "" + if emailVal, ok := (*appleUserInfo)["email"]; ok { + email, _ = emailVal.(string) + } + + l.Infow("apple oauth processing completed", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("unique_id", appleUnique), + logger.Field("email", email), + logger.Field("duration_ms", time.Since(startTime).Milliseconds()), + ) + + return l.findOrRegisterUser(OAuthApple, appleUnique, email, "", requestID, ip, userAgent) +} + +func (l *OAuthLoginGetTokenLogic) telegram(req *types.OAuthLoginGetTokenRequest, requestID, ip, userAgent string) (*user.User, error) { + startTime := time.Now() + l.Infow("telegram oauth processing started", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + ) + + cfg, err := l.getTelegramConfig(requestID) + if err != nil { + return nil, err + } + + encodeText, _ := req.Callback.(map[string]interface{})["tgAuthResult"].(string) + l.Debugw("parsing telegram callback data", + logger.Field("request_id", requestID), + logger.Field("data_length", len(encodeText)), + ) + + callbackData, err := telegram.ParseAndValidateBase64([]byte(encodeText), cfg.BotToken) + if err != nil { + l.Errorw("failed to parse telegram callback data", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "parse telegram callback failed: %v", err) + } + + l.Debugw("validating telegram auth date", + logger.Field("request_id", requestID), + logger.Field("auth_date", *callbackData.AuthDate), + logger.Field("current_time", time.Now().Unix()), + ) + + if time.Now().Unix()-*callbackData.AuthDate > AuthExpire { + l.Errorw("telegram auth date expired", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + logger.Field("auth_date", *callbackData.AuthDate), + logger.Field("current_time", time.Now().Unix()), + logger.Field("expire_seconds", AuthExpire), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "auth date expired") + } + + userID := fmt.Sprintf("%v", *callbackData.Id) + email := fmt.Sprintf("%v@%s", *callbackData.Id, TelegramDomain) + avatar := "" + if callbackData.PhotoUrl != nil { + avatar = *callbackData.PhotoUrl + } + + l.Infow("telegram oauth processing completed", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + logger.Field("user_id", userID), + logger.Field("email", email), + logger.Field("duration_ms", time.Since(startTime).Milliseconds()), + ) + + return l.findOrRegisterUser(OAuthTelegram, userID, email, avatar, requestID, ip, userAgent) +} + +func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, requestID, ip, userAgent string) (*user.User, error) { + startTime := time.Now() + l.Infow("user registration started", + logger.Field("request_id", requestID), + logger.Field("auth_method", method), + logger.Field("email", email), + logger.Field("openid", openid), + ) + + if l.svcCtx.Config.Invite.ForcedInvite { + l.Errorw("registration blocked due to forced invite policy", + logger.Field("request_id", requestID), + logger.Field("auth_method", method), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is required") + } + + var userInfo *user.User + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + if email != "" { + l.Debugw("checking if email already exists", + logger.Field("request_id", requestID), + logger.Field("email", email), + ) + if err := l.checkEmailExists(db, email, requestID); err != nil { + return err + } + } + + l.Debugw("creating new user record", + logger.Field("request_id", requestID), + logger.Field("avatar", avatar), + ) + + userInfo = &user.User{Avatar: avatar, OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase} + if err := db.Create(userInfo).Error; err != nil { + l.Errorw("failed to create user record", + logger.Field("request_id", requestID), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user info failed: %v", err) + } + + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + l.Debugw("updating user refer code", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("refer_code", userInfo.ReferCode), + ) + + if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + l.Errorw("failed to update refer code", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err) + } + + if err := l.createAuthMethod(db, userInfo.Id, method, openid, requestID); err != nil { + return err + } + + if email != "" { + if err := l.createAuthMethod(db, userInfo.Id, AuthEmail, email, requestID); err != nil { + return err + } + } + + if l.svcCtx.Config.Register.EnableTrial { + l.Debugw("activating trial subscription", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + ) + if err := l.activeTrial(userInfo.Id, requestID); err != nil { + return err + } + } + + return nil + }) + + if err != nil { + l.Errorw("user registration failed", + logger.Field("request_id", requestID), + logger.Field("auth_method", method), + logger.Field("error", err.Error()), + logger.Field("duration_ms", time.Since(startTime).Milliseconds()), + ) + return userInfo, err + } + + l.Infow("user registration completed successfully", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("auth_method", method), + logger.Field("email", email), + logger.Field("refer_code", userInfo.ReferCode), + logger.Field("duration_ms", time.Since(startTime).Milliseconds()), + ) + + // Register log + registerLog := log.Register{ + AuthMethod: method, + Identifier: openid, + RegisterIP: ip, + UserAgent: userAgent, + Timestamp: time.Now().UnixMilli(), + } + content, _ := registerLog.Marshal() + + err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeRegister.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }) + if err != nil { + l.Errorw("failed to insert register log", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("ip", ip), + logger.Field("error", err.Error()), + ) + } + + return userInfo, err +} + +func (l *OAuthLoginGetTokenLogic) checkEmailExists(db *gorm.DB, email, requestID string) error { + var methodInfo user.AuthMethods + err := db.Model(&user.AuthMethods{}).Where("auth_identifier = ?", email).First(&methodInfo).Error + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + l.Errorw("failed to check email existence", + logger.Field("request_id", requestID), + logger.Field("email", email), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "check email exists failed: %v", err) + } + if methodInfo.UserId != 0 { + l.Errorw("email already exists for another user", + logger.Field("request_id", requestID), + logger.Field("email", email), + logger.Field("existing_user_id", methodInfo.UserId), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "user email exist: %v", email) + } + l.Debugw("email availability confirmed", + logger.Field("request_id", requestID), + logger.Field("email", email), + ) + return nil +} + +func (l *OAuthLoginGetTokenLogic) createAuthMethod(db *gorm.DB, userID int64, authType, identifier, requestID string) error { + l.Debugw("creating auth method", + logger.Field("request_id", requestID), + logger.Field("user_id", userID), + logger.Field("auth_type", authType), + logger.Field("identifier", identifier), + ) + + authMethod := &user.AuthMethods{ + UserId: userID, + AuthType: authType, + AuthIdentifier: identifier, + Verified: true, + } + if err := db.Create(authMethod).Error; err != nil { + l.Errorw("failed to create auth method", + logger.Field("request_id", requestID), + logger.Field("user_id", userID), + logger.Field("auth_type", authType), + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create auth method failed: %v", err) + } + + l.Debugw("auth method created successfully", + logger.Field("request_id", requestID), + logger.Field("user_id", userID), + logger.Field("auth_type", authType), + logger.Field("auth_method_id", authMethod.Id), + ) + return nil +} + +func (l *OAuthLoginGetTokenLogic) recordLoginStatus(loginStatus bool, userInfo *user.User, ip, userAgent, requestID, authType string) { + + if userInfo != nil && userInfo.Id != 0 { + loginLog := log.Login{ + Method: authType, + LoginIP: ip, + UserAgent: userAgent, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("ip", ip), + logger.Field("error", err.Error()), + ) + } + } +} + +func (l *OAuthLoginGetTokenLogic) handleOAuthProvider(req *types.OAuthLoginGetTokenRequest, requestID, ip, userAgent string) (*user.User, error) { + l.Debugw("handling oauth provider", + logger.Field("request_id", requestID), + logger.Field("provider", req.Method), + ) + + switch req.Method { + case OAuthGoogle: + return l.google(req, requestID, ip, userAgent) + case OAuthApple: + return l.apple(req, requestID, ip, userAgent) + case OAuthTelegram: + return l.telegram(req, requestID, ip, userAgent) + default: + l.Errorw("unsupported oauth login method", + logger.Field("request_id", requestID), + logger.Field("method", req.Method), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "oauth login method not supported: %v", req.Method) + } +} + +func (l *OAuthLoginGetTokenLogic) generateToken(userInfo *user.User, requestID string) (string, error) { + startTime := time.Now() sessionId := uuidx.NewUUID().String() - // Generate token + + l.Debugw("generating jwt token", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("session_id", sessionId), + ) + token, err := jwt.NewJwtToken( l.svcCtx.Config.JwtAuth.AccessSecret, time.Now().Unix(), @@ -83,262 +579,284 @@ func (l *OAuthLoginGetTokenLogic) OAuthLoginGetToken(req *types.OAuthLoginGetTok jwt.WithOption("SessionId", sessionId), ) if err != nil { - l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) + l.Errorw("failed to generate jwt token", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err) } sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) - } - loginStatus = true - return &types.LoginResponse{ - Token: token, - }, nil -} - -func (l *OAuthLoginGetTokenLogic) google(req *types.OAuthLoginGetTokenRequest) (*user.User, error) { - var request googleRequest - err := tool.CloneMapToStruct(req.Callback.(map[string]interface{}), &request) - if err != nil { - l.Errorw("error CloneMapToStruct: %v", logger.Field("error", err.Error())) - return nil, err - } - // validate the state code - redirect, err := l.svcCtx.Redis.Get(l.ctx, fmt.Sprintf("google:%s", request.State)).Result() - if err != nil { - l.Errorw("error get google state code: %v", logger.Field("error", err.Error())) - return nil, err - } - // get google config - authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "google") - if err != nil { - l.Errorw("error find google auth method: %v", logger.Field("error", err.Error())) - return nil, err - } - var cfg auth.GoogleAuthConfig - err = cfg.Unmarshal(authMethod.Config) - if err != nil { - l.Errorw("error unmarshal google config: %v", logger.Field("config", authMethod.Config), logger.Field("error", err.Error())) - return nil, err - } - client := google.New(&google.Config{ - ClientID: cfg.ClientId, - ClientSecret: cfg.ClientSecret, - RedirectURL: redirect, - }) - token, err := client.Exchange(l.ctx, request.Code) - if err != nil { - l.Errorw("error exchange google token: %v", logger.Field("error", err.Error())) - return nil, err - } - googleUserInfo, err := client.GetUserInfo(token.AccessToken) - if err != nil { - l.Errorw("error get google user info: %v", logger.Field("error", err.Error())) - return nil, err - } - // query user info - userAuthMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "google", googleUserInfo.OpenID) - if err != nil { - if errors.As(err, &gorm.ErrRecordNotFound) { - return l.register(googleUserInfo.Email, googleUserInfo.Picture, "google", googleUserInfo.OpenID) - } - return nil, err - } - return l.svcCtx.UserModel.FindOne(l.ctx, userAuthMethod.UserId) -} - -func (l *OAuthLoginGetTokenLogic) apple(req *types.OAuthLoginGetTokenRequest) (*user.User, error) { - // validate the state code - _, err := l.svcCtx.Redis.Get(l.ctx, fmt.Sprintf("apple:%s", req.Callback.(map[string]interface{})["state"])).Result() - if err != nil { - l.Errorw("[AppleLoginCallback] Get State code error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple state code failed: %v", err.Error()) - } - appleAuth, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "apple") - if err != nil { - l.Errorw("[AppleLoginCallback] FindOneByMethod error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find apple auth method failed: %v", err.Error()) - } - var appleCfg auth.AppleAuthConfig - err = appleCfg.Unmarshal(appleAuth.Config) - if err != nil { - l.Errorw("[AppleLoginCallback] Unmarshal error", logger.Field("error", err.Error()), logger.Field("config", appleAuth.Config)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal apple config failed: %v", err.Error()) - } - - client, err := apple.New(apple.Config{ - ClientID: appleCfg.ClientId, - TeamID: appleCfg.TeamID, - KeyID: appleCfg.KeyID, - ClientSecret: appleCfg.ClientSecret, - RedirectURI: appleCfg.RedirectURL, - }) - if err != nil { - l.Errorw("[AppleLoginCallback] New apple client error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "new apple client failed: %v", err.Error()) - } - // verify web token - resp, err := client.VerifyWebToken(l.ctx, req.Callback.(map[string]interface{})["code"].(string)) - if err != nil { - l.Errorw("[AppleLoginCallback] VerifyWebToken error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "verify web token failed: %v", err.Error()) - } - if resp.Error != "" { - l.Errorw("[AppleLoginCallback] VerifyWebToken error", logger.Field("error", resp.Error)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "verify web token failed: %v", resp.Error) - } - // query apple user unique id - appleUnique, err := apple.GetUniqueID(resp.IDToken) - if err != nil { - l.Errorw("[AppleLoginCallback] GetUniqueID error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple unique id failed: %v", err.Error()) - } - // get apple user info - appleUserInfo, err := apple.GetClaims(resp.AccessToken) - if err != nil { - l.Errorw("[AppleLoginCallback] GetClaims error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get apple user info failed: %v", err.Error()) - } - // query user by apple unique id - userAuthMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "apple", appleUnique) - if err != nil { - // if user not exist, handle register - if errors.Is(err, gorm.ErrRecordNotFound) { - return l.register((*appleUserInfo)["email"].(string), "", "apple", appleUnique) - } - l.Errorw("[AppleLoginCallback] FindUserAuthMethodByOpenID error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user auth method by openid failed: %v", err.Error()) - } - // query user info - userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, userAuthMethod.UserId) - - if err != nil { - l.Errorw( - "[AppleLoginCallback] FindOne error", + l.Errorw("failed to cache session id", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("session_id", sessionId), logger.Field("error", err.Error()), ) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user info failed: %v", err.Error()) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err) } - return userInfo, nil + l.Infow("jwt token generated successfully", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("session_id", sessionId), + logger.Field("duration_ms", time.Since(startTime).Milliseconds()), + ) + + return token, nil } -func (l *OAuthLoginGetTokenLogic) telegram(req *types.OAuthLoginGetTokenRequest) (*user.User, error) { - appleAuth, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, "telegram") +func (l *OAuthLoginGetTokenLogic) validateStateCode(provider, state, requestID string) (string, error) { + stateKey := fmt.Sprintf("%s:%s", provider, state) + l.Debugw("validating oauth state code", + logger.Field("request_id", requestID), + logger.Field("provider", provider), + logger.Field("state_key", stateKey), + ) + + redirect, err := l.svcCtx.Redis.Get(l.ctx, stateKey).Result() if err != nil { - l.Errorw("[OAuthLoginGetToken] FindOneByMethod error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find telegram auth method failed: %v", err.Error()) + l.Errorw("failed to validate state code", + logger.Field("request_id", requestID), + logger.Field("provider", provider), + logger.Field("state_key", stateKey), + logger.Field("error", err.Error()), + ) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "get %s state code failed: %v", provider, err) } - var telegramCfg auth.TelegramAuthConfig - err = json.Unmarshal([]byte(appleAuth.Config), &telegramCfg) + + l.Debugw("state code validated successfully", + logger.Field("request_id", requestID), + logger.Field("provider", provider), + logger.Field("redirect_url", redirect), + ) + return redirect, nil +} + +func (l *OAuthLoginGetTokenLogic) getGoogleConfig(requestID string) (*auth.GoogleAuthConfig, error) { + l.Debugw("fetching google oauth config", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + ) + + authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, OAuthGoogle) if err != nil { - l.Errorw("[OAuthLoginGetToken] Unmarshal error", logger.Field("error", err.Error()), logger.Field("config", appleAuth.Config)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal telegram config failed: %v", err.Error()) + l.Errorw("failed to find google auth method", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find google auth method failed: %v", err) } - encodeText := req.Callback.(map[string]interface{})["tgAuthResult"].(string) - // base64 decode - callbackData, err := telegram.ParseAndValidateBase64([]byte(encodeText), telegramCfg.BotToken) + + var cfg auth.GoogleAuthConfig + if err = cfg.Unmarshal(authMethod.Config); err != nil { + l.Errorw("failed to unmarshal google config", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("config", authMethod.Config), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal google config failed: %v", err) + } + + l.Debugw("google oauth config loaded successfully", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthGoogle), + logger.Field("client_id", cfg.ClientId), + ) + return &cfg, nil +} + +func (l *OAuthLoginGetTokenLogic) getAppleConfig(requestID string) (*auth.AppleAuthConfig, error) { + l.Debugw("fetching apple oauth config", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + ) + + authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, OAuthApple) if err != nil { - l.Errorw("[TelegramLoginCallback] ParseAndValidateBase64 error", logger.Field("error", err.Error())) - return nil, err + l.Errorw("failed to find apple auth method", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find apple auth method failed: %v", err) } - // 验证数据有效期 - if time.Now().Unix()-*callbackData.AuthDate > 86400 { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "auth date expired") + + var cfg auth.AppleAuthConfig + if err = cfg.Unmarshal(authMethod.Config); err != nil { + l.Errorw("failed to unmarshal apple config", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("config", authMethod.Config), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal apple config failed: %v", err) } - // query user auth info - userAuthMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "telegram", fmt.Sprintf("%v", *callbackData.Id)) + + l.Debugw("apple oauth config loaded successfully", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthApple), + logger.Field("client_id", cfg.ClientId), + logger.Field("team_id", cfg.TeamID), + ) + return &cfg, nil +} + +func (l *OAuthLoginGetTokenLogic) getTelegramConfig(requestID string) (*auth.TelegramAuthConfig, error) { + l.Debugw("fetching telegram oauth config", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + ) + + authMethod, err := l.svcCtx.AuthModel.FindOneByMethod(l.ctx, OAuthTelegram) if err != nil { - if errors.As(err, &gorm.ErrRecordNotFound) { - return l.register(fmt.Sprintf("%v@%s", *callbackData.Id, "qq.com"), *callbackData.PhotoUrl, "telegram", fmt.Sprintf("%v", callbackData.Id)) + l.Errorw("failed to find telegram auth method", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find telegram auth method failed: %v", err) + } + + var cfg auth.TelegramAuthConfig + if err = json.Unmarshal([]byte(authMethod.Config), &cfg); err != nil { + l.Errorw("failed to unmarshal telegram config", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + logger.Field("config", authMethod.Config), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unmarshal telegram config failed: %v", err) + } + + l.Debugw("telegram oauth config loaded successfully", + logger.Field("request_id", requestID), + logger.Field("provider", OAuthTelegram), + ) + return &cfg, nil +} + +func (l *OAuthLoginGetTokenLogic) findOrRegisterUser(authType, openID, email, avatar, requestID, ip, userAgent string) (*user.User, error) { + l.Debugw("finding or registering user", + logger.Field("request_id", requestID), + logger.Field("auth_type", authType), + logger.Field("openid", openID), + logger.Field("email", email), + ) + + userAuthMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, authType, openID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + l.Infow("user not found, starting registration", + logger.Field("request_id", requestID), + logger.Field("auth_type", authType), + logger.Field("openid", openID), + logger.Field("email", email), + ) + return l.register(email, avatar, authType, openID, requestID, ip, userAgent) } + l.Errorw("failed to find user auth method by openid", + logger.Field("request_id", requestID), + logger.Field("auth_type", authType), + logger.Field("openid", openID), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user auth method by openid failed: %v", err) } - // query user info + + l.Debugw("found existing user auth method", + logger.Field("request_id", requestID), + logger.Field("auth_type", authType), + logger.Field("user_id", userAuthMethod.UserId), + ) + userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, userAuthMethod.UserId) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user info failed: %v", err.Error()) + l.Errorw("failed to find user by id", + logger.Field("request_id", requestID), + logger.Field("user_id", userAuthMethod.UserId), + logger.Field("error", err.Error()), + ) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user info failed: %v", err) } + + l.Infow("existing user found successfully", + logger.Field("request_id", requestID), + logger.Field("user_id", userInfo.Id), + logger.Field("auth_type", authType), + ) + return userInfo, nil } -func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid string) (*user.User, error) { - if l.svcCtx.Config.Invite.ForcedInvite { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is required") - } - var userInfo *user.User - err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - err := db.Model(&user.User{}).Where("email = ?", email).First(&userInfo).Error - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - if userInfo.Id != 0 { - return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "user email exist: %v", email) - } - userInfo = &user.User{ - Avatar: avatar, - } - if err := db.Create(userInfo).Error; err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user info failed: %v", err.Error()) - } - // Generate ReferCode - userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) - // Update ReferCode - err = db.Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err.Error()) - } - authMethod := &user.AuthMethods{ - UserId: userInfo.Id, - AuthType: method, - AuthIdentifier: openid, - Verified: true, - } - if err = db.Create(authMethod).Error; err != nil { - l.Errorw("error create auth method: %v", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create auth method failed: %v", err.Error()) - } - if email != "" { - authMethod = &user.AuthMethods{ - UserId: userInfo.Id, - AuthType: "email", - AuthIdentifier: email, - Verified: true, - } - if err := db.Create(authMethod).Error; err != nil { - l.Errorw("error create auth method: %v", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create auth method failed: %v", err.Error()) - } - } - if l.svcCtx.Config.Register.EnableTrial { - // Active trial - if err = l.activeTrial(userInfo.Id); err != nil { - return err - } - } - return nil - }) - return userInfo, err -} +func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) error { + l.Debugw("fetching trial subscription template", + logger.Field("request_id", requestID), + logger.Field("user_id", uid), + logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe), + ) -func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64) error { sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe) if err != nil { + l.Errorw("failed to find trial subscription template", + logger.Field("request_id", requestID), + logger.Field("user_id", uid), + logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe), + logger.Field("error", err.Error()), + ) return err } + + startTime := time.Now() + expireTime := tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, startTime) + subscribeToken := uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", uid)) + subscribeUUID := uuidx.NewUUID().String() + + l.Debugw("creating trial subscription", + logger.Field("request_id", requestID), + logger.Field("user_id", uid), + logger.Field("subscribe_id", sub.Id), + logger.Field("start_time", startTime), + logger.Field("expire_time", expireTime), + logger.Field("traffic", sub.Traffic), + logger.Field("token", subscribeToken), + logger.Field("uuid", subscribeUUID), + ) + userSub := &user.Subscribe{ Id: 0, UserId: uid, OrderId: 0, SubscribeId: sub.Id, - StartTime: time.Now(), - ExpireTime: tool.AddTime(l.svcCtx.Config.Register.TrialTimeUnit, l.svcCtx.Config.Register.TrialTime, time.Now()), + StartTime: startTime, + ExpireTime: expireTime, Traffic: sub.Traffic, Download: 0, Upload: 0, - Token: uuidx.SubscribeToken(fmt.Sprintf("Trial-%v", uid)), - UUID: uuidx.NewUUID().String(), + Token: subscribeToken, + UUID: subscribeUUID, Status: 1, } - return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub) + + if err := l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub); err != nil { + l.Errorw("failed to insert trial subscription", + logger.Field("request_id", requestID), + logger.Field("user_id", uid), + logger.Field("error", err.Error()), + ) + return err + } + + l.Infow("trial subscription activated successfully", + logger.Field("request_id", requestID), + logger.Field("user_id", uid), + logger.Field("subscribe_id", sub.Id), + logger.Field("expire_time", expireTime), + logger.Field("traffic", sub.Traffic), + ) + return nil } diff --git a/internal/logic/auth/oauth/oAuthLoginLogic.go b/internal/logic/auth/oauth/oAuthLoginLogic.go index 5d2e6a4..063ed1d 100644 --- a/internal/logic/auth/oauth/oAuthLoginLogic.go +++ b/internal/logic/auth/oauth/oAuthLoginLogic.go @@ -6,14 +6,14 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/internal/model/auth" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/oauth/google" - "github.com/perfect-panel/ppanel-server/pkg/oauth/telegram" - "github.com/perfect-panel/ppanel-server/pkg/random" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/oauth/google" + "github.com/perfect-panel/server/pkg/oauth/telegram" + "github.com/perfect-panel/server/pkg/random" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "golang.org/x/oauth2" ) diff --git a/internal/logic/auth/resetPasswordLogic.go b/internal/logic/auth/resetPasswordLogic.go index 2ea5bc7..c4e2ece 100644 --- a/internal/logic/auth/resetPasswordLogic.go +++ b/internal/logic/auth/resetPasswordLogic.go @@ -6,20 +6,21 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/jwt" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" ) type ResetPasswordLogic struct { @@ -43,13 +44,26 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res defer func() { if userInfo.Id != 0 && loginStatus { - if err := l.svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ - UserId: userInfo.Id, + loginLog := log.Login{ + Method: "email", LoginIP: req.IP, UserAgent: req.UserAgent, - Success: &loginStatus, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Id: 0, + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), }); err != nil { - l.Logger.Error("[ResetPassword] insert login log error", logger.Field("error", err.Error())) + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) } } }() @@ -93,6 +107,22 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res if err := l.svcCtx.UserModel.Update(l.ctx, userInfo); err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update user info failed: %v", err.Error()) } + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -102,6 +132,7 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) diff --git a/internal/logic/auth/telephoneLoginLogic.go b/internal/logic/auth/telephoneLoginLogic.go index 4f9dfc3..737157f 100644 --- a/internal/logic/auth/telephoneLoginLogic.go +++ b/internal/logic/auth/telephoneLoginLogic.go @@ -7,18 +7,19 @@ import ( "net/http" "time" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/jwt" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/phone" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/phone" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) @@ -51,13 +52,26 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r // Record login status defer func(svcCtx *svc.ServiceContext) { if userInfo.Id != 0 { - if err := svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ - UserId: userInfo.Id, + loginLog := log.Login{ + Method: "mobile", LoginIP: ip, UserAgent: r.UserAgent(), - Success: &loginStatus, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Id: 0, + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), }); err != nil { - l.Logger.Error("[UserLogin] insert login log error", logger.Field("error", err.Error())) + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) } } }(l.svcCtx) @@ -110,6 +124,23 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r l.svcCtx.Redis.Del(l.ctx, cacheKey) } + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail login if device binding fails, just log the error + } + } + + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } + // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -119,6 +150,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) diff --git a/internal/logic/auth/telephoneResetPasswordLogic.go b/internal/logic/auth/telephoneResetPasswordLogic.go index 51ba9d7..f119c55 100644 --- a/internal/logic/auth/telephoneResetPasswordLogic.go +++ b/internal/logic/auth/telephoneResetPasswordLogic.go @@ -5,16 +5,17 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/jwt" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/phone" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/phone" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) @@ -82,6 +83,21 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "update user password failed: %v", err.Error()) } + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -91,6 +107,7 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Errorw("[UserLogin] token generate error", logger.Field("error", err.Error())) @@ -100,6 +117,31 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) } + defer func() { + if token != "" && userInfo.Id != 0 { + loginLog := log.Login{ + Method: "mobile", + LoginIP: req.IP, + UserAgent: req.UserAgent, + Success: token != "", + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Id: 0, + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + } + }() return &types.LoginResponse{ Token: token, }, nil diff --git a/internal/logic/auth/telephoneUserRegisterLogic.go b/internal/logic/auth/telephoneUserRegisterLogic.go index 24322b9..aa2dde9 100644 --- a/internal/logic/auth/telephoneUserRegisterLogic.go +++ b/internal/logic/auth/telephoneUserRegisterLogic.go @@ -6,18 +6,19 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/jwt" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/phone" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/phone" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) @@ -105,7 +106,8 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR // Generate password pwd := tool.EncodePassWord(req.Password) userInfo := &user.User{ - Password: pwd, + Password: pwd, + OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, AuthMethods: []user.AuthMethods{ { AuthType: "mobile", @@ -136,6 +138,22 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR } return nil }) + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -145,6 +163,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) @@ -154,6 +173,53 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error()) } + + defer func() { + if token != "" && userInfo.Id != 0 { + loginLog := log.Login{ + Method: "mobile", + LoginIP: req.IP, + UserAgent: req.UserAgent, + Success: token != "", + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Id: 0, + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + + // Register log + registerLog := log.Register{ + AuthMethod: "mobile", + Identifier: phoneNumber, + RegisterIP: req.IP, + UserAgent: req.UserAgent, + Timestamp: time.Now().UnixMilli(), + } + content, _ = registerLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeRegister.Uint8(), + ObjectID: userInfo.Id, + Date: time.Now().Format("2006-01-02"), + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error())) + } + } + }() return &types.LoginResponse{ Token: token, }, nil diff --git a/internal/logic/auth/userLoginLogic.go b/internal/logic/auth/userLoginLogic.go index ebc8ae9..3062e63 100644 --- a/internal/logic/auth/userLoginLogic.go +++ b/internal/logic/auth/userLoginLogic.go @@ -5,19 +5,21 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/jwt" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" ) type UserLoginLogic struct { @@ -41,29 +43,59 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log // Record login status defer func(svcCtx *svc.ServiceContext) { if userInfo.Id != 0 { - if err := svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ - UserId: userInfo.Id, + loginLog := log.Login{ + Method: "email", LoginIP: req.IP, UserAgent: req.UserAgent, - Success: &loginStatus, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), }); err != nil { - l.Logger.Error("[UserLogin] insert login log error", logger.Field("error", err.Error())) + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) } } }(l.svcCtx) userInfo, err = l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email) + if err != nil { if errors.As(err, &gorm.ErrRecordNotFound) { - logger.WithContext(l.ctx).Error(err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user email not exist: %v", req.Email) } + logger.WithContext(l.ctx).Error(err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error()) } + // Verify password if !tool.VerifyPassWord(req.Password, userInfo.Password) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "user password") } + + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail login if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -73,6 +105,7 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) diff --git a/internal/logic/auth/userRegisterLogic.go b/internal/logic/auth/userRegisterLogic.go index b03c94d..0b9da43 100644 --- a/internal/logic/auth/userRegisterLogic.go +++ b/internal/logic/auth/userRegisterLogic.go @@ -6,17 +6,18 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/logic/common" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/jwt" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) @@ -88,7 +89,8 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * // Generate password pwd := tool.EncodePassWord(req.Password) userInfo := &user.User{ - Password: pwd, + Password: pwd, + OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, } if referer != nil { userInfo.RefererId = referer.Id @@ -123,6 +125,21 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * } return nil }) + // Bind device to user if identifier is provided + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + // Don't fail register if device binding fails, just log the error + } + } + if l.ctx.Value(constant.LoginType) != nil { + req.LoginType = l.ctx.Value(constant.LoginType).(string) + } // Generate session id sessionId := uuidx.NewUUID().String() // Generate token @@ -132,6 +149,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * l.svcCtx.Config.JwtAuth.AccessExpire, jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), + jwt.WithOption("LoginType", req.LoginType), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) @@ -145,13 +163,47 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * loginStatus := true defer func() { if token != "" && userInfo.Id != 0 { - if err := l.svcCtx.UserModel.InsertLoginLog(l.ctx, &user.LoginLog{ - UserId: userInfo.Id, + loginLog := log.Login{ + Method: "email", LoginIP: req.IP, UserAgent: req.UserAgent, - Success: &loginStatus, + Success: loginStatus, + Timestamp: time.Now().UnixMilli(), + } + content, _ := loginLog.Marshal() + if err := l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Id: 0, + Type: log.TypeLogin.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), }); err != nil { - l.Logger.Error("[UserRegister] insert login log error", logger.Field("error", err.Error())) + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error()), + ) + } + + // Register log + registerLog := log.Register{ + AuthMethod: "email", + Identifier: req.Email, + RegisterIP: req.IP, + UserAgent: req.UserAgent, + Timestamp: time.Now().UnixMilli(), + } + content, _ = registerLog.Marshal() + if err = l.svcCtx.LogModel.Insert(l.ctx, &log.SystemLog{ + Type: log.TypeRegister.Uint8(), + ObjectID: userInfo.Id, + Date: time.Now().Format("2006-01-02"), + Content: string(content), + }); err != nil { + l.Errorw("failed to insert login log", + logger.Field("user_id", userInfo.Id), + logger.Field("ip", req.IP), + logger.Field("error", err.Error())) } } }() diff --git a/internal/logic/common/checkverificationcodelogic.go b/internal/logic/common/checkverificationcodelogic.go index 4aeb85b..1cc5812 100644 --- a/internal/logic/common/checkverificationcodelogic.go +++ b/internal/logic/common/checkverificationcodelogic.go @@ -5,14 +5,14 @@ import ( "encoding/json" "fmt" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/authmethod" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/phone" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/authmethod" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/phone" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/common/getAdsLogic.go b/internal/logic/common/getAdsLogic.go index 124d935..418be17 100644 --- a/internal/logic/common/getAdsLogic.go +++ b/internal/logic/common/getAdsLogic.go @@ -3,11 +3,11 @@ package common import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/ads" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/internal/model/ads" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" ) type GetAdsLogic struct { diff --git a/internal/logic/common/getApplicationLogic.go b/internal/logic/common/getApplicationLogic.go deleted file mode 100644 index 4477b3b..0000000 --- a/internal/logic/common/getApplicationLogic.go +++ /dev/null @@ -1,136 +0,0 @@ -package common - -import ( - "context" - "strings" - - "github.com/perfect-panel/ppanel-server/internal/model/application" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type GetApplicationLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get Tos Content -func NewGetApplicationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetApplicationLogic { - return &GetApplicationLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetApplicationLogic) GetApplication() (resp *types.GetAppcationResponse, err error) { - resp = &types.GetAppcationResponse{} - - cfg, err := l.svcCtx.ApplicationModel.FindOneConfig(l.ctx, 1) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - l.Logger.Error("[GetAppInfo] FindOneAppConfig error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetAppInfo FindOneAppConfig error: %v", err.Error()) - } - if err != nil { - resp.Config = types.ApplicationConfig{} - } else { - resp.Config = types.ApplicationConfig{ - AppId: cfg.AppId, - EncryptionKey: cfg.EncryptionKey, - EncryptionMethod: cfg.EncryptionMethod, - Domains: strings.Split(cfg.Domains, ";"), - StartupPicture: cfg.StartupPicture, - StartupPictureSkipTime: cfg.StartupPictureSkipTime, - } - } - - var applications []*application.Application - err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { - return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error - }) - if err != nil { - l.Errorw("[QueryApplicationConfig] get application error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) - } - - if len(applications) == 0 { - return resp, nil - } - - for _, app := range applications { - applicationResponse := types.ApplicationResponseInfo{ - Id: app.Id, - Name: app.Name, - Icon: app.Icon, - Description: app.Description, - SubscribeType: app.SubscribeType, - } - applicationVersions := app.ApplicationVersions - if len(applicationVersions) != 0 { - for _, applicationVersion := range applicationVersions { - /*if !applicationVersion.IsDefault { - continue - }*/ - switch applicationVersion.Platform { - case "ios": - applicationResponse.Platform.IOS = append(applicationResponse.Platform.IOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "macos": - applicationResponse.Platform.MacOS = append(applicationResponse.Platform.MacOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "linux": - applicationResponse.Platform.Linux = append(applicationResponse.Platform.Linux, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "android": - applicationResponse.Platform.Android = append(applicationResponse.Platform.Android, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "windows": - applicationResponse.Platform.Windows = append(applicationResponse.Platform.Windows, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "harmony": - applicationResponse.Platform.Harmony = append(applicationResponse.Platform.Harmony, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - } - } - } - resp.Applications = append(resp.Applications, applicationResponse) - } - - return -} diff --git a/internal/logic/common/getClientLogic.go b/internal/logic/common/getClientLogic.go new file mode 100644 index 0000000..7938de1 --- /dev/null +++ b/internal/logic/common/getClientLogic.go @@ -0,0 +1,56 @@ +package common + +import ( + "context" + "encoding/json" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type GetClientLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Client +func NewGetClientLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetClientLogic { + return &GetClientLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetClientLogic) GetClient() (resp *types.GetSubscribeClientResponse, err error) { + data, err := l.svcCtx.ClientModel.List(l.ctx) + if err != nil { + l.Errorf("Failed to get subscribe application list: %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Failed to get subscribe application list") + } + var list []types.SubscribeClient + for _, item := range data { + var temp types.DownloadLink + if item.DownloadLink != "" { + _ = json.Unmarshal([]byte(item.DownloadLink), &temp) + } + list = append(list, types.SubscribeClient{ + Id: item.Id, + Name: item.Name, + Description: item.Description, + Icon: item.Icon, + Scheme: item.Scheme, + IsDefault: item.IsDefault, + DownloadLink: temp, + }) + } + resp = &types.GetSubscribeClientResponse{ + Total: int64(len(list)), + List: list, + } + return +} diff --git a/internal/logic/common/getGlobalConfigLogic.go b/internal/logic/common/getGlobalConfigLogic.go index b43d3ab..61b2c1e 100644 --- a/internal/logic/common/getGlobalConfigLogic.go +++ b/internal/logic/common/getGlobalConfigLogic.go @@ -2,12 +2,13 @@ package common import ( "context" + "encoding/json" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -67,6 +68,10 @@ func (l *GetGlobalConfigLogic) GetGlobalConfig() (resp *types.GetGlobalConfigRes for _, method := range authMethods { if *method.Enabled { methods = append(methods, method.Method) + if method.Method == "device" { + _ = json.Unmarshal([]byte(method.Config), &resp.Auth.Device) + resp.Auth.Device.Enable = true + } } } resp.OAuthMethods = methods diff --git a/internal/logic/common/getPrivacyPolicyLogic.go b/internal/logic/common/getPrivacyPolicyLogic.go index ce6145f..c5bdceb 100644 --- a/internal/logic/common/getPrivacyPolicyLogic.go +++ b/internal/logic/common/getPrivacyPolicyLogic.go @@ -3,11 +3,11 @@ package common import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/common/getStatLogic.go b/internal/logic/common/getStatLogic.go index 9a4d604..df14af0 100644 --- a/internal/logic/common/getStatLogic.go +++ b/internal/logic/common/getStatLogic.go @@ -10,13 +10,13 @@ import ( "strings" "time" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/server" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/common/getSubscriptionLogic.go b/internal/logic/common/getSubscriptionLogic.go deleted file mode 100644 index bd5677e..0000000 --- a/internal/logic/common/getSubscriptionLogic.go +++ /dev/null @@ -1,41 +0,0 @@ -package common - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" -) - -type GetSubscriptionLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get Subscription -func NewGetSubscriptionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscriptionLogic { - return &GetSubscriptionLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *GetSubscriptionLogic) GetSubscription() (resp *types.GetSubscriptionResponse, err error) { - resp = &types.GetSubscriptionResponse{ - List: make([]types.Subscribe, 0), - } - // Get the subscription list - data, err := l.svcCtx.SubscribeModel.QuerySubscribeListByShow(l.ctx) - if err != nil { - l.Errorw("[Site GetSubscription]", logger.Field("err", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscription list error: %v", err.Error()) - } - tool.DeepCopy(&resp.List, data) - return -} diff --git a/internal/logic/common/getTosLogic.go b/internal/logic/common/getTosLogic.go index 8884bc4..4297d75 100644 --- a/internal/logic/common/getTosLogic.go +++ b/internal/logic/common/getTosLogic.go @@ -3,11 +3,11 @@ package common import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/common/sendEmailCodeLogic.go b/internal/logic/common/sendEmailCodeLogic.go index 467f977..c538d72 100644 --- a/internal/logic/common/sendEmailCodeLogic.go +++ b/internal/logic/common/sendEmailCodeLogic.go @@ -1,26 +1,24 @@ package common import ( - "bytes" "context" "encoding/json" "fmt" - "text/template" "time" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/limit" - "github.com/perfect-panel/ppanel-server/pkg/random" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/limit" + "github.com/perfect-panel/server/pkg/random" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + queue "github.com/perfect-panel/server/queue/types" ) type SendEmailCodeLogic struct { @@ -88,14 +86,16 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty var taskPayload queue.SendEmailPayload // Generate verification code code := random.Key(6, 0) + taskPayload.Type = queue.EmailTypeVerify taskPayload.Email = req.Email taskPayload.Subject = "Verification code" - content, err := l.initTemplate(req.Type, code) - if err != nil { - l.Logger.Error("[SendEmailCode]: InitTemplate Error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Failed to init template") + taskPayload.Content = map[string]interface{}{ + "Type": req.Type, + "SiteLogo": l.svcCtx.Config.Site.SiteLogo, + "SiteName": l.svcCtx.Config.Site.SiteName, + "Expire": 5, + "Code": code, } - taskPayload.Content = content // Save to Redis payload = CacheKeyPayload{ Code: code, @@ -134,23 +134,3 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty }, nil } } - -func (l *SendEmailCodeLogic) initTemplate(t uint8, code string) (string, error) { - data := VerifyTemplate{ - Type: t, - SiteLogo: l.svcCtx.Config.Site.SiteLogo, - SiteName: l.svcCtx.Config.Site.SiteName, - Expire: 5, - Code: code, - } - tpl, err := template.New("verify").Parse(l.svcCtx.Config.Email.VerifyEmailTemplate) - if err != nil { - return "", err - } - var result bytes.Buffer - err = tpl.Execute(&result, data) - if err != nil { - return "", err - } - return result.String(), nil -} diff --git a/internal/logic/common/sendSmsCodeLogic.go b/internal/logic/common/sendSmsCodeLogic.go index ada3f8d..7f5037a 100644 --- a/internal/logic/common/sendSmsCodeLogic.go +++ b/internal/logic/common/sendSmsCodeLogic.go @@ -7,16 +7,16 @@ import ( "time" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/limit" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/phone" - "github.com/perfect-panel/ppanel-server/pkg/random" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/limit" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/phone" + "github.com/perfect-panel/server/pkg/random" + "github.com/perfect-panel/server/pkg/xerr" + queue "github.com/perfect-panel/server/queue/types" "github.com/pkg/errors" "gorm.io/gorm" ) diff --git a/internal/logic/notify/alipayNotifyLogic.go b/internal/logic/notify/alipayNotifyLogic.go index 125e41f..f0ce2aa 100644 --- a/internal/logic/notify/alipayNotifyLogic.go +++ b/internal/logic/notify/alipayNotifyLogic.go @@ -6,17 +6,17 @@ import ( "fmt" "net/http" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/payment/alipay" - "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/model/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/payment/alipay" + "github.com/perfect-panel/server/queue/types" ) type AlipayNotifyLogic struct { diff --git a/internal/logic/notify/ePayNotifyLogic.go b/internal/logic/notify/ePayNotifyLogic.go index 385ce1b..8def591 100644 --- a/internal/logic/notify/ePayNotifyLogic.go +++ b/internal/logic/notify/ePayNotifyLogic.go @@ -4,21 +4,21 @@ import ( "encoding/json" "net/url" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "github.com/gin-gonic/gin" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/payment/epay" + "github.com/perfect-panel/server/internal/model/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/payment/epay" - queueType "github.com/perfect-panel/ppanel-server/queue/types" + queueType "github.com/perfect-panel/server/queue/types" ) type EPayNotifyLogic struct { diff --git a/internal/logic/notify/stripeNotifyLogic.go b/internal/logic/notify/stripeNotifyLogic.go index a2c7e3a..a364339 100644 --- a/internal/logic/notify/stripeNotifyLogic.go +++ b/internal/logic/notify/stripeNotifyLogic.go @@ -6,17 +6,17 @@ import ( "io" "net/http" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/payment/stripe" - "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/model/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/payment/stripe" + "github.com/perfect-panel/server/queue/types" ) type StripeNotifyLogic struct { diff --git a/internal/logic/public/announcement/queryAnnouncementLogic.go b/internal/logic/public/announcement/queryAnnouncementLogic.go index 1f9b71a..e564ea9 100644 --- a/internal/logic/public/announcement/queryAnnouncementLogic.go +++ b/internal/logic/public/announcement/queryAnnouncementLogic.go @@ -3,14 +3,14 @@ package announcement import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/announcement" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/announcement" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type QueryAnnouncementLogic struct { diff --git a/internal/logic/public/document/queryDocumentDetailLogic.go b/internal/logic/public/document/queryDocumentDetailLogic.go index fbeae76..a5b90f4 100644 --- a/internal/logic/public/document/queryDocumentDetailLogic.go +++ b/internal/logic/public/document/queryDocumentDetailLogic.go @@ -3,11 +3,11 @@ package document import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/document/queryDocumentListLogic.go b/internal/logic/public/document/queryDocumentListLogic.go index bf386ad..9c82c8b 100644 --- a/internal/logic/public/document/queryDocumentListLogic.go +++ b/internal/logic/public/document/queryDocumentListLogic.go @@ -3,11 +3,11 @@ package document import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/order/calculateCoupon.go b/internal/logic/public/order/calculateCoupon.go index e9150cb..05f92c8 100644 --- a/internal/logic/public/order/calculateCoupon.go +++ b/internal/logic/public/order/calculateCoupon.go @@ -1,7 +1,7 @@ package order import ( - "github.com/perfect-panel/ppanel-server/internal/model/coupon" + "github.com/perfect-panel/server/internal/model/coupon" ) func calculateCoupon(amount int64, couponInfo *coupon.Coupon) int64 { diff --git a/internal/logic/public/order/calculateFee.go b/internal/logic/public/order/calculateFee.go index 432ad27..9c0b2b9 100644 --- a/internal/logic/public/order/calculateFee.go +++ b/internal/logic/public/order/calculateFee.go @@ -1,6 +1,6 @@ package order -import "github.com/perfect-panel/ppanel-server/internal/model/payment" +import "github.com/perfect-panel/server/internal/model/payment" func calculateFee(amount int64, config *payment.Payment) int64 { var fee float64 diff --git a/internal/logic/public/order/closeOrderLogic.go b/internal/logic/public/order/closeOrderLogic.go index 3ecfff3..ced53b8 100644 --- a/internal/logic/public/order/closeOrderLogic.go +++ b/internal/logic/public/order/closeOrderLogic.go @@ -3,17 +3,19 @@ package order import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/payment/payssion" - "github.com/perfect-panel/ppanel-server/pkg/payment/stripe" + "time" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/payment/stripe" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/payment/alipay" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/internal/model/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/payment/alipay" ) type CloseOrderLogic struct { @@ -82,7 +84,7 @@ func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error { return err } deduction := userInfo.GiftAmount + orderInfo.GiftAmount - err = tx.Model(&user.User{}).Where("id = ?", orderInfo.UserId).Update("deduction", deduction).Error + err = tx.Model(&user.User{}).Where("id = ?", orderInfo.UserId).Update("gift_amount", deduction).Error if err != nil { l.Errorw("[CloseOrder] Refund deduction amount failed", logger.Field("error", err.Error()), @@ -92,15 +94,25 @@ func (l *CloseOrderLogic) CloseOrder(req *types.CloseOrderRequest) error { return err } // Record the deduction refund log - giftAmountLog := &user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 1, - Balance: deduction, - Remark: "Order cancellation refund", + + giftLog := log.Gift{ + Type: log.GiftTypeIncrease, + OrderNo: orderInfo.OrderNo, + SubscribeId: 0, + Amount: orderInfo.GiftAmount, + Balance: deduction, + Remark: "Order cancellation refund", + Timestamp: time.Now().UnixMilli(), } - err = tx.Model(&user.GiftAmountLog{}).Create(giftAmountLog).Error + content, _ := giftLog.Marshal() + + err = tx.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Id: 0, + Type: log.TypeGift.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: userInfo.Id, + Content: string(content), + }).Error if err != nil { l.Errorw("[CloseOrder] Record cancellation refund log failed", logger.Field("error", err.Error()), @@ -134,8 +146,8 @@ func (l *CloseOrderLogic) confirmationPayment(order *order.Order) bool { if l.queryAlipay(paymentConfig, order.TradeNo) { return true } - case Payssion: - if l.queryPayssion(paymentConfig, order.TradeNo) { + case StripeAlipay: + if l.queryStripe(paymentConfig, order.TradeNo) { return true } case StripeWeChatPay: @@ -195,25 +207,3 @@ func (l *CloseOrderLogic) queryStripe(paymentConfig *payment.Payment, TradeNo st } return status } - -// queryPayssion Query Stripe payment status -// -//nolint:unused -func (l *CloseOrderLogic) queryPayssion(paymentConfig *payment.Payment, TradeNo string) bool { - l.Infof("[CloseOrder]1 Query Payssion called") - payssionConfig := payment.PayssionConfig{} - if err := json.Unmarshal([]byte(paymentConfig.Config), &payssionConfig); err != nil { - l.Errorw("[CloseOrder] Unmarshal error", logger.Field("error", err.Error())) - return false - } - l.Infof("[CloseOrder]2 Query Payssion called") - client := payssion.NewClient(payssionConfig.ApiKey, payssionConfig.SecretKey, payssionConfig.PmId, payssionConfig.Currency, payssionConfig.QueryUrl, payssionConfig.CreateUrl) - // create payment - result, err := client.QueryOrder(TradeNo) - if err != nil { - l.Errorw("[CloseOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) - return false - } - l.Infof("[CloseOrder]3 Query Payssion called") - return result.Transaction.State == "completed" -} diff --git a/internal/logic/public/order/constant.go b/internal/logic/public/order/constant.go index 44bccd8..ca1c44a 100644 --- a/internal/logic/public/order/constant.go +++ b/internal/logic/public/order/constant.go @@ -2,7 +2,6 @@ package order const ( Epay = "epay" - Payssion = "Payssion" AlipayF2f = "alipay_f2f" StripeAlipay = "stripe_alipay" StripeWeChatPay = "stripe_wechat_pay" diff --git a/internal/logic/public/order/getDiscount.go b/internal/logic/public/order/getDiscount.go index 4d896f9..34c16a9 100644 --- a/internal/logic/public/order/getDiscount.go +++ b/internal/logic/public/order/getDiscount.go @@ -1,6 +1,6 @@ package order -import "github.com/perfect-panel/ppanel-server/internal/types" +import "github.com/perfect-panel/server/internal/types" func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 { var finalDiscount int64 = 100 @@ -10,5 +10,6 @@ func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 finalDiscount = discount.Discount } } + return float64(finalDiscount) / float64(100) } diff --git a/internal/logic/public/order/preCreateOrderLogic.go b/internal/logic/public/order/preCreateOrderLogic.go index 2989cde..4e16715 100644 --- a/internal/logic/public/order/preCreateOrderLogic.go +++ b/internal/logic/public/order/preCreateOrderLogic.go @@ -4,13 +4,16 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/constant" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) @@ -21,7 +24,8 @@ type PreCreateOrderLogic struct { svcCtx *svc.ServiceContext } -// Pre create order +// NewPreCreateOrderLogic creates a new pre-create order logic instance for order preview operations. +// It initializes the logger with context and sets up the service context for database operations. func NewPreCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreCreateOrderLogic { return &PreCreateOrderLogic{ Logger: logger.WithContext(ctx), @@ -30,12 +34,21 @@ func NewPreCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Pr } } +// PreCreateOrder calculates order pricing preview including discounts, coupons, gift amounts, and fees +// without actually creating an order. It validates subscription plans, coupons, and payment methods +// to provide accurate pricing information for the frontend order preview. func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (resp *types.PreOrderResponse, err error) { u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok { logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } + + if req.Quantity <= 0 { + l.Debugf("[PreCreateOrder] Quantity is less than or equal to 0, setting to 1") + req.Quantity = 1 + } + // find subscribe plan sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId) if err != nil { @@ -49,9 +62,10 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r discount = getDiscount(dis, req.Quantity) } price := sub.UnitPrice * req.Quantity + amount := int64(float64(price) * discount) discountAmount := price - amount - var coupon int64 + var couponAmount int64 if req.Coupon != "" { couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) if err != nil { @@ -60,12 +74,30 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) } - if couponInfo.Count <= couponInfo.UsedCount { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") + if couponInfo.Count > 0 && couponInfo.Count <= couponInfo.UsedCount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponAlreadyUsed), "coupon used") } - coupon = calculateCoupon(amount, couponInfo) + var count int64 + err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + return tx.Model(&order.Order{}).Where("user_id = ? and coupon = ?", u.Id, req.Coupon).Count(&count).Error + }) + + if err != nil { + l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id), logger.Field("coupon", req.Coupon)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) + } + + if couponInfo.UserLimit > 0 && count >= couponInfo.UserLimit { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon limit exceeded") + } + + couponSub := tool.StringToInt64Slice(couponInfo.Subscribe) + if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match") + } + couponAmount = calculateCoupon(amount, couponInfo) } - amount -= coupon + amount -= couponAmount var deductionAmount int64 // Check user deduction amount @@ -82,7 +114,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r if req.Payment != 0 { payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) if err != nil { - l.Logger.Error("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) } // Calculate the handling fee @@ -98,7 +130,7 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r Discount: discountAmount, GiftAmount: deductionAmount, Coupon: req.Coupon, - CouponDiscount: coupon, + CouponDiscount: couponAmount, FeeAmount: feeAmount, } return diff --git a/internal/logic/public/order/purchaseLogic.go b/internal/logic/public/order/purchaseLogic.go index a559f38..519a80a 100644 --- a/internal/logic/public/order/purchaseLogic.go +++ b/internal/logic/public/order/purchaseLogic.go @@ -5,20 +5,21 @@ import ( "encoding/json" "time" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + queue "github.com/perfect-panel/server/queue/types" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type PurchaseLogic struct { @@ -31,7 +32,8 @@ const ( CloseOrderTimeMinutes = 15 ) -// NewPurchaseLogic purchase Subscription +// NewPurchaseLogic creates a new purchase logic instance for subscription purchase operations. +// It initializes the logger with context and sets up the service context for database operations. func NewPurchaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PurchaseLogic { return &PurchaseLogic{ Logger: logger.WithContext(ctx), @@ -40,6 +42,9 @@ func NewPurchaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Purchase } } +// Purchase processes new subscription purchase orders including validation, discount calculation, +// coupon processing, gift amount deduction, fee calculation, and order creation with database transaction. +// It handles the complete purchase workflow from user validation to order creation and task scheduling. func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.PurchaseOrderResponse, err error) { u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) @@ -47,6 +52,12 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } + + if req.Quantity <= 0 { + l.Debugf("[Purchase] Quantity is less than or equal to 0, setting to 1") + req.Quantity = 1 + } + // find user subscription userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id) if err != nil { @@ -103,12 +114,24 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) } - if couponInfo.Count <= couponInfo.UsedCount { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") + if couponInfo.Count != 0 && couponInfo.Count <= couponInfo.UsedCount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used") } couponSub := tool.StringToInt64Slice(couponInfo.Subscribe) if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotMatch), "coupon not match") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match") + } + var count int64 + err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + return tx.Model(&order.Order{}).Where("user_id = ? and coupon = ?", u.Id, req.Coupon).Count(&count).Error + }) + + if err != nil { + l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id), logger.Field("coupon", req.Coupon)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) + } + if count >= couponInfo.UserLimit { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon limit exceeded") } coupon = calculateCoupon(amount, couponInfo) } @@ -120,7 +143,7 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P if u.GiftAmount >= amount { deductionAmount = amount amount = 0 - u.GiftAmount -= amount + u.GiftAmount -= deductionAmount } else { deductionAmount = u.GiftAmount amount -= u.GiftAmount @@ -130,13 +153,14 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P // find payment method payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) if err != nil { - l.Logger.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) + l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) } var feeAmount int64 // Calculate the handling fee if amount > 0 { feeAmount = calculateFee(amount, payment) + amount += feeAmount } // query user is new purchase or renewal isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id) @@ -168,23 +192,31 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P // update user deduction && Pre deduction ,Return after canceling the order if orderInfo.GiftAmount > 0 { // update user deduction && Pre deduction ,Return after canceling the order - if e := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { - l.Errorw("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) + if e := l.svcCtx.UserModel.Update(l.ctx, u, db); e != nil { + l.Errorw("[Purchase] Database update error", logger.Field("error", e.Error()), logger.Field("user", u)) return e } // create deduction record - giftAmountLog := user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 2, - Balance: u.GiftAmount, - Remark: "Purchase order deduction", + giftLog := log.Gift{ + Type: log.GiftTypeReduce, + OrderNo: orderInfo.OrderNo, + SubscribeId: 0, + Amount: orderInfo.GiftAmount, + Balance: u.GiftAmount, + Remark: "Purchase order deduction", + Timestamp: time.Now().UnixMilli(), } - if e := db.Model(&user.GiftAmountLog{}).Create(&giftAmountLog).Error; e != nil { + content, _ := giftLog.Marshal() + + if e := db.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeGift.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: u.Id, + Content: string(content), + }).Error; e != nil { l.Errorw("[Purchase] Database insert error", - logger.Field("error", err.Error()), - logger.Field("deductionLog", giftAmountLog), + logger.Field("error", e.Error()), + logger.Field("deductionLog", giftLog), ) return e } @@ -201,14 +233,14 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P } val, err := json.Marshal(payload) if err != nil { - l.Errorw("[CreateOrder] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) + l.Errorw("[Purchase] Marshal payload error", logger.Field("error", err.Error()), logger.Field("payload", payload)) } task := asynq.NewTask(queue.DeferCloseOrder, val, asynq.MaxRetry(3)) taskInfo, err := l.svcCtx.Queue.Enqueue(task, asynq.ProcessIn(CloseOrderTimeMinutes*time.Minute)) if err != nil { - l.Errorw("[CreateOrder] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) + l.Errorw("[Purchase] Enqueue task error", logger.Field("error", err.Error()), logger.Field("task", task)) } else { - l.Infow("[CreateOrder] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) + l.Infow("[Purchase] Enqueue task success", logger.Field("TaskID", taskInfo.ID)) } return &types.PurchaseOrderResponse{ diff --git a/internal/logic/public/order/queryOrderDetailLogic.go b/internal/logic/public/order/queryOrderDetailLogic.go index e34392b..21dd68d 100644 --- a/internal/logic/public/order/queryOrderDetailLogic.go +++ b/internal/logic/public/order/queryOrderDetailLogic.go @@ -3,11 +3,11 @@ package order import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/order/queryOrderListLogic.go b/internal/logic/public/order/queryOrderListLogic.go index 544b157..13deaa3 100644 --- a/internal/logic/public/order/queryOrderListLogic.go +++ b/internal/logic/public/order/queryOrderListLogic.go @@ -3,14 +3,14 @@ package order import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/order/rechargeLogic.go b/internal/logic/public/order/rechargeLogic.go index a42d3bf..04ff41c 100644 --- a/internal/logic/public/order/rechargeLogic.go +++ b/internal/logic/public/order/rechargeLogic.go @@ -5,17 +5,17 @@ import ( "encoding/json" "time" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/xerr" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + queue "github.com/perfect-panel/server/queue/types" "github.com/pkg/errors" ) diff --git a/internal/logic/public/order/renewalLogic.go b/internal/logic/public/order/renewalLogic.go index ad59f72..c78824f 100644 --- a/internal/logic/public/order/renewalLogic.go +++ b/internal/logic/public/order/renewalLogic.go @@ -5,19 +5,20 @@ import ( "encoding/json" "time" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" "gorm.io/gorm" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + queue "github.com/perfect-panel/server/queue/types" "github.com/pkg/errors" ) @@ -27,7 +28,7 @@ type RenewalLogic struct { svcCtx *svc.ServiceContext } -// Renewal Subscription +// NewRenewalLogic creates a new renewal logic instance for subscription renewal operations func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLogic { return &RenewalLogic{ Logger: logger.WithContext(ctx), @@ -36,12 +37,19 @@ func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLo } } +// Renewal processes subscription renewal orders including discount calculation, +// coupon validation, gift amount deduction, fee calculation, and order creation func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.RenewalOrderResponse, err error) { u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok { logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } + if req.Quantity <= 0 { + l.Debugf("[Renewal] Quantity is less than or equal to 0, setting to 1") + req.Quantity = 1 + } + orderNo := tool.GenerateTradeNo() // find user subscribe userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID) @@ -76,15 +84,31 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) } - if couponInfo.Count <= couponInfo.UsedCount { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") + if couponInfo.Count != 0 && couponInfo.Count <= couponInfo.UsedCount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used") + } + couponSub := tool.StringToInt64Slice(couponInfo.Subscribe) + + if len(couponSub) > 0 && !tool.Contains(couponSub, sub.Id) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match") + } + var count int64 + err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + return tx.Model(&order.Order{}).Where("user_id = ? and coupon = ?", u.Id, req.Coupon).Count(&count).Error + }) + if err != nil { + l.Errorw("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id), logger.Field("coupon", req.Coupon)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) + } + if count >= couponInfo.UserLimit { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon limit exceeded") } coupon = calculateCoupon(amount, couponInfo) } payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) if err != nil { l.Errorw("[Renewal] Database query error", logger.Field("error", err.Error()), logger.Field("payment", req.Payment)) - return nil, errors.Wrapf(err, "find payment error: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment error: %v", err.Error()) } amount -= coupon @@ -93,8 +117,8 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene if u.GiftAmount > 0 { if u.GiftAmount >= amount { deductionAmount = amount + u.GiftAmount -= deductionAmount amount = 0 - u.GiftAmount -= amount } else { deductionAmount = u.GiftAmount amount -= u.GiftAmount @@ -136,20 +160,28 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene if orderInfo.GiftAmount > 0 { // update user deduction && Pre deduction ,Return after canceling the order if err := l.svcCtx.UserModel.Update(l.ctx, u, db); err != nil { - l.Errorw("[Purchase] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) + l.Errorw("[Renewal] Database update error", logger.Field("error", err.Error()), logger.Field("user", u)) return err } // create deduction record - deductionLog := user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 2, - Balance: u.GiftAmount, - Remark: "Renewal order deduction", + giftLog := log.Gift{ + Type: log.GiftTypeReduce, + OrderNo: orderInfo.OrderNo, + SubscribeId: 0, + Amount: orderInfo.GiftAmount, + Balance: u.GiftAmount, + Remark: "Renewal order deduction", + Timestamp: time.Now().UnixMilli(), } - if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil { - l.Errorw("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog)) + content, _ := giftLog.Marshal() + + if err := db.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeGift.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: u.Id, + Content: string(content), + }).Error; err != nil { + l.Errorw("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", giftLog)) return err } } diff --git a/internal/logic/public/order/resetTrafficLogic.go b/internal/logic/public/order/resetTrafficLogic.go index 2f3ecc1..1fc9b57 100644 --- a/internal/logic/public/order/resetTrafficLogic.go +++ b/internal/logic/public/order/resetTrafficLogic.go @@ -5,19 +5,20 @@ import ( "encoding/json" "time" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/xerr" "gorm.io/gorm" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - queue "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + queue "github.com/perfect-panel/server/queue/types" "github.com/pkg/errors" ) @@ -104,16 +105,24 @@ func (l *ResetTrafficLogic) ResetTraffic(req *types.ResetTrafficOrderRequest) (r return err } // create deduction record - deductionLog := user.GiftAmountLog{ - UserId: orderInfo.UserId, - OrderNo: orderInfo.OrderNo, - Amount: orderInfo.GiftAmount, - Type: 2, - Balance: u.GiftAmount, - Remark: "ResetTraffic order deduction", + giftLog := log.Gift{ + Type: log.GiftTypeReduce, + OrderNo: orderInfo.OrderNo, + SubscribeId: 0, + Amount: orderInfo.GiftAmount, + Balance: u.GiftAmount, + Remark: "Renewal order deduction", + Timestamp: time.Now().UnixMilli(), } - if err := db.Model(&user.GiftAmountLog{}).Create(&deductionLog).Error; err != nil { - l.Errorw("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", deductionLog)) + content, _ := giftLog.Marshal() + + if err = db.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeGift.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: u.Id, + Content: string(content), + }).Error; err != nil { + l.Errorw("[ResetTraffic] Database insert error", logger.Field("error", err.Error()), logger.Field("deductionLog", content)) return err } } diff --git a/internal/logic/public/payment/getAvailablePaymentMethodsLogic.go b/internal/logic/public/payment/getAvailablePaymentMethodsLogic.go index 835f90d..b8c88cb 100644 --- a/internal/logic/public/payment/getAvailablePaymentMethodsLogic.go +++ b/internal/logic/public/payment/getAvailablePaymentMethodsLogic.go @@ -3,11 +3,11 @@ package payment import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/portal/getAvailablePaymentMethodsLogic.go b/internal/logic/public/portal/getAvailablePaymentMethodsLogic.go index 8e5faad..0e6bbc5 100644 --- a/internal/logic/public/portal/getAvailablePaymentMethodsLogic.go +++ b/internal/logic/public/portal/getAvailablePaymentMethodsLogic.go @@ -3,11 +3,11 @@ package portal import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/portal/getSubscriptionLogic.go b/internal/logic/public/portal/getSubscriptionLogic.go index df17f8a..51fbaa6 100644 --- a/internal/logic/public/portal/getSubscriptionLogic.go +++ b/internal/logic/public/portal/getSubscriptionLogic.go @@ -4,11 +4,12 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -27,12 +28,18 @@ func NewGetSubscriptionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *G } } -func (l *GetSubscriptionLogic) GetSubscription() (resp *types.GetSubscriptionResponse, err error) { +func (l *GetSubscriptionLogic) GetSubscription(req *types.GetSubscriptionRequest) (resp *types.GetSubscriptionResponse, err error) { resp = &types.GetSubscriptionResponse{ List: make([]types.Subscribe, 0), } // Get the subscription list - data, err := l.svcCtx.SubscribeModel.QuerySubscribeListByShow(l.ctx) + _, data, err := l.svcCtx.SubscribeModel.FilterList(l.ctx, &subscribe.FilterParams{ + Page: 1, + Size: 9999, + Show: true, + Language: req.Language, + DefaultLanguage: true, + }) if err != nil { l.Errorw("[Site GetSubscription]", logger.Field("err", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get subscription list error: %v", err.Error()) diff --git a/internal/logic/public/portal/prePurchaseOrderLogic.go b/internal/logic/public/portal/prePurchaseOrderLogic.go index 3d51dbe..b2a99ba 100644 --- a/internal/logic/public/portal/prePurchaseOrderLogic.go +++ b/internal/logic/public/portal/prePurchaseOrderLogic.go @@ -4,10 +4,12 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/tool" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) @@ -52,9 +54,15 @@ func (l *PrePurchaseOrderLogic) PrePurchaseOrder(req *types.PrePurchaseOrderRequ } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) } - if couponInfo.Count <= couponInfo.UsedCount { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") + if couponInfo.Count != 0 && couponInfo.Count <= couponInfo.UsedCount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used") } + subs := tool.StringToInt64Slice(couponInfo.Subscribe) + + if len(subs) > 0 && !tool.Contains(subs, req.SubscribeId) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match") + } + coupon = calculateCoupon(amount, couponInfo) } amount -= coupon diff --git a/internal/logic/public/portal/purchaseCheckoutLogic.go b/internal/logic/public/portal/purchaseCheckoutLogic.go index b991188..f72ed4f 100644 --- a/internal/logic/public/portal/purchaseCheckoutLogic.go +++ b/internal/logic/public/portal/purchaseCheckoutLogic.go @@ -3,39 +3,43 @@ package portal import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/pkg/payment/payssion" "strconv" + "time" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" - paymentPlatform "github.com/perfect-panel/ppanel-server/pkg/payment" + paymentPlatform "github.com/perfect-panel/server/pkg/payment" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/user" - queueType "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/model/user" + queueType "github.com/perfect-panel/server/queue/types" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/exchangeRate" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/payment/alipay" - "github.com/perfect-panel/ppanel-server/pkg/payment/epay" - "github.com/perfect-panel/ppanel-server/pkg/payment/stripe" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/internal/model/payment" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/exchangeRate" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/payment/alipay" + "github.com/perfect-panel/server/pkg/payment/epay" + "github.com/perfect-panel/server/pkg/payment/stripe" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) +// PurchaseCheckoutLogic handles the checkout process for various payment methods +// including EPay, Stripe, Alipay F2F, and balance payments type PurchaseCheckoutLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } -// NewPurchaseCheckoutLogic Purchase Checkout +// NewPurchaseCheckoutLogic creates a new instance of PurchaseCheckoutLogic +// for handling purchase checkout operations across different payment platforms func NewPurchaseCheckoutLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PurchaseCheckoutLogic { return &PurchaseCheckoutLogic{ Logger: logger.WithContext(ctx), @@ -44,98 +48,116 @@ func NewPurchaseCheckoutLogic(ctx context.Context, svcCtx *svc.ServiceContext) * } } +// PurchaseCheckout processes the checkout for an order using the specified payment method +// It validates the order, retrieves payment configuration, and routes to the appropriate payment handler func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest) (resp *types.CheckoutOrderResponse, err error) { - // Find order + // Validate and retrieve order information orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) if err != nil { l.Logger.Error("[PurchaseCheckout] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OrderNo)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", req.OrderNo) } + // Verify order is in pending payment status (status = 1) if orderInfo.Status != 1 { l.Logger.Error("[PurchaseCheckout] Order status error", logger.Field("status", orderInfo.Status)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderStatusError), "order status error: %v", orderInfo.Status) } - // find payment method + // Retrieve payment method configuration paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, orderInfo.PaymentId) if err != nil { - l.Logger.Error("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("payment", orderInfo.Method)) + l.Logger.Error("[PurchaseCheckout] Database query error", logger.Field("error", err.Error()), logger.Field("payment", orderInfo.Method)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) } + // Route to appropriate payment handler based on payment platform switch paymentPlatform.ParsePlatform(orderInfo.Method) { case paymentPlatform.EPay: + // Process EPay payment - generates payment URL for redirect url, err := l.epayPayment(paymentConfig, orderInfo, req.ReturnUrl) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "epayPayment error: %v", err.Error()) } resp = &types.CheckoutOrderResponse{ CheckoutUrl: url, - Type: "url", + Type: "url", // Client should redirect to URL } + case paymentPlatform.Stripe: + // Process Stripe payment - creates payment sheet for client-side processing stripePayment, err := l.stripePayment(paymentConfig.Config, orderInfo, "") if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "stripePayment error: %v", err.Error()) } resp = &types.CheckoutOrderResponse{ - Type: "stripe", + Type: "stripe", // Client should use Stripe SDK Stripe: stripePayment, } + case paymentPlatform.AlipayF2F: + // Process Alipay Face-to-Face payment - generates QR code url, err := l.alipayF2fPayment(paymentConfig, orderInfo) if err != nil { - l.Errorw("[CheckoutOrderLogic] alipayF2fPayment error", logger.Field("error", err.Error())) + l.Errorw("[PurchaseCheckout] alipayF2fPayment error", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "alipayF2fPayment error: %v", err.Error()) } resp = &types.CheckoutOrderResponse{ - Type: "qr", + Type: "qr", // Client should display QR code CheckoutUrl: url, } - case paymentPlatform.Payssion: - url, err := l.payssionPayment(paymentConfig, orderInfo, req.ReturnUrl) + + case paymentPlatform.CryptoSaaS: + // Process EPay payment - generates payment URL for redirect + url, err := l.CryptoSaaSPayment(paymentConfig, orderInfo, req.ReturnUrl) if err != nil { - l.Errorw("[CheckoutOrderLogic] payssionPayment error", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "paymentPayment error: %v", err.Error()) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "epayPayment error: %v", err.Error()) } resp = &types.CheckoutOrderResponse{ CheckoutUrl: url, - Type: "url", + Type: "url", // Client should redirect to URL } + case paymentPlatform.Balance: + // Process balance payment - validate user and process payment immediately if orderInfo.UserId == 0 { - l.Errorw("[CheckoutOrderLogic] user not found", logger.Field("userId", orderInfo.UserId)) + l.Errorw("[PurchaseCheckout] user not found", logger.Field("userId", orderInfo.UserId)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user not found") } - // find user + + // Retrieve user information for balance validation userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, orderInfo.UserId) if err != nil { - l.Errorw("[CheckoutOrderLogic] FindOne User error", logger.Field("error", err.Error())) + l.Errorw("[PurchaseCheckout] FindOne User error", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOne error: %s", err.Error()) } - // balance + // Process balance payment with gift amount priority logic if err = l.balancePayment(userInfo, orderInfo); err != nil { return nil, err } + resp = &types.CheckoutOrderResponse{ - Type: "balance", + Type: "balance", // Payment completed immediately } default: - l.Errorw("[CheckoutOrderLogic] payment method not found", logger.Field("method", orderInfo.Method)) + l.Errorw("[PurchaseCheckout] payment method not found", logger.Field("method", orderInfo.Method)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "payment method not found") } return } -// alipay f2f payment +// alipayF2fPayment processes Alipay Face-to-Face payment by generating a QR code +// It handles currency conversion and creates a pre-payment trade for QR code scanning func (l *PurchaseCheckoutLogic) alipayF2fPayment(pay *payment.Payment, info *order.Order) (string, error) { - f2FConfig := payment.AlipayF2FConfig{} - if err := json.Unmarshal([]byte(pay.Config), &f2FConfig); err != nil { - l.Errorw("[PurchaseCheckoutLogic] Unmarshal error", logger.Field("error", err.Error())) + // Parse Alipay F2F configuration from payment settings + f2FConfig := &payment.AlipayF2FConfig{} + if err := f2FConfig.Unmarshal([]byte(pay.Config)); err != nil { + l.Errorw("[PurchaseCheckout] Unmarshal Alipay config error", logger.Field("error", err.Error())) return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) } + + // Build notification URL for payment status callbacks notifyUrl := "" if pay.Domain != "" { notifyUrl = pay.Domain + "/v1/notify/" + pay.Platform + "/" + pay.Token @@ -146,6 +168,8 @@ func (l *PurchaseCheckoutLogic) alipayF2fPayment(pay *payment.Payment, info *ord } notifyUrl = "https://" + host + "/v1/notify/" + pay.Platform + "/" + pay.Token } + + // Initialize Alipay client with configuration client := alipay.NewClient(alipay.Config{ AppId: f2FConfig.AppId, PrivateKey: f2FConfig.PrivateKey, @@ -153,46 +177,54 @@ func (l *PurchaseCheckoutLogic) alipayF2fPayment(pay *payment.Payment, info *ord InvoiceName: f2FConfig.InvoiceName, NotifyURL: notifyUrl, }) - // Calculate the amount with exchange rate + + // Convert order amount to CNY using current exchange rate amount, err := l.queryExchangeRate("CNY", info.Amount) if err != nil { - l.Errorw("[CheckoutOrderLogic] queryExchangeRate error", logger.Field("error", err.Error())) + l.Errorw("[PurchaseCheckout] queryExchangeRate error", logger.Field("error", err.Error())) return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error()) } - convertAmount := int64(amount * 100) - // create payment + convertAmount := int64(amount * 100) // Convert to cents for API + + // Create pre-payment trade and generate QR code QRCode, err := client.PreCreateTrade(l.ctx, alipay.Order{ OrderNo: info.OrderNo, Amount: convertAmount, }) if err != nil { - l.Errorw("[CheckoutOrderLogic] PreCreateTrade error", logger.Field("error", err.Error())) + l.Errorw("[PurchaseCheckout] PreCreateTrade error", logger.Field("error", err.Error())) return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "PreCreateTrade error: %s", err.Error()) } return QRCode, nil } -// Stripe Payment +// stripePayment processes Stripe payment by creating a payment sheet +// It supports various payment methods including WeChat Pay and Alipay through Stripe func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order, identifier string) (*types.StripePayment, error) { - // stripe WeChat pay or stripe alipay - stripeConfig := payment.StripeConfig{} - if err := json.Unmarshal([]byte(config), &stripeConfig); err != nil { - l.Errorw("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) + // Parse Stripe configuration from payment settings + stripeConfig := &payment.StripeConfig{} + + if err := stripeConfig.Unmarshal([]byte(config)); err != nil { + l.Errorw("[PurchaseCheckout] Unmarshal Stripe config error", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) } + + // Initialize Stripe client with API credentials client := stripe.NewClient(stripe.Config{ SecretKey: stripeConfig.SecretKey, PublicKey: stripeConfig.PublicKey, WebhookSecret: stripeConfig.WebhookSecret, }) - // Calculate the amount with exchange rate + + // Convert order amount to CNY using current exchange rate amount, err := l.queryExchangeRate("CNY", info.Amount) if err != nil { - l.Errorw("[CheckoutOrderLogic] queryExchangeRate error", logger.Field("error", err.Error())) + l.Errorw("[PurchaseCheckout] queryExchangeRate error", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "queryExchangeRate error: %s", err.Error()) } - convertAmount := int64(amount * 100) - // create payment + convertAmount := int64(amount * 100) // Convert to cents for Stripe API + + // Create Stripe payment sheet for client-side processing result, err := client.CreatePaymentSheet(&stripe.Order{ OrderNo: info.OrderNo, Subscribe: strconv.FormatInt(info.SubscribeId, 10), @@ -204,37 +236,46 @@ func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order, Email: identifier, }) if err != nil { - l.Errorw("[CheckoutOrderLogic] CreatePaymentSheet error", logger.Field("error", err.Error())) + l.Errorw("[PurchaseCheckout] CreatePaymentSheet error", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "CreatePaymentSheet error: %s", err.Error()) } - tradeNo := result.TradeNo + + // Prepare response data for client-side Stripe integration stripePayment := &types.StripePayment{ PublishableKey: stripeConfig.PublicKey, ClientSecret: result.ClientSecret, Method: stripeConfig.Payment, } - // save payment - info.TradeNo = tradeNo + + // Save Stripe trade number to order for tracking + info.TradeNo = result.TradeNo err = l.svcCtx.OrderModel.Update(l.ctx, info) if err != nil { - l.Errorw("[CheckoutOrderLogic] Update error", logger.Field("error", err.Error())) + l.Errorw("[PurchaseCheckout] Update order error", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update error: %s", err.Error()) } return stripePayment, nil } +// epayPayment processes EPay payment by generating a payment URL for redirect +// It handles currency conversion and creates a payment URL for external payment processing func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) { - epayConfig := payment.EPayConfig{} - if err := json.Unmarshal([]byte(config.Config), &epayConfig); err != nil { - l.Errorw("[CheckoutOrderLogic] Unmarshal error", logger.Field("error", err.Error())) + // Parse EPay configuration from payment settings + epayConfig := &payment.EPayConfig{} + if err := epayConfig.Unmarshal([]byte(config.Config)); err != nil { + l.Errorw("[PurchaseCheckout] Unmarshal EPay config error", logger.Field("error", err.Error())) return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) } + // Initialize EPay client with merchant credentials client := epay.NewClient(epayConfig.Pid, epayConfig.Url, epayConfig.Key) - // Calculate the amount with exchange rate + + // Convert order amount to CNY using current exchange rate amount, err := l.queryExchangeRate("CNY", info.Amount) if err != nil { return "", err } + + // Build notification URL for payment status callbacks notifyUrl := "" if config.Domain != "" { notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token @@ -245,7 +286,8 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order } notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token } - // create payment + + // Create payment URL for user redirection url := client.CreatePayUrl(epay.Order{ Name: l.svcCtx.Config.Site.SiteName, Amount: amount, @@ -257,19 +299,25 @@ func (l *PurchaseCheckoutLogic) epayPayment(config *payment.Payment, info *order return url, nil } -func (l *PurchaseCheckoutLogic) payssionPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) { - payssionConfig := payment.PayssionConfig{} - if err := json.Unmarshal([]byte(config.Config), &payssionConfig); err != nil { - l.Errorw("[CheckoutOrderLogic] payssionPayment Unmarshal error", logger.Field("error", err.Error())) - return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " payssionPaymentUnmarshal error: %s", err.Error()) +// CryptoSaaSPayment processes CryptoSaaSPayment payment by generating a payment URL for redirect +// It handles currency conversion and creates a payment URL for external payment processing +func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info *order.Order, returnUrl string) (string, error) { + // Parse EPay configuration from payment settings + epayConfig := &payment.CryptoSaaSConfig{} + if err := epayConfig.Unmarshal([]byte(config.Config)); err != nil { + l.Errorw("[PurchaseCheckout] Unmarshal EPay config error", logger.Field("error", err.Error())) + return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error()) } - client := payssion.NewClient(payssionConfig.ApiKey, payssionConfig.SecretKey, payssionConfig.PmId, payssionConfig.Currency, payssionConfig.QueryUrl, payssionConfig.CreateUrl) - // Calculate the amount with exchange rate + // Initialize EPay client with merchant credentials + client := epay.NewClient(epayConfig.AccountID, epayConfig.Endpoint, epayConfig.SecretKey) + + // Convert order amount to CNY using current exchange rate amount, err := l.queryExchangeRate("CNY", info.Amount) if err != nil { - l.Errorw("[CheckoutOrderLogic] payssionPayment queryExchangeRate error", logger.Field("error", err.Error())) return "", err } + + // Build notification URL for payment status callbacks notifyUrl := "" if config.Domain != "" { notifyUrl = config.Domain + "/v1/notify/" + config.Platform + "/" + config.Token @@ -280,37 +328,47 @@ func (l *PurchaseCheckoutLogic) payssionPayment(config *payment.Payment, info *o } notifyUrl = "https://" + host + "/v1/notify/" + config.Platform + "/" + config.Token } - // create payment - url, err := client.CreateOrder(payssion.Order{ + + // Create payment URL for user redirection + url := client.CreatePayUrl(epay.Order{ Name: l.svcCtx.Config.Site.SiteName, Amount: amount, OrderNo: info.OrderNo, + SignType: "MD5", NotifyUrl: notifyUrl, ReturnUrl: returnUrl, }) - return url, err + return url, nil } -// Query exchange rate +// queryExchangeRate converts the order amount from system currency to target currency +// It retrieves the current exchange rate and performs currency conversion if needed func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount float64, err error) { + // Convert cents to decimal amount amount = float64(src) / float64(100) - // query system currency + + // Retrieve system currency configuration currency, err := l.svcCtx.SystemModel.GetCurrencyConfig(l.ctx) if err != nil { - l.Errorw("[CheckoutOrderLogic] GetCurrencyConfig error", logger.Field("error", err.Error())) + l.Errorw("[PurchaseCheckout] GetCurrencyConfig error", logger.Field("error", err.Error())) return 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "GetCurrencyConfig error: %s", err.Error()) } + + // Parse currency configuration configs := struct { CurrencyUnit string CurrencySymbol string AccessKey string }{} tool.SystemConfigSliceReflectToStruct(currency, &configs) + + // Skip conversion if no exchange rate API key configured if configs.AccessKey == "" { return amount, nil } + + // Convert currency if system currency differs from target currency if configs.CurrencyUnit != to { - // query exchange rate result, err := exchangeRate.GetExchangeRete(configs.CurrencyUnit, to, configs.AccessKey, 1) if err != nil { return 0, err @@ -320,59 +378,149 @@ func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount return amount, nil } -// Balance payment +// balancePayment processes balance payment with gift amount priority logic +// It prioritizes using gift amount first, then regular balance, and creates proper audit logs func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) error { var userInfo user.User - err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + var err error + if o.Amount == 0 { + // No payment required for zero-amount orders + l.Logger.Info( + "[PurchaseCheckout] No payment required for zero-amount order", + logger.Field("orderNo", o.OrderNo), + logger.Field("userId", u.Id), + ) + err = l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, o.OrderNo, 2) + if err != nil { + l.Errorw("[PurchaseCheckout] Update order status error", + logger.Field("error", err.Error()), + logger.Field("orderNo", o.OrderNo), + logger.Field("userId", u.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update order status error: %s", err.Error()) + } + goto activation + } + + err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Retrieve latest user information with row-level locking err := db.Model(&user.User{}).Where("id = ?", u.Id).First(&userInfo).Error if err != nil { return err } - if userInfo.Balance < o.Amount { - return errors.Wrapf(xerr.NewErrCode(xerr.InsufficientBalance), "Insufficient balance") + // Check if user has sufficient total balance (regular + gift) + totalAvailable := userInfo.Balance + userInfo.GiftAmount + if totalAvailable < o.Amount { + return errors.Wrapf(xerr.NewErrCode(xerr.InsufficientBalance), + "Insufficient balance: required %d, available %d", o.Amount, totalAvailable) } - // deduct balance - userInfo.Balance -= o.Amount + + // Calculate payment distribution: prioritize gift amount first + var giftUsed, balanceUsed int64 + remainingAmount := o.Amount + + if userInfo.GiftAmount >= remainingAmount { + // Gift amount covers the entire payment + giftUsed = remainingAmount + balanceUsed = 0 + } else { + // Use all available gift amount, then regular balance + giftUsed = userInfo.GiftAmount + balanceUsed = remainingAmount - giftUsed + } + + // Update user balances + userInfo.GiftAmount -= giftUsed + userInfo.Balance -= balanceUsed + + // Save updated user information err = l.svcCtx.UserModel.Update(l.ctx, &userInfo) if err != nil { return err } - // create balance log - balanceLog := &user.BalanceLog{ - Id: 0, - UserId: u.Id, - Amount: o.Amount, - Type: 3, - OrderId: o.Id, - Balance: userInfo.Balance, + + // Create gift amount log if gift amount was used + if giftUsed > 0 { + giftLog := &log.Gift{ + OrderNo: o.OrderNo, + Type: log.GiftTypeReduce, // Type 2 represents gift amount decrease/usage + Amount: giftUsed, + Balance: userInfo.GiftAmount, + Remark: "Purchase payment", + } + content, _ := giftLog.Marshal() + + err = db.Create(&log.SystemLog{ + Type: log.TypeGift.Uint8(), + ObjectID: userInfo.Id, + Date: time.Now().Format(time.DateOnly), + Content: string(content), + }).Error + if err != nil { + return err + } } - err = db.Create(balanceLog).Error + + // Create balance log if regular balance was used + if balanceUsed > 0 { + balanceLog := &log.Balance{ + Amount: balanceUsed, + Type: log.BalanceTypePayment, // Type 3 represents payment deduction + OrderNo: o.OrderNo, + Balance: userInfo.Balance, + Timestamp: time.Now().UnixMilli(), + } + content, _ := balanceLog.Marshal() + err = db.Create(&log.SystemLog{ + Type: log.TypeBalance.Uint8(), + ObjectID: userInfo.Id, + Date: time.Now().Format(time.DateOnly), + Content: string(content), + }).Error + if err != nil { + return err + } + } + + // Store gift amount used in order for potential refund tracking + o.GiftAmount = giftUsed + err = l.svcCtx.OrderModel.Update(l.ctx, o, db) if err != nil { return err } - return l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, o.OrderNo, 2) + + // Mark order as paid (status = 2) + return l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, o.OrderNo, 2, db) }) + if err != nil { - l.Errorw("[CheckoutOrderLogic] Transaction error", logger.Field("error", err.Error()), logger.Field("orderNo", o.OrderNo)) + l.Errorw("[PurchaseCheckout] Balance payment transaction error", + logger.Field("error", err.Error()), + logger.Field("orderNo", o.OrderNo), + logger.Field("userId", u.Id)) return err } - // create activity order task + +activation: + // Enqueue order activation task for immediate processing payload := queueType.ForthwithActivateOrderPayload{ OrderNo: o.OrderNo, } bytes, err := json.Marshal(payload) if err != nil { - l.Errorw("[CheckoutOrderLogic] Marshal error", logger.Field("error", err.Error())) + l.Errorw("[PurchaseCheckout] Marshal activation payload error", logger.Field("error", err.Error())) return err } task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes) _, err = l.svcCtx.Queue.EnqueueContext(l.ctx, task) if err != nil { - l.Errorw("[CheckoutOrderLogic] Enqueue error", logger.Field("error", err.Error())) + l.Errorw("[PurchaseCheckout] Enqueue activation task error", logger.Field("error", err.Error())) return err } - l.Logger.Info("[CheckoutOrderLogic] Enqueue success", logger.Field("orderNo", o.OrderNo)) + + l.Logger.Info("[PurchaseCheckout] Balance payment completed successfully", + logger.Field("orderNo", o.OrderNo), + logger.Field("userId", u.Id)) return nil } diff --git a/internal/logic/public/portal/purchaseLogic.go b/internal/logic/public/portal/purchaseLogic.go index 2f445a2..2c0cd69 100644 --- a/internal/logic/public/portal/purchaseLogic.go +++ b/internal/logic/public/portal/purchaseLogic.go @@ -6,18 +6,17 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/pkg/payment" - - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/payment" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" + queue "github.com/perfect-panel/server/queue/types" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - queue "github.com/perfect-panel/ppanel-server/queue/types" "github.com/pkg/errors" "gorm.io/gorm" ) @@ -71,7 +70,7 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types. amount := int64(float64(price) * discount) discountAmount := price - amount - var coupon int64 = 0 + var couponAmount int64 = 0 // Calculate the coupon deduction if req.Coupon != "" { couponInfo, err := l.svcCtx.CouponModel.FindOneByCode(l.ctx, req.Coupon) @@ -81,18 +80,18 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types. } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find coupon error: %v", err.Error()) } - if couponInfo.Count <= couponInfo.UsedCount { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponUsed), "coupon used") + if couponInfo.Count != 0 && couponInfo.Count <= couponInfo.UsedCount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponInsufficientUsage), "coupon used") } couponSub := tool.StringToInt64Slice(couponInfo.Subscribe) if len(couponSub) > 0 && !tool.Contains(couponSub, req.SubscribeId) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotMatch), "coupon not match") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.CouponNotApplicable), "coupon not match") } - coupon = calculateCoupon(amount, couponInfo) + + couponAmount = calculateCoupon(amount, couponInfo) } // Calculate the handling fee - amount -= coupon - var deductionAmount int64 + amount -= couponAmount // find payment method paymentConfig, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment) if err != nil { @@ -117,9 +116,9 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types. Price: price, Amount: amount, Discount: discountAmount, - GiftAmount: deductionAmount, + GiftAmount: 0, Coupon: req.Coupon, - CouponDiscount: coupon, + CouponDiscount: couponAmount, PaymentId: req.Payment, Method: paymentConfig.Platform, FeeAmount: feeAmount, @@ -135,14 +134,17 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types. Identifier: req.Identifier, AuthType: req.AuthType, Password: req.Password, + InviteCode: req.InviteCode, } - if _, err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo), tempOrder.Marshal(), CloseOrderTimeMinutes*time.Minute).Result(); err != nil { + content, _ := tempOrder.Marshal() + + if _, err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo), string(content), CloseOrderTimeMinutes*time.Minute).Result(); err != nil { l.Errorw("[Purchase] Redis set error", logger.Field("error", err.Error()), logger.Field("order_no", orderInfo.OrderNo)) return err } l.Infow("[Purchase] Guest order", logger.Field("order_no", orderInfo.OrderNo), logger.Field("identifier", req.Identifier)) // save guest order - if err := l.svcCtx.OrderModel.Insert(l.ctx, orderInfo, tx); err != nil { + if err = l.svcCtx.OrderModel.Insert(l.ctx, orderInfo, tx); err != nil { return err } return nil diff --git a/internal/logic/public/portal/queryPurchaseOrderLogic.go b/internal/logic/public/portal/queryPurchaseOrderLogic.go index fc67dc4..d8e4795 100644 --- a/internal/logic/public/portal/queryPurchaseOrderLogic.go +++ b/internal/logic/public/portal/queryPurchaseOrderLogic.go @@ -6,18 +6,18 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/internal/model/order" + "github.com/perfect-panel/server/internal/model/order" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/pkg/tool" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/jwt" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -95,7 +95,7 @@ func (l *QueryPurchaseOrderLogic) handleTemporaryOrder(orderInfo *order.Order, r } // Validate user and email - if err := l.validateUserAndEmail(orderInfo, req.Identifier, req.Identifier); err != nil { + if err = l.validateUserAndEmail(orderInfo, req.AuthType, req.Identifier); err != nil { return "", err } diff --git a/internal/logic/public/portal/tool.go b/internal/logic/public/portal/tool.go index 521632d..c2d2bbd 100644 --- a/internal/logic/public/portal/tool.go +++ b/internal/logic/public/portal/tool.go @@ -1,9 +1,9 @@ package portal import ( - "github.com/perfect-panel/ppanel-server/internal/model/coupon" - "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/server/internal/model/coupon" + "github.com/perfect-panel/server/internal/model/payment" + "github.com/perfect-panel/server/internal/types" ) func getDiscount(discounts []types.SubscribeDiscount, inputMonths int64) float64 { diff --git a/internal/logic/public/subscribe/queryApplicationConfigLogic.go b/internal/logic/public/subscribe/queryApplicationConfigLogic.go deleted file mode 100644 index b5a913d..0000000 --- a/internal/logic/public/subscribe/queryApplicationConfigLogic.go +++ /dev/null @@ -1,116 +0,0 @@ -package subscribe - -import ( - "context" - - "github.com/perfect-panel/ppanel-server/internal/model/application" - "github.com/perfect-panel/ppanel-server/pkg/xerr" - "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -type QueryApplicationConfigLogic struct { - logger.Logger - ctx context.Context - svcCtx *svc.ServiceContext -} - -// Get application config -func NewQueryApplicationConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryApplicationConfigLogic { - return &QueryApplicationConfigLogic{ - Logger: logger.WithContext(ctx), - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *QueryApplicationConfigLogic) QueryApplicationConfig() (resp *types.ApplicationResponse, err error) { - resp = &types.ApplicationResponse{} - var applications []*application.Application - err = l.svcCtx.ApplicationModel.Transaction(l.ctx, func(tx *gorm.DB) (err error) { - return tx.Model(applications).Preload("ApplicationVersions").Find(&applications).Error - }) - if err != nil { - l.Errorw("[QueryApplicationConfig] get application error: ", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get application error: %v", err.Error()) - } - - if len(applications) == 0 { - return resp, nil - } - - for _, app := range applications { - applicationResponse := types.ApplicationResponseInfo{ - Id: app.Id, - Name: app.Name, - Icon: app.Icon, - Description: app.Description, - SubscribeType: app.SubscribeType, - } - applicationVersions := app.ApplicationVersions - if len(applicationVersions) != 0 { - for _, applicationVersion := range applicationVersions { - /*if !applicationVersion.IsDefault { - continue - }*/ - switch applicationVersion.Platform { - case "ios": - applicationResponse.Platform.IOS = append(applicationResponse.Platform.IOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "macos": - applicationResponse.Platform.MacOS = append(applicationResponse.Platform.MacOS, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "linux": - applicationResponse.Platform.Linux = append(applicationResponse.Platform.Linux, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "android": - applicationResponse.Platform.Android = append(applicationResponse.Platform.Android, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "windows": - applicationResponse.Platform.Windows = append(applicationResponse.Platform.Windows, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - case "harmony": - applicationResponse.Platform.Harmony = append(applicationResponse.Platform.Harmony, &types.ApplicationVersion{ - Id: applicationVersion.Id, - Url: applicationVersion.Url, - Version: applicationVersion.Version, - IsDefault: applicationVersion.IsDefault, - Description: applicationVersion.Description, - }) - } - } - } - resp.Applications = append(resp.Applications, applicationResponse) - } - - return -} diff --git a/internal/logic/public/subscribe/querySubscribeGroupListLogic.go b/internal/logic/public/subscribe/querySubscribeGroupListLogic.go index d0f08ac..0f7c947 100644 --- a/internal/logic/public/subscribe/querySubscribeGroupListLogic.go +++ b/internal/logic/public/subscribe/querySubscribeGroupListLogic.go @@ -3,12 +3,12 @@ package subscribe import ( "context" - "github.com/perfect-panel/ppanel-server/internal/model/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/subscribe/querySubscribeListLogic.go b/internal/logic/public/subscribe/querySubscribeListLogic.go index c51b5a9..2208559 100644 --- a/internal/logic/public/subscribe/querySubscribeListLogic.go +++ b/internal/logic/public/subscribe/querySubscribeListLogic.go @@ -4,11 +4,12 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -27,15 +28,22 @@ func NewQuerySubscribeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) } } -func (l *QuerySubscribeListLogic) QuerySubscribeList() (resp *types.QuerySubscribeListResponse, err error) { +func (l *QuerySubscribeListLogic) QuerySubscribeList(req *types.QuerySubscribeListRequest) (resp *types.QuerySubscribeListResponse, err error) { - data, err := l.svcCtx.SubscribeModel.QuerySubscribeList(l.ctx) + total, data, err := l.svcCtx.SubscribeModel.FilterList(l.ctx, &subscribe.FilterParams{ + Page: 1, + Size: 9999, + Language: req.Language, + Sell: true, + DefaultLanguage: true, + }) if err != nil { l.Errorw("[QuerySubscribeListLogic] Database Error", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QuerySubscribeList error: %v", err.Error()) } + resp = &types.QuerySubscribeListResponse{ - Total: int64(len(data)), + Total: total, } list := make([]types.Subscribe, len(data)) for i, item := range data { diff --git a/internal/logic/public/ticket/createUserTicketFollowLogic.go b/internal/logic/public/ticket/createUserTicketFollowLogic.go index c540931..06574c0 100644 --- a/internal/logic/public/ticket/createUserTicketFollowLogic.go +++ b/internal/logic/public/ticket/createUserTicketFollowLogic.go @@ -3,14 +3,14 @@ package ticket import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/ticket" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/ticket" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/ticket/createUserTicketLogic.go b/internal/logic/public/ticket/createUserTicketLogic.go index 51ce680..ea6cda1 100644 --- a/internal/logic/public/ticket/createUserTicketLogic.go +++ b/internal/logic/public/ticket/createUserTicketLogic.go @@ -3,16 +3,16 @@ package ticket import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/ticket" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/ticket" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type CreateUserTicketLogic struct { diff --git a/internal/logic/public/ticket/getUserTicketDetailsLogic.go b/internal/logic/public/ticket/getUserTicketDetailsLogic.go index b22b3d5..681e7fb 100644 --- a/internal/logic/public/ticket/getUserTicketDetailsLogic.go +++ b/internal/logic/public/ticket/getUserTicketDetailsLogic.go @@ -3,14 +3,14 @@ package ticket import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/ticket/getUserTicketListLogic.go b/internal/logic/public/ticket/getUserTicketListLogic.go index fdaa92c..6340259 100644 --- a/internal/logic/public/ticket/getUserTicketListLogic.go +++ b/internal/logic/public/ticket/getUserTicketListLogic.go @@ -3,15 +3,15 @@ package ticket import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/ticket/updateUserTicketStatusLogic.go b/internal/logic/public/ticket/updateUserTicketStatusLogic.go index acf71f7..2748a8d 100644 --- a/internal/logic/public/ticket/updateUserTicketStatusLogic.go +++ b/internal/logic/public/ticket/updateUserTicketStatusLogic.go @@ -3,13 +3,13 @@ package ticket import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/user/bindOAuthCallbackLogic.go b/internal/logic/public/user/bindOAuthCallbackLogic.go index dc4044f..26e8e0e 100644 --- a/internal/logic/public/user/bindOAuthCallbackLogic.go +++ b/internal/logic/public/user/bindOAuthCallbackLogic.go @@ -5,17 +5,17 @@ import ( "encoding/json" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/auth" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/oauth/apple" - "github.com/perfect-panel/ppanel-server/pkg/oauth/google" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/oauth/apple" + "github.com/perfect-panel/server/pkg/oauth/google" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) @@ -52,8 +52,10 @@ func (l *BindOAuthCallbackLogic) BindOAuthCallback(req *types.BindOAuthCallbackR err = l.google(req) case "apple": err = l.apple(req) + case "telegram": + err = l.telegram(req) default: - l.Errorw("oauth login method not support: %v", logger.Field("method", req.Method)) + l.Errorw("oauth login method not support", logger.Field("method", req.Method)) return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "oauth login method not support: %v", req.Method) } if err != nil { @@ -212,3 +214,7 @@ func (l *BindOAuthCallbackLogic) apple(req *types.BindOAuthCallbackRequest) erro } return nil } + +func (l *BindOAuthCallbackLogic) telegram(req *types.BindOAuthCallbackRequest) error { + return nil +} diff --git a/internal/logic/public/user/bindOAuthLogic.go b/internal/logic/public/user/bindOAuthLogic.go index b6acfe1..c2d9778 100644 --- a/internal/logic/public/user/bindOAuthLogic.go +++ b/internal/logic/public/user/bindOAuthLogic.go @@ -5,16 +5,16 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/internal/model/auth" - "github.com/perfect-panel/ppanel-server/pkg/oauth/google" - "github.com/perfect-panel/ppanel-server/pkg/random" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/pkg/oauth/google" + "github.com/perfect-panel/server/pkg/random" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "golang.org/x/oauth2" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type BindOAuthLogic struct { diff --git a/internal/logic/public/user/bindTelegramLogic.go b/internal/logic/public/user/bindTelegramLogic.go index 5fefe08..cb1a3b1 100644 --- a/internal/logic/public/user/bindTelegramLogic.go +++ b/internal/logic/public/user/bindTelegramLogic.go @@ -5,9 +5,9 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type BindTelegramLogic struct { diff --git a/internal/logic/public/user/calculateRemainingAmount.go b/internal/logic/public/user/calculateRemainingAmount.go index 9559956..d601c2e 100644 --- a/internal/logic/public/user/calculateRemainingAmount.go +++ b/internal/logic/public/user/calculateRemainingAmount.go @@ -3,11 +3,11 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/deduction" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/deduction" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -38,6 +38,7 @@ func CalculateRemainingAmount(ctx context.Context, svcCtx *svc.ServiceContext, u orderQuantity := orderDetails.Quantity // Calculate Order Amount orderAmount := orderDetails.Amount + orderDetails.GiftAmount + if len(orderDetails.SubOrders) > 0 { for _, subOrder := range orderDetails.SubOrders { if subOrder.Status == 2 || subOrder.Status == 5 { @@ -47,7 +48,7 @@ func CalculateRemainingAmount(ctx context.Context, svcCtx *svc.ServiceContext, u } } // Calculate Remaining Amount - remainingAmount := deduction.CalculateRemainingAmount( + remainingAmount, err := deduction.CalculateRemainingAmount( deduction.Subscribe{ StartTime: userSubscribe.StartTime, ExpireTime: userSubscribe.ExpireTime, @@ -64,5 +65,8 @@ func CalculateRemainingAmount(ctx context.Context, svcCtx *svc.ServiceContext, u Quantity: orderQuantity, }, ) + if err != nil { + return 0, errors.Wrapf(xerr.NewErrCode(500), "CalculateRemainingAmount failed, userSubscribeId: %d, err: %v", userSubscribeId, err) + } return remainingAmount, nil } diff --git a/internal/logic/public/user/getDeviceListLogic.go b/internal/logic/public/user/getDeviceListLogic.go new file mode 100644 index 0000000..76722d5 --- /dev/null +++ b/internal/logic/public/user/getDeviceListLogic.go @@ -0,0 +1,39 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" +) + +type GetDeviceListLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Get Device List +func NewGetDeviceListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetDeviceListLogic { + return &GetDeviceListLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetDeviceListLogic) GetDeviceList() (resp *types.GetDeviceListResponse, err error) { + userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + list, count, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, userInfo.Id) + userRespList := make([]types.UserDevice, 0) + tool.DeepCopy(&userRespList, list) + resp = &types.GetDeviceListResponse{ + Total: count, + List: userRespList, + } + return +} diff --git a/internal/logic/public/user/getLoginLogLogic.go b/internal/logic/public/user/getLoginLogLogic.go index e498911..a6637f4 100644 --- a/internal/logic/public/user/getLoginLogLogic.go +++ b/internal/logic/public/user/getLoginLogLogic.go @@ -3,14 +3,14 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -35,15 +35,34 @@ func (l *GetLoginLogLogic) GetLoginLog(req *types.GetLoginLogRequest) (resp *typ logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } - data, total, err := l.svcCtx.UserModel.FilterLoginLogList(l.ctx, req.Page, req.Size, &user.LoginLogFilterParams{ - UserId: u.Id, + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeLogin.Uint8(), + ObjectID: u.Id, }) if err != nil { l.Errorw("find login log failed:", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find login log failed: %v", err.Error()) } list := make([]types.UserLoginLog, 0) - tool.DeepCopy(&list, data) + + for _, datum := range data { + var content log.Login + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("[GetUserLoginLogs] unmarshal login log content failed: %v", err.Error()) + continue + } + list = append(list, types.UserLoginLog{ + Id: datum.Id, + UserId: datum.ObjectID, + LoginIP: content.LoginIP, + UserAgent: content.UserAgent, + Success: content.Success, + Timestamp: datum.CreatedAt.UnixMilli(), + }) + } + return &types.GetLoginLogResponse{ Total: total, List: list, diff --git a/internal/logic/public/user/getOAuthMethodsLogic.go b/internal/logic/public/user/getOAuthMethodsLogic.go index 3ad0f2b..7678380 100644 --- a/internal/logic/public/user/getOAuthMethodsLogic.go +++ b/internal/logic/public/user/getOAuthMethodsLogic.go @@ -3,14 +3,14 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/user/getSubscribeLogLogic.go b/internal/logic/public/user/getSubscribeLogLogic.go index b495c7a..eeb51b9 100644 --- a/internal/logic/public/user/getSubscribeLogLogic.go +++ b/internal/logic/public/user/getSubscribeLogLogic.go @@ -3,14 +3,14 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -20,7 +20,7 @@ type GetSubscribeLogLogic struct { svcCtx *svc.ServiceContext } -// Get Subscribe Log +// NewGetSubscribeLogLogic Get Subscribe Log func NewGetSubscribeLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSubscribeLogLogic { return &GetSubscribeLogLogic{ Logger: logger.WithContext(ctx), @@ -35,15 +35,34 @@ func (l *GetSubscribeLogLogic) GetSubscribeLog(req *types.GetSubscribeLogRequest logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } - data, total, err := l.svcCtx.UserModel.FilterSubscribeLogList(l.ctx, req.Page, req.Size, &user.SubscribeLogFilterParams{ - UserId: u.Id, + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeSubscribe.Uint8(), + ObjectID: u.Id, // filter by current user id }) if err != nil { l.Errorw("[GetUserSubscribeLogs] Get User Subscribe Logs Error:", logger.Field("err", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Get User Subscribe Logs Error") } var list []types.UserSubscribeLog - tool.DeepCopy(&list, data) + + for _, item := range data { + var content log.Subscribe + if err = content.Unmarshal([]byte(item.Content)); err != nil { + l.Errorf("[GetUserSubscribeLogs] unmarshal subscribe log content failed: %v", err.Error()) + continue + } + list = append(list, types.UserSubscribeLog{ + Id: item.Id, + UserId: item.ObjectID, + UserSubscribeId: content.UserSubscribeId, + Token: content.Token, + IP: content.ClientIP, + UserAgent: content.UserAgent, + Timestamp: item.CreatedAt.UnixMilli(), + }) + } return &types.GetSubscribeLogResponse{ List: list, diff --git a/internal/logic/public/user/preUnsubscribeLogic.go b/internal/logic/public/user/preUnsubscribeLogic.go index 2554a4a..729dcbc 100644 --- a/internal/logic/public/user/preUnsubscribeLogic.go +++ b/internal/logic/public/user/preUnsubscribeLogic.go @@ -3,9 +3,9 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type PreUnsubscribeLogic struct { diff --git a/internal/logic/public/user/queryUserAffiliateListLogic.go b/internal/logic/public/user/queryUserAffiliateListLogic.go index 5182b77..b645270 100644 --- a/internal/logic/public/user/queryUserAffiliateListLogic.go +++ b/internal/logic/public/user/queryUserAffiliateListLogic.go @@ -3,16 +3,16 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type QueryUserAffiliateListLogic struct { diff --git a/internal/logic/public/user/queryUserAffiliateLogic.go b/internal/logic/public/user/queryUserAffiliateLogic.go index 5240fc9..7c8e731 100644 --- a/internal/logic/public/user/queryUserAffiliateLogic.go +++ b/internal/logic/public/user/queryUserAffiliateLogic.go @@ -3,16 +3,17 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type QueryUserAffiliateLogic struct { @@ -44,14 +45,20 @@ func (l *QueryUserAffiliateLogic) QueryUserAffiliate() (resp *types.QueryUserAff if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate failed: %v", err) } - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Model(&user.CommissionLog{}). - Where("user_id = ?", u.Id). - Select("COALESCE(SUM(amount), 0)"). - Scan(&sum).Error + data, _, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: 1, + Size: 99999, + Type: log.TypeCommission.Uint8(), + ObjectID: u.Id, }) - if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Affiliate failed: %v", err) + + for _, datum := range data { + content := log.Commission{} + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("[QueryUserAffiliate] unmarshal comission log failed: %v", err.Error()) + continue + } + sum += content.Amount } return &types.QueryUserAffiliateCountResponse{ diff --git a/internal/logic/public/user/queryUserBalanceLogLogic.go b/internal/logic/public/user/queryUserBalanceLogLogic.go index a48f267..e8c6d8f 100644 --- a/internal/logic/public/user/queryUserBalanceLogLogic.go +++ b/internal/logic/public/user/queryUserBalanceLogLogic.go @@ -3,17 +3,15 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" ) type QueryUserBalanceLogLogic struct { @@ -22,7 +20,7 @@ type QueryUserBalanceLogLogic struct { svcCtx *svc.ServiceContext } -// Query User Balance Log +// NewQueryUserBalanceLogLogic Query User Balance Log func NewQueryUserBalanceLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryUserBalanceLogLogic { return &QueryUserBalanceLogLogic{ Logger: logger.WithContext(ctx), @@ -37,19 +35,37 @@ func (l *QueryUserBalanceLogLogic) QueryUserBalanceLog() (resp *types.QueryUserB logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } - var data []*user.BalanceLog - var total int64 - // Query User Balance Log - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Model(&user.BalanceLog{}).Order("created_at DESC").Where("user_id = ?", u.Id).Count(&total).Find(&data).Error + + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: 1, + Size: 99999, + Type: log.TypeBalance.Uint8(), + ObjectID: u.Id, }) if err != nil { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Balance Log failed: %v", err) + l.Errorw("[QueryUserBalanceLog] Query User Balance Log Error:", logger.Field("err", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Balance Log Error") } - resp = &types.QueryUserBalanceLogListResponse{ - List: make([]types.UserBalanceLog, 0), + + list := make([]types.BalanceLog, 0) + for _, datum := range data { + var content log.Balance + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("[QueryUserBalanceLog] unmarshal balance log content failed: %v", err.Error()) + continue + } + list = append(list, types.BalanceLog{ + UserId: datum.ObjectID, + Amount: content.Amount, + Type: content.Type, + OrderNo: content.OrderNo, + Balance: content.Balance, + Timestamp: content.Timestamp, + }) + } + + return &types.QueryUserBalanceLogListResponse{ Total: total, - } - tool.DeepCopy(&resp.List, data) - return + List: list, + }, nil } diff --git a/internal/logic/public/user/queryUserCommissionLogLogic.go b/internal/logic/public/user/queryUserCommissionLogLogic.go index f4c81e7..c005828 100644 --- a/internal/logic/public/user/queryUserCommissionLogLogic.go +++ b/internal/logic/public/user/queryUserCommissionLogLogic.go @@ -3,17 +3,15 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "gorm.io/gorm" - - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" ) type QueryUserCommissionLogLogic struct { @@ -32,22 +30,40 @@ func NewQueryUserCommissionLogLogic(ctx context.Context, svcCtx *svc.ServiceCont } func (l *QueryUserCommissionLogLogic) QueryUserCommissionLog(req *types.QueryUserCommissionLogListRequest) (resp *types.QueryUserCommissionLogListResponse, err error) { - var data []*user.CommissionLog u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok { logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } - err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - return db.Order("id desc").Limit(req.Size).Offset((req.Page-1)*req.Size).Where("user_id = ?", u.Id).Find(&data).Error + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &log.FilterParams{ + Page: req.Page, + Size: req.Size, + Type: log.TypeCommission.Uint8(), + ObjectID: u.Id, }) if err != nil { l.Errorw("Query User Commission Log failed", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Commission Log failed: %v", err) } var list []types.CommissionLog - tool.DeepCopy(&list, data) + + for _, datum := range data { + var content log.Commission + if err = content.Unmarshal([]byte(datum.Content)); err != nil { + l.Errorf("unmarshal commission log content failed: %v", err.Error()) + continue + } + list = append(list, types.CommissionLog{ + UserId: datum.ObjectID, + Type: content.Type, + Amount: content.Amount, + OrderNo: content.OrderNo, + Timestamp: content.Timestamp, + }) + } + return &types.QueryUserCommissionLogListResponse{ - List: list, + List: list, + Total: total, }, nil } diff --git a/internal/logic/public/user/queryUserInfoLogic.go b/internal/logic/public/user/queryUserInfoLogic.go index 8cbd54a..cf51020 100644 --- a/internal/logic/public/user/queryUserInfoLogic.go +++ b/internal/logic/public/user/queryUserInfoLogic.go @@ -2,17 +2,18 @@ package user import ( "context" + "sort" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/phone" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/phone" + "github.com/perfect-panel/server/pkg/tool" ) type QueryUserInfoLogic struct { @@ -53,10 +54,31 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) { } userMethods = append(userMethods, item) } + + // 按照指定顺序排序:email第一位,mobile第二位,其他按原顺序 + sort.Slice(userMethods, func(i, j int) bool { + return getAuthTypePriority(userMethods[i].AuthType) < getAuthTypePriority(userMethods[j].AuthType) + }) + resp.AuthMethods = userMethods return resp, nil } +// getAuthTypePriority 获取认证类型的排序优先级 +// email: 1 (第一位) +// mobile: 2 (第二位) +// 其他类型: 100+ (后续位置) +func getAuthTypePriority(authType string) int { + switch authType { + case "email": + return 1 + case "mobile": + return 2 + default: + return 100 + } +} + // maskOpenID 脱敏 OpenID,只保留前 3 和后 3 位 func maskOpenID(openID string) string { length := len(openID) diff --git a/internal/logic/public/user/queryUserSubscribeLogic.go b/internal/logic/public/user/queryUserSubscribeLogic.go index 372e19b..757f6fc 100644 --- a/internal/logic/public/user/queryUserSubscribeLogic.go +++ b/internal/logic/public/user/queryUserSubscribeLogic.go @@ -2,16 +2,17 @@ package user import ( "context" + "encoding/json" "time" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -36,7 +37,7 @@ func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSub logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } - data, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 1, 0) + data, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 0, 1, 2, 3) if err != nil { l.Errorw("[QueryUserSubscribeLogic] Query User Subscribe Error:", logger.Field("err", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Subscribe Error") @@ -50,6 +51,15 @@ func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSub for _, item := range data { var sub types.UserSubscribe tool.DeepCopy(&sub, item) + + // 解析Discount字段 避免在续订时只能续订一个月 + if item.Subscribe != nil && item.Subscribe.Discount != "" { + var discounts []types.SubscribeDiscount + if err := json.Unmarshal([]byte(item.Subscribe.Discount), &discounts); err == nil { + sub.Subscribe.Discount = discounts + } + } + sub.ResetTime = calculateNextResetTime(&sub) resp.List = append(resp.List, sub) } @@ -58,7 +68,7 @@ func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSub // 计算下次重置时间 func calculateNextResetTime(sub *types.UserSubscribe) int64 { - startTime := time.UnixMilli(sub.StartTime) + resetTime := time.UnixMilli(sub.ExpireTime) now := time.Now() switch sub.Subscribe.ResetCycle { case 0: @@ -66,15 +76,15 @@ func calculateNextResetTime(sub *types.UserSubscribe) int64 { case 1: return time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location()).UnixMilli() case 2: - if startTime.Day() > now.Day() { - return time.Date(now.Year(), now.Month(), startTime.Day(), 0, 0, 0, 0, now.Location()).UnixMilli() + if resetTime.Day() > now.Day() { + return time.Date(now.Year(), now.Month(), resetTime.Day(), 0, 0, 0, 0, now.Location()).UnixMilli() } else { - return time.Date(now.Year(), now.Month()+1, startTime.Day(), 0, 0, 0, 0, now.Location()).UnixMilli() + return time.Date(now.Year(), now.Month()+1, resetTime.Day(), 0, 0, 0, 0, now.Location()).UnixMilli() } case 3: - targetTime := time.Date(now.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, now.Location()) + targetTime := time.Date(now.Year(), resetTime.Month(), resetTime.Day(), 0, 0, 0, 0, now.Location()) if targetTime.Before(now) { - targetTime = time.Date(now.Year()+1, startTime.Month(), startTime.Day(), 0, 0, 0, 0, now.Location()) + targetTime = time.Date(now.Year()+1, resetTime.Month(), resetTime.Day(), 0, 0, 0, 0, now.Location()) } return targetTime.UnixMilli() default: diff --git a/internal/logic/public/user/resetUserSubscribeTokenLogic.go b/internal/logic/public/user/resetUserSubscribeTokenLogic.go index 30bfd9e..febcae7 100644 --- a/internal/logic/public/user/resetUserSubscribeTokenLogic.go +++ b/internal/logic/public/user/resetUserSubscribeTokenLogic.go @@ -4,16 +4,18 @@ import ( "context" "time" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/internal/model/order" + + "github.com/perfect-panel/server/pkg/constant" "github.com/google/uuid" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -47,12 +49,20 @@ func (l *ResetUserSubscribeTokenLogic) ResetUserSubscribeToken(req *types.ResetU l.Errorw("UserSubscribeId does not belong to the current user") return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "UserSubscribeId does not belong to the current user") } + + var orderDetails *order.Details // find order - orderDetails, err := l.svcCtx.OrderModel.FindOneDetails(l.ctx, userSub.OrderId) - if err != nil { - l.Errorw("FindOneDetails failed:", logger.Field("error", err.Error())) - return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneDetails failed: %v", err.Error()) + if userSub.OrderId != 0 { + orderDetails, err = l.svcCtx.OrderModel.FindOneDetails(l.ctx, userSub.OrderId) + if err != nil { + l.Errorw("FindOneDetails failed:", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneDetails failed: %v", err.Error()) + } + } else { + // if order id is 0, this a admin create user subscribe + orderDetails = &order.Details{} } + userSub.Token = uuidx.SubscribeToken(orderDetails.OrderNo + time.Now().Format("20060102150405.000")) userSub.UUID = uuid.New().String() var newSub user.Subscribe @@ -63,5 +73,16 @@ func (l *ResetUserSubscribeTokenLogic) ResetUserSubscribeToken(req *types.ResetU l.Errorw("UpdateSubscribe failed:", logger.Field("error", err.Error())) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateSubscribe failed: %v", err.Error()) } + //clear user subscription cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, &newSub); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + // Clear subscription cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + return nil } diff --git a/internal/logic/public/user/unbindDeviceLogic.go b/internal/logic/public/user/unbindDeviceLogic.go new file mode 100644 index 0000000..e081871 --- /dev/null +++ b/internal/logic/public/user/unbindDeviceLogic.go @@ -0,0 +1,42 @@ +package user + +import ( + "context" + + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type UnbindDeviceLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// Unbind Device +func NewUnbindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UnbindDeviceLogic { + return &UnbindDeviceLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error { + userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + device, err := l.svcCtx.UserModel.FindOneDevice(l.ctx, req.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DeviceNotExist), "find device") + } + + if device.UserId != userInfo.Id { + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device not belong to user") + } + + return l.svcCtx.UserModel.DeleteDevice(l.ctx, req.Id) +} diff --git a/internal/logic/public/user/unbindOAuthLogic.go b/internal/logic/public/user/unbindOAuthLogic.go index 97887a8..efe0ba1 100644 --- a/internal/logic/public/user/unbindOAuthLogic.go +++ b/internal/logic/public/user/unbindOAuthLogic.go @@ -3,13 +3,13 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -42,6 +42,7 @@ func (l *UnbindOAuthLogic) UnbindOAuth(req *types.UnbindOAuthRequest) error { l.Errorw("delete user auth methods failed:", logger.Field("error", err.Error())) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete user auth methods failed: %v", err.Error()) } + return nil } func (l *UnbindOAuthLogic) validator(req *types.UnbindOAuthRequest) bool { diff --git a/internal/logic/public/user/unbindTelegramLogic.go b/internal/logic/public/user/unbindTelegramLogic.go index e379614..a02196f 100644 --- a/internal/logic/public/user/unbindTelegramLogic.go +++ b/internal/logic/public/user/unbindTelegramLogic.go @@ -5,15 +5,15 @@ import ( "strconv" "time" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "github.com/perfect-panel/ppanel-server/internal/logic/telegram" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/logic/telegram" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/user/unsubscribeLogic.go b/internal/logic/public/user/unsubscribeLogic.go index 0befd5d..d3390fe 100644 --- a/internal/logic/public/user/unsubscribeLogic.go +++ b/internal/logic/public/user/unsubscribeLogic.go @@ -2,17 +2,20 @@ package user import ( "context" + "time" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/server/internal/model/user" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type UnsubscribeLogic struct { @@ -21,7 +24,7 @@ type UnsubscribeLogic struct { svcCtx *svc.ServiceContext } -// NewUnsubscribeLogic Unsubscribe +// NewUnsubscribeLogic creates a new instance of UnsubscribeLogic for handling subscription cancellation func NewUnsubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UnsubscribeLogic { return &UnsubscribeLogic{ Logger: logger.WithContext(ctx), @@ -30,42 +33,135 @@ func NewUnsubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Unsub } } +// Unsubscribe handles the subscription cancellation process with proper refund distribution +// It prioritizes refunding to gift amount for balance-paid orders, then to regular balance func (l *UnsubscribeLogic) Unsubscribe(req *types.UnsubscribeRequest) error { u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok { logger.Error("current user is not found in context") return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } + + // find user subscription by ID + userSub, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.Id) + if err != nil { + l.Errorw("FindOneSubscribe failed", logger.Field("error", err.Error()), logger.Field("reqId", req.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindOneSubscribe failed: %v", err.Error()) + } + + activate := []uint8{0, 1, 2} + + if !tool.Contains(activate, userSub.Status) { + // Only active (2) or paused (5) subscriptions can be cancelled + l.Errorw("Subscription status invalid for cancellation", logger.Field("userSubscribeId", userSub.Id), logger.Field("status", userSub.Status)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Subscription status invalid for cancellation") + } + + // Calculate the remaining amount to refund based on unused subscription time/traffic remainingAmount, err := CalculateRemainingAmount(l.ctx, l.svcCtx, req.Id) if err != nil { return err } - // update user subscribe + + // Process unsubscription in a database transaction to ensure data consistency err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { - var userSub user.Subscribe - if err := db.Model(&user.Subscribe{}).Where("id = ?", req.Id).First(&userSub).Error; err != nil { + // Find and update subscription status to cancelled (status = 4) + userSub.Status = 4 // Set status to cancelled + if err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, userSub); err != nil { return err } - userSub.Status = 4 - if err := l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &userSub); err != nil { + + // Query the original order information to determine refund strategy + orderInfo, err := l.svcCtx.OrderModel.FindOne(l.ctx, userSub.OrderId) + if err != nil { return err } - balance := remainingAmount + u.Balance - // insert deduction log - balanceLog := user.BalanceLog{ - UserId: userSub.UserId, - OrderId: userSub.OrderId, - Amount: remainingAmount, - Type: 4, - Balance: balance, + // Calculate refund distribution based on payment method and gift amount priority + var balance, gift int64 + if orderInfo.Method == "balance" { + // For balance-paid orders, prioritize refunding to gift amount first + if orderInfo.GiftAmount >= remainingAmount { + // Gift amount covers the entire refund - refund all to gift balance + gift = remainingAmount + balance = u.Balance // Regular balance remains unchanged + } else { + // Gift amount insufficient - refund to gift first, remainder to regular balance + gift = orderInfo.GiftAmount + balance = u.Balance + (remainingAmount - orderInfo.GiftAmount) + } + } else { + // For non-balance payment orders, refund entirely to regular balance + balance = remainingAmount + u.Balance + gift = 0 } - if err := db.Model(&user.BalanceLog{}).Create(&balanceLog).Error; err != nil { - return err + + // Create balance log entry only if there's an actual regular balance refund + balanceRefundAmount := balance - u.Balance + if balanceRefundAmount > 0 { + balanceLog := log.Balance{ + OrderNo: orderInfo.OrderNo, + Amount: balanceRefundAmount, + Type: log.BalanceTypeRefund, // Type 4 represents refund transaction + Balance: balance, + Timestamp: time.Now().UnixMilli(), + } + content, _ := balanceLog.Marshal() + + if err := db.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeBalance.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: u.Id, + Content: string(content), + }).Error; err != nil { + return err + } } - // update user balance + + // Create gift amount log entry if there's a gift balance refund + if gift > 0 { + + giftLog := log.Gift{ + SubscribeId: userSub.Id, + OrderNo: orderInfo.OrderNo, + Type: log.GiftTypeIncrease, // Type 1 represents gift amount increase + Amount: gift, + Balance: u.GiftAmount + gift, + Remark: "Unsubscribe refund", + } + content, _ := giftLog.Marshal() + + if err := db.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeGift.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: u.Id, + Content: string(content), + }).Error; err != nil { + return err + } + // Update user's gift amount + u.GiftAmount += gift + } + + // Update user's regular balance and save changes to database u.Balance = balance return l.svcCtx.UserModel.Update(l.ctx, u) }) + if err != nil { + l.Errorw("Unsubscribe transaction failed", logger.Field("error", err.Error()), logger.Field("userId", u.Id), logger.Field("reqId", req.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unsubscribe transaction failed: %v", err.Error()) + } + + //clear user subscription cache + if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, userSub); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", userSub.Id)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + // Clear subscription cache + if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, userSub.SubscribeId); err != nil { + l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId)) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error()) + } + return err } diff --git a/internal/logic/public/user/updateBindEmailLogic.go b/internal/logic/public/user/updateBindEmailLogic.go index 3640f90..f56ff8c 100644 --- a/internal/logic/public/user/updateBindEmailLogic.go +++ b/internal/logic/public/user/updateBindEmailLogic.go @@ -3,16 +3,16 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type UpdateBindEmailLogic struct { @@ -48,7 +48,7 @@ func (l *UpdateBindEmailLogic) UpdateBindEmail(req *types.UpdateBindEmailRequest if m.Id > 0 { return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "email already bind") } - if errors.Is(err, gorm.ErrRecordNotFound) { + if method.Id == 0 { method = &user.AuthMethods{ UserId: u.Id, AuthType: "email", diff --git a/internal/logic/public/user/updateBindMobileLogic.go b/internal/logic/public/user/updateBindMobileLogic.go index 61e100e..c675a46 100644 --- a/internal/logic/public/user/updateBindMobileLogic.go +++ b/internal/logic/public/user/updateBindMobileLogic.go @@ -5,17 +5,17 @@ import ( "encoding/json" "fmt" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/phone" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/phone" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type UpdateBindMobileLogic struct { diff --git a/internal/logic/public/user/updateUserNotifyLogic.go b/internal/logic/public/user/updateUserNotifyLogic.go index 49ca2e6..cc9429e 100644 --- a/internal/logic/public/user/updateUserNotifyLogic.go +++ b/internal/logic/public/user/updateUserNotifyLogic.go @@ -3,13 +3,13 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/public/user/updateUserPasswordLogic.go b/internal/logic/public/user/updateUserPasswordLogic.go index 265f95e..eda10e7 100644 --- a/internal/logic/public/user/updateUserPasswordLogic.go +++ b/internal/logic/public/user/updateUserPasswordLogic.go @@ -3,16 +3,16 @@ package user import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type UpdateUserPasswordLogic struct { diff --git a/internal/logic/public/user/verifyEmailLogic.go b/internal/logic/public/user/verifyEmailLogic.go index 1831c84..4d48df1 100644 --- a/internal/logic/public/user/verifyEmailLogic.go +++ b/internal/logic/public/user/verifyEmailLogic.go @@ -5,13 +5,13 @@ import ( "encoding/json" "fmt" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/server/constant.go b/internal/logic/server/constant.go index 0c54202..e2d1584 100644 --- a/internal/logic/server/constant.go +++ b/internal/logic/server/constant.go @@ -1,3 +1,86 @@ package server -const Unchanged = "Unchanged" +const ( + Unchanged = "Unchanged" + ShadowSocks = "shadowsocks" + Vmess = "vmess" + Vless = "vless" + Trojan = "trojan" + AnyTLS = "anytls" + Tuic = "tuic" + Hysteria = "hysteria" + // Deprecated: Hysteria2 is deprecated, use Hysteria instead + // TODO: remove in future versions + Hysteria2 = "hysteria2" +) + +type SecurityConfig struct { + SNI string `json:"sni"` + AllowInsecure *bool `json:"allow_insecure"` + Fingerprint string `json:"fingerprint"` + RealityServerAddress string `json:"reality_server_addr"` + RealityServerPort int `json:"reality_server_port"` + RealityPrivateKey string `json:"reality_private_key"` + RealityPublicKey string `json:"reality_public_key"` + RealityShortId string `json:"reality_short_id"` + RealityMldsa65seed string `json:"reality_mldsa65seed"` +} + +type TransportConfig struct { + Path string `json:"path"` + Host string `json:"host"` + ServiceName string `json:"service_name"` + DisableSNI bool `json:"disable_sni"` + ReduceRtt bool `json:"reduce_rtt"` + UDPRelayMode string `json:"udp_relay_mode"` + CongestionController string `json:"congestion_controller"` +} + +type VlessNode struct { + Port uint16 `json:"port"` + Flow string `json:"flow"` + Network string `json:"transport"` + TransportConfig *TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type VmessNode struct { + Port uint16 `json:"port"` + Network string `json:"transport"` + TransportConfig *TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type ShadowsocksNode struct { + Port uint16 `json:"port"` + Cipher string `json:"method"` + ServerKey string `json:"server_key"` +} + +type TrojanNode struct { + Port uint16 `json:"port"` + Network string `json:"transport"` + TransportConfig *TransportConfig `json:"transport_config"` + Security string `json:"security"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type AnyTLSNode struct { + Port uint16 `json:"port"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type TuicNode struct { + Port uint16 `json:"port"` + SecurityConfig *SecurityConfig `json:"security_config"` +} + +type Hysteria2Node struct { + Port uint16 `json:"port"` + HopPorts string `json:"hop_ports"` + HopInterval int `json:"hop_interval"` + ObfsPassword string `json:"obfs_password"` + SecurityConfig *SecurityConfig `json:"security_config"` +} diff --git a/internal/logic/server/getServerConfigLogic.go b/internal/logic/server/getServerConfigLogic.go index 1b57ee7..94221a9 100644 --- a/internal/logic/server/getServerConfigLogic.go +++ b/internal/logic/server/getServerConfigLogic.go @@ -1,17 +1,18 @@ package server import ( + "encoding/base64" "encoding/json" "fmt" "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/model/node" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" ) type GetServerConfigLogic struct { @@ -20,7 +21,7 @@ type GetServerConfigLogic struct { svcCtx *svc.ServiceContext } -// Get server config +// NewGetServerConfigLogic Get server config func NewGetServerConfigLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *GetServerConfigLogic { return &GetServerConfigLogic{ Logger: logger.WithContext(ctx.Request.Context()), @@ -30,7 +31,7 @@ func NewGetServerConfigLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *GetS } func (l *GetServerConfigLogic) GetServerConfig(req *types.GetServerConfigRequest) (resp *types.GetServerConfigResponse, err error) { - cacheKey := fmt.Sprintf("%s%d", config.ServerConfigCacheKey, req.ServerId) + cacheKey := fmt.Sprintf("%s%d:%s", node.ServerConfigCacheKey, req.ServerId, req.Protocol) cache, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() if err == nil { if cache != "" { @@ -41,7 +42,7 @@ func (l *GetServerConfigLogic) GetServerConfig(req *types.GetServerConfigRequest return nil, xerr.StatusNotModified } l.ctx.Header("ETag", etag) - resp := &types.GetServerConfigResponse{} + resp = &types.GetServerConfigResponse{} err = json.Unmarshal([]byte(cache), resp) if err != nil { l.Errorw("[ServerConfigCacheKey] json unmarshal error", logger.Field("error", err.Error())) @@ -50,34 +51,191 @@ func (l *GetServerConfigLogic) GetServerConfig(req *types.GetServerConfigRequest return resp, nil } } - nodeInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + data, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil { l.Errorw("[GetServerConfig] FindOne error", logger.Field("error", err.Error())) return nil, err } - cfg := make(map[string]interface{}) - err = json.Unmarshal([]byte(nodeInfo.Config), &cfg) + + // compatible hysteria2, remove in future versions + protocolRequest := req.Protocol + if protocolRequest == Hysteria2 { + protocolRequest = Hysteria + } + + protocols, err := data.UnmarshalProtocols() if err != nil { - l.Errorw("[GetServerConfig] json unmarshal error", logger.Field("error", err.Error())) return nil, err } + var cfg map[string]interface{} + for _, protocol := range protocols { + if protocol.Type == protocolRequest { + cfg = l.compatible(protocol) + break + } + } + resp = &types.GetServerConfigResponse{ Basic: types.ServerBasic{ PullInterval: l.svcCtx.Config.Node.NodePullInterval, PushInterval: l.svcCtx.Config.Node.NodePushInterval, }, - Protocol: nodeInfo.Protocol, + Protocol: req.Protocol, Config: cfg, } - data, err := json.Marshal(resp) + c, err := json.Marshal(resp) if err != nil { l.Errorw("[GetServerConfig] json marshal error", logger.Field("error", err.Error())) return nil, err } - etag := tool.GenerateETag(data) + etag := tool.GenerateETag(c) l.ctx.Header("ETag", etag) - if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, data, -1).Err(); err != nil { + if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, c, -1).Err(); err != nil { l.Errorw("[GetServerConfig] redis set error", logger.Field("error", err.Error())) } + // Check If-None-Match header + match := l.ctx.GetHeader("If-None-Match") + if match == etag { + return nil, xerr.StatusNotModified + } + return resp, nil } + +func (l *GetServerConfigLogic) compatible(config node.Protocol) map[string]interface{} { + var result interface{} + switch config.Type { + case ShadowSocks: + result = ShadowsocksNode{ + Port: config.Port, + Cipher: config.Cipher, + ServerKey: base64.StdEncoding.EncodeToString([]byte(config.ServerKey)), + } + case Vless: + result = VlessNode{ + Port: config.Port, + Flow: config.Flow, + Network: config.Transport, + TransportConfig: &TransportConfig{ + Path: config.Path, + Host: config.Host, + ServiceName: config.ServiceName, + DisableSNI: config.DisableSNI, + ReduceRtt: config.ReduceRtt, + UDPRelayMode: config.UDPRelayMode, + CongestionController: config.CongestionController, + }, + Security: config.Security, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case Vmess: + result = VmessNode{ + Port: config.Port, + Network: config.Transport, + TransportConfig: &TransportConfig{ + Path: config.Path, + Host: config.Host, + ServiceName: config.ServiceName, + DisableSNI: config.DisableSNI, + ReduceRtt: config.ReduceRtt, + UDPRelayMode: config.UDPRelayMode, + CongestionController: config.CongestionController, + }, + Security: config.Security, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case Trojan: + result = TrojanNode{ + Port: config.Port, + Network: config.Transport, + TransportConfig: &TransportConfig{ + Path: config.Path, + Host: config.Host, + ServiceName: config.ServiceName, + DisableSNI: config.DisableSNI, + ReduceRtt: config.ReduceRtt, + UDPRelayMode: config.UDPRelayMode, + CongestionController: config.CongestionController, + }, + Security: config.Security, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case AnyTLS: + result = AnyTLSNode{ + Port: config.Port, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case Tuic: + result = TuicNode{ + Port: config.Port, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + case Hysteria: + result = Hysteria2Node{ + Port: config.Port, + HopPorts: config.HopPorts, + HopInterval: config.HopInterval, + ObfsPassword: config.ObfsPassword, + SecurityConfig: &SecurityConfig{ + SNI: config.SNI, + AllowInsecure: &config.AllowInsecure, + Fingerprint: config.Fingerprint, + RealityServerAddress: config.RealityServerAddr, + RealityServerPort: config.RealityServerPort, + RealityPrivateKey: config.RealityPrivateKey, + RealityPublicKey: config.RealityPublicKey, + RealityShortId: config.RealityShortId, + }, + } + + } + var resp map[string]interface{} + s, _ := json.Marshal(result) + _ = json.Unmarshal(s, &resp) + return resp +} diff --git a/internal/logic/server/getServerUserListLogic.go b/internal/logic/server/getServerUserListLogic.go index 723e648..70ea51f 100644 --- a/internal/logic/server/getServerUserListLogic.go +++ b/internal/logic/server/getServerUserListLogic.go @@ -3,16 +3,18 @@ package server import ( "encoding/json" "fmt" + "strings" "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/model/subscribe" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/pkg/xerr" ) type GetServerUserListLogic struct { @@ -21,7 +23,7 @@ type GetServerUserListLogic struct { svcCtx *svc.ServiceContext } -// Get user list +// NewGetServerUserListLogic Get user list func NewGetServerUserListLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *GetServerUserListLogic { return &GetServerUserListLogic{ Logger: logger.WithContext(ctx.Request.Context()), @@ -31,30 +33,53 @@ func NewGetServerUserListLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *Ge } func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListRequest) (resp *types.GetServerUserListResponse, err error) { - cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, req.ServerId) + cacheKey := fmt.Sprintf("%s%d", node.ServerUserListCacheKey, req.ServerId) cache, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if err == nil { - if cache != "" { - etag := tool.GenerateETag([]byte(cache)) - resp := &types.GetServerUserListResponse{} - // Check If-None-Match header - if match := l.ctx.GetHeader("If-None-Match"); match == etag { - return nil, xerr.StatusNotModified - } - l.ctx.Header("ETag", etag) - err = json.Unmarshal([]byte(cache), resp) - if err != nil { - l.Errorw("[ServerUserListCacheKey] json unmarshal error", logger.Field("error", err.Error())) - return nil, err - } - return resp, nil + if cache != "" { + etag := tool.GenerateETag([]byte(cache)) + resp = &types.GetServerUserListResponse{} + // Check If-None-Match header + if match := l.ctx.GetHeader("If-None-Match"); match == etag { + return nil, xerr.StatusNotModified } + l.ctx.Header("ETag", etag) + err = json.Unmarshal([]byte(cache), resp) + if err != nil { + l.Errorw("[ServerUserListCacheKey] json unmarshal error", logger.Field("error", err.Error())) + return nil, err + } + return resp, nil } - server, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + server, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil { return nil, err } - subs, err := l.svcCtx.SubscribeModel.QuerySubscribeIdsByServerIdAndServerGroupId(l.ctx, server.Id, server.GroupId) + + _, nodes, err := l.svcCtx.NodeModel.FilterNodeList(l.ctx, &node.FilterNodeParams{ + Page: 1, + Size: 1000, + ServerId: []int64{server.Id}, + Protocol: req.Protocol, + }) + if err != nil { + l.Errorw("FilterNodeList error", logger.Field("error", err.Error())) + return nil, err + } + var nodeTag []string + var nodeIds []int64 + for _, n := range nodes { + nodeIds = append(nodeIds, n.Id) + if n.Tags != "" { + nodeTag = append(nodeTag, strings.Split(n.Tags, ",")...) + } + } + + _, subs, err := l.svcCtx.SubscribeModel.FilterList(l.ctx, &subscribe.FilterParams{ + Page: 1, + Size: 9999, + Node: nodeIds, + Tags: nodeTag, + }) if err != nil { l.Errorw("QuerySubscribeIdsByServerIdAndServerGroupId error", logger.Field("error", err.Error())) return nil, err @@ -76,16 +101,10 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR return nil, err } for _, datum := range data { - speedLimit := server.SpeedLimit - if (int(sub.SpeedLimit) < server.SpeedLimit && sub.SpeedLimit != 0) || - (int(sub.SpeedLimit) > server.SpeedLimit && sub.SpeedLimit == 0) { - speedLimit = int(sub.SpeedLimit) - } - users = append(users, types.ServerUser{ Id: datum.Id, UUID: datum.UUID, - SpeedLimit: int64(speedLimit), + SpeedLimit: sub.SpeedLimit, DeviceLimit: sub.DeviceLimit, }) } @@ -106,5 +125,9 @@ func (l *GetServerUserListLogic) GetServerUserList(req *types.GetServerUserListR if err != nil { l.Errorw("[ServerUserListCacheKey] redis set error", logger.Field("error", err.Error())) } + // Check If-None-Match header + if match := l.ctx.GetHeader("If-None-Match"); match == etag { + return nil, xerr.StatusNotModified + } return resp, nil } diff --git a/internal/logic/server/pushOnlineUsersLogic.go b/internal/logic/server/pushOnlineUsersLogic.go index acdb942..5b17656 100644 --- a/internal/logic/server/pushOnlineUsersLogic.go +++ b/internal/logic/server/pushOnlineUsersLogic.go @@ -5,10 +5,10 @@ import ( "errors" "fmt" - "github.com/perfect-panel/ppanel-server/internal/model/cache" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type PushOnlineUsersLogic struct { @@ -40,26 +40,30 @@ func (l *PushOnlineUsersLogic) PushOnlineUsers(req *types.OnlineUsersRequest) er } // Find server info - _, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + _, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil { l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) return fmt.Errorf("server not found: %w", err) } - userOnlineIp := make([]cache.NodeOnlineUser, 0) + onlineUsers := make(node.OnlineUserSubscribe) for _, user := range req.Users { - userOnlineIp = append(userOnlineIp, cache.NodeOnlineUser{ - SID: user.SID, - IP: user.IP, - }) + if online, ok := onlineUsers[user.SID]; ok { + // If user already exists, update IP if different + online = append(online, user.IP) + onlineUsers[user.SID] = online + } else { + // New user, add to map + onlineUsers[user.SID] = []string{user.IP} + } } - err = l.svcCtx.NodeCache.AddOnlineUserIP(l.ctx, userOnlineIp) + err = l.svcCtx.NodeModel.UpdateOnlineUserSubscribe(l.ctx, req.ServerId, req.Protocol, onlineUsers) if err != nil { l.Errorw("[PushOnlineUsers] cache operation error", logger.Field("error", err)) return err } - err = l.svcCtx.NodeCache.UpdateNodeOnlineUser(l.ctx, req.ServerId, userOnlineIp) + err = l.svcCtx.NodeModel.UpdateOnlineUserSubscribeGlobal(l.ctx, onlineUsers) if err != nil { l.Errorw("[PushOnlineUsers] cache operation error", logger.Field("error", err)) diff --git a/internal/logic/server/queryServerProtocolConfigLogic.go b/internal/logic/server/queryServerProtocolConfigLogic.go new file mode 100644 index 0000000..4521c55 --- /dev/null +++ b/internal/logic/server/queryServerProtocolConfigLogic.go @@ -0,0 +1,93 @@ +package server + +import ( + "context" + + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" +) + +type QueryServerProtocolConfigLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// NewQueryServerProtocolConfigLogic Get Server Protocol Config +func NewQueryServerProtocolConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryServerProtocolConfigLogic { + return &QueryServerProtocolConfigLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryServerProtocolConfigLogic) QueryServerProtocolConfig(req *types.QueryServerConfigRequest) (resp *types.QueryServerConfigResponse, err error) { + // find server + data, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerID) + if err != nil { + l.Errorf("[GetServerProtocols] FindOneServer Error: %s", err.Error()) + return nil, err + } + + // handler protocols + var protocols []types.Protocol + dst, err := data.UnmarshalProtocols() + if err != nil { + l.Errorf("[FilterServerList] UnmarshalProtocols Error: %s", err.Error()) + return nil, err + } + tool.DeepCopy(&protocols, dst) + + // filter by req.Protocols + + if len(req.Protocols) > 0 { + var filtered []types.Protocol + protocolSet := make(map[string]struct{}) + for _, p := range req.Protocols { + protocolSet[p] = struct{}{} + } + for _, p := range protocols { + if _, exists := protocolSet[p.Type]; exists { + filtered = append(filtered, p) + } + } + protocols = filtered + } + + var dns []types.NodeDNS + if len(l.svcCtx.Config.Node.DNS) > 0 { + for _, d := range l.svcCtx.Config.Node.DNS { + dns = append(dns, types.NodeDNS{ + Proto: d.Proto, + Address: d.Address, + Domains: d.Domains, + }) + } + } + var outbound []types.NodeOutbound + if len(l.svcCtx.Config.Node.Outbound) > 0 { + for _, o := range l.svcCtx.Config.Node.Outbound { + outbound = append(outbound, types.NodeOutbound{ + Name: o.Name, + Protocol: o.Protocol, + Address: o.Address, + Port: o.Port, + Password: o.Password, + Rules: o.Rules, + }) + } + } + + return &types.QueryServerConfigResponse{ + TrafficReportThreshold: l.svcCtx.Config.Node.TrafficReportThreshold, + IPStrategy: l.svcCtx.Config.Node.IPStrategy, + DNS: dns, + Block: l.svcCtx.Config.Node.Block, + Outbound: outbound, + Protocols: protocols, + Total: int64(len(protocols)), + }, nil +} diff --git a/internal/logic/server/serverPushStatusLogic.go b/internal/logic/server/serverPushStatusLogic.go index e1563d8..d7c0bad 100644 --- a/internal/logic/server/serverPushStatusLogic.go +++ b/internal/logic/server/serverPushStatusLogic.go @@ -3,11 +3,12 @@ package server import ( "context" "errors" + "time" - "github.com/perfect-panel/ppanel-server/internal/model/cache" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" ) type ServerPushStatusLogic struct { @@ -16,7 +17,7 @@ type ServerPushStatusLogic struct { svcCtx *svc.ServiceContext } -// Push server status +// NewServerPushStatusLogic Push server status func NewServerPushStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ServerPushStatusLogic { return &ServerPushStatusLogic{ Logger: logger.WithContext(ctx), @@ -27,12 +28,12 @@ func NewServerPushStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) * func (l *ServerPushStatusLogic) ServerPushStatus(req *types.ServerPushStatusRequest) error { // Find server info - serverInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + serverInfo, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil || serverInfo.Id <= 0 { l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) return errors.New("server not found") } - err = l.svcCtx.NodeCache.UpdateNodeStatus(l.ctx, req.ServerId, cache.NodeStatus{ + err = l.svcCtx.NodeModel.UpdateStatusCache(l.ctx, req.ServerId, &node.Status{ Cpu: req.Cpu, Mem: req.Mem, Disk: req.Disk, @@ -42,5 +43,14 @@ func (l *ServerPushStatusLogic) ServerPushStatus(req *types.ServerPushStatusRequ l.Errorw("[ServerPushStatus] UpdateNodeStatus error", logger.Field("error", err)) return errors.New("update node status failed") } + now := time.Now() + serverInfo.LastReportedAt = &now + + err = l.svcCtx.NodeModel.UpdateServer(l.ctx, serverInfo) + if err != nil { + l.Errorw("[ServerPushStatus] UpdateServer error", logger.Field("error", err)) + return nil + } + return nil } diff --git a/internal/logic/server/serverPushUserTrafficLogic.go b/internal/logic/server/serverPushUserTrafficLogic.go index 4b9461a..c6ab4e6 100644 --- a/internal/logic/server/serverPushUserTrafficLogic.go +++ b/internal/logic/server/serverPushUserTrafficLogic.go @@ -3,14 +3,14 @@ package server import ( "context" "encoding/json" + "time" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/cache" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - task "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + task "github.com/perfect-panel/server/queue/types" "github.com/pkg/errors" ) @@ -32,7 +32,7 @@ func NewServerPushUserTrafficLogic(ctx context.Context, svcCtx *svc.ServiceConte func (l *ServerPushUserTrafficLogic) ServerPushUserTraffic(req *types.ServerPushUserTrafficRequest) error { // Find server info - serverInfo, err := l.svcCtx.ServerModel.FindOne(l.ctx, req.ServerId) + serverInfo, err := l.svcCtx.NodeModel.FindOneServer(l.ctx, req.ServerId) if err != nil { l.Errorw("[PushOnlineUsers] FindOne error", logger.Field("error", err)) return errors.New("server not found") @@ -40,23 +40,10 @@ func (l *ServerPushUserTrafficLogic) ServerPushUserTraffic(req *types.ServerPush // Create traffic task var request task.TrafficStatistics - var userTraffic []cache.UserTraffic request.ServerId = serverInfo.Id + request.Protocol = req.Protocol tool.DeepCopy(&request.Logs, req.Traffic) - tool.DeepCopy(&userTraffic, req.Traffic) - // update today traffic rank - err = l.svcCtx.NodeCache.AddNodeTodayTraffic(l.ctx, serverInfo.Id, userTraffic) - if err != nil { - l.Errorw("[ServerPushUserTraffic] AddNodeTodayTraffic error", logger.Field("error", err)) - return errors.New("add node today traffic error") - } - for _, user := range req.Traffic { - if err = l.svcCtx.NodeCache.AddUserTodayTraffic(l.ctx, user.SID, user.Upload, user.Download); err != nil { - l.Errorw("[ServerPushUserTraffic] AddUserTodayTraffic error", logger.Field("error", err)) - continue - } - } // Push traffic task val, _ := json.Marshal(request) t := asynq.NewTask(task.ForthwithTrafficStatistics, val, asynq.MaxRetry(3)) @@ -66,5 +53,15 @@ func (l *ServerPushUserTrafficLogic) ServerPushUserTraffic(req *types.ServerPush } else { l.Infow("[ServerPushUserTraffic] Push traffic task success", logger.Field("task", t), logger.Field("info", info)) } + + // Update server last reported time + now := time.Now() + serverInfo.LastReportedAt = &now + + err = l.svcCtx.NodeModel.UpdateServer(l.ctx, serverInfo) + if err != nil { + l.Errorw("[ServerPushUserTraffic] UpdateServer error", logger.Field("error", err)) + return nil + } return nil } diff --git a/internal/logic/subscribe/subscribeLogic.go b/internal/logic/subscribe/subscribeLogic.go index 36bb348..6a4a6e5 100644 --- a/internal/logic/subscribe/subscribeLogic.go +++ b/internal/logic/subscribe/subscribeLogic.go @@ -2,23 +2,23 @@ package subscribe import ( "fmt" + "net/url" "strings" "time" - "github.com/perfect-panel/ppanel-server/pkg/adapter" - "github.com/perfect-panel/ppanel-server/pkg/adapter/shadowrocket" - "github.com/perfect-panel/ppanel-server/pkg/adapter/surfboard" + "github.com/perfect-panel/server/adapter" + "github.com/perfect-panel/server/internal/model/client" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/node" - "github.com/perfect-panel/ppanel-server/internal/model/server" - - "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/server/internal/model/user" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -37,37 +37,123 @@ func NewSubscribeLogic(ctx *gin.Context, svc *svc.ServiceContext) *SubscribeLogi } } -func (l *SubscribeLogic) Generate(req *types.SubscribeRequest) (*types.SubscribeResponse, error) { - userSub, err := l.getUserSubscribe(req.Token) +func (l *SubscribeLogic) Handler(req *types.SubscribeRequest) (resp *types.SubscribeResponse, err error) { + // query client list + clients, err := l.svc.ClientModel.List(l.ctx.Request.Context()) if err != nil { + l.Errorw("[SubscribeLogic] Query client list failed", logger.Field("error", err.Error())) + return nil, err + } + + userAgent := strings.ToLower(l.ctx.Request.UserAgent()) + + var targetApp, defaultApp *client.SubscribeApplication + + for _, item := range clients { + u := strings.ToLower(item.UserAgent) + if item.IsDefault { + defaultApp = item + } + + if strings.Contains(userAgent, u) { + // Special handling for Stash + if strings.Contains(userAgent, "stash") && !strings.Contains(u, "stash") { + continue + } + targetApp = item + break + } + } + if targetApp == nil { + l.Debugf("[SubscribeLogic] No matching client found", logger.Field("userAgent", userAgent)) + if defaultApp == nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "No matching client found for user agent: %s", userAgent) + } + targetApp = defaultApp + } + // Find user subscribe by token + userSubscribe, err := l.getUserSubscribe(req.Token) + if err != nil { + l.Errorw("[SubscribeLogic] Get user subscribe failed", logger.Field("error", err.Error()), logger.Field("token", req.Token)) return nil, err } var subscribeStatus = false defer func() { - l.logSubscribeActivity(subscribeStatus, userSub, req) + l.logSubscribeActivity(subscribeStatus, userSubscribe, req) }() + // find subscribe info + subscribeInfo, err := l.svc.SubscribeModel.FindOne(l.ctx.Request.Context(), userSubscribe.SubscribeId) + if err != nil { + l.Errorw("[SubscribeLogic] Find subscribe info failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSubscribe.SubscribeId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Find subscribe info failed: %v", err.Error()) + } - servers, err := l.getServers(userSub) + // Find server list by user subscribe + servers, err := l.getServers(userSubscribe) if err != nil { return nil, err } + a := adapter.NewAdapter( + targetApp.SubscribeTemplate, + adapter.WithServers(servers), + adapter.WithSiteName(l.svc.Config.Site.SiteName), + adapter.WithSubscribeName(subscribeInfo.Name), + adapter.WithOutputFormat(targetApp.OutputFormat), + adapter.WithUserInfo(adapter.User{ + Password: userSubscribe.UUID, + ExpiredAt: userSubscribe.ExpireTime, + Download: userSubscribe.Download, + Upload: userSubscribe.Upload, + Traffic: userSubscribe.Traffic, + SubscribeURL: l.getSubscribeV2URL(req.Token), + }), + ) - rules, err := l.getRules() + // Get client config + adapterClient, err := a.Client() if err != nil { - return nil, err + l.Errorw("[SubscribeLogic] Client error", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(500), "Client error: %v", err.Error()) + } + bytes, err := adapterClient.Build() + if err != nil { + l.Errorw("[SubscribeLogic] Build client config failed", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(500), "Build client config failed: %v", err.Error()) } - resp, headerInfo, err := l.buildClientConfig(req, userSub, servers, rules) - if err != nil { - return nil, err + var formats = []string{"json", "yaml", "conf"} + + for _, format := range formats { + if format == strings.ToLower(targetApp.OutputFormat) { + l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.%s", url.QueryEscape(l.svc.Config.Site.SiteName), format)) + l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8") + + } } + resp = &types.SubscribeResponse{ + Config: bytes, + Header: fmt.Sprintf( + "upload=%d;download=%d;total=%d;expire=%d", + userSubscribe.Upload, userSubscribe.Download, userSubscribe.Traffic, userSubscribe.ExpireTime.Unix(), + ), + } subscribeStatus = true - return &types.SubscribeResponse{ - Config: resp, - Header: headerInfo, - }, nil + return +} + +func (l *SubscribeLogic) getSubscribeV2URL(token string) string { + if l.svc.Config.Subscribe.PanDomain { + return fmt.Sprintf("https://%s", l.ctx.Request.Host) + } + + if l.svc.Config.Subscribe.SubscribeDomain != "" { + domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n") + return fmt.Sprintf("https://%s%s?token=%s", domains[0], l.svc.Config.Subscribe.SubscribePath, token) + } + + return fmt.Sprintf("https://%s%s?token=%s&", l.ctx.Request.Host, l.svc.Config.Subscribe.SubscribePath, token) } func (l *SubscribeLogic) getUserSubscribe(token string) (*user.Subscribe, error) { @@ -77,10 +163,11 @@ func (l *SubscribeLogic) getUserSubscribe(token string) (*user.Subscribe, error) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error()) } - if userSub.Status != 1 { - l.Infow("[Generate Subscribe]subscribe is not available", logger.Field("status", int(userSub.Status)), logger.Field("token", token)) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "subscribe is not available") - } + // Ignore expiration check + //if userSub.Status > 1 { + // l.Infow("[Generate Subscribe]subscribe is not available", logger.Field("status", int(userSub.Status)), logger.Field("token", token)) + // return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "subscribe is not available") + //} return userSub, nil } @@ -90,19 +177,27 @@ func (l *SubscribeLogic) logSubscribeActivity(subscribeStatus bool, userSub *use return } - err := l.svc.UserModel.InsertSubscribeLog(l.ctx.Request.Context(), &user.SubscribeLog{ - UserId: userSub.UserId, - UserSubscribeId: userSub.Id, + subscribeLog := log.Subscribe{ Token: req.Token, - IP: l.ctx.ClientIP(), - UserAgent: l.ctx.Request.UserAgent(), + UserAgent: req.UA, + ClientIP: l.ctx.ClientIP(), + UserSubscribeId: userSub.Id, + } + + content, _ := subscribeLog.Marshal() + + err := l.svc.LogModel.Insert(l.ctx.Request.Context(), &log.SystemLog{ + Type: log.TypeSubscribe.Uint8(), + ObjectID: userSub.UserId, // log user id + Date: time.Now().Format(time.DateOnly), + Content: string(content), }) if err != nil { l.Errorw("[Generate Subscribe]insert subscribe log error: %v", logger.Field("error", err.Error())) } } -func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*server.Server, error) { +func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*node.Node, error) { if l.isSubscriptionExpired(userSub) { return l.createExpiredServers(), nil } @@ -113,44 +208,66 @@ func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*server.Server, return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe details error: %v", err.Error()) } - serverIds := tool.StringToInt64Slice(subDetails.Server) - groupIds := tool.StringToInt64Slice(subDetails.ServerGroup) + nodeIds := tool.StringToInt64Slice(subDetails.Nodes) + tags := strings.Split(subDetails.NodeTags, ",") + + l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags) + + enable := true + + _, nodes, err := l.svc.NodeModel.FilterNodeList(l.ctx.Request.Context(), &node.FilterNodeParams{ + Page: 1, + Size: 1000, + NodeId: nodeIds, + Tag: tool.RemoveDuplicateElements(tags...), + Preload: true, + Enabled: &enable, // Only get enabled nodes + }) + + l.Debugf("[Query Subscribe]found servers: %v", len(nodes)) - servers, err := l.svc.ServerModel.FindServerDetailByGroupIdsAndIds(l.ctx.Request.Context(), groupIds, serverIds) if err != nil { l.Errorw("[Generate Subscribe]find server details error: %v", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server details error: %v", err.Error()) } - - return servers, nil + logger.Debugf("[Generate Subscribe]found servers: %v", len(nodes)) + return nodes, nil } func (l *SubscribeLogic) isSubscriptionExpired(userSub *user.Subscribe) bool { return userSub.ExpireTime.Unix() < time.Now().Unix() && userSub.ExpireTime.Unix() != 0 } -func (l *SubscribeLogic) createExpiredServers() []*server.Server { +func (l *SubscribeLogic) createExpiredServers() []*node.Node { enable := true host := l.getFirstHostLine() - return []*server.Server{ + return []*node.Node{ { - Name: "Subscribe Expired", - ServerAddr: "127.0.0.1", - RelayMode: "none", - Protocol: "shadowsocks", - Config: "{\"method\":\"aes-256-gcm\",\"port\":1}", - Enable: &enable, - Sort: 0, + Name: "Subscribe Expired", + Tags: "", + Port: 18080, + Address: "127.0.0.1", + Server: &node.Server{ + Id: 1, + Name: "Subscribe Expired", + Protocols: "[{\"type\":\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]", + }, + Protocol: "shadowsocks", + Enabled: &enable, }, { - Name: host, - ServerAddr: "127.0.0.1", - RelayMode: "none", - Protocol: "shadowsocks", - Config: "{\"method\":\"aes-256-gcm\",\"port\":1}", - Enable: &enable, - Sort: 0, + Name: host, + Tags: "", + Port: 18080, + Address: "127.0.0.1", + Server: &node.Server{ + Id: 1, + Name: "Subscribe Expired", + Protocols: "[{\"type\":\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]", + }, + Protocol: "shadowsocks", + Enabled: &enable, }, } } @@ -163,127 +280,3 @@ func (l *SubscribeLogic) getFirstHostLine() string { } return host } - -func (l *SubscribeLogic) getRules() ([]*server.RuleGroup, error) { - rules, err := l.svc.ServerModel.QueryAllRuleGroup(l.ctx) - if err != nil { - l.Errorw("[Generate Subscribe]find rule group error: %v", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find rule group error: %v", err.Error()) - } - return rules, nil -} - -func (l *SubscribeLogic) buildClientConfig(req *types.SubscribeRequest, userSub *user.Subscribe, servers []*server.Server, rules []*server.RuleGroup) ([]byte, string, error) { - proxyManager := adapter.NewAdapter(servers, rules) - clientType := l.getClientType(req) - var resp []byte - var err error - - l.Logger.Info(fmt.Sprintf("[Generate Subscribe] %s", clientType), logger.Field("ua", req.UA), logger.Field("flag", req.Flag)) - - switch clientType { - case "clash": - resp, err = proxyManager.BuildClash(userSub.UUID) - if err != nil { - l.Errorw("[Generate Subscribe] build clash error", logger.Field("error", err.Error())) - return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "build clash error: %v", err.Error()) - } - l.setClashHeaders() - case "sing-box": - resp, err = proxyManager.BuildSingbox(userSub.UUID) - if err != nil { - l.Errorw("[Generate Subscribe] build sing-box error", logger.Field("error", err.Error())) - return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "build sing-box error: %v", err.Error()) - } - case "quantumult": - resp = []byte(proxyManager.BuildQuantumultX(userSub.UUID)) - case "shadowrocket": - resp = proxyManager.BuildShadowrocket(userSub.UUID, shadowrocket.UserInfo{ - Upload: userSub.Upload, - Download: userSub.Download, - TotalTraffic: userSub.Traffic, - ExpiredDate: userSub.ExpireTime, - }) - case "loon": - resp = proxyManager.BuildLoon(userSub.UUID) - case "surfboard": - subsURL := l.getSubscribeURL(userSub.Token) - resp = proxyManager.BuildSurfboard(l.svc.Config.Site.SiteName, surfboard.UserInfo{ - Upload: userSub.Upload, - Download: userSub.Download, - TotalTraffic: userSub.Traffic, - ExpiredDate: userSub.ExpireTime, - UUID: userSub.UUID, - SubscribeURL: subsURL, - }) - l.setSurfboardHeaders() - default: - resp = proxyManager.BuildGeneral(userSub.UUID) - } - - headerInfo := fmt.Sprintf("upload=%d;download=%d;total=%d;expire=%d", - userSub.Upload, userSub.Download, userSub.Traffic, userSub.ExpireTime.Unix()) - - return resp, headerInfo, nil -} - -func (l *SubscribeLogic) setClashHeaders() { - l.ctx.Header("content-disposition", fmt.Sprintf("tattachment;filename*=UTF-8''%s.yaml", l.svc.Config.Site.SiteName)) - l.ctx.Header("Profile-Update-Interval", "24") - l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8") -} - -func (l *SubscribeLogic) setSurfboardHeaders() { - l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", l.svc.Config.Site.SiteName)) - l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8") -} - -func (l *SubscribeLogic) getSubscribeURL(token string) string { - if l.svc.Config.Subscribe.PanDomain { - return fmt.Sprintf("https://%s", l.ctx.Request.Host) - } - - if l.svc.Config.Subscribe.SubscribeDomain != "" { - domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n") - return fmt.Sprintf("https://%s%s?token=%s&flag=surfboard", domains[0], l.svc.Config.Subscribe.SubscribePath, token) - } - - return fmt.Sprintf("https://%s%s?token=%s&flag=surfboard", l.ctx.Request.Host, l.svc.Config.Subscribe.SubscribePath, token) -} - -func (l *SubscribeLogic) getClientType(req *types.SubscribeRequest) string { - clientTypeMap := map[string]string{ - "clash": "clash", - "meta": "clash", - "sing-box": "sing-box", - "hiddify": "sing-box", - "surge": "surge", - "quantumult": "quantumult", - "shadowrocket": "shadowrocket", - "loon": "loon", - "surfboard": "surfboard", - } - - findClient := func(s string) string { - s = strings.ToLower(strings.TrimSpace(s)) - if s == "" { - return "" - } - - for key, clientType := range clientTypeMap { - if strings.Contains(s, key) { - return clientType - } - } - - return "" - } - - // 优先检查Flag参数 - if typ := findClient(req.Flag); typ != "" { - return typ - } - - // 其次检查UA参数 - return findClient(req.UA) -} diff --git a/internal/logic/telegram/bot.go b/internal/logic/telegram/bot.go index 3dd7bd8..3ba2306 100644 --- a/internal/logic/telegram/bot.go +++ b/internal/logic/telegram/bot.go @@ -8,15 +8,15 @@ import ( "strings" "time" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/model/auth" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) diff --git a/internal/logic/telegram/telegramLogic.go b/internal/logic/telegram/telegramLogic.go index 0eedc4d..bc5f9f8 100644 --- a/internal/logic/telegram/telegramLogic.go +++ b/internal/logic/telegram/telegramLogic.go @@ -7,12 +7,12 @@ import ( "time" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "github.com/redis/go-redis/v9" "gorm.io/gorm" diff --git a/internal/middleware/authMiddleware.go b/internal/middleware/authMiddleware.go index 8bf45a8..fbf3758 100644 --- a/internal/middleware/authMiddleware.go +++ b/internal/middleware/authMiddleware.go @@ -5,17 +5,17 @@ import ( "fmt" "strings" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/jwt" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/jwt" + "github.com/perfect-panel/server/pkg/result" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" ) @@ -40,6 +40,11 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { c.Abort() return } + + loginType := "" + if claims["LoginType"] != nil { + loginType = claims["LoginType"].(string) + } // get user id from token userId := int64(claims["UserId"].(float64)) // get session id from token @@ -77,6 +82,7 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { c.Abort() return } + ctx = context.WithValue(ctx, constant.LoginType, loginType) ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo) ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId) c.Request = c.Request.WithContext(ctx) diff --git a/internal/middleware/appMiddleware.go b/internal/middleware/deviceMiddleware.go similarity index 85% rename from internal/middleware/appMiddleware.go rename to internal/middleware/deviceMiddleware.go index c1bef7e..501d4d0 100644 --- a/internal/middleware/appMiddleware.go +++ b/internal/middleware/deviceMiddleware.go @@ -3,6 +3,7 @@ package middleware import ( "bufio" "bytes" + "context" "encoding/json" "fmt" "io" @@ -10,29 +11,40 @@ import ( "net/http" "strings" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/result" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/internal/svc" + pkgaes "github.com/perfect-panel/server/pkg/aes" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/result" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/svc" - pkgaes "github.com/perfect-panel/ppanel-server/pkg/aes" ) const ( noWritten = -1 defaultStatus = http.StatusOK - key = "123456" ) -func AppMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { +func DeviceMiddleware(srvCtx *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - if !strings.Contains(c.Request.URL.Path, "/v1/app") { + loginType := c.GetString(string(constant.LoginType)) + if loginType == "" { + loginType = c.GetHeader("Login-Type") + } + + if loginType != "device" { c.Next() return } - rw := NewResponseWriter(c, svc) + + c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), constant.LoginType, loginType)) + + if !srvCtx.Config.Device.Enable || srvCtx.Config.Device.SecuritySecret == "" { + c.Next() + return + } + rw := NewResponseWriter(c, srvCtx) if !rw.Decrypt() { result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidCiphertext), "Invalid ciphertext")) c.Abort() @@ -50,17 +62,10 @@ func NewResponseWriter(c *gin.Context, srvCtx *svc.ServiceContext) (rw *Response body: new(bytes.Buffer), ResponseWriter: c.Writer, } - applicationConfig, err := srvCtx.ApplicationModel.FindOneConfig(c, 1) - if err != nil { - logger.Errorf("[AppMiddleware] find application config error: %v", err.Error()) - return - } - if strings.ToUpper(applicationConfig.EncryptionMethod) == "AES" && applicationConfig.EncryptionKey != "" { - rw.encryptionKey = applicationConfig.EncryptionKey - rw.encryptionMethod = applicationConfig.EncryptionMethod - rw.encryption = true - } - return + rw.encryptionKey = srvCtx.Config.Device.SecuritySecret + rw.encryptionMethod = "AES" + rw.encryption = true + return rw } func (rw *ResponseWriter) Encrypt() { diff --git a/internal/middleware/loggerMiddleware.go b/internal/middleware/loggerMiddleware.go index a7faaa7..7bb2def 100644 --- a/internal/middleware/loggerMiddleware.go +++ b/internal/middleware/loggerMiddleware.go @@ -6,13 +6,13 @@ import ( "io" "time" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/server/internal/svc" ) type responseBodyWriter struct { @@ -44,6 +44,9 @@ func LoggerMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { // Start recording logs cost := time.Since(start) responseStatus := c.Writer.Status() + + host := c.Request.Host + logs := []logger.LogField{ { Key: "status", @@ -51,7 +54,7 @@ func LoggerMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { }, { Key: "request", - Value: c.Request.Method + " " + c.Request.URL.String(), + Value: c.Request.Method + " " + host + c.Request.URL.String(), }, { Key: "query", @@ -88,6 +91,10 @@ func LoggerMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { } else { logger.WithContext(c.Request.Context()).Infow("HTTP Request", logs...) } + + if responseStatus == 404 { + logger.WithContext(c.Request.Context()).Debugf("404 Not Found: Host:%s Path:%s IsPanDomain:%v", host, c.Request.URL.Path, svc.Config.Subscribe.PanDomain) + } } } diff --git a/internal/middleware/notifyMiddleware.go b/internal/middleware/notifyMiddleware.go index ba9dc32..bbee8a9 100644 --- a/internal/middleware/notifyMiddleware.go +++ b/internal/middleware/notifyMiddleware.go @@ -3,10 +3,10 @@ package middleware import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/server/internal/svc" ) type PaymentParams struct { diff --git a/internal/middleware/panDomainMiddleware.go b/internal/middleware/panDomainMiddleware.go index 3e51bee..518c787 100644 --- a/internal/middleware/panDomainMiddleware.go +++ b/internal/middleware/panDomainMiddleware.go @@ -1,17 +1,60 @@ package middleware import ( + "net/http" "strings" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/logic/subscribe" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/server/internal/logic/subscribe" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" ) func PanDomainMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { return func(c *gin.Context) { - if svc.Config.Subscribe.PanDomain { + + if svc.Config.Subscribe.PanDomain && c.Request.URL.Path == "/" { + // intercept browser + ua := c.GetHeader("User-Agent") + + if svc.Config.Subscribe.UserAgentLimit { + if ua == "" { + c.String(http.StatusForbidden, "Access denied") + c.Abort() + return + } + browserKeywords := tool.RemoveDuplicateElements(strings.Split(svc.Config.Subscribe.UserAgentList, "\n")...) + var allow = false + + // query client list + clients, err := svc.ClientModel.List(c.Request.Context()) + if err != nil { + logger.Errorw("[PanDomainMiddleware] Query client list failed", logger.Field("error", err.Error())) + } + for _, item := range clients { + u := strings.ToLower(item.UserAgent) + u = strings.Trim(u, " ") + browserKeywords = append(browserKeywords, u) + } + + for _, keyword := range browserKeywords { + keyword = strings.ToLower(strings.Trim(keyword, " ")) + if keyword == "" { + continue + } + if strings.Contains(strings.ToLower(ua), keyword) { + allow = true + } + } + if !allow { + c.String(http.StatusForbidden, "Access denied") + c.Abort() + return + } + } + domain := c.Request.Host domainArr := strings.Split(domain, ".") domainFirst := domainArr[0] @@ -21,7 +64,7 @@ func PanDomainMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { UA: c.Request.Header.Get("User-Agent"), } l := subscribe.NewSubscribeLogic(c, svc) - resp, err := l.Generate(&request) + resp, err := l.Handler(&request) if err != nil { return } diff --git a/internal/middleware/serverMiddleware.go b/internal/middleware/serverMiddleware.go index 0c7c221..2114342 100644 --- a/internal/middleware/serverMiddleware.go +++ b/internal/middleware/serverMiddleware.go @@ -2,7 +2,7 @@ package middleware import ( "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/server/internal/svc" ) func ServerMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { diff --git a/internal/middleware/traceMiddleware.go b/internal/middleware/traceMiddleware.go index 0793041..6e143a8 100644 --- a/internal/middleware/traceMiddleware.go +++ b/internal/middleware/traceMiddleware.go @@ -6,18 +6,16 @@ import ( "net/http" "strings" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" "github.com/gin-gonic/gin" - "github.com/google/uuid" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" oteltrace "go.opentelemetry.io/otel/trace" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/trace" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/trace" ) // statusByWriter returns a span status code and message for an HTTP status code @@ -71,19 +69,13 @@ func TraceMiddleware(_ *svc.ServiceContext) func(ctx *gin.Context) { ) defer span.End() - requestId, err := uuid.NewV7() - if err != nil { - logger.Errorw( - "failed to generate request id in uuid v7 format, fallback to uuid v4", - logger.Field("error", err), - ) - requestId = uuid.New() - } - c.Header(trace.RequestIdKey, requestId.String()) + requestId := trace.TraceIDFromContext(ctx) + + c.Header(trace.RequestIdKey, requestId) span.SetAttributes(requestAttributes(c.Request)...) span.SetAttributes( - attribute.String("http.request_id", requestId.String()), + attribute.String("http.request_id", requestId), semconv.HTTPRouteKey.String(c.FullPath()), ) // context with request host diff --git a/internal/model/ads/default.go b/internal/model/ads/default.go index 52f9e99..11f5db4 100644 --- a/internal/model/ads/default.go +++ b/internal/model/ads/default.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/perfect-panel/server/pkg/cache" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) diff --git a/internal/model/announcement/default.go b/internal/model/announcement/default.go index 36ccae4..80e6ab0 100644 --- a/internal/model/announcement/default.go +++ b/internal/model/announcement/default.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/perfect-panel/server/pkg/cache" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) diff --git a/internal/model/application/application.go b/internal/model/application/application.go deleted file mode 100644 index 196e9e8..0000000 --- a/internal/model/application/application.go +++ /dev/null @@ -1,54 +0,0 @@ -package application - -import ( - "time" -) - -type Application struct { - Id int64 `gorm:"primary_key"` - Name string `gorm:"type:varchar(255);default:'';not null;comment:应用名称"` - Icon string `gorm:"type:text;not null;comment:应用图标"` - Description string `gorm:"type:text;comment:更新描述"` - SubscribeType string `gorm:"type:varchar(50);default:'';not null;comment:订阅类型"` - ApplicationVersions []ApplicationVersion - CreatedAt time.Time `gorm:"<-:create;comment:创建时间"` - UpdatedAt time.Time `gorm:"comment:更新时间"` -} - -func (Application) TableName() string { - return "application" -} - -type ApplicationVersion struct { - Id int64 `gorm:"primary_key"` - Url string `gorm:"type:varchar(255);default:'';not null;comment:应用地址"` - Version string `gorm:"type:varchar(255);default:'';not null;comment:应用版本"` - Platform string `gorm:"type:varchar(50);default:'';not null;comment:应用平台"` - IsDefault bool `gorm:"type:tinyint(1);not null;default:0;comment:默认版本"` - Description string `gorm:"type:text;comment:更新描述"` - ApplicationId int64 `gorm:"comment:所属应用"` - CreatedAt time.Time `gorm:"<-:create;comment:创建时间"` - UpdatedAt time.Time `gorm:"comment:更新时间"` -} - -func (ApplicationVersion) TableName() string { - return "application_version" -} - -type ApplicationConfig struct { - Id int64 `gorm:"primary_key"` - AppId int64 `gorm:"type:int;not null;default:0;comment:App id"` - EncryptionKey string `gorm:"type:text;comment:Encryption Key"` - EncryptionMethod string `gorm:"type:varchar(255);comment:Encryption Method"` - Domains string `gorm:"type:text"` - StartupPicture string `gorm:"type:text"` - StartupPictureSkipTime int64 `gorm:"type:int;not null;default:0;comment:Startup Picture Skip Time"` - InvitationLink string `gorm:"Invitation link"` - KrWebsiteId string `gorm:"type:varchar(255);default:'';comment:Kr Website ID"` - CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` -} - -func (ApplicationConfig) TableName() string { - return "application_config" -} diff --git a/internal/model/application/default.go b/internal/model/application/default.go deleted file mode 100644 index 857637d..0000000 --- a/internal/model/application/default.go +++ /dev/null @@ -1,245 +0,0 @@ -package application - -import ( - "context" - "errors" - "fmt" - - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/pkg/cache" - "github.com/redis/go-redis/v9" - "gorm.io/gorm" -) - -var _ Model = (*customApplicationModel)(nil) -var ( - cacheApplicationIdPrefix = "cache:application:id:" - cacheApplicationConfigIdPrefix = "cache:application:config:id:" - cacheApplicationVersionIdPrefix = "cache:application:version:id:" -) - -type ( - Model interface { - applicationModel - customApplicationLogicModel - } - applicationModel interface { - Insert(ctx context.Context, data *Application) error - FindOne(ctx context.Context, id int64) (*Application, error) - Update(ctx context.Context, data *Application) error - Delete(ctx context.Context, id int64) error - InsertVersion(ctx context.Context, data *ApplicationVersion) error - FindOneVersion(ctx context.Context, id int64) (*ApplicationVersion, error) - UpdateVersion(ctx context.Context, data *ApplicationVersion) error - InsertConfig(ctx context.Context, data *ApplicationConfig) error - FindOneConfig(ctx context.Context, id int64) (*ApplicationConfig, error) - UpdateConfig(ctx context.Context, data *ApplicationConfig) error - DeleteVersion(ctx context.Context, id int64) error - Transaction(ctx context.Context, fn func(db *gorm.DB) error) error - } - - customApplicationModel struct { - *defaultApplicationModel - } - defaultApplicationModel struct { - cache.CachedConn - table string - } -) - -func newApplicationModel(db *gorm.DB, c *redis.Client) *defaultApplicationModel { - return &defaultApplicationModel{ - CachedConn: cache.NewConn(db, c), - table: "`Application`", - } -} - -func (m *defaultApplicationModel) getCacheKeys(data *Application) []string { - if data == nil { - return []string{} - } - ApplicationIdKey := fmt.Sprintf("%s%v", cacheApplicationIdPrefix, data.Id) - cacheKeys := []string{ - ApplicationIdKey, - config.ApplicationKey, - } - return cacheKeys -} - -func (m *defaultApplicationModel) Insert(ctx context.Context, data *Application) error { - err := m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Create(&data).Error - }, m.getCacheKeys(data)...) - return err -} - -func (m *defaultApplicationModel) FindOne(ctx context.Context, id int64) (*Application, error) { - ApplicationIdKey := fmt.Sprintf("%s%v", cacheApplicationIdPrefix, id) - var resp Application - err := m.QueryCtx(ctx, &resp, ApplicationIdKey, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Application{}).Preload("ApplicationVersions").Where("`id` = ?", id).First(&resp).Error - }) - switch { - case err == nil: - return &resp, nil - default: - return nil, err - } -} - -func (m *defaultApplicationModel) Update(ctx context.Context, data *Application) error { - old, err := m.FindOne(ctx, data.Id) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - return db.Save(data).Error - }, m.getCacheKeys(old)...) - return err -} - -func (m *defaultApplicationModel) Delete(ctx context.Context, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil - } - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - err = db.Where("application_id = ?", id).Delete(&ApplicationVersion{}).Error - if err != nil { - return err - } - return db.Delete(&Application{}, id).Error - }, m.getCacheKeys(data)...) - return err -} - -func (m *defaultApplicationModel) getVersionCacheKeys(data *ApplicationVersion) []string { - if data == nil { - return []string{} - } - ApplicationVersionIdKey := fmt.Sprintf("%s%v", cacheApplicationVersionIdPrefix, data.Id) - cacheKeys := []string{ - ApplicationVersionIdKey, - config.ApplicationKey, - } - return cacheKeys -} -func (m *defaultApplicationModel) getConfigCacheKeys(data *ApplicationConfig) []string { - if data == nil { - return []string{} - } - ApplicationConfigIdKey := fmt.Sprintf("%s%v", cacheApplicationConfigIdPrefix, data.Id) - cacheKeys := []string{ - ApplicationConfigIdKey, - config.ApplicationKey, - } - return cacheKeys -} - -func (m *defaultApplicationModel) InsertVersion(ctx context.Context, data *ApplicationVersion) error { - err := m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Transaction(func(tx *gorm.DB) error { - if data.IsDefault { - err := tx.Model(&ApplicationVersion{}). - Where("application_id = ? and platform = ? and default_version = ?", data.ApplicationId, data.Platform, data.IsDefault). - Updates(map[string]interface{}{"default_version": false}).Error - if err != nil { - return err - } - } - return tx.Create(&data).Error - }) - }, m.getVersionCacheKeys(data)...) - return err -} - -func (m *defaultApplicationModel) FindOneVersion(ctx context.Context, id int64) (*ApplicationVersion, error) { - ApplicationVersionIdKey := fmt.Sprintf("%s%v", cacheApplicationVersionIdPrefix, id) - var resp ApplicationVersion - err := m.QueryCtx(ctx, &resp, ApplicationVersionIdKey, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&ApplicationVersion{}).Where("`id` = ?", id).First(&resp).Error - }) - switch { - case err == nil: - return &resp, nil - default: - return nil, err - } -} - -func (m *defaultApplicationModel) UpdateVersion(ctx context.Context, data *ApplicationVersion) error { - old, err := m.FindOneVersion(ctx, data.Id) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Transaction(func(tx *gorm.DB) error { - if data.IsDefault { - err := tx.Model(&ApplicationVersion{}). - Where("application_id = ? and platform = ? and default_version = ?", data.ApplicationId, data.Platform, data.IsDefault). - Updates(map[string]interface{}{"default_version": false}).Error - if err != nil { - return err - } - } - return tx.Save(data).Error - }) - }, m.getVersionCacheKeys(old)...) - return err -} - -func (m *defaultApplicationModel) InsertConfig(ctx context.Context, data *ApplicationConfig) error { - err := m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Create(&data).Error - }, m.getConfigCacheKeys(data)...) - return err -} - -func (m *defaultApplicationModel) FindOneConfig(ctx context.Context, id int64) (*ApplicationConfig, error) { - ApplicationConfigIdKey := fmt.Sprintf("%s%v", cacheApplicationConfigIdPrefix, id) - var resp ApplicationConfig - err := m.QueryCtx(ctx, &resp, ApplicationConfigIdKey, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&ApplicationConfig{}).Where("`id` = ?", id).First(&resp).Error - }) - switch { - case err == nil: - return &resp, nil - default: - return nil, err - } -} - -func (m *defaultApplicationModel) UpdateConfig(ctx context.Context, data *ApplicationConfig) error { - old, err := m.FindOneConfig(ctx, data.Id) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Save(data).Error - }, m.getConfigCacheKeys(old)...) - return err -} - -func (m *defaultApplicationModel) DeleteVersion(ctx context.Context, id int64) error { - data, err := m.FindOneVersion(ctx, id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil - } - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - return db.Delete(&ApplicationVersion{}, id).Error - }, m.getVersionCacheKeys(data)...) - return err -} - -func (m *defaultApplicationModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { - return m.TransactCtx(ctx, fn) -} diff --git a/internal/model/application/model.go b/internal/model/application/model.go deleted file mode 100644 index 3bd5b0a..0000000 --- a/internal/model/application/model.go +++ /dev/null @@ -1,16 +0,0 @@ -package application - -import ( - "github.com/redis/go-redis/v9" - "gorm.io/gorm" -) - -type customApplicationLogicModel interface { -} - -// NewModel returns a model for the database table. -func NewModel(conn *gorm.DB, c *redis.Client) Model { - return &customApplicationModel{ - defaultApplicationModel: newApplicationModel(conn, c), - } -} diff --git a/internal/model/auth/auth.go b/internal/model/auth/auth.go index f98d5cd..f85070c 100644 --- a/internal/model/auth/auth.go +++ b/internal/model/auth/auth.go @@ -3,6 +3,8 @@ package auth import ( "encoding/json" "time" + + "github.com/perfect-panel/server/pkg/email" ) type Auth struct { @@ -124,15 +126,55 @@ type EmailAuthConfig struct { } func (l *EmailAuthConfig) Marshal() string { + if l.ExpirationEmailTemplate == "" { + l.ExpirationEmailTemplate = email.DefaultExpirationEmailTemplate + } + if l.ExpirationEmailTemplate == "" { + l.MaintenanceEmailTemplate = email.DefaultMaintenanceEmailTemplate + } + if l.TrafficExceedEmailTemplate == "" { + l.TrafficExceedEmailTemplate = email.DefaultTrafficExceedEmailTemplate + } + if l.VerifyEmailTemplate == "" { + l.VerifyEmailTemplate = email.DefaultEmailVerifyTemplate + } bytes, err := json.Marshal(l) if err != nil { - bytes, _ = json.Marshal(new(EmailAuthConfig)) + config := &EmailAuthConfig{ + Platform: "smtp", + PlatformConfig: new(SMTPConfig), + EnableVerify: true, + EnableNotify: true, + EnableDomainSuffix: false, + DomainSuffixList: "", + VerifyEmailTemplate: email.DefaultEmailVerifyTemplate, + ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate, + MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate, + TrafficExceedEmailTemplate: email.DefaultTrafficExceedEmailTemplate, + } + + bytes, _ = json.Marshal(config) } return string(bytes) } -func (l *EmailAuthConfig) Unmarshal(data string) error { - return json.Unmarshal([]byte(data), &l) +func (l *EmailAuthConfig) Unmarshal(data string) { + err := json.Unmarshal([]byte(data), &l) + if err != nil { + config := &EmailAuthConfig{ + Platform: "smtp", + PlatformConfig: new(SMTPConfig), + EnableVerify: true, + EnableNotify: true, + EnableDomainSuffix: false, + DomainSuffixList: "", + VerifyEmailTemplate: email.DefaultEmailVerifyTemplate, + ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate, + MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate, + TrafficExceedEmailTemplate: email.DefaultTrafficExceedEmailTemplate, + } + _ = json.Unmarshal([]byte(config.Marshal()), &l) + } } // SMTPConfig Email SMTP configuration @@ -167,13 +209,28 @@ type MobileAuthConfig struct { func (l *MobileAuthConfig) Marshal() string { bytes, err := json.Marshal(l) if err != nil { - bytes, _ = json.Marshal(new(MobileAuthConfig)) + config := &MobileAuthConfig{ + Platform: "alibaba_cloud", + PlatformConfig: new(AlibabaCloudConfig), + EnableWhitelist: false, + Whitelist: []string{}, + } + bytes, _ = json.Marshal(config) } return string(bytes) } -func (l *MobileAuthConfig) Unmarshal(data string) error { - return json.Unmarshal([]byte(data), &l) +func (l *MobileAuthConfig) Unmarshal(data string) { + err := json.Unmarshal([]byte(data), &l) + if err != nil { + config := &MobileAuthConfig{ + Platform: "alibaba_cloud", + PlatformConfig: new(AlibabaCloudConfig), + EnableWhitelist: false, + Whitelist: []string{}, + } + _ = json.Unmarshal([]byte(config.Marshal()), &l) + } } type AlibabaCloudConfig struct { diff --git a/internal/model/auth/default.go b/internal/model/auth/default.go index 8131d78..df80ace 100644 --- a/internal/model/auth/default.go +++ b/internal/model/auth/default.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/perfect-panel/server/pkg/cache" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) diff --git a/internal/model/cache/constant.go b/internal/model/cache/constant.go deleted file mode 100644 index 5ae995c..0000000 --- a/internal/model/cache/constant.go +++ /dev/null @@ -1,52 +0,0 @@ -package cache - -const ( - // UserTodayUploadTrafficCacheKey 用户当日上传流量 - UserTodayUploadTrafficCacheKey = "node:user_today_upload_traffic" - // UserTodayDownloadTrafficCacheKey 用户当日下载流量 - UserTodayDownloadTrafficCacheKey = "node:user_today_download_traffic" - // UserTodayTotalTrafficCacheKey 用户当日总流量 - UserTodayTotalTrafficCacheKey = "node:user_today_total_traffic" - // NodeTodayUploadTrafficCacheKey 节点当日上传流量 - NodeTodayUploadTrafficCacheKey = "node:node_today_upload_traffic" - // NodeTodayDownloadTrafficCacheKey 节点当日下载流量 - NodeTodayDownloadTrafficCacheKey = "node:node_today_download_traffic" - // NodeTodayTotalTrafficCacheKey 节点当日总流量 - NodeTodayTotalTrafficCacheKey = "node:node_today_total_traffic" - // UserTodayUploadTrafficRankKey 用户当日上传流量排行榜 - UserTodayUploadTrafficRankKey = "node:user_today_upload_traffic_rank" - // UserTodayDownloadTrafficRankKey 用户当日下载流量排行榜 - UserTodayDownloadTrafficRankKey = "node:user_today_download_traffic_rank" - // UserTodayTotalTrafficRankKey 用户当日总流量排行榜 - UserTodayTotalTrafficRankKey = "node:user_today_total_traffic_rank" - // NodeTodayUploadTrafficRankKey 节点当日上传流量排行榜 - NodeTodayUploadTrafficRankKey = "node:node_today_upload_traffic_rank" - // NodeTodayDownloadTrafficRankKey 节点当日下载流量排行榜 - NodeTodayDownloadTrafficRankKey = "node:node_today_download_traffic_rank" - // NodeTodayTotalTrafficRankKey 节点当日总流量排行榜 - NodeTodayTotalTrafficRankKey = "node:node_today_total_traffic_rank" - // NodeOnlineUserCacheKey 节点在线用户 - NodeOnlineUserCacheKey = "node:node_online_user:%d" - // UserOnlineIpCacheKey 用户在线IP - UserOnlineIpCacheKey = "node:user_online_ip:%d" - // AllNodeOnlineUserCacheKey 所有节点在线用户 - AllNodeOnlineUserCacheKey = "node:all_node_online_user" - // NodeStatusCacheKey 节点状态 - NodeStatusCacheKey = "node:status:%d" - // AllNodeDownloadTrafficCacheKey 所有节点下载流量 - AllNodeDownloadTrafficCacheKey = "node:all_node_download_traffic" - // AllNodeUploadTrafficCacheKey 所有节点上传流量 - AllNodeUploadTrafficCacheKey = "node:all_node_upload_traffic" - // YesterdayTotalTrafficRank 昨日节点总流量排行榜 - YesterdayNodeTotalTrafficRank = "node:yesterday_total_traffic_rank" - // YesterdayUploadTrafficRank 昨日节点上传流量排行榜 - YesterdayNodeUploadTrafficRank = "node:yesterday_upload_traffic_rank" - // YesterdayDownloadTrafficRank 昨日节点下载流量排行榜 - YesterdayNodeDownloadTrafficRank = "node:yesterday_download_traffic_rank" - // YesterdayUserTotalTrafficRank 昨日用户总流量排行榜 - YesterdayUserTotalTrafficRank = "node:yesterday_user_total_traffic_rank" - // YesterdayUserUploadTrafficRank 昨日用户上传流量排行榜 - YesterdayUserUploadTrafficRank = "node:yesterday_user_upload_traffic_rank" - // YesterdayUserDownloadTrafficRank 昨日用户下载流量排行榜 - YesterdayUserDownloadTrafficRank = "node:yesterday_user_download_traffic_rank" -) diff --git a/internal/model/cache/node.go b/internal/model/cache/node.go deleted file mode 100644 index 5e4d792..0000000 --- a/internal/model/cache/node.go +++ /dev/null @@ -1,584 +0,0 @@ -package cache - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "sync" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/redis/go-redis/v9" -) - -type NodeCacheClient struct { - *redis.Client - resetMutex sync.Mutex -} - -func NewNodeCacheClient(rds *redis.Client) *NodeCacheClient { - return &NodeCacheClient{ - Client: rds, - } -} - -// AddOnlineUserIP adds user's online IP -func (c *NodeCacheClient) AddOnlineUserIP(ctx context.Context, users []NodeOnlineUser) error { - if len(users) == 0 { - // No users to add - return nil - } - - // Use Pipeline to optimize Redis operations - pipe := c.Pipeline() - - // Add user online IPs and clean up expired IPs for each user - for _, user := range users { - if user.SID <= 0 || user.IP == "" { - logger.Errorf("invalid user data: uid=%d, ip=%s", user.SID, user.IP) - continue - } - - key := fmt.Sprintf(UserOnlineIpCacheKey, user.SID) - now := time.Now() - expireTime := now.Add(5 * time.Minute) - - // Clean up expired user online IPs - pipe.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", now.Unix())) - pipe.ZRemRangeByScore(ctx, AllNodeOnlineUserCacheKey, "0", fmt.Sprintf("%d", now.Unix())) - - // Add or update user online IP - // XX: Only update elements that already exist - // NX: Only add new elements - _ = pipe.ZAdd(ctx, key, redis.Z{ - Score: float64(expireTime.Unix()), - Member: user.IP, - }).Err() - _ = pipe.ZAdd(ctx, AllNodeOnlineUserCacheKey, redis.Z{ - Score: float64(expireTime.Unix()), - Member: user.IP, - }).Err() - - // Set key expiration to 5 minutes (slightly longer than IP expiration) - pipe.Expire(ctx, key, 5*time.Minute) - pipe.Expire(ctx, AllNodeOnlineUserCacheKey, 5*time.Minute) - } - - // Execute all commands - _, err := pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to add node user online ip: %w", err) - } - return nil -} - -// GetUserOnlineIp gets user's online IPs -func (c *NodeCacheClient) GetUserOnlineIp(ctx context.Context, uid int64) ([]string, error) { - if uid <= 0 { - return nil, fmt.Errorf("invalid parameters: uid=%d", uid) - } - - // Get user's online IPs - ips, err := c.ZRevRangeByScore(ctx, fmt.Sprintf(UserOnlineIpCacheKey, uid), &redis.ZRangeBy{ - Min: "0", - Max: fmt.Sprintf("%d", time.Now().Add(5*time.Minute).Unix()), - Offset: 0, - Count: 100, - }).Result() - if err != nil { - return nil, fmt.Errorf("failed to get user online ip: %w", err) - } - return ips, nil -} - -// UpdateNodeOnlineUser updates node's online users and IPs -func (c *NodeCacheClient) UpdateNodeOnlineUser(ctx context.Context, nodeId int64, users []NodeOnlineUser) error { - if nodeId <= 0 || len(users) == 0 { - return fmt.Errorf("invalid parameters: nodeId=%d, users=%v", nodeId, users) - } - // Organize data - data := make(map[int64][]string) - for _, user := range users { - data[user.SID] = append(data[user.SID], user.IP) - } - - value, err := json.Marshal(data) - if err != nil { - return fmt.Errorf("failed to marshal data: %w", err) - } - - c.Set(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, nodeId), value, time.Minute*5) - return nil -} - -// GetNodeOnlineUser gets node's online users and IPs -func (c *NodeCacheClient) GetNodeOnlineUser(ctx context.Context, nodeId int64) (map[int64][]string, error) { - if nodeId <= 0 { - return nil, fmt.Errorf("invalid parameters: nodeId=%d", nodeId) - } - value, err := c.Get(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, nodeId)).Result() - if err != nil { - return nil, fmt.Errorf("failed to get node online user: %w", err) - } - var data map[int64][]string - if err := json.Unmarshal([]byte(value), &data); err != nil { - return nil, fmt.Errorf("failed to unmarshal data: %w", err) - } - return data, nil -} - -// AddUserTodayTraffic Add user's today traffic -func (c *NodeCacheClient) AddUserTodayTraffic(ctx context.Context, uid int64, upload, download int64) error { - if uid <= 0 || upload <= 0 { - return fmt.Errorf("invalid parameters: uid=%d, upload=%d", uid, upload) - } - pipe := c.Pipeline() - // User's today upload traffic - pipe.HIncrBy(ctx, UserTodayUploadTrafficCacheKey, fmt.Sprintf("%d", uid), upload) - // User's today download traffic - pipe.HIncrBy(ctx, UserTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", uid), download) - // User's today total traffic - pipe.HIncrBy(ctx, UserTodayTotalTrafficCacheKey, fmt.Sprintf("%d", uid), upload+download) - // User's today traffic ranking - pipe.ZIncrBy(ctx, UserTodayUploadTrafficRankKey, float64(upload), fmt.Sprintf("%d", uid)) - pipe.ZIncrBy(ctx, UserTodayDownloadTrafficRankKey, float64(download), fmt.Sprintf("%d", uid)) - pipe.ZIncrBy(ctx, UserTodayTotalTrafficRankKey, float64(upload+download), fmt.Sprintf("%d", uid)) - - // All node upload traffic - pipe.IncrBy(ctx, AllNodeUploadTrafficCacheKey, upload) - // All node download traffic - pipe.IncrBy(ctx, AllNodeDownloadTrafficCacheKey, download) - // Execute commands - _, err := pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to add user today upload traffic: %w", err) - } - return nil -} - -// AddNodeTodayTraffic Add node's today traffic -func (c *NodeCacheClient) AddNodeTodayTraffic(ctx context.Context, nodeId int64, userTraffic []UserTraffic) error { - if nodeId <= 0 || len(userTraffic) == 0 { - return fmt.Errorf("invalid parameters: nodeId=%d, userTraffic=%v", nodeId, userTraffic) - } - pipe := c.Pipeline() - upload, download, total := c.calculateTraffic(userTraffic) - pipe.HIncrBy(ctx, NodeTodayUploadTrafficCacheKey, fmt.Sprintf("%d", nodeId), upload) - pipe.HIncrBy(ctx, NodeTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", nodeId), download) - pipe.HIncrBy(ctx, NodeTodayTotalTrafficCacheKey, fmt.Sprintf("%d", nodeId), total) - pipe.ZIncrBy(ctx, NodeTodayUploadTrafficRankKey, float64(upload), fmt.Sprintf("%d", nodeId)) - pipe.ZIncrBy(ctx, NodeTodayDownloadTrafficRankKey, float64(download), fmt.Sprintf("%d", nodeId)) - pipe.ZIncrBy(ctx, NodeTodayTotalTrafficRankKey, float64(total), fmt.Sprintf("%d", nodeId)) - // Execute commands - _, err := pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to add node today upload traffic: %w", err) - } - return nil -} - -// Get user's traffic data -func (c *NodeCacheClient) getUserTrafficData(ctx context.Context, uid int64) (upload, download int64, err error) { - upload, err = c.HGet(ctx, UserTodayUploadTrafficCacheKey, fmt.Sprintf("%d", uid)).Int64() - if err != nil { - return 0, 0, fmt.Errorf("failed to get user today upload traffic: %w", err) - } - download, err = c.HGet(ctx, UserTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", uid)).Int64() - if err != nil { - return 0, 0, fmt.Errorf("failed to get user today download traffic: %w", err) - } - return upload, download, nil -} - -// Get node's traffic data -func (c *NodeCacheClient) getNodeTrafficData(ctx context.Context, nodeId int64) (upload, download int64, err error) { - upload, err = c.HGet(ctx, NodeTodayUploadTrafficCacheKey, fmt.Sprintf("%d", nodeId)).Int64() - if err != nil { - return 0, 0, fmt.Errorf("failed to get node today upload traffic: %w", err) - } - download, err = c.HGet(ctx, NodeTodayDownloadTrafficCacheKey, fmt.Sprintf("%d", nodeId)).Int64() - if err != nil { - return 0, 0, fmt.Errorf("failed to get node today download traffic: %w", err) - } - return upload, download, nil -} - -// Parse ID -func (c *NodeCacheClient) parseID(member interface{}, idType string) (int64, error) { - id, err := strconv.ParseInt(member.(string), 10, 64) - if err != nil { - return 0, fmt.Errorf("failed to parse %s id %v: %w", idType, member, err) - } - return id, nil -} - -// GetUserTodayTotalTrafficRank Get user's today total traffic ranking top N -func (c *NodeCacheClient) GetUserTodayTotalTrafficRank(ctx context.Context, n int64) ([]UserTodayTrafficRank, error) { - if n <= 0 { - return nil, fmt.Errorf("invalid parameters: n=%d", n) - } - data, err := c.ZRevRangeWithScores(ctx, UserTodayTotalTrafficRankKey, 0, n-1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get user today total traffic rank: %w", err) - } - users := make([]UserTodayTrafficRank, 0, len(data)) - for _, user := range data { - uid, err := c.parseID(user.Member, "user") - if err != nil { - logger.Errorf("%v", err) - continue - } - upload, download, err := c.getUserTrafficData(ctx, uid) - if err != nil { - logger.Errorf("%v", err) - continue - } - users = append(users, UserTodayTrafficRank{ - SID: uid, - Upload: upload, - Download: download, - Total: int64(user.Score), - }) - } - return users, nil -} - -// GetNodeTodayTotalTrafficRank Get node's today total traffic ranking top N -func (c *NodeCacheClient) GetNodeTodayTotalTrafficRank(ctx context.Context, n int64) ([]NodeTodayTrafficRank, error) { - if n <= 0 { - return nil, fmt.Errorf("invalid parameters: n=%d", n) - } - data, err := c.ZRevRangeWithScores(ctx, NodeTodayTotalTrafficRankKey, 0, n-1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get node today total traffic rank: %w", err) - } - nodes := make([]NodeTodayTrafficRank, 0, len(data)) - for _, node := range data { - nodeId, err := c.parseID(node.Member, "node") - if err != nil { - logger.Errorf("%v", err) - continue - } - upload, download, err := c.getNodeTrafficData(ctx, nodeId) - if err != nil { - logger.Errorf("%v", err) - continue - } - nodes = append(nodes, NodeTodayTrafficRank{ - ID: nodeId, - Upload: upload, - Download: download, - Total: int64(node.Score), - }) - } - return nodes, nil -} - -// GetUserTodayUploadTrafficRank Get user's today upload traffic ranking top N -func (c *NodeCacheClient) GetUserTodayUploadTrafficRank(ctx context.Context, n int64) ([]UserTodayTrafficRank, error) { - if n <= 0 { - return nil, fmt.Errorf("invalid parameters: n=%d", n) - } - data, err := c.ZRevRangeWithScores(ctx, UserTodayUploadTrafficRankKey, 0, n-1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get user today upload traffic rank: %w", err) - } - users := make([]UserTodayTrafficRank, 0, len(data)) - for _, user := range data { - uid, err := c.parseID(user.Member, "user") - if err != nil { - logger.Errorf("%v", err) - continue - } - upload, download, err := c.getUserTrafficData(ctx, uid) - if err != nil { - logger.Errorf("%v", err) - continue - } - users = append(users, UserTodayTrafficRank{ - SID: uid, - Upload: upload, - Download: download, - Total: int64(user.Score), - }) - } - return users, nil -} - -// GetUserTodayDownloadTrafficRank Get user's today download traffic ranking top N -func (c *NodeCacheClient) GetUserTodayDownloadTrafficRank(ctx context.Context, n int64) ([]UserTodayTrafficRank, error) { - if n <= 0 { - return nil, fmt.Errorf("invalid parameters: n=%d", n) - } - data, err := c.ZRevRangeWithScores(ctx, UserTodayDownloadTrafficRankKey, 0, n-1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get user today download traffic rank: %w", err) - } - users := make([]UserTodayTrafficRank, 0, len(data)) - for _, user := range data { - uid, err := c.parseID(user.Member, "user") - if err != nil { - logger.Errorf("%v", err) - continue - } - upload, download, err := c.getUserTrafficData(ctx, uid) - if err != nil { - logger.Errorf("%v", err) - continue - } - users = append(users, UserTodayTrafficRank{ - SID: uid, - Upload: upload, - Download: download, - Total: int64(user.Score), - }) - } - return users, nil -} - -// GetNodeTodayUploadTrafficRank Get node's today upload traffic ranking top N -func (c *NodeCacheClient) GetNodeTodayUploadTrafficRank(ctx context.Context, n int64) ([]NodeTodayTrafficRank, error) { - if n <= 0 { - return nil, fmt.Errorf("invalid parameters: n=%d", n) - } - data, err := c.ZRevRangeWithScores(ctx, NodeTodayUploadTrafficRankKey, 0, n-1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get node today upload traffic rank: %w", err) - } - nodes := make([]NodeTodayTrafficRank, 0, len(data)) - for _, node := range data { - nodeId, err := c.parseID(node.Member, "node") - if err != nil { - logger.Errorf("%v", err) - continue - } - upload, download, err := c.getNodeTrafficData(ctx, nodeId) - if err != nil { - logger.Errorf("%v", err) - continue - } - nodes = append(nodes, NodeTodayTrafficRank{ - ID: nodeId, - Upload: upload, - Download: download, - Total: int64(node.Score), - }) - } - return nodes, nil -} - -// GetNodeTodayDownloadTrafficRank Get node's today download traffic ranking top N -func (c *NodeCacheClient) GetNodeTodayDownloadTrafficRank(ctx context.Context, n int64) ([]NodeTodayTrafficRank, error) { - if n <= 0 { - return nil, fmt.Errorf("invalid parameters: n=%d", n) - } - data, err := c.ZRevRangeWithScores(ctx, NodeTodayDownloadTrafficRankKey, 0, n-1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get node today download traffic rank: %w", err) - } - nodes := make([]NodeTodayTrafficRank, 0, len(data)) - for _, node := range data { - nodeId, err := c.parseID(node.Member, "node") - if err != nil { - logger.Errorf("%v", err) - continue - } - upload, download, err := c.getNodeTrafficData(ctx, nodeId) - if err != nil { - logger.Errorf("%v", err) - continue - } - nodes = append(nodes, NodeTodayTrafficRank{ - ID: nodeId, - Upload: upload, - Download: download, - Total: int64(node.Score), - }) - } - return nodes, nil -} - -// ResetTodayTrafficData Reset today's traffic data -func (c *NodeCacheClient) ResetTodayTrafficData(ctx context.Context) error { - c.resetMutex.Lock() - defer c.resetMutex.Unlock() - pipe := c.Pipeline() - pipe.Del(ctx, UserTodayUploadTrafficCacheKey) - pipe.Del(ctx, UserTodayDownloadTrafficCacheKey) - pipe.Del(ctx, UserTodayTotalTrafficCacheKey) - pipe.Del(ctx, NodeTodayUploadTrafficCacheKey) - pipe.Del(ctx, NodeTodayDownloadTrafficCacheKey) - pipe.Del(ctx, NodeTodayTotalTrafficCacheKey) - pipe.Del(ctx, UserTodayUploadTrafficRankKey) - pipe.Del(ctx, UserTodayDownloadTrafficRankKey) - pipe.Del(ctx, UserTodayTotalTrafficRankKey) - pipe.Del(ctx, NodeTodayUploadTrafficRankKey) - pipe.Del(ctx, NodeTodayDownloadTrafficRankKey) - pipe.Del(ctx, NodeTodayTotalTrafficRankKey) - pipe.Del(ctx, AllNodeDownloadTrafficCacheKey) - pipe.Del(ctx, AllNodeUploadTrafficCacheKey) - _, err := pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to reset today traffic data: %w", err) - } - return nil -} - -// Calculate traffic -func (c *NodeCacheClient) calculateTraffic(data []UserTraffic) (upload, download, total int64) { - for _, userTraffic := range data { - upload += userTraffic.Upload - download += userTraffic.Download - total += userTraffic.Upload + userTraffic.Download - } - return upload, download, total -} - -// GetAllNodeOnlineUser Get all node online user -func (c *NodeCacheClient) GetAllNodeOnlineUser(ctx context.Context) ([]string, error) { - users, err := c.ZRevRange(ctx, AllNodeOnlineUserCacheKey, 0, -1).Result() - if err != nil { - return nil, fmt.Errorf("failed to get all node online user: %w", err) - } - return users, nil -} - -// UpdateNodeStatus Update node status -func (c *NodeCacheClient) UpdateNodeStatus(ctx context.Context, nodeId int64, status NodeStatus) error { - // 参数验证 - if nodeId <= 0 { - return fmt.Errorf("invalid node id: %d", nodeId) - } - - // 验证状态数据 - if status.UpdatedAt <= 0 { - return fmt.Errorf("invalid status data: updated_at=%d", status.UpdatedAt) - } - - // 序列化状态数据 - value, err := json.Marshal(status) - if err != nil { - return fmt.Errorf("failed to marshal node status: %w", err) - } - - // 使用 Pipeline 优化性能 - pipe := c.Pipeline() - - // 设置状态数据 - pipe.Set(ctx, fmt.Sprintf(NodeStatusCacheKey, nodeId), value, time.Minute*5) - - // 执行命令 - _, err = pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to update node status: %w", err) - } - - return nil -} - -// GetNodeStatus Get node status -func (c *NodeCacheClient) GetNodeStatus(ctx context.Context, nodeId int64) (NodeStatus, error) { - status, err := c.Get(ctx, fmt.Sprintf(NodeStatusCacheKey, nodeId)).Result() - if err != nil { - return NodeStatus{}, fmt.Errorf("failed to get node status: %w", err) - } - var nodeStatus NodeStatus - if err := json.Unmarshal([]byte(status), &nodeStatus); err != nil { - return NodeStatus{}, fmt.Errorf("failed to unmarshal node status: %w", err) - } - return nodeStatus, nil -} - -// GetOnlineNodeStatusCount Get Online Node Status Count -func (c *NodeCacheClient) GetOnlineNodeStatusCount(ctx context.Context) (int64, error) { - // 获取所有节点Key - keys, err := c.Keys(ctx, "node:status:*").Result() - if err != nil { - return 0, fmt.Errorf("failed to get all node status keys: %w", err) - } - var count int64 - for _, key := range keys { - status, err := c.Get(ctx, key).Result() - if err != nil { - logger.Errorf("failed to get node status: %v", err.Error()) - continue - } - if status != "" { - count++ - } - } - return count, nil -} - -// GetAllNodeUploadTraffic Get all node upload traffic -func (c *NodeCacheClient) GetAllNodeUploadTraffic(ctx context.Context) (int64, error) { - upload, err := c.Get(ctx, AllNodeUploadTrafficCacheKey).Int64() - if err != nil { - return 0, fmt.Errorf("failed to get all node upload traffic: %w", err) - } - return upload, nil -} - -// GetAllNodeDownloadTraffic Get all node download traffic -func (c *NodeCacheClient) GetAllNodeDownloadTraffic(ctx context.Context) (int64, error) { - download, err := c.Get(ctx, AllNodeDownloadTrafficCacheKey).Int64() - if err != nil { - return 0, fmt.Errorf("failed to get all node download traffic: %w", err) - } - return download, nil -} - -// UpdateYesterdayNodeTotalTrafficRank Update yesterday node total traffic rank -func (c *NodeCacheClient) UpdateYesterdayNodeTotalTrafficRank(ctx context.Context, nodes []NodeTodayTrafficRank) error { - expireAt := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local).Add(time.Hour * 24) - t := time.Until(expireAt) - pipe := c.Pipeline() - value, _ := json.Marshal(nodes) - pipe.Set(ctx, YesterdayNodeTotalTrafficRank, value, t) - _, err := pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to update yesterday node total traffic rank: %w", err) - } - return nil -} - -// UpdateYesterdayUserTotalTrafficRank Update yesterday user total traffic rank -func (c *NodeCacheClient) UpdateYesterdayUserTotalTrafficRank(ctx context.Context, users []UserTodayTrafficRank) error { - expireAt := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local).Add(time.Hour * 24) - t := time.Until(expireAt) - pipe := c.Pipeline() - value, _ := json.Marshal(users) - pipe.Set(ctx, YesterdayUserTotalTrafficRank, value, t) - _, err := pipe.Exec(ctx) - if err != nil { - return fmt.Errorf("failed to update yesterday user total traffic rank: %w", err) - } - return nil -} - -// GetYesterdayNodeTotalTrafficRank Get yesterday node total traffic rank -func (c *NodeCacheClient) GetYesterdayNodeTotalTrafficRank(ctx context.Context) ([]NodeTodayTrafficRank, error) { - value, err := c.Get(ctx, YesterdayNodeTotalTrafficRank).Result() - if err != nil { - return nil, fmt.Errorf("failed to get yesterday node total traffic rank: %w", err) - } - var nodes []NodeTodayTrafficRank - if err := json.Unmarshal([]byte(value), &nodes); err != nil { - return nil, fmt.Errorf("failed to unmarshal yesterday node total traffic rank: %w", err) - } - return nodes, nil -} - -// GetYesterdayUserTotalTrafficRank Get yesterday user total traffic rank -func (c *NodeCacheClient) GetYesterdayUserTotalTrafficRank(ctx context.Context) ([]UserTodayTrafficRank, error) { - value, err := c.Get(ctx, YesterdayUserTotalTrafficRank).Result() - if err != nil { - return nil, fmt.Errorf("failed to get yesterday user total traffic rank: %w", err) - } - var users []UserTodayTrafficRank - if err := json.Unmarshal([]byte(value), &users); err != nil { - return nil, fmt.Errorf("failed to unmarshal yesterday user total traffic rank: %w", err) - } - return users, nil -} diff --git a/internal/model/cache/node_test.go b/internal/model/cache/node_test.go deleted file mode 100644 index b7660ab..0000000 --- a/internal/model/cache/node_test.go +++ /dev/null @@ -1,575 +0,0 @@ -package cache - -import ( - "context" - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/alicebob/miniredis/v2" - "github.com/redis/go-redis/v9" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// Create a test Redis client -func newTestRedisClient(t *testing.T) *redis.Client { - mr, err := miniredis.Run() - require.NoError(t, err) - - client := redis.NewClient(&redis.Options{ - Addr: mr.Addr(), - }) - require.NoError(t, client.Ping(context.Background()).Err()) - return client -} - -// Clean up test data -func cleanupTestData(t *testing.T, client *redis.Client) { - ctx := context.Background() - keys := []string{ - UserTodayUploadTrafficCacheKey, - UserTodayDownloadTrafficCacheKey, - UserTodayTotalTrafficCacheKey, - NodeTodayUploadTrafficCacheKey, - NodeTodayDownloadTrafficCacheKey, - NodeTodayTotalTrafficCacheKey, - UserTodayUploadTrafficRankKey, - UserTodayDownloadTrafficRankKey, - UserTodayTotalTrafficRankKey, - NodeTodayUploadTrafficRankKey, - NodeTodayDownloadTrafficRankKey, - NodeTodayTotalTrafficRankKey, - } - - // Clean up all cache keys - for _, key := range keys { - require.NoError(t, client.Del(ctx, key).Err()) - } - - // Clean up user online IP cache - for uid := int64(1); uid <= 3; uid++ { - require.NoError(t, client.Del(ctx, fmt.Sprintf(UserOnlineIpCacheKey, uid)).Err()) - } - - // Clean up node online user cache - for nodeId := int64(1); nodeId <= 3; nodeId++ { - require.NoError(t, client.Del(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, nodeId)).Err()) - } -} - -func TestNodeCacheClient_AddUserTodayTraffic(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - tests := []struct { - name string - uid int64 - upload int64 - download int64 - wantErr bool - }{ - { - name: "Add traffic normally", - uid: 1, - upload: 100, - download: 200, - wantErr: false, - }, - { - name: "Invalid SID", - uid: 0, - upload: 100, - download: 200, - wantErr: true, - }, - { - name: "Invalid upload traffic", - uid: 1, - upload: 0, - download: 200, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := cache.AddUserTodayTraffic(ctx, tt.uid, tt.upload, tt.download) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - - // Verify data is added correctly - upload, err := client.HGet(ctx, UserTodayUploadTrafficCacheKey, "1").Int64() - assert.NoError(t, err) - assert.Equal(t, tt.upload, upload) - - download, err := client.HGet(ctx, UserTodayDownloadTrafficCacheKey, "1").Int64() - assert.NoError(t, err) - assert.Equal(t, tt.download, download) - }) - } -} - -func TestNodeCacheClient_AddNodeTodayTraffic(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - tests := []struct { - name string - nodeId int64 - userTraffic []UserTraffic - wantErr bool - }{ - { - name: "Add node traffic normally", - nodeId: 1, - userTraffic: []UserTraffic{ - {UID: 1, Upload: 100, Download: 200}, - {UID: 2, Upload: 300, Download: 400}, - }, - wantErr: false, - }, - { - name: "Invalid node ID", - nodeId: 0, - userTraffic: []UserTraffic{ - {UID: 1, Upload: 100, Download: 200}, - }, - wantErr: true, - }, - { - name: "Empty user traffic data", - nodeId: 1, - userTraffic: []UserTraffic{}, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := cache.AddNodeTodayTraffic(ctx, tt.nodeId, tt.userTraffic) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - - // Verify data is added correctly - upload, err := client.HGet(ctx, NodeTodayUploadTrafficCacheKey, "1").Int64() - assert.NoError(t, err) - assert.Equal(t, int64(400), upload) // 100 + 300 - - download, err := client.HGet(ctx, NodeTodayDownloadTrafficCacheKey, "1").Int64() - assert.NoError(t, err) - assert.Equal(t, int64(600), download) // 200 + 400 - }) - } -} - -func TestNodeCacheClient_GetUserTodayTrafficRank(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - // Prepare test data - testData := []struct { - uid int64 - upload int64 - download int64 - }{ - {1, 100, 200}, - {2, 300, 400}, - {3, 500, 600}, - } - - for _, data := range testData { - err := cache.AddUserTodayTraffic(ctx, data.uid, data.upload, data.download) - require.NoError(t, err) - } - - tests := []struct { - name string - n int64 - wantErr bool - }{ - { - name: "Get top 2 ranks", - n: 2, - wantErr: false, - }, - { - name: "Get all ranks", - n: 3, - wantErr: false, - }, - { - name: "Invalid N value", - n: 0, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ranks, err := cache.GetUserTodayTotalTrafficRank(ctx, tt.n) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Len(t, ranks, int(tt.n)) - - // Verify sorting is correct - for i := 1; i < len(ranks); i++ { - assert.GreaterOrEqual(t, ranks[i-1].Total, ranks[i].Total) - } - }) - } -} - -func TestNodeCacheClient_ResetTodayTrafficData(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - // Prepare test data - err := cache.AddUserTodayTraffic(ctx, 1, 100, 200) - require.NoError(t, err) - err = cache.AddNodeTodayTraffic(ctx, 1, []UserTraffic{{UID: 1, Upload: 100, Download: 200}}) - require.NoError(t, err) - - // Test reset functionality - err = cache.ResetTodayTrafficData(ctx) - assert.NoError(t, err) - - // Verify data is cleared - keys := []string{ - UserTodayUploadTrafficCacheKey, - UserTodayDownloadTrafficCacheKey, - UserTodayTotalTrafficCacheKey, - NodeTodayUploadTrafficCacheKey, - NodeTodayDownloadTrafficCacheKey, - NodeTodayTotalTrafficCacheKey, - } - - for _, key := range keys { - exists, err := client.Exists(ctx, key).Result() - assert.NoError(t, err) - assert.Equal(t, int64(0), exists) - } -} - -func TestNodeCacheClient_GetNodeTodayTrafficRank(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - // Prepare test data - testData := []struct { - nodeId int64 - traffic []UserTraffic - }{ - {1, []UserTraffic{{UID: 1, Upload: 100, Download: 200}}}, - {2, []UserTraffic{{UID: 2, Upload: 300, Download: 400}}}, - {3, []UserTraffic{{UID: 3, Upload: 500, Download: 600}}}, - } - - for _, data := range testData { - err := cache.AddNodeTodayTraffic(ctx, data.nodeId, data.traffic) - require.NoError(t, err) - } - - tests := []struct { - name string - n int64 - wantErr bool - }{ - { - name: "Get top 2 ranks", - n: 2, - wantErr: false, - }, - { - name: "Get all ranks", - n: 3, - wantErr: false, - }, - { - name: "Invalid N value", - n: 0, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ranks, err := cache.GetNodeTodayTotalTrafficRank(ctx, tt.n) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Len(t, ranks, int(tt.n)) - - // Verify sorting is correct - for i := 1; i < len(ranks); i++ { - assert.GreaterOrEqual(t, ranks[i-1].Total, ranks[i].Total) - } - }) - } -} - -func TestNodeCacheClient_AddNodeOnlineUser(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - tests := []struct { - name string - nodeId int64 - users []NodeOnlineUser - wantErr bool - }{ - { - name: "Add online users normally", - nodeId: 1, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - {SID: 2, IP: "192.168.1.2"}, - }, - wantErr: false, - }, - { - name: "Invalid node ID", - nodeId: 0, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - }, - wantErr: false, - }, - { - name: "Empty user list", - nodeId: 1, - users: []NodeOnlineUser{}, - wantErr: false, - }, - { - name: "Add duplicate user IP", - nodeId: 1, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - {SID: 1, IP: "192.168.1.1"}, - }, - wantErr: false, - }, - { - name: "Multiple IPs for same user", - nodeId: 1, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - {SID: 1, IP: "192.168.1.2"}, - }, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := cache.AddOnlineUserIP(ctx, tt.users) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - - // Verify data is added correctly - for _, user := range tt.users { - // Get user online IPs - ips, err := cache.GetUserOnlineIp(ctx, user.SID) - assert.NoError(t, err) - assert.Contains(t, ips, user.IP) - - // Verify score is within valid range (current time to 5 minutes later) - score, err := client.ZScore(ctx, fmt.Sprintf(UserOnlineIpCacheKey, user.SID), user.IP).Result() - assert.NoError(t, err) - now := time.Now().Unix() - assert.GreaterOrEqual(t, score, float64(now)) - assert.LessOrEqual(t, score, float64(now+300)) // 5 minutes = 300 seconds - - // Verify key exists - exists, err := client.Exists(ctx, fmt.Sprintf(UserOnlineIpCacheKey, user.SID)).Result() - assert.NoError(t, err) - assert.Equal(t, int64(1), exists) - } - }) - } -} - -func TestNodeCacheClient_GetUserOnlineIp(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - // Prepare test data - testData := []struct { - nodeId int64 - users []NodeOnlineUser - }{ - { - nodeId: 1, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - {SID: 1, IP: "192.168.1.2"}, - {SID: 2, IP: "192.168.1.3"}, - }, - }, - } - - // Add test data - for _, data := range testData { - err := cache.AddOnlineUserIP(ctx, data.users) - require.NoError(t, err) - } - - tests := []struct { - name string - uid int64 - wantErr bool - wantIPs []string - }{ - { - name: "Get existing user IPs", - uid: 1, - wantErr: false, - wantIPs: []string{"192.168.1.1", "192.168.1.2"}, - }, - { - name: "Get another user's IPs", - uid: 2, - wantErr: false, - wantIPs: []string{"192.168.1.3"}, - }, - { - name: "Get non-existent user IPs", - uid: 3, - wantErr: false, - wantIPs: []string{}, - }, - { - name: "Invalid user ID", - uid: 0, - wantErr: true, - }, - { - name: "Expired IPs should not be returned", - uid: 1, - wantErr: false, - wantIPs: []string{"192.168.1.1", "192.168.1.2"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ips, err := cache.GetUserOnlineIp(ctx, tt.uid) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.ElementsMatch(t, tt.wantIPs, ips) - - // Verify all returned IPs are valid - for _, ip := range ips { - score, err := client.ZScore(ctx, fmt.Sprintf(UserOnlineIpCacheKey, tt.uid), ip).Result() - assert.NoError(t, err) - now := time.Now().Unix() - assert.GreaterOrEqual(t, score, float64(now)) - } - }) - } -} - -func TestNodeCacheClient_UpdateNodeOnlineUser(t *testing.T) { - client := newTestRedisClient(t) - defer cleanupTestData(t, client) - - cache := NewNodeCacheClient(client) - ctx := context.Background() - - tests := []struct { - name string - nodeId int64 - users []NodeOnlineUser - wantErr bool - }{ - { - name: "Update online users normally", - nodeId: 1, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - {SID: 2, IP: "192.168.1.2"}, - }, - wantErr: false, - }, - { - name: "Invalid node ID", - nodeId: 0, - users: []NodeOnlineUser{ - {SID: 1, IP: "192.168.1.1"}, - }, - wantErr: true, - }, - { - name: "Empty user list", - nodeId: 1, - users: []NodeOnlineUser{}, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := cache.UpdateNodeOnlineUser(ctx, tt.nodeId, tt.users) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - - // Verify data is updated correctly - data, err := client.Get(ctx, fmt.Sprintf(NodeOnlineUserCacheKey, tt.nodeId)).Result() - assert.NoError(t, err) - - var result map[int64][]string - err = json.Unmarshal([]byte(data), &result) - assert.NoError(t, err) - - // Verify data content - for _, user := range tt.users { - ips, exists := result[user.SID] - assert.True(t, exists) - assert.Contains(t, ips, user.IP) - } - }) - } -} diff --git a/internal/model/cache/types.go b/internal/model/cache/types.go deleted file mode 100644 index 89b6144..0000000 --- a/internal/model/cache/types.go +++ /dev/null @@ -1,34 +0,0 @@ -package cache - -type NodeOnlineUser struct { - SID int64 - IP string -} - -type NodeTodayTrafficRank struct { - ID int64 - Name string - Upload int64 - Download int64 - Total int64 -} - -type UserTodayTrafficRank struct { - SID int64 - Upload int64 - Download int64 - Total int64 -} - -type UserTraffic struct { - UID int64 - Upload int64 - Download int64 -} - -type NodeStatus struct { - Cpu float64 - Mem float64 - Disk float64 - UpdatedAt int64 -} diff --git a/internal/model/client/application.go b/internal/model/client/application.go new file mode 100644 index 0000000..ff121d0 --- /dev/null +++ b/internal/model/client/application.go @@ -0,0 +1,75 @@ +package client + +import ( + "encoding/json" + "time" +) + +type SubscribeApplication struct { + Id int64 `gorm:"primaryKey"` + Name string `gorm:"type:varchar(255);default:'';not null;comment:Application Name"` + Icon string `gorm:"type:MEDIUMTEXT;default:null;comment:Application Icon"` + Description string `gorm:"type:varchar(255);default:null;comment:Application Description"` + Scheme string `gorm:"type:varchar(255);default:'';not null;comment:Scheme"` + UserAgent string `gorm:"type:varchar(255);default:'';not null;comment:User Agent"` + IsDefault bool `gorm:"type:tinyint(1);not null;default:0;comment:Is Default Application"` + SubscribeTemplate string `gorm:"type:MEDIUMTEXT;default:null;comment:Subscribe Template"` + OutputFormat string `gorm:"type:varchar(50);default:'yaml';not null;comment:Output Format"` + DownloadLink string `gorm:"type:text;not null;comment:Download Link"` + CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (SubscribeApplication) TableName() string { + return "subscribe_application" +} + +type DownloadLink struct { + IOS string `json:"ios,omitempty"` + Android string `json:"android,omitempty"` + Windows string `json:"windows,omitempty"` + Mac string `json:"mac,omitempty"` + Linux string `json:"linux,omitempty"` + Harmony string `json:"harmony,omitempty"` +} + +// GetDownloadLink returns the download link for the specified platform. +func (d *DownloadLink) GetDownloadLink(platform string) string { + if d == nil { + return "" + } + switch platform { + case "ios": + return d.IOS + case "android": + return d.Android + case "windows": + return d.Windows + case "mac": + return d.Mac + case "linux": + return d.Linux + case "harmony": + return d.Harmony + default: + return "" + } +} + +// Marshal serializes the DownloadLink to JSON format. +func (d *DownloadLink) Marshal() ([]byte, error) { + if d == nil { + var empty DownloadLink + return json.Marshal(empty) + } + return json.Marshal(d) +} + +// Unmarshal parses the JSON-encoded data and stores the result in the DownloadLink. +func (d *DownloadLink) Unmarshal(data []byte) error { + if data == nil || len(data) == 0 { + *d = DownloadLink{} + return nil + } + return json.Unmarshal(data, d) +} diff --git a/internal/model/client/default.go b/internal/model/client/default.go new file mode 100644 index 0000000..c52108f --- /dev/null +++ b/internal/model/client/default.go @@ -0,0 +1,81 @@ +package client + +import ( + "context" + + "gorm.io/gorm" +) + +type ( + Model interface { + subscribeApplicationModel + } + subscribeApplicationModel interface { + Insert(ctx context.Context, data *SubscribeApplication) error + FindOne(ctx context.Context, id int64) (*SubscribeApplication, error) + Update(ctx context.Context, data *SubscribeApplication) error + Delete(ctx context.Context, id int64) error + List(ctx context.Context) ([]*SubscribeApplication, error) + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + DefaultSubscribeApplicationModel struct { + *gorm.DB + } +) + +func NewSubscribeApplicationModel(db *gorm.DB) Model { + return &DefaultSubscribeApplicationModel{ + DB: db, + } +} + +func (m *DefaultSubscribeApplicationModel) Insert(ctx context.Context, data *SubscribeApplication) error { + if err := m.WithContext(ctx).Model(&SubscribeApplication{}).Create(data).Error; err != nil { + return err + } + return nil +} + +func (m *DefaultSubscribeApplicationModel) FindOne(ctx context.Context, id int64) (*SubscribeApplication, error) { + var resp SubscribeApplication + if err := m.WithContext(ctx).Model(&SubscribeApplication{}).Where("id = ?", id).First(&resp).Error; err != nil { + return nil, err + } + return &resp, nil +} + +func (m *DefaultSubscribeApplicationModel) Update(ctx context.Context, data *SubscribeApplication) error { + if _, err := m.FindOne(ctx, data.Id); err != nil { + return err + } + if err := m.WithContext(ctx).Model(&SubscribeApplication{}).Where("`id` = ?", data.Id).Save(data).Error; err != nil { + return err + } + return nil +} + +func (m *DefaultSubscribeApplicationModel) Delete(ctx context.Context, id int64) error { + if err := m.WithContext(ctx).Model(&SubscribeApplication{}).Where("`id` = ?", id).Delete(&SubscribeApplication{}).Error; err != nil { + return err + } + return nil +} + +func (m *DefaultSubscribeApplicationModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + tx := m.WithContext(ctx).Begin() + if err := fn(tx); err != nil { + if rbErr := tx.Rollback().Error; rbErr != nil { + return rbErr + } + return err + } + return tx.Commit().Error +} + +func (m *DefaultSubscribeApplicationModel) List(ctx context.Context) ([]*SubscribeApplication, error) { + var resp []*SubscribeApplication + if err := m.WithContext(ctx).Find(&resp).Error; err != nil { + return nil, err + } + return resp, nil +} diff --git a/internal/model/coupon/default.go b/internal/model/coupon/default.go index 2abca66..17df8f9 100644 --- a/internal/model/coupon/default.go +++ b/internal/model/coupon/default.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/perfect-panel/server/pkg/cache" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) diff --git a/internal/model/document/default.go b/internal/model/document/default.go index 921fdcb..908747a 100644 --- a/internal/model/document/default.go +++ b/internal/model/document/default.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/perfect-panel/server/pkg/cache" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) diff --git a/internal/model/log/default.go b/internal/model/log/default.go index 5b8d731..242891a 100644 --- a/internal/model/log/default.go +++ b/internal/model/log/default.go @@ -6,74 +6,50 @@ import ( "gorm.io/gorm" ) -var _ Model = (*customLogModel)(nil) +var _ Model = (*customSystemLogModel)(nil) type ( Model interface { - messageLogModel + systemLogModel + customSystemLogLogicModel } - messageLogModel interface { - InsertMessageLog(ctx context.Context, data *MessageLog) error - FindOneMessageLog(ctx context.Context, id int64) (*MessageLog, error) - UpdateMessageLog(ctx context.Context, data *MessageLog) error - DeleteMessageLog(ctx context.Context, id int64) error - FindMessageLogList(ctx context.Context, page, size int, filter MessageLogFilterParams) (int64, []*MessageLog, error) + systemLogModel interface { + Insert(ctx context.Context, data *SystemLog) error + FindOne(ctx context.Context, id int64) (*SystemLog, error) + Update(ctx context.Context, data *SystemLog) error + Delete(ctx context.Context, id int64) error } - - customLogModel struct { + customSystemLogModel struct { *defaultLogModel } defaultLogModel struct { - Connection *gorm.DB + *gorm.DB } ) -func newLogModel(db *gorm.DB) *defaultLogModel { +func newSystemLogModel(db *gorm.DB) *defaultLogModel { return &defaultLogModel{ - Connection: db, + DB: db, } } -func (m *defaultLogModel) InsertMessageLog(ctx context.Context, data *MessageLog) error { - return m.Connection.WithContext(ctx).Create(&data).Error +func (m *defaultLogModel) Insert(ctx context.Context, data *SystemLog) error { + return m.WithContext(ctx).Create(data).Error } -func (m *defaultLogModel) FindOneMessageLog(ctx context.Context, id int64) (*MessageLog, error) { - var resp MessageLog - err := m.Connection.WithContext(ctx).Model(&MessageLog{}).Where("`id` = ?", id).First(&resp).Error - return &resp, err +func (m *defaultLogModel) FindOne(ctx context.Context, id int64) (*SystemLog, error) { + var log SystemLog + err := m.WithContext(ctx).Where("id = ?", id).First(&log).Error + if err != nil { + return nil, err + } + return &log, nil } -func (m *defaultLogModel) UpdateMessageLog(ctx context.Context, data *MessageLog) error { - return m.Connection.WithContext(ctx).Model(&MessageLog{}).Where("id = ?", data.Id).Updates(data).Error +func (m *defaultLogModel) Update(ctx context.Context, data *SystemLog) error { + return m.WithContext(ctx).Where("`id` = ?", data.Id).Save(data).Error } -func (m *defaultLogModel) DeleteMessageLog(ctx context.Context, id int64) error { - return m.Connection.WithContext(ctx).Model(&MessageLog{}).Where("id = ?", id).Delete(&MessageLog{}).Error -} - -func (m *defaultLogModel) FindMessageLogList(ctx context.Context, page, size int, filter MessageLogFilterParams) (int64, []*MessageLog, error) { - var list []*MessageLog - var total int64 - conn := m.Connection.WithContext(ctx).Model(&MessageLog{}) - if filter.Type != "" { - conn = conn.Where("`type` = ?", filter.Type) - } - if filter.Platform != "" { - conn = conn.Where("`platform` = ?", filter.Platform) - } - if filter.To != "" { - conn = conn.Where("`to` LIKE ?", "%"+filter.To+"%") - } - if filter.Subject != "" { - conn = conn.Where("`subject` LIKE ?", "%"+filter.Subject+"%") - } - if filter.Content != "" { - conn = conn.Where("`content` = ?", "%"+filter.Content+"%") - } - if filter.Status > 0 { - conn = conn.Where("`status` = ?", filter.Status) - } - err := conn.Count(&total).Offset((page - 1) * size).Limit(size).Find(&list).Error - return total, list, err +func (m *defaultLogModel) Delete(ctx context.Context, id int64) error { + return m.WithContext(ctx).Where("`id` = ?", id).Delete(&SystemLog{}).Error } diff --git a/internal/model/log/log.go b/internal/model/log/log.go index af66746..af3eb98 100644 --- a/internal/model/log/log.go +++ b/internal/model/log/log.go @@ -1,45 +1,424 @@ package log -import "time" - -type MessageType int - -const ( - Email MessageType = iota + 1 - Mobile +import ( + "encoding/json" + "time" ) -func (t MessageType) String() string { - switch t { - case Email: - return "email" - case Mobile: - return "mobile" - } - return "unknown" +type Type uint8 + +/* + +Log Types: + 1X Message Logs + 2X Subscription Logs + 3X User Logs + 4X Traffic Ranking Logs +*/ + +const ( + TypeEmailMessage Type = 10 // Message log + TypeMobileMessage Type = 11 // Mobile message log + TypeSubscribe Type = 20 // Subscription log + TypeSubscribeTraffic Type = 21 // Subscription traffic log + TypeServerTraffic Type = 22 // Server traffic log + TypeResetSubscribe Type = 23 // Reset subscription log + TypeLogin Type = 30 // Login log + TypeRegister Type = 31 // Registration log + TypeBalance Type = 32 // Balance log + TypeCommission Type = 33 // Commission log + TypeGift Type = 34 // Gift log + TypeUserTrafficRank Type = 40 // Top 10 User traffic rank log + TypeServerTrafficRank Type = 41 // Top 10 Server traffic rank log + TypeTrafficStat Type = 42 // Daily traffic statistics log +) +const ( + ResetSubscribeTypeAuto uint16 = 231 // Auto reset + ResetSubscribeTypeAdvance uint16 = 232 // Advance reset + ResetSubscribeTypePaid uint16 = 233 // Paid reset + ResetSubscribeTypeQuota uint16 = 234 // Quota reset + BalanceTypeRecharge uint16 = 321 // Recharge + BalanceTypeWithdraw uint16 = 322 // Withdraw + BalanceTypePayment uint16 = 323 // Payment + BalanceTypeRefund uint16 = 324 // Refund + BalanceTypeAdjust uint16 = 326 // Admin Adjust + BalanceTypeReward uint16 = 325 // Reward + CommissionTypePurchase uint16 = 331 // Purchase + CommissionTypeRenewal uint16 = 332 // Renewal + CommissionTypeRefund uint16 = 333 // Refund + commissionTypeWithdraw uint16 = 334 // withdraw + CommissionTypeAdjust uint16 = 335 // Admin Adjust + GiftTypeIncrease uint16 = 341 // Increase + GiftTypeReduce uint16 = 342 // Reduce +) + +// Uint8 converts Type to uint8. +func (t Type) Uint8() uint8 { + return uint8(t) } -type MessageLog struct { - Id int64 `gorm:"primaryKey"` - Type string `gorm:"type:varchar(50);not null;default:'email';comment:Message Type"` - Platform string `gorm:"type:varchar(50);not null;default:'smtp';comment:Platform"` - To string `gorm:"type:text;not null;comment:To"` - Subject string `gorm:"type:varchar(255);not null;default:'';comment:Subject"` - Content string `gorm:"type:text;comment:Content"` - Status int `gorm:"type:tinyint(1);not null;default:0;comment:Status"` +// SystemLog represents a log entry in the system. +type SystemLog struct { + Id int64 `gorm:"primaryKey;AUTO_INCREMENT"` + Type uint8 `gorm:"index:idx_type;type:tinyint(1);not null;default:0;comment:Log Type: 1: Email Message 2: Mobile Message 3: Subscribe 4: Subscribe Traffic 5: Server Traffic 6: Login 7: Register 8: Balance 9: Commission 10: Reset Subscribe 11: Gift"` + Date string `gorm:"type:varchar(20);default:null;comment:Log Date"` + ObjectID int64 `gorm:"index:idx_object_id;type:bigint(20);not null;default:0;comment:Object ID"` + Content string `gorm:"type:text;not null;comment:Log Content"` CreatedAt time.Time `gorm:"<-:create;comment:Create Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` } -func (m *MessageLog) TableName() string { - return "message_log" +// TableName returns the name of the table for SystemLogs. +func (SystemLog) TableName() string { + return "system_logs" } -type MessageLogFilterParams struct { - Type string - Platform string - To string - Subject string - Content string - Status int +// Message represents a message log entry. +type Message struct { + To string `json:"to"` + Subject string `json:"subject,omitempty"` + Content map[string]interface{} `json:"content"` + Platform string `json:"platform"` + Template string `json:"template"` + Status uint8 `json:"status"` // 1: Sent, 2: Failed +} + +// Marshal implements the json.Marshaler interface for Message. +func (m *Message) Marshal() ([]byte, error) { + type Alias Message + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(m), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Message. +func (m *Message) Unmarshal(data []byte) error { + type Alias Message + aux := (*Alias)(m) + return json.Unmarshal(data, aux) +} + +// Traffic represents a subscription traffic log entry. +type Traffic struct { + Download int64 `json:"download"` + Upload int64 `json:"upload"` +} + +// Marshal implements the json.Marshaler interface for SubscribeTraffic. +func (s *Traffic) Marshal() ([]byte, error) { + type Alias Traffic + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for SubscribeTraffic. +func (s *Traffic) Unmarshal(data []byte) error { + type Alias Traffic + aux := (*Alias)(s) + return json.Unmarshal(data, aux) +} + +// Login represents a login log entry. +type Login struct { + Method string `json:"method"` + LoginIP string `json:"login_ip"` + UserAgent string `json:"user_agent"` + Success bool `json:"success"` + Timestamp int64 `json:"timestamp"` +} + +// Marshal implements the json.Marshaler interface for Login. +func (l *Login) Marshal() ([]byte, error) { + type Alias Login + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(l), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Login. +func (l *Login) Unmarshal(data []byte) error { + type Alias Login + aux := (*Alias)(l) + return json.Unmarshal(data, aux) +} + +// Register represents a registration log entry. +type Register struct { + AuthMethod string `json:"auth_method"` + Identifier string `json:"identifier"` + RegisterIP string `json:"register_ip"` + UserAgent string `json:"user_agent"` + Timestamp int64 `json:"timestamp"` +} + +// Marshal implements the json.Marshaler interface for Register. +func (r *Register) Marshal() ([]byte, error) { + type Alias Register + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(r), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Register. + +func (r *Register) Unmarshal(data []byte) error { + type Alias Register + aux := (*Alias)(r) + return json.Unmarshal(data, aux) +} + +// Subscribe represents a subscription log entry. +type Subscribe struct { + Token string `json:"token"` + UserAgent string `json:"user_agent"` + ClientIP string `json:"client_ip"` + UserSubscribeId int64 `json:"user_subscribe_id"` +} + +// Marshal implements the json.Marshaler interface for Subscribe. +func (s *Subscribe) Marshal() ([]byte, error) { + type Alias Subscribe + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Subscribe. +func (s *Subscribe) Unmarshal(data []byte) error { + type Alias Subscribe + aux := (*Alias)(s) + return json.Unmarshal(data, aux) +} + +// ResetSubscribe represents a reset subscription log entry. +type ResetSubscribe struct { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + OrderNo string `json:"order_no,omitempty"` + Timestamp int64 `json:"timestamp"` +} + +// Marshal implements the json.Marshaler interface for ResetSubscribe. +func (r *ResetSubscribe) Marshal() ([]byte, error) { + type Alias ResetSubscribe + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(r), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for ResetSubscribe. +func (r *ResetSubscribe) Unmarshal(data []byte) error { + type Alias ResetSubscribe + aux := (*Alias)(r) + return json.Unmarshal(data, aux) +} + +// Balance represents a balance log entry. +type Balance struct { + Type uint16 `json:"type"` + Amount int64 `json:"amount"` + OrderNo string `json:"order_no,omitempty"` + Balance int64 `json:"balance"` + Timestamp int64 `json:"timestamp"` +} + +// Marshal implements the json.Marshaler interface for Balance. +func (b *Balance) Marshal() ([]byte, error) { + type Alias Balance + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(b), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Balance. +func (b *Balance) Unmarshal(data []byte) error { + type Alias Balance + aux := (*Alias)(b) + return json.Unmarshal(data, aux) +} + +// Commission represents a commission log entry. +type Commission struct { + Type uint16 `json:"type"` + Amount int64 `json:"amount"` + OrderNo string `json:"order_no"` + Timestamp int64 `json:"timestamp"` +} + +// Marshal implements the json.Marshaler interface for Commission. +func (c *Commission) Marshal() ([]byte, error) { + type Alias Commission + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(c), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Commission. +func (c *Commission) Unmarshal(data []byte) error { + type Alias Commission + aux := (*Alias)(c) + return json.Unmarshal(data, aux) +} + +// Gift represents a gift log entry. +type Gift struct { + Type uint16 `json:"type"` + OrderNo string `json:"order_no"` + SubscribeId int64 `json:"subscribe_id"` + Amount int64 `json:"amount"` + Balance int64 `json:"balance"` + Remark string `json:"remark,omitempty"` + Timestamp int64 `json:"timestamp"` +} + +// Marshal implements the json.Marshaler interface for Gift. +func (g *Gift) Marshal() ([]byte, error) { + type Alias Gift + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(g), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for Gift. +func (g *Gift) Unmarshal(data []byte) error { + type Alias Gift + aux := (*Alias)(g) + return json.Unmarshal(data, aux) +} + +// UserTraffic represents a user traffic log entry. +type UserTraffic struct { + SubscribeId int64 `json:"subscribe_id"` // Subscribe ID + UserId int64 `json:"user_id"` // User ID + Upload int64 `json:"upload"` // Upload traffic in bytes + Download int64 `json:"download"` // Download traffic in bytes + Total int64 `json:"total"` // Total traffic in bytes (Upload + Download) +} + +// Marshal implements the json.Marshaler interface for UserTraffic. +func (u *UserTraffic) Marshal() ([]byte, error) { + type Alias UserTraffic + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(u), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for UserTraffic. +func (u *UserTraffic) Unmarshal(data []byte) error { + type Alias UserTraffic + aux := (*Alias)(u) + return json.Unmarshal(data, aux) +} + +// UserTrafficRank represents a user traffic rank entry. +type UserTrafficRank struct { + Rank map[uint8]UserTraffic `json:"rank"` // Key is rank ,type is UserTraffic +} + +// Marshal implements the json.Marshaler interface for UserTrafficRank. +func (u *UserTrafficRank) Marshal() ([]byte, error) { + type Alias UserTrafficRank + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(u), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for UserTrafficRank. +func (u *UserTrafficRank) Unmarshal(data []byte) error { + type Alias UserTrafficRank + aux := (*Alias)(u) + return json.Unmarshal(data, aux) +} + +// ServerTraffic represents a server traffic log entry. +type ServerTraffic struct { + ServerId int64 `json:"server_id"` // Server ID + Upload int64 `json:"upload"` // Upload traffic in bytes + Download int64 `json:"download"` // Download traffic in bytes + Total int64 `json:"total"` // Total traffic in bytes (Upload + Download) +} + +// Marshal implements the json.Marshaler interface for ServerTraffic. +func (s *ServerTraffic) Marshal() ([]byte, error) { + type Alias ServerTraffic + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for ServerTraffic. +func (s *ServerTraffic) Unmarshal(data []byte) error { + type Alias ServerTraffic + aux := (*Alias)(s) + return json.Unmarshal(data, aux) +} + +// ServerTrafficRank represents a server traffic rank entry. +type ServerTrafficRank struct { + Rank map[uint8]ServerTraffic `json:"rank"` // Key is rank ,type is ServerTraffic +} + +// Marshal implements the json.Marshaler interface for ServerTrafficRank. +func (s *ServerTrafficRank) Marshal() ([]byte, error) { + type Alias ServerTrafficRank + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for ServerTrafficRank. +func (s *ServerTrafficRank) Unmarshal(data []byte) error { + type Alias ServerTrafficRank + aux := (*Alias)(s) + return json.Unmarshal(data, aux) +} + +// TrafficStat represents a daily traffic statistics log entry. +type TrafficStat struct { + Upload int64 `json:"upload"` + Download int64 `json:"download"` + Total int64 `json:"total"` +} + +// Marshal implements the json.Marshaler interface for TrafficStat. +func (t *TrafficStat) Marshal() ([]byte, error) { + type Alias TrafficStat + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(t), + }) +} + +// Unmarshal implements the json.Unmarshaler interface for TrafficStat. +func (t *TrafficStat) Unmarshal(data []byte) error { + type Alias TrafficStat + aux := (*Alias)(t) + return json.Unmarshal(data, aux) } diff --git a/internal/model/log/model.go b/internal/model/log/model.go index 8bacf15..783b6b8 100644 --- a/internal/model/log/model.go +++ b/internal/model/log/model.go @@ -1,9 +1,63 @@ package log import ( + "context" + "gorm.io/gorm" ) -func NewModel(conn *gorm.DB) Model { - return newLogModel(conn) +func NewModel(db *gorm.DB) Model { + return &customSystemLogModel{ + defaultLogModel: newSystemLogModel(db), + } +} + +type FilterParams struct { + Page int + Size int + Type uint8 + Data string + Search string + ObjectID int64 +} + +type customSystemLogLogicModel interface { + FilterSystemLog(ctx context.Context, filter *FilterParams) ([]*SystemLog, int64, error) +} + +func (m *customSystemLogModel) FilterSystemLog(ctx context.Context, filter *FilterParams) ([]*SystemLog, int64, error) { + tx := m.WithContext(ctx).Model(&SystemLog{}).Order("id DESC") + if filter == nil { + filter = &FilterParams{ + Page: 1, + Size: 10, + } + } + + if filter.Page < 1 { + filter.Page = 1 + } + if filter.Size < 1 { + filter.Size = 10 + } + + if filter.Type != 0 { + tx = tx.Where("`type` = ?", filter.Type) + } + + if filter.Data != "" { + tx = tx.Where("`date` = ?", filter.Data) + } + + if filter.ObjectID != 0 { + tx = tx.Where("`object_id` = ?", filter.ObjectID) + } + if filter.Search != "" { + tx = tx.Where("`content` LIKE ?", "%"+filter.Search+"%") + } + + var total int64 + var logs []*SystemLog + err := tx.Count(&total).Limit(filter.Size).Offset((filter.Page - 1) * filter.Size).Find(&logs).Error + return logs, total, err } diff --git a/internal/model/node/cache.go b/internal/model/node/cache.go new file mode 100644 index 0000000..3d16c78 --- /dev/null +++ b/internal/model/node/cache.go @@ -0,0 +1,163 @@ +package node + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" +) + +type ( + customCacheLogicModel interface { + StatusCache(ctx context.Context, serverId int64) (Status, error) + UpdateStatusCache(ctx context.Context, serverId int64, status *Status) error + OnlineUserSubscribe(ctx context.Context, serverId int64, protocol string) (OnlineUserSubscribe, error) + UpdateOnlineUserSubscribe(ctx context.Context, serverId int64, protocol string, subscribe OnlineUserSubscribe) error + OnlineUserSubscribeGlobal(ctx context.Context) (int64, error) + UpdateOnlineUserSubscribeGlobal(ctx context.Context, subscribe OnlineUserSubscribe) error + } + + Status struct { + Cpu float64 `json:"cpu"` + Mem float64 `json:"mem"` + Disk float64 `json:"disk"` + UpdatedAt int64 `json:"updated_at"` + } + + OnlineUserSubscribe map[int64][]string +) + +// Marshal to json string +func (s *Status) Marshal() string { + type Alias Status + data, _ := json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) + return string(data) +} + +// Unmarshal from json string +func (s *Status) Unmarshal(data string) error { + type Alias Status + aux := &struct { + *Alias + }{ + Alias: (*Alias)(s), + } + return json.Unmarshal([]byte(data), &aux) +} + +const ( + Expiry = 300 * time.Second // Cache expiry time in seconds + StatusCacheKey = "node:status:%d" // Node status cache key format (Server ID and protocol) Example: node:status:1:shadowsocks + OnlineUserCacheKeyWithSubscribe = "node:online:subscribe:%d:%s" // Online user subscribe cache key format (Server ID and protocol) Example: node:online:subscribe:1:shadowsocks + OnlineUserSubscribeCacheKeyWithGlobal = "node:online:subscribe:global" // Online user global subscribe cache key +) + +// UpdateStatusCache Update server status to cache +func (m *customServerModel) UpdateStatusCache(ctx context.Context, serverId int64, status *Status) error { + key := fmt.Sprintf(StatusCacheKey, serverId) + return m.Cache.Set(ctx, key, status.Marshal(), Expiry).Err() + +} + +// DeleteStatusCache Delete server status from cache +func (m *customServerModel) DeleteStatusCache(ctx context.Context, serverId int64) error { + key := fmt.Sprintf(StatusCacheKey, serverId) + return m.Cache.Del(ctx, key).Err() +} + +// StatusCache Get server status from cache +func (m *customServerModel) StatusCache(ctx context.Context, serverId int64) (Status, error) { + var status Status + key := fmt.Sprintf(StatusCacheKey, serverId) + + result, err := m.Cache.Get(ctx, key).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return status, nil + } + return status, err + } + if result == "" { + return status, nil + } + err = status.Unmarshal(result) + return status, err +} + +// OnlineUserSubscribe Get online user subscribe +func (m *customServerModel) OnlineUserSubscribe(ctx context.Context, serverId int64, protocol string) (OnlineUserSubscribe, error) { + key := fmt.Sprintf(OnlineUserCacheKeyWithSubscribe, serverId, protocol) + result, err := m.Cache.Get(ctx, key).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return OnlineUserSubscribe{}, nil + } + return nil, err + } + if result == "" { + return OnlineUserSubscribe{}, nil + } + var subscribe OnlineUserSubscribe + err = json.Unmarshal([]byte(result), &subscribe) + return subscribe, err +} + +// UpdateOnlineUserSubscribe Update online user subscribe +func (m *customServerModel) UpdateOnlineUserSubscribe(ctx context.Context, serverId int64, protocol string, subscribe OnlineUserSubscribe) error { + key := fmt.Sprintf(OnlineUserCacheKeyWithSubscribe, serverId, protocol) + data, err := json.Marshal(subscribe) + if err != nil { + return err + } + return m.Cache.Set(ctx, key, data, Expiry).Err() +} + +// DeleteOnlineUserSubscribe Delete online user subscribe +func (m *customServerModel) DeleteOnlineUserSubscribe(ctx context.Context, serverId int64, protocol string) error { + key := fmt.Sprintf(OnlineUserCacheKeyWithSubscribe, serverId, protocol) + return m.Cache.Del(ctx, key).Err() +} + +// OnlineUserSubscribeGlobal Get global online user subscribe count +func (m *customServerModel) OnlineUserSubscribeGlobal(ctx context.Context) (int64, error) { + now := time.Now().Unix() + // Clear expired data + if err := m.Cache.ZRemRangeByScore(ctx, OnlineUserSubscribeCacheKeyWithGlobal, "-inf", fmt.Sprintf("%d", now)).Err(); err != nil { + return 0, err + } + return m.Cache.ZCard(ctx, OnlineUserSubscribeCacheKeyWithGlobal).Result() +} + +// UpdateOnlineUserSubscribeGlobal Update global online user subscribe count +func (m *customServerModel) UpdateOnlineUserSubscribeGlobal(ctx context.Context, subscribe OnlineUserSubscribe) error { + now := time.Now() + expireTime := now.Add(5 * time.Minute).Unix() // set expire time 5 minutes later + + pipe := m.Cache.Pipeline() + + // Clear expired data + pipe.ZRemRangeByScore(ctx, OnlineUserSubscribeCacheKeyWithGlobal, "-inf", fmt.Sprintf("%d", now.Unix())) + // Add or update each subscribe with new expire time + for sub := range subscribe { + // Use ZAdd to add or update the member with new score (expire time) + pipe.ZAdd(ctx, OnlineUserSubscribeCacheKeyWithGlobal, redis.Z{ + Score: float64(expireTime), + Member: sub, + }) + } + + _, err := pipe.Exec(ctx) + return err +} + +// DeleteOnlineUserSubscribeGlobal Delete global online user subscribe count +func (m *customServerModel) DeleteOnlineUserSubscribeGlobal(ctx context.Context) error { + return m.Cache.Del(ctx, OnlineUserSubscribeCacheKeyWithGlobal).Err() +} diff --git a/internal/model/node/default.go b/internal/model/node/default.go new file mode 100644 index 0000000..575ea5f --- /dev/null +++ b/internal/model/node/default.go @@ -0,0 +1,131 @@ +package node + +import ( + "context" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +var _ Model = (*customServerModel)(nil) + +//goland:noinspection GoNameStartsWithPackageName +type ( + Model interface { + serverModel + NodeModel + customCacheLogicModel + customServerLogicModel + } + serverModel interface { + InsertServer(ctx context.Context, data *Server, tx ...*gorm.DB) error + FindOneServer(ctx context.Context, id int64) (*Server, error) + UpdateServer(ctx context.Context, data *Server, tx ...*gorm.DB) error + DeleteServer(ctx context.Context, id int64, tx ...*gorm.DB) error + Transaction(ctx context.Context, fn func(db *gorm.DB) error) error + } + + NodeModel interface { + InsertNode(ctx context.Context, data *Node, tx ...*gorm.DB) error + FindOneNode(ctx context.Context, id int64) (*Node, error) + UpdateNode(ctx context.Context, data *Node, tx ...*gorm.DB) error + DeleteNode(ctx context.Context, id int64, tx ...*gorm.DB) error + } + + customServerModel struct { + *defaultServerModel + } + defaultServerModel struct { + *gorm.DB + Cache *redis.Client + } +) + +func newServerModel(db *gorm.DB, cache *redis.Client) *defaultServerModel { + return &defaultServerModel{ + DB: db, + Cache: cache, + } +} + +// NewModel returns a model for the database table. +func NewModel(conn *gorm.DB, cache *redis.Client) Model { + return &customServerModel{ + defaultServerModel: newServerModel(conn, cache), + } +} + +func (m *defaultServerModel) InsertServer(ctx context.Context, data *Server, tx ...*gorm.DB) error { + db := m.DB + if len(tx) > 0 { + db = tx[0] + } + return db.WithContext(ctx).Create(data).Error +} + +func (m *defaultServerModel) FindOneServer(ctx context.Context, id int64) (*Server, error) { + var server Server + err := m.WithContext(ctx).Model(&Server{}).Where("id = ?", id).First(&server).Error + return &server, err +} + +func (m *defaultServerModel) UpdateServer(ctx context.Context, data *Server, tx ...*gorm.DB) error { + _, err := m.FindOneServer(ctx, data.Id) + if err != nil { + return err + } + + db := m.DB + if len(tx) > 0 { + db = tx[0] + } + return db.WithContext(ctx).Where("`id` = ?", data.Id).Save(data).Error + +} + +func (m *defaultServerModel) DeleteServer(ctx context.Context, id int64, tx ...*gorm.DB) error { + db := m.DB + if len(tx) > 0 { + db = tx[0] + } + return db.WithContext(ctx).Where("`id` = ?", id).Delete(&Server{}).Error +} + +func (m *defaultServerModel) InsertNode(ctx context.Context, data *Node, tx ...*gorm.DB) error { + db := m.DB + if len(tx) > 0 { + db = tx[0] + } + return db.WithContext(ctx).Create(data).Error +} + +func (m *defaultServerModel) FindOneNode(ctx context.Context, id int64) (*Node, error) { + var node Node + err := m.WithContext(ctx).Model(&Node{}).Where("id = ?", id).First(&node).Error + return &node, err +} + +func (m *defaultServerModel) UpdateNode(ctx context.Context, data *Node, tx ...*gorm.DB) error { + _, err := m.FindOneNode(ctx, data.Id) + if err != nil { + return err + } + + db := m.DB + if len(tx) > 0 { + db = tx[0] + } + return db.WithContext(ctx).Where("`id` = ?", data.Id).Save(data).Error +} + +func (m *defaultServerModel) DeleteNode(ctx context.Context, id int64, tx ...*gorm.DB) error { + db := m.DB + if len(tx) > 0 { + db = tx[0] + } + return db.WithContext(ctx).Where("`id` = ?", id).Delete(&Node{}).Error +} + +func (m *defaultServerModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { + return m.WithContext(ctx).Transaction(fn) +} diff --git a/internal/model/node/model.go b/internal/model/node/model.go new file mode 100644 index 0000000..30eba18 --- /dev/null +++ b/internal/model/node/model.go @@ -0,0 +1,185 @@ +package node + +import ( + "context" + "fmt" + "strings" + + "github.com/perfect-panel/server/pkg/tool" + "gorm.io/gorm" +) + +type customServerLogicModel interface { + FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error) + FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error) + ClearNodeCache(ctx context.Context, params *FilterNodeParams) error +} + +const ( + // ServerUserListCacheKey Server User List Cache Key + ServerUserListCacheKey = "server:user:" + + // ServerConfigCacheKey Server Config Cache Key + ServerConfigCacheKey = "server:config:" +) + +// FilterParams Filter Server Params +type FilterParams struct { + Page int + Size int + Ids []int64 // Server IDs + Search string +} + +type FilterNodeParams struct { + Page int // Page Number + Size int // Page Size + NodeId []int64 // Node IDs + ServerId []int64 // Server IDs + Tag []string // Tags + Search string // Search Address or Name + Protocol string // Protocol + Preload bool // Preload Server + Enabled *bool // Enabled +} + +// FilterServerList Filter Server List +func (m *customServerModel) FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error) { + var servers []*Server + var total int64 + query := m.WithContext(ctx).Model(&Server{}) + if params == nil { + params = &FilterParams{ + Page: 1, + Size: 10, + } + } + if params.Search != "" { + s := "%" + params.Search + "%" + query = query.Where("`name` LIKE ? OR `address` LIKE ?", s, s) + } + if len(params.Ids) > 0 { + query = query.Where("id IN ?", params.Ids) + } + err := query.Count(&total).Order("sort ASC").Limit(params.Size).Offset((params.Page - 1) * params.Size).Find(&servers).Error + return total, servers, err +} + +// FilterNodeList Filter Node List +func (m *customServerModel) FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error) { + var nodes []*Node + var total int64 + query := m.WithContext(ctx).Model(&Node{}) + if params == nil { + params = &FilterNodeParams{ + Page: 1, + Size: 10, + } + } + if params.Search != "" { + s := "%" + params.Search + "%" + query = query.Where("`name` LIKE ? OR `address` LIKE ? OR `tags` LIKE ? OR `port` LIKE ? ", s, s, s, s) + } + if len(params.NodeId) > 0 { + query = query.Where("id IN ?", params.NodeId) + } + if len(params.ServerId) > 0 { + query = query.Where("server_id IN ?", params.ServerId) + } + if len(params.Tag) > 0 { + query = query.Scopes(InSet("tags", params.Tag)) + } + if params.Protocol != "" { + query = query.Where("protocol = ?", params.Protocol) + } + + if params.Enabled != nil { + query = query.Where("enabled = ?", *params.Enabled) + } + + if params.Preload { + query = query.Preload("Server") + } + + err := query.Count(&total).Order("sort ASC").Limit(params.Size).Offset((params.Page - 1) * params.Size).Find(&nodes).Error + return total, nodes, err +} + +// ClearNodeCache Clear Node Cache +func (m *customServerModel) ClearNodeCache(ctx context.Context, params *FilterNodeParams) error { + _, nodes, err := m.FilterNodeList(ctx, params) + if err != nil { + return err + } + var cacheKeys []string + for _, node := range nodes { + cacheKeys = append(cacheKeys, fmt.Sprintf("%s%d", ServerUserListCacheKey, node.ServerId)) + if node.Protocol != "" { + var cursor uint64 + for { + keys, newCursor, err := m.Cache.Scan(ctx, cursor, fmt.Sprintf("%s%d*", ServerConfigCacheKey, node.ServerId), 100).Result() + if err != nil { + return err + } + if len(keys) > 0 { + cacheKeys = append(keys, keys...) + } + cursor = newCursor + if cursor == 0 { + break + } + } + } + } + + if len(cacheKeys) > 0 { + cacheKeys = tool.RemoveDuplicateElements(cacheKeys...) + return m.Cache.Del(ctx, cacheKeys...).Err() + } + return nil +} + +// ClearServerCache Clear Server Cache +func (m *customServerModel) ClearServerCache(ctx context.Context, serverId int64) error { + var cacheKeys []string + cacheKeys = append(cacheKeys, fmt.Sprintf("%s%d", ServerUserListCacheKey, serverId)) + var cursor uint64 + for { + keys, newCursor, err := m.Cache.Scan(ctx, 0, fmt.Sprintf("%s%d*", ServerConfigCacheKey, serverId), 100).Result() + if err != nil { + return err + } + if len(keys) > 0 { + cacheKeys = append(cacheKeys, keys...) + } + cursor = newCursor + if cursor == 0 { + break + } + } + + if len(cacheKeys) > 0 { + cacheKeys = tool.RemoveDuplicateElements(cacheKeys...) + return m.Cache.Del(ctx, cacheKeys...).Err() + } + return nil +} + +// InSet 支持多值 OR 查询 +func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + if len(values) == 0 { + return db + } + + conds := make([]string, len(values)) + args := make([]interface{}, len(values)) + for i, v := range values { + conds[i] = "FIND_IN_SET(?, " + field + ")" + args[i] = v + } + + // 用括号包裹 OR 条件,保证外层 AND 不受影响 + return db.Where("("+strings.Join(conds, " OR ")+")", args...) + } +} diff --git a/internal/model/node/node.go b/internal/model/node/node.go new file mode 100644 index 0000000..89d665d --- /dev/null +++ b/internal/model/node/node.go @@ -0,0 +1,82 @@ +package node + +import ( + "time" + + "github.com/perfect-panel/server/pkg/logger" + "gorm.io/gorm" +) + +type Node struct { + Id int64 `gorm:"primary_key"` + Name string `gorm:"type:varchar(100);not null;default:'';comment:Node Name"` + Tags string `gorm:"type:varchar(255);not null;default:'';comment:Tags"` + Port uint16 `gorm:"not null;default:0;comment:Connect Port"` + Address string `gorm:"type:varchar(255);not null;default:'';comment:Connect Address"` + ServerId int64 `gorm:"not null;default:0;comment:Server ID"` + Server *Server `gorm:"foreignKey:ServerId;references:Id"` + Protocol string `gorm:"type:varchar(100);not null;default:'';comment:Protocol"` + Enabled *bool `gorm:"type:boolean;not null;default:true;comment:Enabled"` + Sort int `gorm:"uniqueIndex;not null;default:0;comment:Sort"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (n *Node) TableName() string { + return "nodes" +} + +func (n *Node) BeforeCreate(tx *gorm.DB) error { + if n.Sort == 0 { + var maxSort int + if err := tx.Model(&Node{}).Select("COALESCE(MAX(sort), 0)").Scan(&maxSort).Error; err != nil { + return err + } + n.Sort = maxSort + 1 + } + return nil +} + +func (n *Node) BeforeDelete(tx *gorm.DB) error { + if err := tx.Exec("UPDATE `nodes` SET sort = sort - 1 WHERE sort > ?", n.Sort).Error; err != nil { + return err + } + return nil +} + +func (n *Node) BeforeUpdate(tx *gorm.DB) error { + var count int64 + if err := tx.Set("gorm:query_option", "FOR UPDATE").Model(&Server{}). + Where("sort = ? AND id != ?", n.Sort, n.Id).Count(&count).Error; err != nil { + return err + } + if count > 1 { + // reorder sort + if err := reorderSortWithNode(tx); err != nil { + logger.Errorf("[Server] BeforeUpdate reorderSort error: %v", err.Error()) + return err + } + // get max sort + var maxSort int + if err := tx.Model(&Server{}).Select("MAX(sort)").Scan(&maxSort).Error; err != nil { + return err + } + n.Sort = maxSort + 1 + } + return nil +} + +func reorderSortWithNode(tx *gorm.DB) error { + var nodes []Node + if err := tx.Order("sort, id").Find(&nodes).Error; err != nil { + return err + } + for i, node := range nodes { + if node.Sort != i+1 { + if err := tx.Exec("UPDATE `nodes` SET sort = ? WHERE id = ?", i+1, node.Id).Error; err != nil { + return err + } + } + } + return nil +} diff --git a/internal/model/node/server.go b/internal/model/node/server.go new file mode 100644 index 0000000..00e433e --- /dev/null +++ b/internal/model/node/server.go @@ -0,0 +1,188 @@ +package node + +import ( + "encoding/json" + "time" + + "github.com/perfect-panel/server/pkg/logger" + "github.com/pkg/errors" + "gorm.io/gorm" +) + +type Server struct { + Id int64 `gorm:"primary_key"` + Name string `gorm:"type:varchar(100);not null;default:'';comment:Server Name"` + Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"` + City string `gorm:"type:varchar(128);not null;default:'';comment:City"` + //Ratio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"` + Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"` + Sort int `gorm:"type:int;not null;default:0;comment:Sort"` + Protocols string `gorm:"type:text;default:null;comment:Protocol"` + LastReportedAt *time.Time `gorm:"comment:Last Reported Time"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (*Server) TableName() string { + return "servers" +} + +func (m *Server) BeforeCreate(tx *gorm.DB) error { + if m.Sort == 0 { + var maxSort int + if err := tx.Model(&Server{}).Select("COALESCE(MAX(sort), 0)").Scan(&maxSort).Error; err != nil { + return err + } + m.Sort = maxSort + 1 + } + return nil +} + +func (m *Server) BeforeDelete(tx *gorm.DB) error { + if err := tx.Exec("UPDATE `servers` SET sort = sort - 1 WHERE sort > ?", m.Sort).Error; err != nil { + return err + } + return nil +} + +func (m *Server) BeforeUpdate(tx *gorm.DB) error { + var count int64 + if err := tx.Set("gorm:query_option", "FOR UPDATE").Model(&Server{}). + Where("sort = ? AND id != ?", m.Sort, m.Id).Count(&count).Error; err != nil { + return err + } + if count > 1 { + // reorder sort + if err := reorderSortWithServer(tx); err != nil { + logger.Errorf("[Server] BeforeUpdate reorderSort error: %v", err.Error()) + return err + } + // get max sort + var maxSort int + if err := tx.Model(&Server{}).Select("MAX(sort)").Scan(&maxSort).Error; err != nil { + return err + } + m.Sort = maxSort + 1 + } + return nil +} + +// MarshalProtocols Marshal server protocols to json +func (m *Server) MarshalProtocols(list []Protocol) error { + var validate = make(map[string]bool) + for _, protocol := range list { + if protocol.Type == "" { + return errors.New("protocol type is required") + } + if _, exists := validate[protocol.Type]; exists { + return errors.New("duplicate protocol type: " + protocol.Type) + } + validate[protocol.Type] = true + } + data, err := json.Marshal(list) + if err != nil { + return err + } + m.Protocols = string(data) + return nil +} + +// UnmarshalProtocols Unmarshal server protocols from json +func (m *Server) UnmarshalProtocols() ([]Protocol, error) { + var list []Protocol + if m.Protocols == "" { + return list, nil + } + err := json.Unmarshal([]byte(m.Protocols), &list) + if err != nil { + return nil, err + } + return list, nil +} + +type Protocol struct { + Type string `json:"type"` + Port uint16 `json:"port"` + Enable bool `json:"enable"` + Security string `json:"security,omitempty"` + SNI string `json:"sni,omitempty"` + AllowInsecure bool `json:"allow_insecure,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + RealityServerAddr string `json:"reality_server_addr,omitempty"` + RealityServerPort int `json:"reality_server_port,omitempty"` + RealityPrivateKey string `json:"reality_private_key,omitempty"` + RealityPublicKey string `json:"reality_public_key,omitempty"` + RealityShortId string `json:"reality_short_id,omitempty"` + Transport string `json:"transport,omitempty"` + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + ServiceName string `json:"service_name,omitempty"` + Cipher string `json:"cipher,omitempty"` + ServerKey string `json:"server_key,omitempty"` + Flow string `json:"flow,omitempty"` + HopPorts string `json:"hop_ports,omitempty"` + HopInterval int `json:"hop_interval,omitempty"` + ObfsPassword string `json:"obfs_password,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ReduceRtt bool `json:"reduce_rtt,omitempty"` + UDPRelayMode string `json:"udp_relay_mode,omitempty"` + CongestionController string `json:"congestion_controller,omitempty"` + Multiplex string `json:"multiplex,omitempty"` // mux, eg: off/low/medium/high + PaddingScheme string `json:"padding_scheme,omitempty"` // padding scheme + UpMbps int `json:"up_mbps,omitempty"` // upload speed limit + DownMbps int `json:"down_mbps,omitempty"` // download speed limit + Obfs string `json:"obfs,omitempty"` // obfs, 'none', 'http', 'tls' + ObfsHost string `json:"obfs_host,omitempty"` // obfs host + ObfsPath string `json:"obfs_path,omitempty"` // obfs path + XhttpMode string `json:"xhttp_mode,omitempty"` // xhttp mode + XhttpExtra string `json:"xhttp_extra,omitempty"` // xhttp extra path + Encryption string `json:"encryption,omitempty"` // encryption,'none', 'mlkem768x25519plus' + EncryptionMode string `json:"encryption_mode,omitempty"` // encryption mode,'native', 'xorpub', 'random' + EncryptionRtt string `json:"encryption_rtt,omitempty"` // encryption rtt,'0rtt', '1rtt' + EncryptionTicket string `json:"encryption_ticket,omitempty"` // encryption ticket + EncryptionServerPadding string `json:"encryption_server_padding,omitempty"` // encryption server padding + EncryptionPrivateKey string `json:"encryption_private_key,omitempty"` // encryption private key + EncryptionClientPadding string `json:"encryption_client_padding,omitempty"` // encryption client padding + EncryptionPassword string `json:"encryption_password,omitempty"` // encryption password + + Ratio float64 `json:"ratio,omitempty"` // Traffic ratio, default is 1 + CertMode string `json:"cert_mode,omitempty"` // Certificate mode, `none`|`http`|`dns`|`self` + CertDNSProvider string `json:"cert_dns_provider,omitempty"` // DNS provider for certificate + CertDNSEnv string `json:"cert_dns_env"` // Environment for DNS provider +} + +// Marshal protocol to json +func (m *Protocol) Marshal() ([]byte, error) { + type Alias Protocol + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(m), + }) +} + +// Unmarshal json to protocol +func (m *Protocol) Unmarshal(data []byte) error { + type Alias Protocol + aux := &struct { + *Alias + }{ + Alias: (*Alias)(m), + } + return json.Unmarshal(data, &aux) +} + +func reorderSortWithServer(tx *gorm.DB) error { + var servers []Server + if err := tx.Order("sort, id").Find(&servers).Error; err != nil { + return err + } + for i, server := range servers { + if server.Sort != i+1 { + if err := tx.Exec("UPDATE `servers` SET sort = ? WHERE id = ?", i+1, server.Id).Error; err != nil { + return err + } + } + } + return nil +} diff --git a/internal/model/order/default.go b/internal/model/order/default.go index 6f83585..a59eeb0 100644 --- a/internal/model/order/default.go +++ b/internal/model/order/default.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/perfect-panel/server/pkg/cache" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) diff --git a/internal/model/order/model.go b/internal/model/order/model.go index 9909d80..d98e361 100644 --- a/internal/model/order/model.go +++ b/internal/model/order/model.go @@ -4,9 +4,9 @@ import ( "context" "time" - "github.com/perfect-panel/ppanel-server/internal/model/payment" + "github.com/perfect-panel/server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/model/subscribe" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -40,6 +40,13 @@ type Details struct { UpdatedAt time.Time `gorm:"comment:Update Time"` } +type OrdersTotalWithDate struct { + Date string + AmountTotal int64 + NewOrderAmount int64 + RenewalOrderAmount int64 +} + type customOrderLogicModel interface { UpdateOrderStatus(ctx context.Context, orderNo string, status uint8, tx ...*gorm.DB) error QueryOrderListByPage(ctx context.Context, page, size int, status uint8, user, subscribe int64, search string) (int64, []*Details, error) @@ -47,11 +54,19 @@ type customOrderLogicModel interface { FindOneDetailsByOrderNo(ctx context.Context, orderNo string) (*Details, error) QueryMonthlyOrders(ctx context.Context, date time.Time) (OrdersTotal, error) QueryDateOrders(ctx context.Context, date time.Time) (OrdersTotal, error) - QueryPendingOrders(ctx context.Context) ([]*Order, error) QueryTotalOrders(ctx context.Context) (OrdersTotal, error) QueryMonthlyUserCounts(ctx context.Context, date time.Time) (int64, int64, error) QueryDateUserCounts(ctx context.Context, date time.Time) (int64, int64, error) + QueryTotalUserCounts(ctx context.Context) (int64, int64, error) IsUserEligibleForNewOrder(ctx context.Context, userID int64) (bool, error) + QueryDailyOrdersList(ctx context.Context, date time.Time) ([]OrdersTotalWithDate, error) + QueryMonthlyOrdersList(ctx context.Context, date time.Time) ([]OrdersTotalWithDate, error) +} + +// UserCounts User counts for new and renewal users +type UserCounts struct { + NewUsers int64 `gorm:"column:new_users"` + RenewalUsers int64 `gorm:"column:renewal_users"` } // NewModel returns a model for the database table. @@ -156,51 +171,78 @@ func (m *customOrderModel) QueryDateOrders(ctx context.Context, date time.Time) func (m *customOrderModel) QueryTotalOrders(ctx context.Context) (OrdersTotal, error) { var result OrdersTotal - err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, v interface{}) error { + + err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, _ interface{}) error { return conn.Model(&Order{}). + Select(` + SUM(amount) AS amount_total, + SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount, + SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount + `). Where("status IN ? AND method != ?", []int64{2, 5}, "balance"). - Select( - "SUM(amount) as amount_total, " + - "SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) as new_order_amount, " + - "SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) as renewal_order_amount", - ). - Scan(v).Error + Scan(&result).Error }) + return result, err } func (m *customOrderModel) QueryMonthlyUserCounts(ctx context.Context, date time.Time) (int64, int64, error) { + // 获取当月第一天零点 firstDay := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()) - lastDay := firstDay.AddDate(0, 1, -1) + // 获取下个月第一天零点(避免漏掉最后一天的订单) + nextMonth := firstDay.AddDate(0, 1, 0) - var newUsers int64 - var renewalUsers int64 + var counts UserCounts + + // 执行查询 err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error { return conn.Model(&Order{}). - Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, firstDay, lastDay, "balance"). - Select( - "COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) as new_users, "+ - "COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) as renewal_users"). - Row().Scan(&newUsers, &renewalUsers) + Select(` + COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users, + COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users + `). + Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?", + []int64{2, 5}, firstDay, nextMonth, "balance"). + Scan(&counts).Error }) - return newUsers, renewalUsers, err + + return counts.NewUsers, counts.RenewalUsers, err } - func (m *customOrderModel) QueryDateUserCounts(ctx context.Context, date time.Time) (int64, int64, error) { - start := date.Truncate(24 * time.Hour) - end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + // 当天 00:00:00 + start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location()) + // 下一天 00:00:00 + nextDay := start.Add(24 * time.Hour) + + var counts UserCounts - var newUsers int64 - var renewalUsers int64 err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error { return conn.Model(&Order{}). - Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, start, end, "balance"). - Select( - "COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) as new_users, "+ - "COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) as renewal_users"). - Row().Scan(&newUsers, &renewalUsers) + Select(` + COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users, + COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users + `). + Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?", + []int64{2, 5}, start, nextDay, "balance"). + Scan(&counts).Error }) - return newUsers, renewalUsers, err + + return counts.NewUsers, counts.RenewalUsers, err +} +func (m *customOrderModel) QueryTotalUserCounts(ctx context.Context) (int64, int64, error) { + var counts UserCounts + + err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error { + return conn.Model(&Order{}). + Where("status IN ? AND method != ?", []int64{2, 5}, "balance"). + Select(` + COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users, + COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users + `). + Scan(&counts).Error + }) + + return counts.NewUsers, counts.RenewalUsers, err } func (m *customOrderModel) IsUserEligibleForNewOrder(ctx context.Context, userID int64) (bool, error) { @@ -213,12 +255,54 @@ func (m *customOrderModel) IsUserEligibleForNewOrder(ctx context.Context, userID return count == 0, err } -func (m *customOrderModel) QueryPendingOrders(ctx context.Context) ([]*Order, error) { - var orderInfo []*Order - err := m.QueryNoCacheCtx(ctx, &orderInfo, func(conn *gorm.DB, v interface{}) error { +// QueryDailyOrdersList 查询当月每日订单统计 +func (m *customOrderModel) QueryDailyOrdersList(ctx context.Context, date time.Time) ([]OrdersTotalWithDate, error) { + var results []OrdersTotalWithDate + + err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error { + // 当月 1 号 00:00:00 + firstDay := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()) + // 第二天 00:00:00 + nextDay := date.AddDate(0, 0, 1).Truncate(24 * time.Hour) + return conn.Model(&Order{}). - Where("status = ?", 1). - Find(v).Error + Select(` + DATE_FORMAT(created_at, '%Y-%m-%d') AS date, + SUM(amount) AS amount_total, + SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount, + SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount + `). + Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?", + []int64{2, 5}, firstDay, nextDay, "balance"). + Group("DATE_FORMAT(created_at, '%Y-%m-%d')"). + Order("date ASC"). + Scan(v).Error }) - return orderInfo, err + return results, err +} + +// QueryMonthlyOrdersList 查询过去 6 个月订单统计(包含当前月) +func (m *customOrderModel) QueryMonthlyOrdersList(ctx context.Context, date time.Time) ([]OrdersTotalWithDate, error) { + var results []OrdersTotalWithDate + + err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error { + // 六个月前(取月初) + start := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()).AddDate(0, -5, 0) + // 下个月月初 + end := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()).AddDate(0, 1, 0) + + return conn.Model(&Order{}). + Select(` + DATE_FORMAT(created_at, '%Y-%m') AS date, + SUM(amount) AS amount_total, + SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount, + SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount + `). + Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?", + []int64{2, 5}, start, end, "balance"). + Group("DATE_FORMAT(created_at, '%Y-%m')"). + Order("date ASC"). + Scan(v).Error + }) + return results, err } diff --git a/internal/model/payment/default.go b/internal/model/payment/default.go index cd4afdd..0ebd891 100644 --- a/internal/model/payment/default.go +++ b/internal/model/payment/default.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/perfect-panel/server/pkg/cache" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -22,10 +22,10 @@ type ( customPaymentLogicModel } paymentModel interface { - Insert(ctx context.Context, data *Payment) error + Insert(ctx context.Context, data *Payment, tx ...*gorm.DB) error FindOne(ctx context.Context, id int64) (*Payment, error) - Update(ctx context.Context, data *Payment) error - Delete(ctx context.Context, id int64) error + Update(ctx context.Context, data *Payment, tx ...*gorm.DB) error + Delete(ctx context.Context, id int64, tx ...*gorm.DB) error Transaction(ctx context.Context, fn func(db *gorm.DB) error) error } @@ -67,8 +67,11 @@ func (m *defaultPaymentModel) getCacheKeys(data *Payment) []string { return cacheKeys } -func (m *defaultPaymentModel) Insert(ctx context.Context, data *Payment) error { +func (m *defaultPaymentModel) Insert(ctx context.Context, data *Payment, tx ...*gorm.DB) error { err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } return conn.Create(&data).Error }, m.getCacheKeys(data)...) return err @@ -88,19 +91,21 @@ func (m *defaultPaymentModel) FindOne(ctx context.Context, id int64) (*Payment, } } -func (m *defaultPaymentModel) Update(ctx context.Context, data *Payment) error { +func (m *defaultPaymentModel) Update(ctx context.Context, data *Payment, tx ...*gorm.DB) error { old, err := m.FindOne(ctx, data.Id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - return db.Save(data).Error + if len(tx) > 0 { + conn = tx[0] + } + return conn.Save(data).Error }, m.getCacheKeys(old)...) return err } -func (m *defaultPaymentModel) Delete(ctx context.Context, id int64) error { +func (m *defaultPaymentModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error { data, err := m.FindOne(ctx, id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -109,8 +114,10 @@ func (m *defaultPaymentModel) Delete(ctx context.Context, id int64) error { return err } err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - return db.Delete(&Payment{}, id).Error + if len(tx) > 0 { + conn = tx[0] + } + return conn.Delete(&Payment{}, id).Error }, m.getCacheKeys(data)...) return err } diff --git a/internal/model/payment/payment.go b/internal/model/payment/payment.go index be00208..46ee0a0 100644 --- a/internal/model/payment/payment.go +++ b/internal/model/payment/payment.go @@ -46,13 +46,19 @@ type StripeConfig struct { Payment string `json:"payment"` } -func (l *StripeConfig) Marshal() string { - b, _ := json.Marshal(l) - return string(b) +func (l *StripeConfig) Marshal() ([]byte, error) { + type Alias StripeConfig + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(l), + }) } -func (l *StripeConfig) Unmarshal(s string) error { - return json.Unmarshal([]byte(s), l) +func (l *StripeConfig) Unmarshal(data []byte) error { + type Alias StripeConfig + aux := (*Alias)(l) + return json.Unmarshal(data, &aux) } type AlipayF2FConfig struct { @@ -63,13 +69,19 @@ type AlipayF2FConfig struct { Sandbox bool `json:"sandbox"` } -func (l *AlipayF2FConfig) Marshal() string { - b, _ := json.Marshal(l) - return string(b) +func (l *AlipayF2FConfig) Marshal() ([]byte, error) { + type Alias AlipayF2FConfig + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(l), + }) } -func (l *AlipayF2FConfig) Unmarshal(s string) error { - return json.Unmarshal([]byte(s), l) +func (l *AlipayF2FConfig) Unmarshal(data []byte) error { + type Alias AlipayF2FConfig + aux := (*Alias)(l) + return json.Unmarshal(data, &aux) } type EPayConfig struct { @@ -78,29 +90,38 @@ type EPayConfig struct { Key string `json:"key"` } -func (l *EPayConfig) Marshal() string { - b, _ := json.Marshal(l) - return string(b) +func (l *EPayConfig) Marshal() ([]byte, error) { + type Alias EPayConfig + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(l), + }) } -func (l *EPayConfig) Unmarshal(s string) error { - return json.Unmarshal([]byte(s), l) +func (l *EPayConfig) Unmarshal(data []byte) error { + type Alias EPayConfig + aux := (*Alias)(l) + return json.Unmarshal(data, &aux) } -type PayssionConfig struct { - PmId string `json:"pm_id"` - ApiKey string `json:"api_key"` +type CryptoSaaSConfig struct { + Endpoint string `json:"endpoint"` + AccountID string `json:"account_id"` SecretKey string `json:"secret_key"` - Currency string `json:"currency"` - QueryUrl string `json:"query_url"` - CreateUrl string `json:"create_url"` } -func (l *PayssionConfig) Marshal() string { - b, _ := json.Marshal(l) - return string(b) +func (l *CryptoSaaSConfig) Marshal() ([]byte, error) { + type Alias CryptoSaaSConfig + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(l), + }) } -func (l *PayssionConfig) Unmarshal(s string) error { - return json.Unmarshal([]byte(s), l) +func (l *CryptoSaaSConfig) Unmarshal(data []byte) error { + type Alias CryptoSaaSConfig + aux := (*Alias)(l) + return json.Unmarshal(data, &aux) } diff --git a/internal/model/server/default.go b/internal/model/server/default.go index 4396d85..b013f1a 100644 --- a/internal/model/server/default.go +++ b/internal/model/server/default.go @@ -5,9 +5,7 @@ import ( "errors" "fmt" - "github.com/perfect-panel/ppanel-server/internal/config" - - "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/perfect-panel/server/pkg/cache" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -23,10 +21,10 @@ type ( customServerLogicModel } serverModel interface { - Insert(ctx context.Context, data *Server) error + Insert(ctx context.Context, data *Server, tx ...*gorm.DB) error FindOne(ctx context.Context, id int64) (*Server, error) - Update(ctx context.Context, data *Server) error - Delete(ctx context.Context, id int64) error + Update(ctx context.Context, data *Server, tx ...*gorm.DB) error + Delete(ctx context.Context, id int64, tx ...*gorm.DB) error Transaction(ctx context.Context, fn func(db *gorm.DB) error) error } @@ -62,23 +60,32 @@ func (m *defaultServerModel) batchGetCacheKeys(Servers ...*Server) []string { return keys } + func (m *defaultServerModel) getCacheKeys(data *Server) []string { if data == nil { return []string{} } detailsKey := fmt.Sprintf("%s%v", CacheServerDetailPrefix, data.Id) ServerIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, data.Id) - configIdKey := fmt.Sprintf("%s%v", config.ServerConfigCacheKey, data.Id) + //configIdKey := fmt.Sprintf("%s%v", config.ServerConfigCacheKey, data.Id) + //userIDKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, data.Id) + + // query protocols to get config keys + cacheKeys := []string{ ServerIdKey, detailsKey, - configIdKey, + //configIdKey, + //userIDKey, } return cacheKeys } -func (m *defaultServerModel) Insert(ctx context.Context, data *Server) error { +func (m *defaultServerModel) Insert(ctx context.Context, data *Server, tx ...*gorm.DB) error { err := m.ExecCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } return conn.Create(&data).Error }, m.getCacheKeys(data)...) return err @@ -98,19 +105,21 @@ func (m *defaultServerModel) FindOne(ctx context.Context, id int64) (*Server, er } } -func (m *defaultServerModel) Update(ctx context.Context, data *Server) error { +func (m *defaultServerModel) Update(ctx context.Context, data *Server, tx ...*gorm.DB) error { old, err := m.FindOne(ctx, data.Id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return err } err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - return db.Save(data).Error + if len(tx) > 0 { + conn = tx[0] + } + return conn.Save(data).Error }, m.getCacheKeys(old)...) return err } -func (m *defaultServerModel) Delete(ctx context.Context, id int64) error { +func (m *defaultServerModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) error { data, err := m.FindOne(ctx, id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -119,8 +128,10 @@ func (m *defaultServerModel) Delete(ctx context.Context, id int64) error { return err } err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - return db.Delete(&Server{}, id).Error + if len(tx) > 0 { + conn = tx[0] + } + return conn.Delete(&Server{}, id).Error }, m.getCacheKeys(data)...) return err } diff --git a/internal/model/server/model.go b/internal/model/server/model.go index 01913fd..58ae7b7 100644 --- a/internal/model/server/model.go +++ b/internal/model/server/model.go @@ -3,8 +3,8 @@ package server import ( "context" "fmt" + "strings" - "github.com/perfect-panel/ppanel-server/internal/config" "gorm.io/gorm" ) @@ -29,6 +29,10 @@ type customServerLogicModel interface { UpdateRuleGroup(ctx context.Context, data *RuleGroup) error DeleteRuleGroup(ctx context.Context, id int64) error QueryAllRuleGroup(ctx context.Context) ([]*RuleGroup, error) + FindServersByTag(ctx context.Context, tag string) ([]*Server, error) + FindServerTags(ctx context.Context) ([]string, error) + + SetDefaultRuleGroup(ctx context.Context, id int64) error } var ( @@ -40,9 +44,10 @@ var ( // ClearCache Clear Cache func (m *customServerModel) ClearCache(ctx context.Context, id int64) error { serverIdKey := fmt.Sprintf("%s%v", cacheServerIdPrefix, id) - configKey := fmt.Sprintf("%s%d", config.ServerConfigCacheKey, id) + //configKey := fmt.Sprintf("%s%d", config.ServerConfigCacheKey, id) + //userListKey := fmt.Sprintf("%s%v", config.ServerUserListCacheKey, id) - return m.DelCacheCtx(ctx, serverIdKey, configKey) + return m.DelCacheCtx(ctx, serverIdKey) } // QueryServerCountByServerGroups Query Server Count By Server Groups @@ -114,13 +119,16 @@ func (m *customServerModel) FindServerDetailByGroupIdsAndIds(ctx context.Context err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { conn = conn. Model(&Server{}). - Where("enable = ?", true) - if len(groupId) > 0 { - conn = conn.Where("group_id IN ?", groupId) - } - if len(ids) > 0 { - conn = conn.Where("id IN ?", ids) + Where("`enable` = ?", true) + if len(groupId) > 0 && len(ids) > 0 { + // OR is used to connect group_id and id conditions + conn = conn.Where("(`group_id` IN ? OR `id` IN ?)", groupId, ids) + } else if len(groupId) > 0 { + conn = conn.Where("`group_id` IN ?", groupId) + } else if len(ids) > 0 { + conn = conn.Where("`id` IN ?", ids) } + return conn.Order("sort ASC").Find(v).Error }) return list, err @@ -227,10 +235,16 @@ func (m *customServerModel) FindServerListByFilter(ctx context.Context, filter * query = conn.Where("group_id = ?", filter.Group) } if filter.Search != "" { - query = query.Where("name LIKE ? OR server_addr LIKE ?", "%"+filter.Search+"%", "%"+filter.Search+"%") + query = query.Where("name LIKE ? OR server_addr LIKE ? OR tags LIKE ?", "%"+filter.Search+"%", "%"+filter.Search+"%", "%"+filter.Search+"%") } - if filter.Tag != "" { - query = query.Where("tag LIKE ?", "%"+filter.Tag+"%") + if len(filter.Tags) > 0 { + for i, tag := range filter.Tags { + if i == 0 { + query = query.Where("tags LIKE ?", "%"+tag+"%") + } else { + query = query.Or("tags LIKE ?", "%"+tag+"%") + } + } } return query.Count(&total).Limit(filter.Size).Offset((filter.Page - 1) * filter.Size).Find(v).Error }) @@ -239,3 +253,40 @@ func (m *customServerModel) FindServerListByFilter(ctx context.Context, filter * } return total, data, nil } + +func (m *customServerModel) FindServerTags(ctx context.Context) ([]string, error) { + var data []string + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Server{}).Distinct("tags").Pluck("tags", v).Error + }) + var tags []string + for _, tag := range data { + if strings.Contains(tag, ",") { + tags = append(tags, strings.Split(tag, ",")...) + } else { + tags = append(tags, tag) + } + } + return tags, err +} + +func (m *customServerModel) FindServersByTag(ctx context.Context, tag string) ([]*Server, error) { + var data []*Server + err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Server{}).Where("FIND_IN_SET(?, tags)", tag).Order("sort ASC").Find(v).Error + }) + return data, err +} + +// SetDefaultRuleGroup sets the default rule group. + +func (m *customServerModel) SetDefaultRuleGroup(ctx context.Context, id int64) error { + return m.ExecCtx(ctx, func(conn *gorm.DB) error { + // Reset all groups to not default + if err := conn.Model(&RuleGroup{}).Where("`id` != ?", id).Update("default", false).Error; err != nil { + return err + } + // Set the specified group as default + return conn.Model(&RuleGroup{}).Where("`id` = ?", id).Update("default", true).Error + }, cacheServerRuleGroupAllKeys, fmt.Sprintf("cache:serverRuleGroup:%v", id)) +} diff --git a/internal/model/server/server.go b/internal/model/server/server.go index 3dbbee5..2da10da 100644 --- a/internal/model/server/server.go +++ b/internal/model/server/server.go @@ -3,26 +3,30 @@ package server import ( "time" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "gorm.io/gorm" ) const ( - RelayModeNone = "none" - RelayModeAll = "all" - RelayModeRandom = "random" + RelayModeNone = "none" + RelayModeAll = "all" + RelayModeRandom = "random" + RuleGroupTypeReject = "reject" + RuleGroupTypeDefault = "default" + RuleGroupTypeDirect = "direct" ) type ServerFilter struct { Id int64 - Tag string + Tags []string Group int64 Search string Page int Size int } +// Deprecated: use internal/model/node/server.go type Server struct { Id int64 `gorm:"primary_key"` Name string `gorm:"type:varchar(100);not null;default:'';comment:Node Name"` @@ -52,33 +56,32 @@ func (*Server) TableName() string { func (s *Server) BeforeDelete(tx *gorm.DB) error { logger.Debugf("[Server] BeforeDelete") - if err := tx.Exec("UPDATE `server` SET sort = sort - 1 WHERE sort > ?", s.Sort).Error; err != nil { return err } - // 删除后重新排序,防止因 sort 缺口导致问题 - if err := reorderSort(tx); err != nil { - return err - } - return nil } func (s *Server) BeforeUpdate(tx *gorm.DB) error { logger.Debugf("[Server] BeforeUpdate") - var count int64 - if err := tx.Model(&Server{}).Where("sort = ? AND id != ?", s.Sort, s.Id).Count(&count).Error; err != nil { + if err := tx.Set("gorm:query_option", "FOR UPDATE").Model(&Server{}). + Where("sort = ? AND id != ?", s.Sort, s.Id).Count(&count).Error; err != nil { return err } - - if count > 0 { - logger.Debugf("[Server] Duplicate sort found, reordering...") + if count > 1 { + // reorder sort if err := reorderSort(tx); err != nil { + logger.Errorf("[Server] BeforeUpdate reorderSort error: %v", err.Error()) return err } + // get max sort + var maxSort int64 + if err := tx.Model(&Server{}).Select("MAX(sort)").Scan(&maxSort).Error; err != nil { + return err + } + s.Sort = maxSort + 1 } - return nil } @@ -136,6 +139,15 @@ type Hysteria2 struct { } type Tuic struct { + Port int `json:"port"` + DisableSNI bool `json:"disable_sni"` + ReduceRtt bool `json:"reduce_rtt"` + UDPRelayMode string `json:"udp_relay_mode"` + CongestionController string `json:"congestion_controller"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +type AnyTLS struct { Port int `json:"port"` SecurityConfig SecurityConfig `json:"security_config"` } @@ -179,9 +191,11 @@ type RuleGroup struct { Id int64 `gorm:"primary_key"` Icon string `gorm:"type:MEDIUMTEXT;comment:Rule Group Icon"` Name string `gorm:"type:varchar(100);not null;default:'';comment:Rule Group Name"` + Type string `gorm:"type:varchar(100);not null;default:'';comment:Rule Group Type"` Tags string `gorm:"type:text;comment:Selected Node Tags"` Rules string `gorm:"type:MEDIUMTEXT;comment:Rules"` Enable bool `gorm:"type:tinyint(1);not null;default:1;comment:Rule Group Enable"` + Default bool `gorm:"type:tinyint(1);not null;default:0;comment:Rule Group is Default"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` } @@ -189,19 +203,14 @@ type RuleGroup struct { func (RuleGroup) TableName() string { return "server_rule_group" } - func reorderSort(tx *gorm.DB) error { - var servers []*Server - if err := tx.Model(&Server{}).Order("sort ASC").Find(&servers).Error; err != nil { + var servers []Server + if err := tx.Order("sort, id").Find(&servers).Error; err != nil { return err } - for i, server := range servers { - newSort := int64(i + 1) - if server.Sort != newSort { - if err := tx.Model(&Server{}). - Where("id = ?", server.Id). - Update("sort", newSort).Error; err != nil { + if server.Sort != int64(i)+1 { + if err := tx.Exec("UPDATE `server` SET sort = ? WHERE id = ?", i+1, server.Id).Error; err != nil { return err } } diff --git a/internal/model/subscribe/default.go b/internal/model/subscribe/default.go index 6832026..29e748c 100644 --- a/internal/model/subscribe/default.go +++ b/internal/model/subscribe/default.go @@ -4,8 +4,11 @@ import ( "context" "errors" "fmt" + "strings" - "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/pkg/cache" + "github.com/perfect-panel/server/pkg/tool" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -57,11 +60,34 @@ func (m *defaultSubscribeModel) getCacheKeys(data *Subscribe) []string { if data == nil { return []string{} } - SubscribeIdKey := fmt.Sprintf("%s%v", cacheSubscribeIdPrefix, data.Id) - cacheKeys := []string{ - SubscribeIdKey, + var keys []string + if data.Nodes != "" { + var nodes []*node.Node + ids := strings.Split(data.Nodes, ",") + + err := m.QueryNoCacheCtx(context.Background(), &nodes, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&node.Node{}).Where("id IN (?)", tool.StringSliceToInt64Slice(ids)).Find(&nodes).Error + }) + if err == nil { + for _, n := range nodes { + keys = append(keys, fmt.Sprintf("%s%d", node.ServerUserListCacheKey, n.ServerId)) + } + } } - return cacheKeys + if data.NodeTags != "" { + var nodes []*node.Node + tags := tool.RemoveDuplicateElements(strings.Split(data.NodeTags, ",")...) + err := m.QueryNoCacheCtx(context.Background(), &nodes, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&node.Node{}).Scopes(InSet("tags", tags)).Find(&nodes).Error + }) + if err == nil { + for _, n := range nodes { + keys = append(keys, fmt.Sprintf("%s%d", node.ServerUserListCacheKey, n.ServerId)) + } + } + } + + return append(keys, fmt.Sprintf("%s%v", cacheSubscribeIdPrefix, data.Id)) } func (m *defaultSubscribeModel) Insert(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error { diff --git a/internal/model/subscribe/model.go b/internal/model/subscribe/model.go index f3b1142..9942046 100644 --- a/internal/model/subscribe/model.go +++ b/internal/model/subscribe/model.go @@ -3,38 +3,37 @@ package subscribe import ( "context" + "github.com/perfect-panel/server/pkg/tool" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) -// type Details struct { -// Id int64 `gorm:"primaryKey"` -// Name string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Name"` -// Description string `gorm:"type:text;comment:Subscribe Description"` -// UnitPrice int64 `gorm:"type:int;not null;default:0;comment:Unit Price"` -// UnitTime string `gorm:"type:varchar(255);not null;default:'';comment:Unit Time"` -// Discount string `gorm:"type:text;comment:Discount"` -// Replacement int64 `gorm:"type:int;not null;default:0;comment:Replacement"` -// Inventory int64 `gorm:"type:int;not null;default:0;comment:Inventory"` -// Traffic int64 `gorm:"type:int;not null;default:0;comment:Traffic"` -// SpeedLimit int64 `gorm:"type:int;not null;default:0;comment:Speed Limit"` -// DeviceLimit int64 `gorm:"type:int;not null;default:0;comment:Device Limit"` -// GroupId int64 `gorm:"type:bigint;comment:Group Id"` -// Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"` -// Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show"` -// Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"` -// DeductionRatio int64 `gorm:"type:int;default:0;comment:Deduction Ratio"` -// PurchaseWithDiscount bool `gorm:"type:tinyint(1);default:0;comment:PurchaseWithDiscount"` -// ResetCycle int64 `gorm:"type:int;default:0;comment:Reset Cycle"` -// RenewalReset bool `gorm:"type:tinyint(1);default:0;comment:Renew Reset"` -// } +type FilterParams struct { + Page int // Page Number + Size int // Page Size + Ids []int64 // Subscribe IDs + Node []int64 // Node IDs + Tags []string // Node Tags + Show bool // Show Portal Page + Sell bool // Sell + Language string // Language + DefaultLanguage bool // Default Subscribe Language Data + Search string // Search Keywords +} + +func (p *FilterParams) Normalize() { + if p.Page <= 0 { + p.Page = 1 + } + if p.Size <= 0 { + p.Size = 10 + } +} + type customSubscribeLogicModel interface { - QuerySubscribeListByPage(ctx context.Context, page, size int, group int64, search string) (total int64, list []*Subscribe, err error) - QuerySubscribeList(ctx context.Context) ([]*Subscribe, error) - QuerySubscribeListByShow(ctx context.Context) ([]*Subscribe, error) - QuerySubscribeIdsByServerIdAndServerGroupId(ctx context.Context, serverId, serverGroupId int64) ([]*Subscribe, error) + FilterList(ctx context.Context, params *FilterParams) (int64, []*Subscribe, error) + ClearCache(ctx context.Context, id ...int64) error QuerySubscribeMinSortByIds(ctx context.Context, ids []int64) (int64, error) - QuerySubscribeListByIds(ctx context.Context, ids []int64) ([]*Subscribe, error) } // NewModel returns a model for the database table. @@ -44,54 +43,6 @@ func NewModel(conn *gorm.DB, c *redis.Client) Model { } } -// QuerySubscribeListByPage Get Subscribe List -func (m *customSubscribeModel) QuerySubscribeListByPage(ctx context.Context, page, size int, group int64, search string) (total int64, list []*Subscribe, err error) { - err = m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - // About to be abandoned - _ = conn.Model(&Subscribe{}). - Where("sort = ?", 0). - Update("sort", gorm.Expr("id")) - - conn = conn.Model(&Subscribe{}) - if group > 0 { - conn = conn.Where("group_id = ?", group) - } - if search != "" { - conn = conn.Where("`name` like ? or `description` like ?", "%"+search+"%", "%"+search+"%") - } - return conn.Count(&total).Order("sort ASC").Limit(size).Offset((page - 1) * size).Find(v).Error - }) - return total, list, err -} - -// QuerySubscribeList Get Subscribe List -func (m *customSubscribeModel) QuerySubscribeList(ctx context.Context) ([]*Subscribe, error) { - var list []*Subscribe - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - conn = conn.Model(&Subscribe{}) - return conn.Where("`sell` = true").Order("sort ").Find(v).Error - }) - return list, err -} - -func (m *customSubscribeModel) QuerySubscribeIdsByServerIdAndServerGroupId(ctx context.Context, serverId, serverGroupId int64) ([]*Subscribe, error) { - var data []*Subscribe - err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Subscribe{}).Where("FIND_IN_SET(?, server)", serverId).Or("FIND_IN_SET(?, server_group)", serverGroupId).Find(v).Error - }) - return data, err -} - -// QuerySubscribeListByShow Get Subscribe List By Show -func (m *customSubscribeModel) QuerySubscribeListByShow(ctx context.Context) ([]*Subscribe, error) { - var list []*Subscribe - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - conn = conn.Model(&Subscribe{}) - return conn.Where("`show` = true").Find(v).Error - }) - return list, err -} - func (m *customSubscribeModel) QuerySubscribeMinSortByIds(ctx context.Context, ids []int64) (int64, error) { var minSort int64 err := m.QueryNoCacheCtx(ctx, &minSort, func(conn *gorm.DB, v interface{}) error { @@ -100,10 +51,106 @@ func (m *customSubscribeModel) QuerySubscribeMinSortByIds(ctx context.Context, i return minSort, err } -func (m *customSubscribeModel) QuerySubscribeListByIds(ctx context.Context, ids []int64) ([]*Subscribe, error) { - var list []*Subscribe - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Subscribe{}).Where("id IN ?", ids).Find(v).Error - }) - return list, err +func (m *customSubscribeModel) ClearCache(ctx context.Context, ids ...int64) error { + if len(ids) <= 0 { + return nil + } + + var cacheKeys []string + for _, id := range ids { + data, err := m.FindOne(ctx, id) + if err != nil { + return err + } + cacheKeys = append(cacheKeys, m.getCacheKeys(data)...) + } + return m.CachedConn.DelCacheCtx(ctx, cacheKeys...) +} + +// FilterList Filter Subscribe List +func (m *customSubscribeModel) FilterList(ctx context.Context, params *FilterParams) (int64, []*Subscribe, error) { + if params == nil { + params = &FilterParams{} + } + params.Normalize() + + var list []*Subscribe + var total int64 + + // 构建查询函数 + buildQuery := func(conn *gorm.DB, lang string) *gorm.DB { + query := conn.Model(&Subscribe{}) + + if params.Search != "" { + s := "%" + params.Search + "%" + query = query.Where("`name` LIKE ? OR `description` LIKE ?", s, s) + } + if params.Show { + query = query.Where("`show` = true") + } + if params.Sell { + query = query.Where("`sell` = true") + } + + if len(params.Ids) > 0 { + query = query.Where("id IN ?", params.Ids) + } + if len(params.Node) > 0 { + query = query.Scopes(InSet("nodes", tool.Int64SliceToStringSlice(params.Node))) + } + + if len(params.Tags) > 0 { + query = query.Scopes(InSet("node_tags", params.Tags)) + } + if lang != "" { + query = query.Where("language = ?", lang) + } else if params.DefaultLanguage { + query = query.Where("language = ''") + } + + return query + } + + // 查询数据 + queryFunc := func(lang string) error { + return m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + query := buildQuery(conn, lang) + if err := query.Count(&total).Error; err != nil { + return err + } + return query.Order("sort ASC"). + Limit(params.Size). + Offset((params.Page - 1) * params.Size). + Find(v).Error + }) + } + + err := queryFunc(params.Language) + if err != nil { + return 0, nil, err + } + + // fallback 默认语言 + if params.DefaultLanguage && total == 0 { + err = queryFunc("") + if err != nil { + return 0, nil, err + } + } + + return total, list, nil +} + +func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB { + return func(db *gorm.DB) *gorm.DB { + if len(values) == 0 { + return db + } + + query := db.Where("1=0") + for _, v := range values { + query = query.Or("FIND_IN_SET(?, "+field+")", v) + } + return query + } } diff --git a/internal/model/subscribe/subscribe.go b/internal/model/subscribe/subscribe.go index 2c705f8..a80ea63 100644 --- a/internal/model/subscribe/subscribe.go +++ b/internal/model/subscribe/subscribe.go @@ -9,6 +9,7 @@ import ( type Subscribe struct { Id int64 `gorm:"primaryKey"` Name string `gorm:"type:varchar(255);not null;default:'';comment:Subscribe Name"` + Language string `gorm:"type:varchar(255);not null;default:'';comment:Language"` Description string `gorm:"type:text;comment:Subscribe Description"` UnitPrice int64 `gorm:"type:int;not null;default:0;comment:Unit Price"` UnitTime string `gorm:"type:varchar(255);not null;default:'';comment:Unit Time"` @@ -19,9 +20,8 @@ type Subscribe struct { SpeedLimit int64 `gorm:"type:int;not null;default:0;comment:Speed Limit"` DeviceLimit int64 `gorm:"type:int;not null;default:0;comment:Device Limit"` Quota int64 `gorm:"type:int;not null;default:0;comment:Quota"` - GroupId int64 `gorm:"type:bigint;comment:Group Id"` - ServerGroup string `gorm:"type:varchar(255);comment:Server Group"` - Server string `gorm:"type:varchar(255);comment:Server"` + Nodes string `gorm:"type:varchar(255);comment:Node Ids"` + NodeTags string `gorm:"type:varchar(255);comment:Node Tags"` Show *bool `gorm:"type:tinyint(1);not null;default:0;comment:Show portal page"` Sell *bool `gorm:"type:tinyint(1);not null;default:0;comment:Sell"` Sort int64 `gorm:"type:int;not null;default:0;comment:Sort"` @@ -48,6 +48,28 @@ func (s *Subscribe) BeforeCreate(tx *gorm.DB) error { return nil } +func (s *Subscribe) BeforeDelete(tx *gorm.DB) error { + if err := tx.Exec("UPDATE `subscribe` SET sort = sort - 1 WHERE sort > ?", s.Sort).Error; err != nil { + return err + } + return nil +} +func (s *Subscribe) BeforeUpdate(tx *gorm.DB) error { + var count int64 + if err := tx.Set("gorm:query_option", "FOR UPDATE").Model(&Subscribe{}). + Where("sort = ? AND id != ?", s.Sort, s.Id).Count(&count).Error; err != nil { + return err + } + if count > 0 { + var maxSort int64 + if err := tx.Model(&Subscribe{}).Select("MAX(sort)").Scan(&maxSort).Error; err != nil { + return err + } + s.Sort = maxSort + 1 + } + return nil +} + type Discount struct { Months int64 `json:"months"` Discount int64 `json:"discount"` diff --git a/internal/model/subscribeType/default.go b/internal/model/subscribeType/default.go deleted file mode 100644 index 788f8f2..0000000 --- a/internal/model/subscribeType/default.go +++ /dev/null @@ -1,117 +0,0 @@ -package subscribeType - -import ( - "context" - "errors" - "fmt" - - "github.com/perfect-panel/ppanel-server/pkg/cache" - "github.com/redis/go-redis/v9" - "gorm.io/gorm" -) - -var _ Model = (*customSubscribeTypeModel)(nil) -var ( - cacheSubscribeTypeIdPrefix = "cache:subscribeType:id:" -) - -type ( - Model interface { - subscribeTypeModel - customSubscribeTypeLogicModel - } - subscribeTypeModel interface { - Insert(ctx context.Context, data *SubscribeType) error - FindOne(ctx context.Context, id int64) (*SubscribeType, error) - Update(ctx context.Context, data *SubscribeType) error - Delete(ctx context.Context, id int64) error - Transaction(ctx context.Context, fn func(db *gorm.DB) error) error - } - - customSubscribeTypeModel struct { - *defaultSubscribeTypeModel - } - defaultSubscribeTypeModel struct { - cache.CachedConn - table string - } -) - -func newSubscribeTypeModel(db *gorm.DB, c *redis.Client) *defaultSubscribeTypeModel { - return &defaultSubscribeTypeModel{ - CachedConn: cache.NewConn(db, c), - table: "`SubscribeType`", - } -} - -//nolint:unused -func (m *defaultSubscribeTypeModel) batchGetCacheKeys(SubscribeTypes ...*SubscribeType) []string { - var keys []string - for _, subscribeType := range SubscribeTypes { - keys = append(keys, m.getCacheKeys(subscribeType)...) - } - return keys - -} -func (m *defaultSubscribeTypeModel) getCacheKeys(data *SubscribeType) []string { - if data == nil { - return []string{} - } - SubscribeTypeIdKey := fmt.Sprintf("%s%v", cacheSubscribeTypeIdPrefix, data.Id) - cacheKeys := []string{ - SubscribeTypeIdKey, - } - return cacheKeys -} - -func (m *defaultSubscribeTypeModel) Insert(ctx context.Context, data *SubscribeType) error { - err := m.ExecCtx(ctx, func(conn *gorm.DB) error { - return conn.Create(&data).Error - }, m.getCacheKeys(data)...) - return err -} - -func (m *defaultSubscribeTypeModel) FindOne(ctx context.Context, id int64) (*SubscribeType, error) { - SubscribeTypeIdKey := fmt.Sprintf("%s%v", cacheSubscribeTypeIdPrefix, id) - var resp SubscribeType - err := m.QueryCtx(ctx, &resp, SubscribeTypeIdKey, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&SubscribeType{}).Where("`id` = ?", id).First(&resp).Error - }) - switch { - case err == nil: - return &resp, nil - default: - return nil, err - } -} - -func (m *defaultSubscribeTypeModel) Update(ctx context.Context, data *SubscribeType) error { - old, err := m.FindOne(ctx, data.Id) - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - return db.Save(data).Error - }, m.getCacheKeys(old)...) - return err -} - -func (m *defaultSubscribeTypeModel) Delete(ctx context.Context, id int64) error { - data, err := m.FindOne(ctx, id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil - } - return err - } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - db := conn - return db.Delete(&SubscribeType{}, id).Error - }, m.getCacheKeys(data)...) - return err -} - -func (m *defaultSubscribeTypeModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { - return m.TransactCtx(ctx, fn) -} diff --git a/internal/model/subscribeType/model.go b/internal/model/subscribeType/model.go deleted file mode 100644 index 52e7e0f..0000000 --- a/internal/model/subscribeType/model.go +++ /dev/null @@ -1,16 +0,0 @@ -package subscribeType - -import ( - "github.com/redis/go-redis/v9" - "gorm.io/gorm" -) - -type customSubscribeTypeLogicModel interface { -} - -// NewModel returns a model for the database table. -func NewModel(conn *gorm.DB, c *redis.Client) Model { - return &customSubscribeTypeModel{ - defaultSubscribeTypeModel: newSubscribeTypeModel(conn, c), - } -} diff --git a/internal/model/subscribeType/subscribeType.go b/internal/model/subscribeType/subscribeType.go deleted file mode 100644 index c0a9d94..0000000 --- a/internal/model/subscribeType/subscribeType.go +++ /dev/null @@ -1,15 +0,0 @@ -package subscribeType - -import "time" - -type SubscribeType struct { - Id int64 `gorm:"primary_key"` - Name string `gorm:"type:varchar(50);default:'';not null;comment:订阅类型"` - Mark string `gorm:"type:varchar(255);default:'';not null;comment:订阅标识"` - CreatedAt time.Time `gorm:"<-:create;comment:创建时间"` - UpdatedAt time.Time `gorm:"comment:更新时间"` -} - -func (SubscribeType) TableName() string { - return "subscribe_type" -} diff --git a/internal/model/system/default.go b/internal/model/system/default.go index 09f53fc..e34013f 100644 --- a/internal/model/system/default.go +++ b/internal/model/system/default.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/perfect-panel/server/pkg/cache" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) diff --git a/internal/model/system/model.go b/internal/model/system/model.go index 1439431..a7c28e1 100644 --- a/internal/model/system/model.go +++ b/internal/model/system/model.go @@ -3,7 +3,7 @@ package system import ( "context" - "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/server/internal/config" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -19,6 +19,7 @@ type customSystemLogicModel interface { GetTosConfig(ctx context.Context) ([]*System, error) GetCurrencyConfig(ctx context.Context) ([]*System, error) GetVerifyCodeConfig(ctx context.Context) ([]*System, error) + GetLogConfig(ctx context.Context) ([]*System, error) UpdateNodeMultiplierConfig(ctx context.Context, config string) error FindNodeMultiplierConfig(ctx context.Context) (*System, error) } @@ -152,3 +153,12 @@ func (m *customSystemModel) GetVerifyCodeConfig(ctx context.Context) ([]*System, }) return configs, err } + +// GetLogConfig returns the log config. +func (m *customSystemModel) GetLogConfig(ctx context.Context) ([]*System, error) { + var configs []*System + err := m.QueryNoCacheCtx(ctx, &configs, func(conn *gorm.DB, v interface{}) error { + return conn.Where("`category` = ?", "log").Find(v).Error + }) + return configs, err +} diff --git a/internal/model/task/task.go b/internal/model/task/task.go new file mode 100644 index 0000000..5c1b987 --- /dev/null +++ b/internal/model/task/task.go @@ -0,0 +1,151 @@ +package task + +import ( + "encoding/json" + "time" +) + +type Type int8 + +const ( + Undefined Type = -1 + TypeEmail = iota + TypeQuota +) + +type Task struct { + Id int64 `gorm:"primaryKey;autoIncrement;comment:ID"` + Type int8 `gorm:"not null;comment:Task Type"` + Scope string `gorm:"type:text;comment:Task Scope"` + Content string `gorm:"type:text;comment:Task Content"` + Status int8 `gorm:"not null;default:0;comment:Task Status: 0: Pending, 1: In Progress, 2: Completed, 3: Failed"` + Errors string `gorm:"type:text;comment:Task Errors"` + Total uint64 `gorm:"column:total;not null;default:0;comment:Total Number"` + Current uint64 `gorm:"column:current;not null;default:0;comment:Current Number"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` +} + +func (Task) TableName() string { + return "task" +} + +type ScopeType int8 + +const ( + ScopeAll ScopeType = iota + 1 // All users + ScopeActive // Active users + ScopeExpired // Expired users + ScopeNone // No Subscribe + ScopeSkip // Skip user filtering +) + +func (t ScopeType) Int8() int8 { + return int8(t) +} + +type EmailScope struct { + Type int8 `gorm:"not null;comment:Scope Type"` + RegisterStartTime int64 `json:"register_start_time"` + RegisterEndTime int64 `json:"register_end_time"` + Recipients []string `json:"recipients"` // list of email addresses + Additional []string `json:"additional"` // additional email addresses + Scheduled int64 `json:"scheduled"` // scheduled time (unix timestamp) + Interval uint8 `json:"interval"` // interval in seconds + Limit uint64 `json:"limit"` // daily send limit +} + +func (s *EmailScope) Marshal() ([]byte, error) { + type Alias EmailScope + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) +} + +func (s *EmailScope) Unmarshal(data []byte) error { + type Alias EmailScope + aux := (*Alias)(s) + return json.Unmarshal(data, &aux) +} + +type EmailContent struct { + Subject string `json:"subject"` + Content string `json:"content"` +} + +func (c *EmailContent) Marshal() ([]byte, error) { + type Alias EmailContent + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(c), + }) +} + +func (c *EmailContent) Unmarshal(data []byte) error { + type Alias EmailContent + aux := (*Alias)(c) + return json.Unmarshal(data, &aux) +} + +type QuotaScope struct { + Subscribers []int64 `json:"subscribers"` // Subscribe IDs + IsActive *bool `json:"is_active"` // filter by active status + StartTime int64 `json:"start_time"` // filter by subscription start time + EndTime int64 `json:"end_time"` // filter by subscription end time + Objects []int64 `json:"recipients"` // list of user subs IDs +} + +func (s *QuotaScope) Marshal() ([]byte, error) { + type Alias QuotaScope + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(s), + }) +} + +func (s *QuotaScope) Unmarshal(data []byte) error { + type Alias QuotaScope + aux := (*Alias)(s) + return json.Unmarshal(data, &aux) +} + +type QuotaContent struct { + ResetTraffic bool `json:"reset_traffic"` // whether to reset traffic + Days uint64 `json:"days,omitempty"` // days to add + GiftType uint8 `json:"gift_type,omitempty"` // 1: Fixed, 2: Ratio + GiftValue uint64 `json:"gift_value,omitempty"` // value of the gift type +} + +func (c *QuotaContent) Marshal() ([]byte, error) { + type Alias QuotaContent + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(c), + }) +} + +func (c *QuotaContent) Unmarshal(data []byte) error { + type Alias QuotaContent + aux := (*Alias)(c) + return json.Unmarshal(data, &aux) +} + +func ParseScopeType(t int8) ScopeType { + switch t { + case 1: + return ScopeAll + case 2: + return ScopeActive + case 3: + return ScopeExpired + case 4: + return ScopeNone + default: + return ScopeSkip + } +} diff --git a/internal/model/ticket/default.go b/internal/model/ticket/default.go index d52a6df..10db8fe 100644 --- a/internal/model/ticket/default.go +++ b/internal/model/ticket/default.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/perfect-panel/server/pkg/cache" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) diff --git a/internal/model/user/authMethod.go b/internal/model/user/authMethod.go index 07faabd..18ce951 100644 --- a/internal/model/user/authMethod.go +++ b/internal/model/user/authMethod.go @@ -3,6 +3,7 @@ package user import ( "context" + "github.com/perfect-panel/server/pkg/logger" "gorm.io/gorm" ) @@ -31,24 +32,50 @@ func (m *defaultUserModel) FindUserAuthMethodByPlatform(ctx context.Context, use } func (m *defaultUserModel) InsertUserAuthMethods(ctx context.Context, data *AuthMethods, tx ...*gorm.DB) error { + u, err := m.FindOne(ctx, data.UserId) + if err != nil { + return err + } + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } - return conn.Model(&AuthMethods{}).Create(data).Error + if err = conn.Model(&AuthMethods{}).Create(data).Error; err != nil { + return err + } + return m.ClearUserCache(ctx, u) }) } func (m *defaultUserModel) UpdateUserAuthMethods(ctx context.Context, data *AuthMethods, tx ...*gorm.DB) error { + u, err := m.FindOne(ctx, data.UserId) + if err != nil { + return err + } + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } - return conn.Model(&AuthMethods{}).Where("user_id = ? AND auth_type = ?", data.UserId, data.AuthType).Save(data).Error + err = conn.Model(&AuthMethods{}).Where("user_id = ? AND auth_type = ?", data.UserId, data.AuthType).Save(data).Error + if err != nil { + return err + } + return m.ClearUserCache(ctx, u) }) } func (m *defaultUserModel) DeleteUserAuthMethods(ctx context.Context, userId int64, platform string, tx ...*gorm.DB) error { + u, err := m.FindOne(ctx, userId) + if err != nil { + return err + } + defer func() { + if err = m.ClearUserCache(context.Background(), u); err != nil { + logger.Errorf("[UserModel] clear user cache failed: %v", err.Error()) + } + }() return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] diff --git a/internal/model/user/cache.go b/internal/model/user/cache.go new file mode 100644 index 0000000..a39f748 --- /dev/null +++ b/internal/model/user/cache.go @@ -0,0 +1,285 @@ +package user + +import ( + "context" + "fmt" + + "github.com/perfect-panel/server/pkg/logger" +) + +type CacheKeyGenerator interface { + GetCacheKeys() []string +} + +type CacheManager interface { + ClearCache(ctx context.Context, keys ...string) error + ClearModelCache(ctx context.Context, models ...CacheKeyGenerator) error +} + +type UserCacheManager struct { + model *defaultUserModel +} + +func NewUserCacheManager(model *defaultUserModel) *UserCacheManager { + return &UserCacheManager{ + model: model, + } +} + +func (c *UserCacheManager) ClearCache(ctx context.Context, keys ...string) error { + if len(keys) == 0 { + return nil + } + return c.model.CachedConn.DelCacheCtx(ctx, keys...) +} + +func (c *UserCacheManager) ClearModelCache(ctx context.Context, models ...CacheKeyGenerator) error { + var allKeys []string + for _, model := range models { + if model != nil { + allKeys = append(allKeys, model.GetCacheKeys()...) + } + } + return c.ClearCache(ctx, allKeys...) +} + +func (u *User) GetCacheKeys() []string { + if u == nil { + return []string{} + } + keys := []string{ + fmt.Sprintf("%s%d", cacheUserIdPrefix, u.Id), + } + + for _, auth := range u.AuthMethods { + if auth.AuthType == "email" { + keys = append(keys, fmt.Sprintf("%s%s", cacheUserEmailPrefix, auth.AuthIdentifier)) + break + } + } + return keys +} + +func (s *Subscribe) GetCacheKeys() []string { + if s == nil { + return []string{} + } + keys := make([]string, 0) + + if s.Token != "" { + keys = append(keys, fmt.Sprintf("%s%s", cacheUserSubscribeTokenPrefix, s.Token)) + } + if s.UserId != 0 { + keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, s.UserId)) + } + if s.Id != 0 { + keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeIdPrefix, s.Id)) + } + return keys +} + +func (s *Subscribe) GetExtendedCacheKeys(model *defaultUserModel) []string { + keys := s.GetCacheKeys() + + if s.SubscribeId != 0 && model != nil { + serverKeys := model.getServerRelatedCacheKeys(s.SubscribeId) + keys = append(keys, serverKeys...) + } + + return keys +} + +func (d *Device) GetCacheKeys() []string { + if d == nil { + return []string{} + } + keys := []string{} + + if d.Id != 0 { + keys = append(keys, fmt.Sprintf("%s%d", cacheUserDeviceIdPrefix, d.Id)) + } + if d.Identifier != "" { + keys = append(keys, fmt.Sprintf("%s%s", cacheUserDeviceNumberPrefix, d.Identifier)) + } + return keys +} + +func (a *AuthMethods) GetCacheKeys() []string { + if a == nil { + return []string{} + } + keys := []string{} + + if a.UserId != 0 { + keys = append(keys, fmt.Sprintf("%s%d", cacheUserIdPrefix, a.UserId)) + } + if a.AuthType == "email" && a.AuthIdentifier != "" { + keys = append(keys, fmt.Sprintf("%s%s", cacheUserEmailPrefix, a.AuthIdentifier)) + } + return keys +} + +func (m *defaultUserModel) GetCacheManager() *UserCacheManager { + return NewUserCacheManager(m) +} + +func (m *defaultUserModel) getServerRelatedCacheKeys(subscribeId int64) []string { + // 这里复用了 model.go 中的逻辑,但简化了实现 + keys := []string{} + + if subscribeId == 0 { + return keys + } + + // 这里需要从 getSubscribeCacheKey 方法中提取服务器相关的逻辑 + // 为了避免重复查询,我们可以在需要时才获取 + // 或者可以将这个逻辑移到一个统一的地方 + + return keys +} + +func (m *defaultUserModel) ClearUserCache(ctx context.Context, users ...*User) error { + cacheManager := m.GetCacheManager() + models := make([]CacheKeyGenerator, len(users)) + for i, user := range users { + models[i] = user + } + return cacheManager.ClearModelCache(ctx, models...) +} + +func (m *defaultUserModel) ClearSubscribeCacheByModels(ctx context.Context, subscribes ...*Subscribe) error { + cacheManager := m.GetCacheManager() + models := make([]CacheKeyGenerator, len(subscribes)) + for i, subscribe := range subscribes { + models[i] = subscribe + } + return cacheManager.ClearModelCache(ctx, models...) +} + +func (m *defaultUserModel) ClearDeviceCache(ctx context.Context, devices ...*Device) error { + cacheManager := m.GetCacheManager() + models := make([]CacheKeyGenerator, len(devices)) + for i, device := range devices { + models[i] = device + } + return cacheManager.ClearModelCache(ctx, models...) +} + +func (m *defaultUserModel) ClearAuthMethodCache(ctx context.Context, authMethods ...*AuthMethods) error { + cacheManager := m.GetCacheManager() + models := make([]CacheKeyGenerator, len(authMethods)) + for i, auth := range authMethods { + models[i] = auth + } + return cacheManager.ClearModelCache(ctx, models...) +} + +func (m *defaultUserModel) BatchClearRelatedCache(ctx context.Context, user *User) error { + if user == nil { + return nil + } + + cacheManager := m.GetCacheManager() + + var allModels []CacheKeyGenerator + allModels = append(allModels, user) + + for _, auth := range user.AuthMethods { + allModels = append(allModels, &auth) + } + + for _, device := range user.UserDevices { + allModels = append(allModels, &device) + } + + subscribes, err := m.QueryUserSubscribe(ctx, user.Id) + if err != nil { + logger.Errorf("failed to query user subscribes for cache clearing: %v", err) + } else { + for _, sub := range subscribes { + subModel := &Subscribe{ + Id: sub.Id, + UserId: sub.UserId, + Token: sub.Token, + SubscribeId: sub.SubscribeId, + } + allModels = append(allModels, subModel) + } + } + + return cacheManager.ClearModelCache(ctx, allModels...) +} + +func (m *defaultUserModel) CacheInvalidationHandler(ctx context.Context, operation string, modelType string, model interface{}) error { + switch operation { + case "create", "update", "delete": + switch modelType { + case "user": + if user, ok := model.(*User); ok { + return m.BatchClearRelatedCache(ctx, user) + } + case "subscribe": + if subscribe, ok := model.(*Subscribe); ok { + return m.ClearSubscribeCacheByModels(ctx, subscribe) + } + case "device": + if device, ok := model.(*Device); ok { + return m.ClearDeviceCache(ctx, device) + } + case "authmethod": + if authMethod, ok := model.(*AuthMethods); ok { + return m.ClearAuthMethodCache(ctx, authMethod) + } + } + } + return nil +} + +func (m *customUserModel) GetRelatedCacheKeys(ctx context.Context, modelType string, modelId int64) ([]string, error) { + var keys []string + + switch modelType { + case "user": + user, err := m.FindOne(ctx, modelId) + if err != nil { + return nil, err + } + keys = append(keys, user.GetCacheKeys()...) + + auths, err := m.FindUserAuthMethods(ctx, modelId) + if err == nil { + for _, auth := range auths { + keys = append(keys, auth.GetCacheKeys()...) + } + } + + subscribes, err := m.QueryUserSubscribe(ctx, modelId) + if err == nil { + for _, sub := range subscribes { + subModel := &Subscribe{ + Id: sub.Id, + UserId: sub.UserId, + Token: sub.Token, + SubscribeId: sub.SubscribeId, + } + keys = append(keys, subModel.GetCacheKeys()...) + } + } + + case "subscribe": + subscribe, err := m.FindOneSubscribe(ctx, modelId) + if err != nil { + return nil, err + } + keys = append(keys, subscribe.GetCacheKeys()...) + + case "device": + device, err := m.FindOneDevice(ctx, modelId) + if err != nil { + return nil, err + } + keys = append(keys, device.GetCacheKeys()...) + } + + return keys, nil +} diff --git a/internal/model/user/default.go b/internal/model/user/default.go index 07ebc16..e2a326a 100644 --- a/internal/model/user/default.go +++ b/internal/model/user/default.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/cache" + "github.com/perfect-panel/server/pkg/cache" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -48,29 +48,20 @@ func newUserModel(db *gorm.DB, c *redis.Client) *defaultUserModel { func (m *defaultUserModel) batchGetCacheKeys(users ...*User) []string { var keys []string for _, user := range users { - keys = append(keys, m.getCacheKeys(user)...) + keys = append(keys, user.GetCacheKeys()...) } return keys - } + func (m *defaultUserModel) getCacheKeys(data *User) []string { if data == nil { return []string{} } - userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id) - cacheKeys := []string{ - userIdKey, - } - // email key - if len(data.AuthMethods) > 0 { - for _, auth := range data.AuthMethods { - if auth.AuthType == "email" { - cacheKeys = append(cacheKeys, fmt.Sprintf("%s%v", cacheUserEmailPrefix, auth.AuthIdentifier)) - break - } - } - } - return cacheKeys + return data.GetCacheKeys() +} + +func (m *defaultUserModel) clearUserCache(ctx context.Context, data ...*User) error { + return m.ClearUserCache(ctx, data...) } func (m *defaultUserModel) FindOneByEmail(ctx context.Context, email string) (*User, error) { @@ -127,53 +118,38 @@ func (m *defaultUserModel) Delete(ctx context.Context, id int64, tx ...*gorm.DB) } return err } - err = m.ExecCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] + + // 使用批量相关缓存清理,包含所有相关数据的缓存 + defer func() { + if clearErr := m.BatchClearRelatedCache(ctx, data); clearErr != nil { + // 记录清理缓存错误,但不阻断删除操作 } - return conn.Transaction(func(db *gorm.DB) error { - if err := db.Model(&User{}).Where("`id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&AuthMethods{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&Subscribe{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&BalanceLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&GiftAmountLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&LoginLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&SubscribeLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - if err := db.Model(&Device{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } + }() - subs, err := m.QueryUserSubscribe(ctx, id) - if err != nil { - return err - } - for _, sub := range subs { - if err := m.DeleteSubscribeById(ctx, sub.Id, db); err != nil { - return err - } - } + return m.TransactCtx(ctx, func(db *gorm.DB) error { + if len(tx) > 0 { + db = tx[0] + } - if err := db.Model(&CommissionLog{}).Where("`user_id` = ?", id).Delete(&User{}).Error; err != nil { - return err - } - return nil - }) - }, m.getCacheKeys(data)...) - return err + // 删除用户相关的所有数据 + if err := db.Model(&User{}).Where("`id` = ?", id).Delete(&User{}).Error; err != nil { + return err + } + + if err := db.Model(&AuthMethods{}).Where("`user_id` = ?", id).Delete(&AuthMethods{}).Error; err != nil { + return err + } + + if err := db.Model(&Subscribe{}).Where("`user_id` = ?", id).Delete(&Subscribe{}).Error; err != nil { + return err + } + + if err := db.Model(&Device{}).Where("`user_id` = ?", id).Delete(&Device{}).Error; err != nil { + return err + } + + return nil + }) } func (m *defaultUserModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error { diff --git a/internal/model/user/device.go b/internal/model/user/device.go index 7258819..f823991 100644 --- a/internal/model/user/device.go +++ b/internal/model/user/device.go @@ -46,18 +46,27 @@ func (m *customUserModel) QueryDevicePageList(ctx context.Context, userId, subsc return list, total, err } +// QueryDeviceList returns a list of records that meet the conditions. +func (m *customUserModel) QueryDeviceList(ctx context.Context, userId int64) ([]*Device, int64, error) { + var list []*Device + var total int64 + err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { + return conn.Model(&Device{}).Where("`user_id` = ? and `subscribe_id` = ?", userId).Count(&total).Find(&list).Error + }) + return list, total, err +} + func (m *customUserModel) UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error { old, err := m.FindOneDevice(ctx, data.Id) if err != nil { return err } - deviceIdKey := fmt.Sprintf("%s%v", cacheUserDeviceIdPrefix, old.Id) err = m.ExecCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } return conn.Save(data).Error - }, deviceIdKey) + }, old.GetCacheKeys()...) return err } @@ -69,12 +78,26 @@ func (m *customUserModel) DeleteDevice(ctx context.Context, id int64, tx ...*gor } return err } - deviceIdKey := fmt.Sprintf("%s%v", cacheUserDeviceIdPrefix, data.Id) err = m.ExecCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } return conn.Delete(&Device{}, id).Error - }, deviceIdKey) + }, data.GetCacheKeys()...) return err } + +func (m *customUserModel) InsertDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error { + defer func() { + if clearErr := m.ClearDeviceCache(ctx, data); clearErr != nil { + // log cache clear error + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { + if len(tx) > 0 { + conn = tx[0] + } + return conn.Create(data).Error + }) +} diff --git a/internal/model/user/log.go b/internal/model/user/log.go deleted file mode 100644 index d3e1105..0000000 --- a/internal/model/user/log.go +++ /dev/null @@ -1,81 +0,0 @@ -package user - -import ( - "context" - - "github.com/pkg/errors" - "gorm.io/gorm" -) - -func (m *customUserModel) InsertSubscribeLog(ctx context.Context, log *SubscribeLog) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - return conn.Create(log).Error - }) -} - -func (m *customUserModel) FilterSubscribeLogList(ctx context.Context, page, size int, filter *SubscribeLogFilterParams) ([]*SubscribeLog, int64, error) { - var list []*SubscribeLog - var total int64 - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - query := conn.Model(&SubscribeLog{}) - if filter != nil { - if filter.UserId != 0 { - query = query.Where("user_id = ?", filter.UserId) - } - if filter.UserSubscribeId != 0 { - query = query.Where("user_subscribe_id = ?", filter.UserSubscribeId) - } - if filter.IP != "" { - query = query.Where("ip LIKE ?", "%"+filter.IP+"%") - } - if filter.Token != "" { - query = query.Where("token LIKE ?", "%"+filter.Token+"%") - } - if filter.UserAgent != "" { - query = query.Where("user_agent LIKE ?", "%"+filter.UserAgent+"%") - } - } - return query.Count(&total).Limit(size).Offset((page - 1) * size).Find(v).Error - }) - - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, 0, err - } - - return list, total, nil -} - -func (m *customUserModel) InsertLoginLog(ctx context.Context, log *LoginLog) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - return conn.Create(log).Error - }) -} - -func (m *customUserModel) FilterLoginLogList(ctx context.Context, page, size int, filter *LoginLogFilterParams) ([]*LoginLog, int64, error) { - var list []*LoginLog - var total int64 - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - query := conn.Model(&LoginLog{}) - if filter != nil { - if filter.UserId != 0 { - query = query.Where("user_id = ?", filter.UserId) - } - if filter.IP != "" { - query = query.Where("ip LIKE ?", "%"+filter.IP+"%") - } - if filter.UserAgent != "" { - query = query.Where("user_agent LIKE ?", "%"+filter.UserAgent+"%") - } - if filter.Success != nil { - query = query.Where("success = ?", *filter.Success) - } - } - return query.Count(&total).Limit(size).Offset((page - 1) * size).Find(v).Error - }) - - if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, 0, err - } - - return list, total, nil -} diff --git a/internal/model/user/model.go b/internal/model/user/model.go index 3175331..ffc5020 100644 --- a/internal/model/user/model.go +++ b/internal/model/user/model.go @@ -2,15 +2,12 @@ package user import ( "context" - "errors" "fmt" "time" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/internal/model/subscribe" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/redis/go-redis/v9" "gorm.io/gorm" ) @@ -32,6 +29,7 @@ type SubscribeDetails struct { Subscribe *subscribe.Subscribe `gorm:"foreignKey:SubscribeId;references:Id"` StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"` ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"` + FinishedAt *time.Time `gorm:"default:NULL;comment:Finished Time"` Traffic int64 `gorm:"default:0;comment:Traffic"` Download int64 `gorm:"default:0;comment:Download Traffic"` Upload int64 `gorm:"default:0;comment:Upload Traffic"` @@ -62,6 +60,7 @@ type UserFilterParams struct { UserId *int64 SubscribeId *int64 UserSubscribeId *int64 + Order string // Order by id, e.g., "desc" } type customUserLogicModel interface { @@ -78,7 +77,6 @@ type customUserLogicModel interface { QueryUserSubscribe(ctx context.Context, userId int64, status ...int64) ([]*SubscribeDetails, error) FindOneSubscribeDetailsById(ctx context.Context, id int64) (*SubscribeDetails, error) FindOneUserSubscribe(ctx context.Context, id int64) (*SubscribeDetails, error) - InsertBalanceLog(ctx context.Context, data *BalanceLog, tx ...*gorm.DB) error FindUsersSubscribeBySubscribeId(ctx context.Context, subscribeId int64) ([]*Subscribe, error) UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, tx ...*gorm.DB) error QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error) @@ -87,7 +85,6 @@ type customUserLogicModel interface { QueryAdminUsers(ctx context.Context) ([]*User, error) UpdateUserCache(ctx context.Context, data *User) error UpdateUserSubscribeCache(ctx context.Context, data *Subscribe) error - InsertCommissionLog(ctx context.Context, data *CommissionLog, tx ...*gorm.DB) error QueryActiveSubscriptions(ctx context.Context, subscribeId ...int64) (map[int64]int64, error) FindUserAuthMethods(ctx context.Context, userId int64) ([]*AuthMethods, error) InsertUserAuthMethods(ctx context.Context, data *AuthMethods, tx ...*gorm.DB) error @@ -98,23 +95,25 @@ type customUserLogicModel interface { FindUserAuthMethodByPlatform(ctx context.Context, userId int64, platform string) (*AuthMethods, error) FindOneByEmail(ctx context.Context, email string) (*User, error) FindOneDevice(ctx context.Context, id int64) (*Device, error) + QueryDeviceList(ctx context.Context, userid int64) ([]*Device, int64, error) QueryDevicePageList(ctx context.Context, userid, subscribeId int64, page, size int) ([]*Device, int64, error) UpdateDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error FindOneDeviceByIdentifier(ctx context.Context, id string) (*Device, error) DeleteDevice(ctx context.Context, id int64, tx ...*gorm.DB) error - - InsertSubscribeLog(ctx context.Context, log *SubscribeLog) error - FilterSubscribeLogList(ctx context.Context, page, size int, filter *SubscribeLogFilterParams) ([]*SubscribeLog, int64, error) - InsertLoginLog(ctx context.Context, log *LoginLog) error - FilterLoginLogList(ctx context.Context, page, size int, filter *LoginLogFilterParams) ([]*LoginLog, int64, error) + InsertDevice(ctx context.Context, data *Device, tx ...*gorm.DB) error ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error + ClearUserCache(ctx context.Context, data ...*User) error - InsertResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error - UpdateResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error - FindResetSubscribeLog(ctx context.Context, id int64) (*ResetSubscribeLog, error) - DeleteResetSubscribeLog(ctx context.Context, id int64, tx ...*gorm.DB) error - FilterResetSubscribeLogList(ctx context.Context, filter *FilterResetSubscribeLogParams) ([]*ResetSubscribeLog, int64, error) + QueryDailyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error) + QueryMonthlyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error) +} + +type UserStatisticsWithDate struct { + Date string + Register int64 + NewOrderUsers int64 + RenewalOrderUsers int64 } // NewModel returns a model for the database table. @@ -124,56 +123,6 @@ func NewModel(conn *gorm.DB, c *redis.Client) Model { } } -func (m *defaultUserModel) getSubscribeCacheKey(data *Subscribe) []string { - if data == nil { - return []string{} - } - var keys []string - if data.Token != "" { - keys = append(keys, fmt.Sprintf("%s%s", cacheUserSubscribeTokenPrefix, data.Token)) - } - if data.UserId != 0 { - keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, data.UserId)) - } - if data.Id != 0 { - keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeIdPrefix, data.Id)) - } - - if data.SubscribeId != 0 { - var sub *subscribe.Subscribe - err := m.QueryNoCacheCtx(context.Background(), &sub, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&subscribe.Subscribe{}).Where("id = ?", data.SubscribeId).First(&sub).Error - }) - if err != nil { - logger.Error("getUserSubscribeCacheKey", logger.Field("error", err.Error()), logger.Field("subscribeId", data.SubscribeId)) - return keys - } - if sub.Server != "" { - ids := tool.StringToInt64Slice(sub.Server) - for _, id := range ids { - keys = append(keys, fmt.Sprintf("%s%d", config.ServerUserListCacheKey, id)) - } - } - if sub.ServerGroup != "" { - ids := tool.StringToInt64Slice(sub.ServerGroup) - var servers []*server.Server - err = m.QueryNoCacheCtx(context.Background(), &servers, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&server.Server{}).Where("group_id in ?", ids).Find(v).Error - }) - if err != nil { - logger.Error("getUserSubscribeCacheKey", logger.Field("error", err.Error()), logger.Field("subscribeId", data.SubscribeId)) - return keys - } - for _, s := range servers { - keys = append(keys, fmt.Sprintf("%s%d", config.ServerUserListCacheKey, s.Id)) - } - } - } - - return keys - -} - // QueryPageList returns a list of records that meet the conditions. func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, filter *UserFilterParams) ([]*User, int64, error) { var list []*User @@ -195,6 +144,9 @@ func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, fil conn = conn.Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id"). Where("user_subscribe.subscribe_id =? and `status` IN (0,1)", *filter.SubscribeId) } + if filter.Order != "" { + conn = conn.Order(fmt.Sprintf("user.id %s", filter.Order)) + } } return conn.Model(&User{}).Group("user.id").Count(&total).Limit(size).Offset((page - 1) * size).Preload("UserDevices").Preload("AuthMethods").Find(&list).Error }) @@ -218,33 +170,20 @@ func (m *customUserModel) BatchDeleteUser(ctx context.Context, ids []int64, tx . }, m.batchGetCacheKeys(users...)...) } -// InsertBalanceLog insert BalanceLog into the database. -func (m *customUserModel) InsertBalanceLog(ctx context.Context, data *BalanceLog, tx ...*gorm.DB) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] - } - return conn.Create(data).Error - }) -} - -// FindUserBalanceLogList returns a list of records that meet the conditions. -func (m *customUserModel) FindUserBalanceLogList(ctx context.Context, userId int64, page, size int) ([]*BalanceLog, int64, error) { - var list []*BalanceLog - var total int64 - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - - return conn.Model(&BalanceLog{}).Where("`user_id` = ?", userId).Count(&total).Limit(size).Offset((page - 1) * size).Find(&list).Error - }) - return list, total, err -} - func (m *customUserModel) UpdateUserSubscribeWithTraffic(ctx context.Context, id, download, upload int64, tx ...*gorm.DB) error { sub, err := m.FindOneSubscribe(ctx, id) if err != nil { return err } - return m.ExecCtx(ctx, func(conn *gorm.DB) error { + + // 使用 defer 确保更新后清理缓存 + defer func() { + if clearErr := m.ClearSubscribeCacheByModels(ctx, sub); clearErr != nil { + // 记录清理缓存错误 + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } @@ -252,7 +191,7 @@ func (m *customUserModel) UpdateUserSubscribeWithTraffic(ctx context.Context, id "download": gorm.Expr("download + ?", download), "upload": gorm.Expr("upload + ?", upload), }).Error - }, m.getSubscribeCacheKey(sub)...) + }) } func (m *customUserModel) QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error) { @@ -292,16 +231,7 @@ func (m *customUserModel) QueryAdminUsers(ctx context.Context) ([]*User, error) } func (m *customUserModel) UpdateUserCache(ctx context.Context, data *User) error { - return m.CachedConn.DelCacheCtx(ctx, m.getCacheKeys(data)...) -} - -func (m *customUserModel) InsertCommissionLog(ctx context.Context, data *CommissionLog, tx ...*gorm.DB) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] - } - return conn.Model(&CommissionLog{}).Create(data).Error - }) + return m.ClearUserCache(ctx, data) } func (m *customUserModel) FindOneByReferCode(ctx context.Context, referCode string) (*User, error) { @@ -320,81 +250,77 @@ func (m *customUserModel) FindOneSubscribeDetailsById(ctx context.Context, id in return &data, err } -func (m *customUserModel) InsertResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] - } - return conn.Model(&ResetSubscribeLog{}).Create(log).Error - }) -} +// QueryDailyUserStatisticsList Query daily user statistics list for the current month (from 1st to current date) +func (m *customUserModel) QueryDailyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error) { + var results []UserStatisticsWithDate -func (m *customUserModel) UpdateResetSubscribeLog(ctx context.Context, log *ResetSubscribeLog, tx ...*gorm.DB) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] - } - return conn.Model(&ResetSubscribeLog{}).Where("id = ?", log.Id).Updates(log).Error - }) -} + err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error { + firstDay := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location()) -func (m *customUserModel) FindResetSubscribeLog(ctx context.Context, id int64) (*ResetSubscribeLog, error) { - var data ResetSubscribeLog - err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&ResetSubscribeLog{}).Where("id = ?", id).First(&data).Error - }) - return &data, err -} + // 子查询:统计每天的新用户订单数量 + newOrderSub := conn.Model(&order.Order{}). + Select("DATE_FORMAT(created_at, '%Y-%m-%d') AS date, COUNT(DISTINCT user_id) AS new_order_users"). + Where("is_new = 1 AND created_at BETWEEN ? AND ? AND status IN ?", firstDay, date, []int64{2, 5}). + Group("DATE_FORMAT(created_at, '%Y-%m-%d')") -func (m *customUserModel) DeleteResetSubscribeLog(ctx context.Context, id int64, tx ...*gorm.DB) error { - return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { - if len(tx) > 0 { - conn = tx[0] - } - return conn.Model(&ResetSubscribeLog{}).Where("id = ?", id).Delete(&ResetSubscribeLog{}).Error - }) -} + // 子查询:统计每天的续费订单数量 + renewalOrderSub := conn.Model(&order.Order{}). + Select("DATE_FORMAT(created_at, '%Y-%m-%d') AS date, COUNT(DISTINCT user_id) AS renewal_order_users"). + Where("is_new = 0 AND created_at BETWEEN ? AND ? AND status IN ?", firstDay, date, []int64{2, 5}). + Group("DATE_FORMAT(created_at, '%Y-%m-%d')") -func (m *customUserModel) FilterResetSubscribeLogList(ctx context.Context, filter *FilterResetSubscribeLogParams) ([]*ResetSubscribeLog, int64, error) { - if filter == nil { - return nil, 0, errors.New("filter params is nil") - } - - var list []*ResetSubscribeLog - var total int64 - - err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { - query := conn.Model(&ResetSubscribeLog{}) - - // 应用筛选条件 - if filter.UserId != 0 { - query = query.Where("user_id = ?", filter.UserId) - } - if filter.UserSubscribeId != 0 { - query = query.Where("user_subscribe_id = ?", filter.UserSubscribeId) - } - if filter.Type != 0 { - query = query.Where("type = ?", filter.Type) - } - if filter.OrderNo != "" { - query = query.Where("order_no = ?", filter.OrderNo) - } - - // 计算总数 - if err := query.Count(&total).Error; err != nil { - return err - } - - // 应用分页 - if filter.Page > 0 && filter.Size > 0 { - query = query.Offset((filter.Page - 1) * filter.Size) - } - if filter.Size > 0 { - query = query.Limit(filter.Size) - } - - return query.Find(&list).Error + return conn.Model(&User{}). + Select(` + DATE_FORMAT(user.created_at, '%Y-%m-%d') AS date, + COUNT(*) AS register, + IFNULL(MAX(n.new_order_users), 0) AS new_order_users, + IFNULL(MAX(r.renewal_order_users), 0) AS renewal_order_users + `). + Joins("LEFT JOIN (?) AS n ON DATE_FORMAT(user.created_at, '%Y-%m-%d') = n.date", newOrderSub). + Joins("LEFT JOIN (?) AS r ON DATE_FORMAT(user.created_at, '%Y-%m-%d') = r.date", renewalOrderSub). + Where("user.created_at BETWEEN ? AND ?", firstDay, date). + Group("DATE_FORMAT(user.created_at, '%Y-%m-%d')"). + Order("date ASC"). + Scan(v).Error }) - return list, total, err + return results, err +} + +// QueryMonthlyUserStatisticsList Query monthly user statistics list for the past 6 months +func (m *customUserModel) QueryMonthlyUserStatisticsList(ctx context.Context, date time.Time) ([]UserStatisticsWithDate, error) { + var results []UserStatisticsWithDate + + err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error { + // 获取 6 个月前的日期 + sixMonthsAgo := date.AddDate(0, -5, 0) + + // 子查询:每月新订单用户数量 + newOrderSub := conn.Model(&order.Order{}). + Select("DATE_FORMAT(created_at, '%Y-%m') AS date, COUNT(DISTINCT user_id) AS new_order_users"). + Where("is_new = 1 AND created_at >= ? AND status IN ?", sixMonthsAgo, []int64{2, 5}). + Group("DATE_FORMAT(created_at, '%Y-%m')") + + // 子查询:每月续费订单用户数量 + renewalOrderSub := conn.Model(&order.Order{}). + Select("DATE_FORMAT(created_at, '%Y-%m') AS date, COUNT(DISTINCT user_id) AS renewal_order_users"). + Where("is_new = 0 AND created_at >= ? AND status IN ?", sixMonthsAgo, []int64{2, 5}). + Group("DATE_FORMAT(created_at, '%Y-%m')") + + return conn.Model(&User{}). + Select(` + DATE_FORMAT(user.created_at, '%Y-%m') AS date, + COUNT(*) AS register, + IFNULL(MAX(n.new_order_users), 0) AS new_order_users, + IFNULL(MAX(r.renewal_order_users), 0) AS renewal_order_users + `). + Joins("LEFT JOIN (?) AS n ON DATE_FORMAT(user.created_at, '%Y-%m') = n.date", newOrderSub). + Joins("LEFT JOIN (?) AS r ON DATE_FORMAT(user.created_at, '%Y-%m') = r.date", renewalOrderSub). + Where("user.created_at >= ?", sixMonthsAgo). + Group("DATE_FORMAT(user.created_at, '%Y-%m')"). + Order("date ASC"). + Scan(v).Error + }) + + return results, err } diff --git a/internal/model/user/subscribe.go b/internal/model/user/subscribe.go index 0c8d5ba..362fb99 100644 --- a/internal/model/user/subscribe.go +++ b/internal/model/user/subscribe.go @@ -9,7 +9,7 @@ import ( ) func (m *defaultUserModel) UpdateUserSubscribeCache(ctx context.Context, data *Subscribe) error { - return m.CachedConn.DelCacheCtx(ctx, m.getSubscribeCacheKey(data)...) + return m.ClearSubscribeCacheByModels(ctx, data) } // QueryActiveSubscriptions returns the number of active subscriptions. @@ -21,7 +21,7 @@ func (m *defaultUserModel) QueryActiveSubscriptions(ctx context.Context, subscri var result []SubscriptionCount err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, v interface{}) error { return conn.Model(&Subscribe{}). - Where("subscribe_id IN ? AND `status` IN ?", subscribeId, []int64{1, 0, 3}). + Where("subscribe_id IN ? AND `status` IN ?", subscribeId, []int64{1, 0}). Select("subscribe_id, COUNT(id) as total"). Group("subscribe_id"). Scan(&result). @@ -55,13 +55,18 @@ func (m *defaultUserModel) FindOneSubscribe(ctx context.Context, id int64) (*Sub return conn.Model(&Subscribe{}).Where("id = ?", id).First(&data).Error }) return &data, err - } func (m *defaultUserModel) FindUsersSubscribeBySubscribeId(ctx context.Context, subscribeId int64) ([]*Subscribe, error) { var data []*Subscribe err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error { - return conn.Model(&Subscribe{}).Where("subscribe_id = ? AND `status` IN ?", subscribeId, []int64{1, 0}).Find(&data).Error + err := conn.Model(&Subscribe{}).Where("subscribe_id = ? AND `status` IN ?", subscribeId, []int64{1, 0}).Find(v).Error + + if err != nil { + return err + } + // update user subscribe status + return conn.Model(&Subscribe{}).Where("subscribe_id = ? AND `status` = ?", subscribeId, 0).Update("status", 1).Error }) return data, err } @@ -76,8 +81,12 @@ func (m *defaultUserModel) QueryUserSubscribe(ctx context.Context, userId int64, // 获取当前时间向前推 7 天 sevenDaysAgo := time.Now().Add(-7 * 24 * time.Hour) // 基础条件查询 - conn = conn.Model(&Subscribe{}).Where("`user_id` = ? and `status` IN ?", userId, status) - return conn.Where("`expire_time` > ? OR `finished_at` >= ?", now, sevenDaysAgo). + conn = conn.Model(&Subscribe{}).Where("`user_id` = ?", userId) + if len(status) > 0 { + conn = conn.Where("`status` IN ?", status) + } + // 订阅过期时间大于当前时间或者订阅结束时间大于当前时间 + return conn.Where("`expire_time` > ? OR `finished_at` >= ? OR `expire_time` = ?", now, sevenDaysAgo, time.UnixMilli(0)). Preload("Subscribe"). Find(&list).Error }) @@ -106,12 +115,24 @@ func (m *defaultUserModel) FindOneSubscribeByToken(ctx context.Context, token st // UpdateSubscribe updates a record. func (m *defaultUserModel) UpdateSubscribe(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error { - return m.ExecCtx(ctx, func(conn *gorm.DB) error { + old, err := m.FindOneSubscribe(ctx, data.Id) + if err != nil { + return err + } + + // 使用 defer 确保更新后清理缓存 + defer func() { + if clearErr := m.ClearSubscribeCacheByModels(ctx, old, data); clearErr != nil { + // 记录清理缓存错误 + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } - return conn.Model(&Subscribe{}).Where("token = ?", data.Token).Save(data).Error - }, m.getSubscribeCacheKey(data)...) + return conn.Model(&Subscribe{}).Where("id = ?", data.Id).Save(data).Error + }) } // DeleteSubscribe deletes a record. @@ -120,22 +141,37 @@ func (m *defaultUserModel) DeleteSubscribe(ctx context.Context, token string, tx if err != nil { return err } - return m.ExecCtx(ctx, func(conn *gorm.DB) error { + + // 使用 defer 确保删除后清理缓存 + defer func() { + if clearErr := m.ClearSubscribeCacheByModels(ctx, data); clearErr != nil { + // 记录清理缓存错误 + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } return conn.Where("token = ?", token).Delete(&Subscribe{}).Error - }, m.getSubscribeCacheKey(data)...) + }) } // InsertSubscribe insert Subscribe into the database. func (m *defaultUserModel) InsertSubscribe(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error { - return m.ExecCtx(ctx, func(conn *gorm.DB) error { + // 使用 defer 确保插入后清理相关缓存 + defer func() { + if clearErr := m.ClearSubscribeCacheByModels(ctx, data); clearErr != nil { + // 记录清理缓存错误 + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } return conn.Create(data).Error - }, m.getSubscribeCacheKey(data)...) + }) } func (m *defaultUserModel) DeleteSubscribeById(ctx context.Context, id int64, tx ...*gorm.DB) error { @@ -143,18 +179,22 @@ func (m *defaultUserModel) DeleteSubscribeById(ctx context.Context, id int64, tx if err != nil { return err } - return m.ExecCtx(ctx, func(conn *gorm.DB) error { + + // 使用 defer 确保删除后清理缓存 + defer func() { + if clearErr := m.ClearSubscribeCacheByModels(ctx, data); clearErr != nil { + // 记录清理缓存错误 + } + }() + + return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error { if len(tx) > 0 { conn = tx[0] } return conn.Where("id = ?", id).Delete(&Subscribe{}).Error - }, m.getSubscribeCacheKey(data)...) + }) } func (m *defaultUserModel) ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error { - var keys []string - for _, item := range data { - keys = append(keys, m.getSubscribeCacheKey(item)...) - } - return m.CachedConn.DelCacheCtx(ctx, keys...) + return m.ClearSubscribeCacheByModels(ctx, data...) } diff --git a/internal/model/user/user.go b/internal/model/user/user.go index 5fff3ee..603f98e 100644 --- a/internal/model/user/user.go +++ b/internal/model/user/user.go @@ -2,9 +2,6 @@ package user import ( "time" - - "gorm.io/gorm" - "gorm.io/plugin/soft_delete" ) type User struct { @@ -14,7 +11,9 @@ type User struct { Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"` RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"` - Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount + Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount + ReferralPercentage uint8 `gorm:"default:0;comment:Referral"` // Referral Percentage + OnlyFirstPurchase *bool `gorm:"default:true;not null;comment:Only First Purchase"` // Only First Purchase Referral GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"` Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"` IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"` @@ -28,107 +27,33 @@ type User struct { UpdatedAt time.Time `gorm:"comment:Update Time"` } -func (User) TableName() string { - return "user" -} - -type OldUser struct { - Id int64 `gorm:"primaryKey"` - Email string `gorm:"index:idx_email;type:varchar(100);comment:Email"` - //Telephone string `gorm:"index:idx_telephone;type:varchar(20);default:'';comment:Telephone"` - //TelephoneAreaCode string `gorm:"index:idx_telephone;type:varchar(20);default:'';comment:TelephoneAreaCode"` - Password string `gorm:"type:varchar(100);not null;comment:User Password"` - Avatar string `gorm:"type:varchar(200);default:'';comment:User Avatar"` - Balance int64 `gorm:"default:0;comment:User Balance"` // User Balance Amount - Telegram int64 `gorm:"default:null;comment:Telegram Account"` - ReferCode string `gorm:"type:varchar(20);default:'';comment:Referral Code"` - RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"` - Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount - GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"` - Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"` - IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"` - ValidEmail *bool `gorm:"default:false;not null;comment:Is Email Verified"` - EnableEmailNotify *bool `gorm:"default:false;not null;comment:Enable Email Notifications"` - EnableTelegramNotify *bool `gorm:"default:false;not null;comment:Enable Telegram Notifications"` - EnableBalanceNotify *bool `gorm:"default:false;not null;comment:Enable Balance Change Notifications"` - EnableLoginNotify *bool `gorm:"default:false;not null;comment:Enable Login Notifications"` - EnableSubscribeNotify *bool `gorm:"default:false;not null;comment:Enable Subscription Notifications"` - EnableTradeNotify *bool `gorm:"default:false;not null;comment:Enable Trade Notifications"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` - DeletedAt gorm.DeletedAt `gorm:"default:null;comment:Deletion Time"` - IsDel soft_delete.DeletedAt `gorm:"softDelete:flag,DeletedAtField:DeletedAt;comment:1: Normal 0: Deleted"` // Using `1` and `0` to indicate -} - -func (OldUser) TableName() string { +func (*User) TableName() string { return "user" } type Subscribe struct { - Id int64 `gorm:"primaryKey"` - UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` - User User `gorm:"foreignKey:UserId;references:Id"` - OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order ID"` - SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"` - StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"` - ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"` - FinishedAt time.Time `gorm:"default:NULL;comment:Finished Time"` - Traffic int64 `gorm:"default:0;comment:Traffic"` - Download int64 `gorm:"default:0;comment:Download Traffic"` - Upload int64 `gorm:"default:0;comment:Upload Traffic"` - Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` - UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` - Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` - UpdatedAt time.Time `gorm:"comment:Update Time"` + Id int64 `gorm:"primaryKey"` + UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` + User User `gorm:"foreignKey:UserId;references:Id"` + OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order ID"` + SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"` + StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"` + ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"` + FinishedAt *time.Time `gorm:"default:NULL;comment:Finished Time"` + Traffic int64 `gorm:"default:0;comment:Traffic"` + Download int64 `gorm:"default:0;comment:Download Traffic"` + Upload int64 `gorm:"default:0;comment:Upload Traffic"` + Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"` + UUID string `gorm:"type:varchar(255);unique;index:idx_uuid;default:'';comment:UUID"` + Status uint8 `gorm:"type:tinyint(1);default:0;comment:Subscription Status: 0: Pending 1: Active 2: Finished 3: Expired 4: Deducted"` + CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` + UpdatedAt time.Time `gorm:"comment:Update Time"` } -func (Subscribe) TableName() string { +func (*Subscribe) TableName() string { return "user_subscribe" } -type BalanceLog struct { - Id int64 `gorm:"primaryKey"` - UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` - Amount int64 `gorm:"not null;comment:Amount"` - Type uint8 `gorm:"type:tinyint(1);not null;comment:Type: 1: Recharge 2: Withdraw 3: Payment 4: Refund 5: Reward"` - OrderId int64 `gorm:"default:null;comment:Order ID"` - Balance int64 `gorm:"not null;comment:Balance"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` -} - -func (BalanceLog) TableName() string { - return "user_balance_log" -} - -type GiftAmountLog struct { - Id int64 `gorm:"primaryKey"` - UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` - UserSubscribeId int64 `gorm:"default:null;comment:Deduction User Subscribe ID"` - OrderNo string `gorm:"default:null;comment:Order No."` - Type uint8 `gorm:"type:tinyint(1);not null;comment:Type: 1: Increase 2: Reduce"` - Amount int64 `gorm:"not null;comment:Amount"` - Balance int64 `gorm:"not null;comment:Balance"` - Remark string `gorm:"type:varchar(255);default:'';comment:Remark"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` -} - -func (GiftAmountLog) TableName() string { - return "user_gift_amount_log" -} - -type CommissionLog struct { - Id int64 `gorm:"primaryKey"` - UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` - OrderNo string `gorm:"default:null;comment:Order No."` - Amount int64 `gorm:"not null;comment:Amount"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` -} - -func (CommissionLog) TableName() string { - return "user_commission_log" -} - type AuthMethods struct { Id int64 `gorm:"primaryKey"` UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` @@ -139,7 +64,7 @@ type AuthMethods struct { UpdatedAt time.Time `gorm:"comment:Update Time"` } -func (AuthMethods) TableName() string { +func (*AuthMethods) TableName() string { return "user_auth_methods" } @@ -155,14 +80,14 @@ type Device struct { UpdatedAt time.Time `gorm:"comment:Update Time"` } -func (Device) TableName() string { +func (*Device) TableName() string { return "user_device" } type DeviceOnlineRecord struct { Id int64 `gorm:"primaryKey"` - UserId int64 `gorm:"comment:User ID"` - Identifier string `gorm:"comment:Device Identifier"` + UserId int64 `gorm:"type:bigint;not null;comment:User ID"` + Identifier string `gorm:"type:varchar(255);not null;comment:Device Identifier"` OnlineTime time.Time `gorm:"comment:Online Time"` // The time when the device goes online OfflineTime time.Time `gorm:"comment:Offline Time"` OnlineSeconds int64 `gorm:"comment:Offline Seconds"` @@ -173,58 +98,3 @@ type DeviceOnlineRecord struct { func (DeviceOnlineRecord) TableName() string { return "user_device_online_record" } - -type LoginLog struct { - Id int64 `gorm:"primaryKey"` - UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` - LoginIP string `gorm:"type:varchar(255);not null;comment:Login IP"` - UserAgent string `gorm:"type:text;not null;comment:UserAgent"` - Success *bool `gorm:"default:false;not null;comment:Login Success"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` -} - -func (LoginLog) TableName() string { - return "user_login_log" -} - -type SubscribeLog struct { - Id int64 `gorm:"primaryKey"` - UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"` - UserSubscribeId int64 `gorm:"index:idx_user_subscribe_id;not null;comment:User Subscribe ID"` - Token string `gorm:"type:varchar(255);not null;comment:Token"` - IP string `gorm:"type:varchar(255);not null;comment:IP"` - UserAgent string `gorm:"type:text;not null;comment:UserAgent"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` -} - -func (SubscribeLog) TableName() string { - return "user_subscribe_log" -} - -const ( - ResetSubscribeTypeAuto uint8 = 1 - ResetSubscribeTypeAdvance uint8 = 2 - ResetSubscribeTypePaid uint8 = 3 -) - -type FilterResetSubscribeLogParams struct { - Page int - Size int - Type uint8 - UserId int64 - OrderNo string - UserSubscribeId int64 -} - -type ResetSubscribeLog struct { - Id int64 `gorm:"primaryKey"` - UserId int64 `gorm:"type:bigint;index:idx_user_id;not null;comment:User ID"` - Type uint8 `gorm:"type:tinyint(1);not null;comment:Type: 1: Auto 2: Advance 3: Paid"` - OrderNo string `gorm:"type:varchar(255);default:null;comment:Order No."` - UserSubscribeId int64 `gorm:"type:bigint;index:idx_user_subscribe_id;not null;comment:User Subscribe ID"` - CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` -} - -func (ResetSubscribeLog) TableName() string { - return "user_reset_subscribe_log" -} diff --git a/internal/server.go b/internal/server.go index 6f47675..64c2704 100644 --- a/internal/server.go +++ b/internal/server.go @@ -8,18 +8,18 @@ import ( "net/http" "time" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/proc" - "github.com/perfect-panel/ppanel-server/pkg/trace" + "github.com/perfect-panel/server/pkg/proc" + "github.com/perfect-panel/server/pkg/trace" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/redis" "github.com/gin-gonic/gin" - "github.com/perfect-panel/ppanel-server/initialize" - "github.com/perfect-panel/ppanel-server/internal/handler" - "github.com/perfect-panel/ppanel-server/internal/middleware" - "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/server/initialize" + "github.com/perfect-panel/server/internal/handler" + "github.com/perfect-panel/server/internal/middleware" + "github.com/perfect-panel/server/internal/svc" ) type Service struct { diff --git a/internal/svc/asynq.go b/internal/svc/asynq.go index 07a3003..510ccfb 100644 --- a/internal/svc/asynq.go +++ b/internal/svc/asynq.go @@ -2,7 +2,7 @@ package svc import ( "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/config" + "github.com/perfect-panel/server/internal/config" ) func NewAsynqClient(c config.Config) *asynq.Client { diff --git a/internal/svc/devce.go b/internal/svc/devce.go index 87b6668..2d1fd23 100644 --- a/internal/svc/devce.go +++ b/internal/svc/devce.go @@ -6,11 +6,11 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/user" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/device" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/device" + "github.com/perfect-panel/server/pkg/logger" "github.com/pkg/errors" "gorm.io/gorm" ) diff --git a/internal/svc/logger.go b/internal/svc/logger.go index c5e1e94..8291473 100644 --- a/internal/svc/logger.go +++ b/internal/svc/logger.go @@ -1,8 +1,8 @@ package svc import ( - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/pkg/logger" ) func NewLogger(c config.Config) *logger.Logger { diff --git a/internal/svc/serviceContext.go b/internal/svc/serviceContext.go index a6bbd1b..184dc69 100644 --- a/internal/svc/serviceContext.go +++ b/internal/svc/serviceContext.go @@ -3,58 +3,57 @@ package svc import ( "context" - "github.com/perfect-panel/ppanel-server/pkg/device" + "github.com/perfect-panel/server/internal/model/client" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/pkg/device" - "github.com/perfect-panel/ppanel-server/internal/model/ads" - "github.com/perfect-panel/ppanel-server/internal/model/cache" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/model/ads" + "github.com/perfect-panel/server/internal/model/announcement" + "github.com/perfect-panel/server/internal/model/auth" + "github.com/perfect-panel/server/internal/model/coupon" + "github.com/perfect-panel/server/internal/model/document" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/internal/model/payment" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/model/system" + "github.com/perfect-panel/server/internal/model/ticket" + "github.com/perfect-panel/server/internal/model/traffic" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/limit" + "github.com/perfect-panel/server/pkg/nodeMultiplier" + "github.com/perfect-panel/server/pkg/orm" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/model/announcement" - "github.com/perfect-panel/ppanel-server/internal/model/application" - "github.com/perfect-panel/ppanel-server/internal/model/auth" - "github.com/perfect-panel/ppanel-server/internal/model/coupon" - "github.com/perfect-panel/ppanel-server/internal/model/document" - "github.com/perfect-panel/ppanel-server/internal/model/log" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/internal/model/subscribe" - "github.com/perfect-panel/ppanel-server/internal/model/subscribeType" - "github.com/perfect-panel/ppanel-server/internal/model/system" - "github.com/perfect-panel/ppanel-server/internal/model/ticket" - "github.com/perfect-panel/ppanel-server/internal/model/traffic" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/pkg/limit" - "github.com/perfect-panel/ppanel-server/pkg/nodeMultiplier" - "github.com/perfect-panel/ppanel-server/pkg/orm" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) type ServiceContext struct { - DB *gorm.DB - Redis *redis.Client - Config config.Config - Queue *asynq.Client - NodeCache *cache.NodeCacheClient - AuthModel auth.Model - AdsModel ads.Model - LogModel log.Model - UserModel user.Model - OrderModel order.Model - TicketModel ticket.Model - ServerModel server.Model - SystemModel system.Model - CouponModel coupon.Model - PaymentModel payment.Model - DocumentModel document.Model - SubscribeModel subscribe.Model - TrafficLogModel traffic.Model - ApplicationModel application.Model - AnnouncementModel announcement.Model - SubscribeTypeModel subscribeType.Model + DB *gorm.DB + Redis *redis.Client + Config config.Config + Queue *asynq.Client + //NodeCache *cache.NodeCacheClient + AuthModel auth.Model + AdsModel ads.Model + LogModel log.Model + NodeModel node.Model + UserModel user.Model + OrderModel order.Model + ClientModel client.Model + TicketModel ticket.Model + //ServerModel server.Model + SystemModel system.Model + CouponModel coupon.Model + PaymentModel payment.Model + DocumentModel document.Model + SubscribeModel subscribe.Model + TrafficLogModel traffic.Model + AnnouncementModel announcement.Model + Restart func() error TelegramBot *tgbotapi.BotAPI NodeMultiplierManager *nodeMultiplier.Manager @@ -83,26 +82,27 @@ func NewServiceContext(c config.Config) *ServiceContext { } authLimiter := limit.NewPeriodLimit(86400, 15, rds, config.SendCountLimitKeyPrefix, limit.Align()) srv := &ServiceContext{ - DB: db, - Redis: rds, - Config: c, - Queue: NewAsynqClient(c), - NodeCache: cache.NewNodeCacheClient(rds), - AuthLimiter: authLimiter, - AdsModel: ads.NewModel(db, rds), - LogModel: log.NewModel(db), - AuthModel: auth.NewModel(db, rds), - UserModel: user.NewModel(db, rds), - OrderModel: order.NewModel(db, rds), - TicketModel: ticket.NewModel(db, rds), - ServerModel: server.NewModel(db, rds), + DB: db, + Redis: rds, + Config: c, + Queue: NewAsynqClient(c), + //NodeCache: cache.NewNodeCacheClient(rds), + AuthLimiter: authLimiter, + AdsModel: ads.NewModel(db, rds), + LogModel: log.NewModel(db), + NodeModel: node.NewModel(db, rds), + AuthModel: auth.NewModel(db, rds), + UserModel: user.NewModel(db, rds), + OrderModel: order.NewModel(db, rds), + ClientModel: client.NewSubscribeApplicationModel(db), + TicketModel: ticket.NewModel(db, rds), + //ServerModel: server.NewModel(db, rds), SystemModel: system.NewModel(db, rds), CouponModel: coupon.NewModel(db, rds), PaymentModel: payment.NewModel(db, rds), DocumentModel: document.NewModel(db, rds), SubscribeModel: subscribe.NewModel(db, rds), TrafficLogModel: traffic.NewModel(db), - ApplicationModel: application.NewModel(db, rds), AnnouncementModel: announcement.NewModel(db, rds), } srv.DeviceManager = NewDeviceManager(srv) diff --git a/internal/types/types.go b/internal/types/types.go index 27a4d31..89d404c 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -32,79 +32,9 @@ type Announcement struct { UpdatedAt int64 `json:"updated_at"` } -type AppAuthCheckRequest struct { - Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"` - Account string `json:"account"` - Identifier string `json:"identifier" validate:"required"` - UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` - AreaCode string `json:"area_code"` -} - -type AppAuthCheckResponse struct { - Status bool -} - -type AppAuthRequest struct { - Method string `json:"method" validate:"required" validate:"required,oneof=device email mobile"` - Account string `json:"account"` - Password string `json:"password"` - Identifier string `json:"identifier" validate:"required"` - UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` - Code string `json:"code"` - Invite string `json:"invite"` - AreaCode string `json:"area_code"` - CfToken string `json:"cf_token,optional"` -} - -type AppAuthRespone struct { - Token string `json:"token"` -} - -type AppConfigRequest struct { - UserAgent string `json:"user_agent" validate:"required,oneof=windows mac linux android ios harmony"` -} - -type AppConfigResponse struct { - EncryptionKey string `json:"encryption_key"` - EncryptionMethod string `json:"encryption_method"` - Domains []string `json:"domains"` - StartupPicture string `json:"startup_picture"` - StartupPictureSkipTime int64 `json:"startup_picture_skip_time"` - Application AppInfo `json:"applications"` - OfficialEmail string `json:"official_email"` - OfficialWebsite string `json:"official_website"` - OfficialTelegram string `json:"official_telegram"` - OfficialTelephone string `json:"official_telephone"` - InvitationLink string `json:"invitation_link"` - KrWebsiteId string `json:"kr_website_id"` -} - -type AppInfo struct { - Id int64 `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Url string `json:"url"` - Version string `json:"version"` - VersionReview string `json:"version_review"` - VersionDescription string `json:"version_description"` - IsDefault bool `json:"is_default"` -} - -type AppRuleGroupListResponse struct { - Total int64 `json:"total"` - List []ServerRuleGroup `json:"list"` -} - -type AppSendCodeRequest struct { - Method string `json:"method" validate:"required" validate:"required,oneof=email mobile"` - Account string `json:"account"` - AreaCode string `json:"area_code"` - CfToken string `json:"cf_token,optional"` -} - -type AppSendCodeRespone struct { - Status bool `json:"status"` - Code string `json:"code,omitempty"` +type AnyTLS struct { + Port int `json:"port" validate:"required"` + SecurityConfig SecurityConfig `json:"security_config"` } type AppUserSubcbribe struct { @@ -120,43 +50,25 @@ type AppUserSubcbribe struct { } type AppUserSubscbribeNode struct { - Id int64 `json:"id"` - Name string `json:"name"` - Uuid string `json:"uuid"` - Protocol string `json:"protocol"` - RelayMode string `json:"relay_mode"` - RelayNode string `json:"relay_node"` - ServerAddr string `json:"server_addr"` - SpeedLimit int `json:"speed_limit"` - Tags []string `json:"tags"` - Traffic int64 `json:"traffic"` - TrafficRatio float64 `json:"traffic_ratio"` - Upload int64 `json:"upload"` - Config string `json:"config"` - Country string `json:"country"` - City string `json:"city"` - Latitude string `json:"latitude"` - Longitude string `json:"longitude"` - LatitudeCountry string `json:"latitudeCountry"` - LongitudeCountry string `json:"longitudeCountry"` - CreatedAt int64 `json:"created_at"` - Download int64 `json:"download"` -} - -type AppUserSubscbribeNodeRequest struct { - Id int64 `form:"id" validate:"required"` -} - -type AppUserSubscbribeNodeResponse struct { - List []AppUserSubscbribeNode `json:"list"` -} - -type AppUserSubscbribeResponse struct { - List []AppUserSubcbribe `json:"list"` -} - -type AppUserSubscribeRequest struct { - ContainsNodes *bool `form:"contains_nodes"` + Id int64 `json:"id"` + Name string `json:"name"` + Uuid string `json:"uuid"` + Protocol string `json:"protocol"` + RelayMode string `json:"relay_mode"` + RelayNode string `json:"relay_node"` + ServerAddr string `json:"server_addr"` + SpeedLimit int `json:"speed_limit"` + Tags []string `json:"tags"` + Traffic int64 `json:"traffic"` + TrafficRatio float64 `json:"traffic_ratio"` + Upload int64 `json:"upload"` + Config string `json:"config"` + Country string `json:"country"` + City string `json:"city"` + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` + CreatedAt int64 `json:"created_at"` + Download int64 `json:"download"` } type AppleLoginCallbackRequest struct { @@ -173,15 +85,6 @@ type Application struct { SubscribeType string `json:"subscribe_type"` } -type ApplicationConfig struct { - AppId int64 `json:"app_id"` - EncryptionKey string `json:"encryption_key"` - EncryptionMethod string `json:"encryption_method"` - Domains []string `json:"domains" validate:"required"` - StartupPicture string `json:"startup_picture"` - StartupPictureSkipTime int64 `json:"startup_picture_skip_time"` -} - type ApplicationPlatform struct { IOS []*ApplicationVersion `json:"ios,omitempty"` MacOS []*ApplicationVersion `json:"macos,omitempty"` @@ -215,6 +118,7 @@ type ApplicationVersion struct { type AuthConfig struct { Mobile MobileAuthenticateConfig `json:"mobile"` Email EmailAuthticateConfig `json:"email"` + Device DeviceAuthticateConfig `json:"device"` Register PubilcRegisterConfig `json:"register"` } @@ -225,6 +129,15 @@ type AuthMethodConfig struct { Enabled bool `json:"enabled"` } +type BalanceLog struct { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + Amount int64 `json:"amount"` + OrderNo string `json:"order_no,omitempty"` + Balance int64 `json:"balance"` + Timestamp int64 `json:"timestamp"` +} + type BatchDeleteCouponRequest struct { Ids []int64 `json:"ids" validate:"required"` } @@ -233,14 +146,6 @@ type BatchDeleteDocumentRequest struct { Ids []int64 `json:"ids" validate:"required"` } -type BatchDeleteNodeGroupRequest struct { - Ids []int64 `json:"ids" validate:"required"` -} - -type BatchDeleteNodeRequest struct { - Ids []int64 `json:"ids" validate:"required"` -} - type BatchDeleteSubscribeGroupRequest struct { Ids []int64 `json:"ids" validate:"required"` } @@ -253,6 +158,26 @@ type BatchDeleteUserRequest struct { Ids []int64 `json:"ids" validate:"required"` } +type BatchSendEmailTask struct { + Id int64 `json:"id"` + Subject string `json:"subject"` + Content string `json:"content"` + Recipients string `json:"recipients"` + Scope int8 `json:"scope"` + RegisterStartTime int64 `json:"register_start_time"` + RegisterEndTime int64 `json:"register_end_time"` + Additional string `json:"additional"` + Scheduled int64 `json:"scheduled"` + Interval uint8 `json:"interval"` + Limit uint64 `json:"limit"` + Status uint8 `json:"status"` + Errors string `json:"errors"` + Total uint64 `json:"total"` + Current uint64 `json:"current"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + type BindOAuthCallbackRequest struct { Method string `json:"method"` Callback interface{} `json:"callback"` @@ -307,17 +232,11 @@ type CloseOrderRequest struct { } type CommissionLog struct { - Id int64 `json:"id"` + Type uint16 `json:"type"` UserId int64 `json:"user_id"` - OrderNo string `json:"order_no"` Amount int64 `json:"amount"` - CreatedAt int64 `json:"created_at"` -} - -type ConnectionRecords struct { - CurrentContinuousDays int64 `json:"current_continuous_days"` - HistoryContinuousDays int64 `json:"history_continuous_days"` - LongestSingleConnection int64 `json:"longest_single_connection"` + OrderNo string `json:"order_no"` + Timestamp int64 `json:"timestamp"` } type Coupon struct { @@ -353,21 +272,16 @@ type CreateAnnouncementRequest struct { Content string `json:"content" validate:"required"` } -type CreateApplicationRequest struct { - Icon string `json:"icon"` - Name string `json:"name"` - Description string `json:"description"` - SubscribeType string `json:"subscribe_type"` - Platform ApplicationPlatform `json:"platform"` -} - -type CreateApplicationVersionRequest struct { - Url string `json:"url"` - Version string `json:"version" validate:"required"` - Description string `json:"description"` - Platform string `json:"platform" validate:"required,oneof=windows mac linux android ios harmony"` - IsDefault bool `json:"is_default"` - ApplicationId int64 `json:"application_id" validate:"required"` +type CreateBatchSendEmailTaskRequest struct { + Subject string `json:"subject"` + Content string `json:"content"` + Scope int8 `json:"scope"` + RegisterStartTime int64 `json:"register_start_time,omitempty"` + RegisterEndTime int64 `json:"register_end_time,omitempty"` + Additional string `json:"additional,omitempty"` + Scheduled int64 `json:"scheduled,omitempty"` + Interval uint8 `json:"interval,omitempty"` + Limit uint64 `json:"limit,omitempty"` } type CreateCouponRequest struct { @@ -391,26 +305,14 @@ type CreateDocumentRequest struct { Show *bool `json:"show"` } -type CreateNodeGroupRequest struct { - Name string `json:"name" validate:"required"` - Description string `json:"description"` -} - type CreateNodeRequest struct { - Name string `json:"name" validate:"required"` - Tags []string `json:"tags"` - Country string `json:"country"` - City string `json:"city"` - ServerAddr string `json:"server_addr" validate:"required"` - RelayMode string `json:"relay_mode"` - RelayNode []NodeRelay `json:"relay_node"` - SpeedLimit int `json:"speed_limit"` - TrafficRatio float32 `json:"traffic_ratio"` - GroupId int64 `json:"group_id"` - Protocol string `json:"protocol" validate:"required"` - Config interface{} `json:"config" validate:"required"` - Enable *bool `json:"enable"` - Sort int64 `json:"sort"` + Name string `json:"name"` + Tags []string `json:"tags,omitempty"` + Port uint16 `json:"port"` + Address string `json:"address"` + ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` + Enabled *bool `json:"enabled"` } type CreateOrderRequest struct { @@ -443,12 +345,36 @@ type CreatePaymentMethodRequest struct { Enable *bool `json:"enable" validate:"required"` } -type CreateRuleGroupRequest struct { - Name string `json:"name" validate:"required"` - Icon string `json:"icon"` - Tags []string `json:"tags"` - Rules string `json:"rules"` - Enable bool `json:"enable"` +type CreateQuotaTaskRequest struct { + Subscribers []int64 `json:"subscribers"` + IsActive *bool `json:"is_active"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + ResetTraffic bool `json:"reset_traffic"` + Days uint64 `json:"days"` + GiftType uint8 `json:"gift_type"` + GiftValue uint64 `json:"gift_value"` +} + +type CreateServerRequest struct { + Name string `json:"name"` + Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` + Address string `json:"address"` + Sort int `json:"sort,omitempty"` + Protocols []Protocol `json:"protocols"` +} + +type CreateSubscribeApplicationRequest struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + UserAgent string `json:"user_agent"` + IsDefault bool `json:"is_default"` + SubscribeTemplate string `json:"template"` + OutputFormat string `json:"output_format"` + DownloadLink DownloadLink `json:"download_link"` } type CreateSubscribeGroupRequest struct { @@ -458,6 +384,7 @@ type CreateSubscribeGroupRequest struct { type CreateSubscribeRequest struct { Name string `json:"name" validate:"required"` + Language string `json:"language"` Description string `json:"description"` UnitPrice int64 `json:"unit_price"` UnitTime string `json:"unit_time"` @@ -468,9 +395,8 @@ type CreateSubscribeRequest struct { SpeedLimit int64 `json:"speed_limit"` DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` - GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show *bool `json:"show"` Sell *bool `json:"sell"` DeductionRatio int64 `json:"deduction_ratio"` @@ -493,18 +419,20 @@ type CreateUserAuthMethodRequest struct { } type CreateUserRequest struct { - Email string `json:"email"` - Telephone string `json:"telephone"` - TelephoneAreaCode string `json:"telephone_area_code"` - Password string `json:"password"` - ProductId int64 `json:"product_id"` - Duration int64 `json:"duration"` - RefererUser string `json:"referer_user"` - ReferCode string `json:"refer_code"` - Balance int64 `json:"balance"` - Commission int64 `json:"commission"` - GiftAmount int64 `json:"gift_amount"` - IsAdmin bool `json:"is_admin"` + Email string `json:"email"` + Telephone string `json:"telephone"` + TelephoneAreaCode string `json:"telephone_area_code"` + Password string `json:"password"` + ProductId int64 `json:"product_id"` + Duration int64 `json:"duration"` + ReferralPercentage uint8 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` + RefererUser string `json:"referer_user"` + ReferCode string `json:"refer_code"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + GiftAmount int64 `json:"gift_amount"` + IsAdmin bool `json:"is_admin"` } type CreateUserSubscribeRequest struct { @@ -537,11 +465,6 @@ type CurrencyConfig struct { CurrencySymbol string `json:"currency_symbol"` } -type DeleteAccountRequest struct { - Method string `json:"method" validate:"required" validate:"required,oneof=email telephone device"` - Code string `json:"code"` -} - type DeleteAdsRequest struct { Id int64 `json:"id"` } @@ -550,14 +473,6 @@ type DeleteAnnouncementRequest struct { Id int64 `json:"id" validate:"required"` } -type DeleteApplicationRequest struct { - Id int64 `json:"id" validate:"required"` -} - -type DeleteApplicationVersionRequest struct { - Id int64 `json:"id" validate:"required"` -} - type DeleteCouponRequest struct { Id int64 `json:"id" validate:"required"` } @@ -566,20 +481,20 @@ type DeleteDocumentRequest struct { Id int64 `json:"id" validate:"required"` } -type DeleteNodeGroupRequest struct { - Id int64 `json:"id" validate:"required"` -} - type DeleteNodeRequest struct { - Id int64 `json:"id" validate:"required"` + Id int64 `json:"id"` } type DeletePaymentMethodRequest struct { Id int64 `json:"id" validate:"required"` } -type DeleteRuleGroupRequest struct { - Id int64 `json:"id" validate:"required"` +type DeleteServerRequest struct { + Id int64 `json:"id"` +} + +type DeleteSubscribeApplicationRequest struct { + Id int64 `json:"id"` } type DeleteSubscribeGroupRequest struct { @@ -603,6 +518,20 @@ type DeleteUserSubscribeRequest struct { UserSubscribeId int64 `json:"user_subscribe_id"` } +type DeviceAuthticateConfig struct { + Enable bool `json:"enable"` + ShowAds bool `json:"show_ads"` + EnableSecurity bool `json:"enable_security"` + OnlyRealDevice bool `json:"only_real_device"` +} + +type DeviceLoginRequest struct { + Identifier string `json:"identifier" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `json:"user_agent" validate:"required"` + CfToken string `json:"cf_token,optional"` +} + type Document struct { Id int64 `json:"id"` Title string `json:"title"` @@ -613,6 +542,15 @@ type Document struct { UpdatedAt int64 `json:"updated_at"` } +type DownloadLink struct { + IOS string `json:"ios,omitempty"` + Android string `json:"android,omitempty"` + Windows string `json:"windows,omitempty"` + Mac string `json:"mac,omitempty"` + Linux string `json:"linux,omitempty"` + Harmony string `json:"harmony,omitempty"` +} + type EPayNotifyRequest struct { Pid int64 `json:"pid" form:"pid"` TradeNo string `json:"trade_no" form:"trade_no"` @@ -633,6 +571,149 @@ type EmailAuthticateConfig struct { DomainSuffixList string `json:"domain_suffix_list"` } +type FilterBalanceLogRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` +} + +type FilterBalanceLogResponse struct { + Total int64 `json:"total"` + List []BalanceLog `json:"list"` +} + +type FilterCommissionLogRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` +} + +type FilterCommissionLogResponse struct { + Total int64 `json:"total"` + List []CommissionLog `json:"list"` +} + +type FilterEmailLogResponse struct { + Total int64 `json:"total"` + List []MessageLog `json:"list"` +} + +type FilterGiftLogRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` +} + +type FilterGiftLogResponse struct { + Total int64 `json:"total"` + List []GiftLog `json:"list"` +} + +type FilterLogParams struct { + Page int `form:"page"` + Size int `form:"size"` + Date string `form:"date,optional"` + Search string `form:"search,optional"` +} + +type FilterLoginLogRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` +} + +type FilterLoginLogResponse struct { + Total int64 `json:"total"` + List []LoginLog `json:"list"` +} + +type FilterMobileLogResponse struct { + Total int64 `json:"total"` + List []MessageLog `json:"list"` +} + +type FilterNodeListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Search string `form:"search,omitempty"` +} + +type FilterNodeListResponse struct { + Total int64 `json:"total"` + List []Node `json:"list"` +} + +type FilterRegisterLogRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` +} + +type FilterRegisterLogResponse struct { + Total int64 `json:"total"` + List []RegisterLog `json:"list"` +} + +type FilterResetSubscribeLogRequest struct { + FilterLogParams + UserSubscribeId int64 `form:"user_subscribe_id,optional"` +} + +type FilterResetSubscribeLogResponse struct { + Total int64 `json:"total"` + List []ResetSubscribeLog `json:"list"` +} + +type FilterServerListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Search string `form:"search,omitempty"` +} + +type FilterServerListResponse struct { + Total int64 `json:"total"` + List []Server `json:"list"` +} + +type FilterServerTrafficLogRequest struct { + FilterLogParams + ServerId int64 `form:"server_id,optional"` +} + +type FilterServerTrafficLogResponse struct { + Total int64 `json:"total"` + List []ServerTrafficLog `json:"list"` +} + +type FilterSubscribeLogRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` + UserSubscribeId int64 `form:"user_subscribe_id,optional"` +} + +type FilterSubscribeLogResponse struct { + Total int64 `json:"total"` + List []SubscribeLog `json:"list"` +} + +type FilterSubscribeTrafficRequest struct { + FilterLogParams + UserId int64 `form:"user_id,optional"` + UserSubscribeId int64 `form:"user_subscribe_id,optional"` +} + +type FilterSubscribeTrafficResponse struct { + Total int64 `json:"total"` + List []UserSubscribeTrafficLog `json:"list"` +} + +type FilterTrafficLogDetailsRequest struct { + FilterLogParams + ServerId int64 `form:"server_id,optional"` + SubscribeId int64 `form:"subscribe_id,optional"` + UserId int64 `form:"user_id,optional"` +} + +type FilterTrafficLogDetailsResponse struct { + Total int64 `json:"total"` + List []TrafficLogDetails `json:"list"` +} + type Follow struct { Id int64 `json:"id"` TicketId int64 `json:"ticket_id"` @@ -685,11 +766,6 @@ type GetAnnouncementRequest struct { Id int64 `form:"id" validate:"required"` } -type GetAppcationResponse struct { - Config ApplicationConfig `json:"config"` - Applications []ApplicationResponseInfo `json:"applications"` -} - type GetAuthMethodConfigRequest struct { Method string `form:"method"` } @@ -702,6 +778,29 @@ type GetAvailablePaymentMethodsResponse struct { List []PaymentMethod `json:"list"` } +type GetBatchSendEmailTaskListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Scope *int8 `form:"scope,omitempty"` + Status *uint8 `form:"status,omitempty"` +} + +type GetBatchSendEmailTaskListResponse struct { + Total int64 `json:"total"` + List []BatchSendEmailTask `json:"list"` +} + +type GetBatchSendEmailTaskStatusRequest struct { + Id int64 `json:"id"` +} + +type GetBatchSendEmailTaskStatusResponse struct { + Status uint8 `json:"status"` + Current int64 `json:"current"` + Total int64 `json:"total"` + Errors string `json:"errors"` +} + type GetCouponListRequest struct { Page int64 `form:"page" validate:"required"` Size int64 `form:"size" validate:"required"` @@ -718,6 +817,11 @@ type GetDetailRequest struct { Id int64 `form:"id" validate:"required"` } +type GetDeviceListResponse struct { + List []UserDevice `json:"list"` + Total int64 `json:"total"` +} + type GetDocumentDetailRequest struct { Id int64 `json:"id" validate:"required"` } @@ -757,14 +861,10 @@ type GetLoginLogResponse struct { } type GetMessageLogListRequest struct { - Page int `form:"page"` - Size int `form:"size"` - Type string `form:"type"` - Platform string `form:"platform,omitempty"` - To string `form:"to,omitempty"` - Subject string `form:"subject,omitempty"` - Content string `form:"content,omitempty"` - Status int `form:"status,omitempty"` + Page int `form:"page"` + Size int `form:"size"` + Type uint8 `form:"type"` + Search string `form:"search,optional"` } type GetMessageLogListResponse struct { @@ -772,36 +872,10 @@ type GetMessageLogListResponse struct { List []MessageLog `json:"list"` } -type GetNodeDetailRequest struct { - Id int64 `form:"id" validate:"required"` -} - -type GetNodeGroupListResponse struct { - Total int64 `json:"total"` - List []ServerGroup `json:"list"` -} - type GetNodeMultiplierResponse struct { Periods []TimePeriod `json:"periods"` } -type GetNodeServerListRequest struct { - Page int `form:"page" validate:"required"` - Size int `form:"size" validate:"required"` - Tag string `form:"tag,omitempty"` - GroupId int64 `form:"group_id,omitempty"` - Search string `form:"search,omitempty"` -} - -type GetNodeServerListResponse struct { - Total int64 `json:"total"` - List []Server `json:"list"` -} - -type GetNodeTagListResponse struct { - Tags []string `json:"tags"` -} - type GetOAuthMethodsResponse struct { Methods []UserAuthMethod `json:"methods"` } @@ -833,9 +907,14 @@ type GetPaymentMethodListResponse struct { List []PaymentMethodDetail `json:"list"` } -type GetRuleGroupResponse struct { - Total int64 `json:"total"` - List []ServerRuleGroup `json:"list"` +type GetPreSendEmailCountRequest struct { + Scope int8 `json:"scope"` + RegisterStartTime int64 `json:"register_start_time,omitempty"` + RegisterEndTime int64 `json:"register_end_time,omitempty"` +} + +type GetPreSendEmailCountResponse struct { + Count int64 `json:"count"` } type GetServerConfigRequest struct { @@ -848,6 +927,14 @@ type GetServerConfigResponse struct { Config interface{} `json:"config"` } +type GetServerProtocolsRequest struct { + Id int64 `form:"id"` +} + +type GetServerProtocolsResponse struct { + Protocols []Protocol `json:"protocols"` +} + type GetServerUserListRequest struct { ServerCommon } @@ -863,6 +950,21 @@ type GetStatResponse struct { Protocol []string `json:"protocol"` } +type GetSubscribeApplicationListRequest struct { + Page int `form:"page"` + Size int `form:"size"` +} + +type GetSubscribeApplicationListResponse struct { + Total int64 `json:"total"` + List []SubscribeApplication `json:"list"` +} + +type GetSubscribeClientResponse struct { + Total int64 `json:"total"` + List []SubscribeClient `json:"list"` +} + type GetSubscribeDetailsRequest struct { Id int64 `form:"id" validate:"required"` } @@ -873,10 +975,10 @@ type GetSubscribeGroupListResponse struct { } type GetSubscribeListRequest struct { - Page int64 `form:"page" validate:"required"` - Size int64 `form:"size" validate:"required"` - GroupId int64 `form:"group_id,omitempty"` - Search string `form:"search,omitempty"` + Page int64 `form:"page" validate:"required"` + Size int64 `form:"size" validate:"required"` + Language string `form:"language,omitempty"` + Search string `form:"search,omitempty"` } type GetSubscribeListResponse struct { @@ -894,6 +996,10 @@ type GetSubscribeLogResponse struct { Total int64 `json:"total"` } +type GetSubscriptionRequest struct { + Language string `form:"language"` +} + type GetSubscriptionResponse struct { List []Subscribe `json:"list"` } @@ -952,11 +1058,6 @@ type GetUserLoginLogsResponse struct { Total int64 `json:"total"` } -type GetUserOnlineTimeStatisticsResponse struct { - WeeklyStats []WeeklyStat `json:"weekly_stats"` - ConnectionRecords ConnectionRecords `json:"connection_records"` -} - type GetUserSubscribeByIdRequest struct { Id int64 `form:"id" validate:"required"` } @@ -996,6 +1097,17 @@ type GetUserSubscribeLogsResponse struct { Total int64 `json:"total"` } +type GetUserSubscribeResetTrafficLogsRequest struct { + Page int `form:"page"` + Size int `form:"size"` + UserSubscribeId int64 `form:"user_subscribe_id"` +} + +type GetUserSubscribeResetTrafficLogsResponse struct { + List []ResetSubscribeTrafficLog `json:"list"` + Total int64 `json:"total"` +} + type GetUserSubscribeTrafficLogsRequest struct { Page int `form:"page"` Size int `form:"size"` @@ -1026,11 +1138,26 @@ type GetUserTicketListResponse struct { List []Ticket `json:"list"` } +type GiftLog struct { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + OrderNo string `json:"order_no"` + SubscribeId int64 `json:"subscribe_id"` + Amount int64 `json:"amount"` + Balance int64 `json:"balance"` + Remark string `json:"remark,omitempty"` + Timestamp int64 `json:"timestamp"` +} + type GoogleLoginCallbackRequest struct { Code string `form:"code"` State string `form:"state"` } +type HasMigrateSeverNodeResponse struct { + HasMigrate bool `json:"has_migrate"` +} + type Hysteria2 struct { Port int `json:"port" validate:"required"` HopPorts string `json:"hop_ports" validate:"required"` @@ -1053,20 +1180,39 @@ type LogResponse struct { List interface{} `json:"list"` } +type LogSetting struct { + AutoClear *bool `json:"auto_clear"` + ClearDays int64 `json:"clear_days"` +} + +type LoginLog struct { + UserId int64 `json:"user_id"` + Method string `json:"method"` + LoginIP string `json:"login_ip"` + UserAgent string `json:"user_agent"` + Success bool `json:"success"` + Timestamp int64 `json:"timestamp"` +} + type LoginResponse struct { Token string `json:"token"` } type MessageLog struct { - Id int64 `json:"id"` - Type string `json:"type"` - Platform string `json:"platform"` - To string `json:"to"` - Subject string `json:"subject"` - Content string `json:"content"` - Status int `json:"status"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + Id int64 `json:"id"` + Type uint8 `json:"type"` + Platform string `json:"platform"` + To string `json:"to"` + Subject string `json:"subject"` + Content interface{} `json:"content"` + Status uint8 `json:"status"` + CreatedAt int64 `json:"created_at"` +} + +type MigrateServerNodeResponse struct { + Succee uint64 `json:"succee"` + Fail uint64 `json:"fail"` + Message string `json:"message,omitempty"` } type MobileAuthenticateConfig struct { @@ -1075,10 +1221,44 @@ type MobileAuthenticateConfig struct { Whitelist []string `json:"whitelist"` } +type Node struct { + Id int64 `json:"id"` + Name string `json:"name"` + Tags []string `json:"tags"` + Port uint16 `json:"port"` + Address string `json:"address"` + ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` + Enabled *bool `json:"enabled"` + Sort int `json:"sort,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + type NodeConfig struct { - NodeSecret string `json:"node_secret"` - NodePullInterval int64 `json:"node_pull_interval"` - NodePushInterval int64 `json:"node_push_interval"` + NodeSecret string `json:"node_secret"` + NodePullInterval int64 `json:"node_pull_interval"` + NodePushInterval int64 `json:"node_push_interval"` + TrafficReportThreshold int64 `json:"traffic_report_threshold"` + IPStrategy string `json:"ip_strategy"` + DNS []NodeDNS `json:"dns"` + Block []string `json:"block"` + Outbound []NodeOutbound `json:"outbound"` +} + +type NodeDNS struct { + Proto string `json:"proto"` + Address string `json:"address"` + Domains []string `json:"domains"` +} + +type NodeOutbound struct { + Name string `json:"name"` + Protocol string `json:"protocol"` + Address string `json:"address"` + Port int64 `json:"port"` + Password string `json:"password"` + Rules []string `json:"rules"` } type NodeRelay struct { @@ -1087,18 +1267,6 @@ type NodeRelay struct { Prefix string `json:"prefix"` } -type NodeSortRequest struct { - Sort []SortItem `json:"sort"` -} - -type NodeStatus struct { - Online interface{} `json:"online"` - Cpu float64 `json:"cpu"` - Mem float64 `json:"mem"` - Disk float64 `json:"disk"` - UpdatedAt int64 `json:"updated_at"` -} - type OAthLoginRequest struct { Method string `json:"method" validate:"required"` // google, facebook, apple, telegram, github etc. Redirect string `json:"redirect"` @@ -1235,6 +1403,7 @@ type PortalPurchaseRequest struct { SubscribeId int64 `json:"subscribe_id"` Quantity int64 `json:"quantity"` Coupon string `json:"coupon,omitempty"` + InviteCode string `json:"invite_code,omitempty"` TurnstileToken string `json:"turnstile_token,omitempty"` } @@ -1280,10 +1449,73 @@ type PreUnsubscribeResponse struct { DeductionAmount int64 `json:"deduction_amount"` } +type PreViewNodeMultiplierResponse struct { + CurrentTime string `json:"current_time"` + Ratio float32 `json:"ratio"` +} + +type PreviewSubscribeTemplateRequest struct { + Id int64 `form:"id"` +} + +type PreviewSubscribeTemplateResponse struct { + Template string `json:"template"` // 预览的模板内容 +} + type PrivacyPolicyConfig struct { PrivacyPolicy string `json:"privacy_policy"` } +type Protocol struct { + Type string `json:"type"` + Port uint16 `json:"port"` + Enable bool `json:"enable"` + Security string `json:"security,omitempty"` + SNI string `json:"sni,omitempty"` + AllowInsecure bool `json:"allow_insecure,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + RealityServerAddr string `json:"reality_server_addr,omitempty"` + RealityServerPort int `json:"reality_server_port,omitempty"` + RealityPrivateKey string `json:"reality_private_key,omitempty"` + RealityPublicKey string `json:"reality_public_key,omitempty"` + RealityShortId string `json:"reality_short_id,omitempty"` + Transport string `json:"transport,omitempty"` + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + ServiceName string `json:"service_name,omitempty"` + Cipher string `json:"cipher,omitempty"` + ServerKey string `json:"server_key,omitempty"` + Flow string `json:"flow,omitempty"` + HopPorts string `json:"hop_ports,omitempty"` + HopInterval int `json:"hop_interval,omitempty"` + ObfsPassword string `json:"obfs_password,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ReduceRtt bool `json:"reduce_rtt,omitempty"` + UDPRelayMode string `json:"udp_relay_mode,omitempty"` + CongestionController string `json:"congestion_controller,omitempty"` + Multiplex string `json:"multiplex,omitempty"` // mux, eg: off/low/medium/high + PaddingScheme string `json:"padding_scheme,omitempty"` // padding scheme + UpMbps int `json:"up_mbps,omitempty"` // upload speed limit + DownMbps int `json:"down_mbps,omitempty"` // download speed limit + Obfs string `json:"obfs,omitempty"` // obfs, 'none', 'http', 'tls' + ObfsHost string `json:"obfs_host,omitempty"` // obfs host + ObfsPath string `json:"obfs_path,omitempty"` // obfs path + XhttpMode string `json:"xhttp_mode,omitempty"` // xhttp mode + XhttpExtra string `json:"xhttp_extra,omitempty"` // xhttp extra path + Encryption string `json:"encryption,omitempty"` // encryption,'none', 'mlkem768x25519plus' + EncryptionMode string `json:"encryption_mode,omitempty"` // encryption mode,'native', 'xorpub', 'random' + EncryptionRtt string `json:"encryption_rtt,omitempty"` // encryption rtt,'0rtt', '1rtt' + EncryptionTicket string `json:"encryption_ticket,omitempty"` // encryption ticket + EncryptionServerPadding string `json:"encryption_server_padding,omitempty"` // encryption server padding + EncryptionPrivateKey string `json:"encryption_private_key,omitempty"` // encryption private key + EncryptionClientPadding string `json:"encryption_client_padding,omitempty"` // encryption client padding + EncryptionPassword string `json:"encryption_password,omitempty"` // encryption password + Ratio float64 `json:"ratio,omitempty"` // Traffic ratio, default is 1 + CertMode string `json:"cert_mode,omitempty"` // Certificate mode, `none`|`http`|`dns`|`self` + CertDNSProvider string `json:"cert_dns_provider,omitempty"` // DNS provider for certificate + CertDNSEnv string `json:"cert_dns_env,omitempty"` // Environment for DNS provider +} + type PubilcRegisterConfig struct { StopRegister bool `json:"stop_register"` EnableIpRegisterLimit bool `json:"enable_ip_register_limit"` @@ -1297,7 +1529,7 @@ type PubilcVerifyCodeConfig struct { type PurchaseOrderRequest struct { SubscribeId int64 `json:"subscribe_id"` - Quantity int64 `json:"quantity"` + Quantity int64 `json:"quantity" validate:"required,gt=0"` Payment int64 `json:"payment,omitempty"` Coupon string `json:"coupon,omitempty"` } @@ -1327,6 +1559,10 @@ type QueryDocumentListResponse struct { List []Document `json:"list"` } +type QueryNodeTagResponse struct { + Tags []string `json:"tags"` +} + type QueryOrderDetailRequest struct { OrderNo string `form:"order_no" validate:"required"` } @@ -1363,11 +1599,64 @@ type QueryPurchaseOrderResponse struct { Token string `json:"token,omitempty"` } +type QueryQuotaTaskListRequest struct { + Page int `form:"page"` + Size int `form:"size"` + Status *uint8 `form:"status,omitempty"` +} + +type QueryQuotaTaskListResponse struct { + Total int64 `json:"total"` + List []QuotaTask `json:"list"` +} + +type QueryQuotaTaskPreCountRequest struct { + Subscribers []int64 `json:"subscribers"` + IsActive *bool `json:"is_active"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` +} + +type QueryQuotaTaskPreCountResponse struct { + Count int64 `json:"count"` +} + +type QueryQuotaTaskStatusRequest struct { + Id int64 `json:"id"` +} + +type QueryQuotaTaskStatusResponse struct { + Status uint8 `json:"status"` + Current int64 `json:"current"` + Total int64 `json:"total"` + Errors string `json:"errors"` +} + +type QueryServerConfigRequest struct { + ServerID int64 `path:"server_id"` + SecretKey string `form:"secret_key"` + Protocols []string `form:"protocols,omitempty"` +} + +type QueryServerConfigResponse struct { + TrafficReportThreshold int64 `json:"traffic_report_threshold"` + IPStrategy string `json:"ip_strategy"` + DNS []NodeDNS `json:"dns"` + Block []string `json:"block"` + Outbound []NodeOutbound `json:"outbound"` + Protocols []Protocol `json:"protocols"` + Total int64 `json:"total"` +} + type QuerySubscribeGroupListResponse struct { List []SubscribeGroup `json:"list"` Total int64 `json:"total"` } +type QuerySubscribeListRequest struct { + Language string `form:"language"` +} + type QuerySubscribeListResponse struct { List []Subscribe `json:"list"` Total int64 `json:"total"` @@ -1389,8 +1678,8 @@ type QueryUserAffiliateListResponse struct { } type QueryUserBalanceLogListResponse struct { - List []UserBalanceLog `json:"list"` - Total int64 `json:"total"` + List []BalanceLog `json:"list"` + Total int64 `json:"total"` } type QueryUserCommissionLogListRequest struct { @@ -1408,8 +1697,23 @@ type QueryUserSubscribeListResponse struct { Total int64 `json:"total"` } -type QueryUserSubscribeResp struct { - Data []UserSubscribeData `json:"data"` +type QuotaTask struct { + Id int64 `json:"id"` + Subscribers []int64 `json:"subscribers"` + IsActive *bool `json:"is_active"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + ResetTraffic bool `json:"reset_traffic"` + Days uint64 `json:"days"` + GiftType uint8 `json:"gift_type"` + GiftValue uint64 `json:"gift_value"` + Objects []int64 `json:"objects"` // UserSubscribe IDs + Status uint8 `json:"status"` + Total int64 `json:"total"` + Current int64 `json:"current"` + Errors string `json:"errors"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } type RechargeOrderRequest struct { @@ -1432,6 +1736,15 @@ type RegisterConfig struct { IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"` } +type RegisterLog struct { + UserId int64 `json:"user_id"` + AuthMethod string `json:"auth_method"` + Identifier string `json:"identifier"` + RegisterIP string `json:"register_ip"` + UserAgent string `json:"user_agent"` + Timestamp int64 `json:"timestamp"` +} + type RenewalOrderRequest struct { UserSubscribeID int64 `json:"user_subscribe_id"` Quantity int64 `json:"quantity"` @@ -1444,12 +1757,34 @@ type RenewalOrderResponse struct { } type ResetPasswordRequest struct { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` +} + +type ResetSortRequest struct { + Sort []SortItem `json:"sort"` +} + +type ResetSubscribeLog struct { + Type uint16 `json:"type"` + UserId int64 `json:"user_id"` + UserSubscribeId int64 `json:"user_subscribe_id"` + OrderNo string `json:"order_no,omitempty"` + Timestamp int64 `json:"timestamp"` +} + +type ResetSubscribeTrafficLog struct { + Id int64 `json:"id"` + Type uint16 `json:"type"` + UserSubscribeId int64 `json:"user_subscribe_id"` + OrderNo string `json:"order_no,omitempty"` + Timestamp int64 `json:"timestamp"` } type ResetTrafficOrderRequest struct { @@ -1499,24 +1834,17 @@ type SendSmsCodeRequest struct { } type Server struct { - Id int64 `json:"id"` - Tags []string `json:"tags"` - Country string `json:"country"` - City string `json:"city"` - Name string `json:"name"` - ServerAddr string `json:"server_addr"` - RelayMode string `json:"relay_mode"` - RelayNode []NodeRelay `json:"relay_node"` - SpeedLimit int `json:"speed_limit"` - TrafficRatio float32 `json:"traffic_ratio"` - GroupId int64 `json:"group_id"` - Protocol string `json:"protocol"` - Config interface{} `json:"config"` - Enable *bool `json:"enable"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` - Status *NodeStatus `json:"status"` - Sort int64 `json:"sort"` + Id int64 `json:"id"` + Name string `json:"name"` + Country string `json:"country"` + City string `json:"city"` + Address string `json:"address"` + Sort int `json:"sort"` + Protocols []Protocol `json:"protocols"` + LastReportedAt int64 `json:"last_reported_at"` + Status ServerStatus `json:"status"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } type ServerBasic struct { @@ -1538,6 +1866,20 @@ type ServerGroup struct { UpdatedAt int64 `json:"updated_at"` } +type ServerOnlineIP struct { + IP string `json:"ip"` + Protocol string `json:"protocol"` +} + +type ServerOnlineUser struct { + IP []ServerOnlineIP `json:"ip"` + UserId int64 `json:"user_id"` + Subscribe string `json:"subscribe"` + SubscribeId int64 `json:"subscribe_id"` + Traffic int64 `json:"traffic"` + ExpiredAt int64 `json:"expired_at"` +} + type ServerPushStatusRequest struct { ServerCommon Cpu float64 `json:"cpu"` @@ -1555,15 +1897,26 @@ type ServerRuleGroup struct { Id int64 `json:"id"` Icon string `json:"icon"` Name string `json:"name" validate:"required"` + Type string `json:"type"` Tags []string `json:"tags"` Rules string `json:"rules"` Enable bool `json:"enable"` + Default bool `json:"default"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } +type ServerStatus struct { + Cpu float64 `json:"cpu"` + Mem float64 `json:"mem"` + Disk float64 `json:"disk"` + Protocol string `json:"protocol"` + Online []ServerOnlineUser `json:"online"` + Status string `json:"status"` +} + type ServerTotalDataResponse struct { - OnlineUserIPs int64 `json:"online_user_ips"` + OnlineUsers int64 `json:"online_users"` OnlineServers int64 `json:"online_servers"` OfflineServers int64 `json:"offline_servers"` TodayUpload int64 `json:"today_upload"` @@ -1584,6 +1937,15 @@ type ServerTrafficData struct { Download int64 `json:"download"` } +type ServerTrafficLog struct { + ServerId int64 `json:"server_id"` // Server ID + Upload int64 `json:"upload"` // Upload traffic in bytes + Download int64 `json:"download"` // Download traffic in bytes + Total int64 `json:"total"` // Total traffic in bytes (Upload + Download) + Date string `json:"date"` // Date in YYYY-MM-DD format + Details bool `json:"details"` // Whether to show detailed traffic +} + type ServerUser struct { Id int64 `json:"id"` UUID string `json:"uuid"` @@ -1627,6 +1989,10 @@ type SortItem struct { Sort int64 `json:"sort" validate:"required"` } +type StopBatchSendEmailTaskRequest struct { + Id int64 `json:"id"` +} + type StripePayment struct { Method string `json:"method"` ClientSecret string `json:"client_secret"` @@ -1636,6 +2002,7 @@ type StripePayment struct { type Subscribe struct { 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"` @@ -1646,9 +2013,8 @@ type Subscribe struct { SpeedLimit int64 `json:"speed_limit"` DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` - GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show bool `json:"show"` Sell bool `json:"sell"` Sort int64 `json:"sort"` @@ -1660,11 +2026,38 @@ type Subscribe struct { UpdatedAt int64 `json:"updated_at"` } +type SubscribeApplication struct { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + UserAgent string `json:"user_agent"` + IsDefault bool `json:"is_default"` + SubscribeTemplate string `json:"template"` + OutputFormat string `json:"output_format"` + DownloadLink DownloadLink `json:"download_link,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type SubscribeClient struct { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + IsDefault bool `json:"is_default"` + DownloadLink DownloadLink `json:"download_link,omitempty"` +} + type SubscribeConfig struct { SingleModel bool `json:"single_model"` SubscribePath string `json:"subscribe_path"` SubscribeDomain string `json:"subscribe_domain"` PanDomain bool `json:"pan_domain"` + UserAgentLimit bool `json:"user_agent_limit"` + UserAgentList string `json:"user_agent_list"` } type SubscribeDiscount struct { @@ -1685,6 +2078,15 @@ type SubscribeItem struct { Sold int64 `json:"sold"` } +type SubscribeLog struct { + UserId int64 `json:"user_id"` + Token string `json:"token"` + UserAgent string `json:"user_agent"` + ClientIP string `json:"client_ip"` + UserSubscribeId int64 `json:"user_subscribe_id"` + Timestamp int64 `json:"timestamp"` +} + type SubscribeSortRequest struct { Sort []SortItem `json:"sort"` } @@ -1710,30 +2112,39 @@ type TelephoneCheckUserResponse struct { } type TelephoneLoginRequest struct { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneCode string `json:"telephone_code"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password"` IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` CfToken string `json:"cf_token,optional"` } type TelephoneRegisterRequest struct { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` Invite string `json:"invite,optional"` Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } type TelephoneResetPasswordRequest struct { + Identifier string `json:"identifier"` Telephone string `json:"telephone" validate:"required"` TelephoneAreaCode string `json:"telephone_area_code" validate:"required"` Password string `json:"password" validate:"required"` Code string `json:"code,optional"` IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type,optional"` CfToken string `json:"cf_token,optional"` } @@ -1767,6 +2178,11 @@ type TimePeriod struct { Multiplier float32 `json:"multiplier"` } +type ToggleNodeStatusRequest struct { + Id int64 `json:"id"` + Enable *bool `json:"enable"` +} + type TosConfig struct { TosContent string `json:"tos_content"` } @@ -1781,6 +2197,16 @@ type TrafficLog struct { Timestamp int64 `json:"timestamp"` } +type TrafficLogDetails struct { + Id int64 `json:"id"` + ServerId int64 `json:"server_id"` + UserId int64 `json:"user_id"` + SubscribeId int64 `json:"subscribe_id"` + Download int64 `json:"download"` + Upload int64 `json:"upload"` + Timestamp int64 `json:"timestamp"` +} + type TransportConfig struct { Path string `json:"path"` Host string `json:"host"` @@ -1805,8 +2231,16 @@ type TrojanProtocol struct { } type Tuic struct { - Port int `json:"port" validate:"required"` - SecurityConfig SecurityConfig `json:"security_config"` + Port int `json:"port" validate:"required"` + DisableSNI bool `json:"disable_sni"` + ReduceRtt bool `json:"reduce_rtt"` + UDPRelayMode string `json:"udp_relay_mode"` + CongestionController string `json:"congestion_controller"` + SecurityConfig SecurityConfig `json:"security_config"` +} + +type UnbindDeviceRequest struct { + Id int64 `json:"id" validate:"required"` } type UnbindOAuthRequest struct { @@ -1843,25 +2277,6 @@ type UpdateAnnouncementRequest struct { Popup *bool `json:"popup"` } -type UpdateApplicationRequest struct { - Id int64 `json:"id" validate:"required"` - Icon string `json:"icon"` - Name string `json:"name"` - Description string `json:"description"` - SubscribeType string `json:"subscribe_type"` - Platform ApplicationPlatform `json:"platform"` -} - -type UpdateApplicationVersionRequest struct { - Id int64 `json:"id" validate:"required"` - Url string `json:"url"` - Version string `json:"version" validate:"required"` - Description string `json:"description"` - Platform string `json:"platform" validate:"required,oneof=windows mac linux android ios harmony"` - IsDefault bool `json:"is_default"` - ApplicationId int64 `json:"application_id" validate:"required"` -} - type UpdateAuthMethodConfigRequest struct { Id int64 `json:"id"` Method string `json:"method"` @@ -1902,28 +2317,15 @@ type UpdateDocumentRequest struct { Show *bool `json:"show"` } -type UpdateNodeGroupRequest struct { - Id int64 `json:"id" validate:"required"` - Name string `json:"name" validate:"required"` - Description string `json:"description"` -} - type UpdateNodeRequest struct { - Id int64 `json:"id" validate:"required"` - Tags []string `json:"tags"` - Country string `json:"country"` - City string `json:"city"` - Name string `json:"name" validate:"required"` - ServerAddr string `json:"server_addr" validate:"required"` - RelayMode string `json:"relay_mode"` - RelayNode []NodeRelay `json:"relay_node"` - SpeedLimit int `json:"speed_limit"` - TrafficRatio float32 `json:"traffic_ratio"` - GroupId int64 `json:"group_id"` - Protocol string `json:"protocol" validate:"required"` - Config interface{} `json:"config" validate:"required"` - Enable *bool `json:"enable"` - Sort int64 `json:"sort"` + Id int64 `json:"id"` + Name string `json:"name"` + Tags []string `json:"tags,omitempty"` + Port uint16 `json:"port"` + Address string `json:"address"` + ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` + Enabled *bool `json:"enabled"` } type UpdateOrderStatusRequest struct { @@ -1933,11 +2335,6 @@ type UpdateOrderStatusRequest struct { TradeNo string `json:"trade_no,omitempty"` } -type UpdatePasswordRequeset struct { - Password string `json:"password"` - NewPassword string `json:"new_password"` -} - type UpdatePaymentMethodRequest struct { Id int64 `json:"id" validate:"required"` Name string `json:"name" validate:"required"` @@ -1952,13 +2349,27 @@ type UpdatePaymentMethodRequest struct { Enable *bool `json:"enable" validate:"required"` } -type UpdateRuleGroupRequest struct { - Id int64 `json:"id" validate:"required"` - Icon string `json:"icon"` - Name string `json:"name" validate:"required"` - Tags []string `json:"tags"` - Rules string `json:"rules"` - Enable bool `json:"enable"` +type UpdateServerRequest struct { + Id int64 `json:"id"` + Name string `json:"name"` + Country string `json:"country,omitempty"` + City string `json:"city,omitempty"` + Address string `json:"address"` + Sort int `json:"sort,omitempty"` + Protocols []Protocol `json:"protocols"` +} + +type UpdateSubscribeApplicationRequest struct { + Id int64 `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Scheme string `json:"scheme,omitempty"` + UserAgent string `json:"user_agent"` + IsDefault bool `json:"is_default"` + SubscribeTemplate string `json:"template"` + OutputFormat string `json:"output_format"` + DownloadLink DownloadLink `json:"download_link,omitempty"` } type UpdateSubscribeGroupRequest struct { @@ -1970,6 +2381,7 @@ type UpdateSubscribeGroupRequest struct { type UpdateSubscribeRequest struct { 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"` @@ -1980,9 +2392,8 @@ type UpdateSubscribeRequest struct { SpeedLimit int64 `json:"speed_limit"` DeviceLimit int64 `json:"device_limit"` Quota int64 `json:"quota"` - GroupId int64 `json:"group_id"` - ServerGroup []int64 `json:"server_group"` - Server []int64 `json:"server"` + Nodes []int64 `json:"nodes"` + NodeTags []string `json:"node_tags"` Show *bool `json:"show"` Sell *bool `json:"sell"` Sort int64 `json:"sort"` @@ -2004,17 +2415,19 @@ type UpdateUserAuthMethodRequest struct { } type UpdateUserBasiceInfoRequest struct { - UserId int64 `json:"user_id" validate:"required"` - Password string `json:"password"` - Avatar string `json:"avatar"` - Balance int64 `json:"balance"` - Commission int64 `json:"commission"` - GiftAmount int64 `json:"gift_amount"` - Telegram int64 `json:"telegram"` - ReferCode string `json:"refer_code"` - RefererId int64 `json:"referer_id"` - Enable bool `json:"enable"` - IsAdmin bool `json:"is_admin"` + UserId int64 `json:"user_id" validate:"required"` + Password string `json:"password"` + Avatar string `json:"avatar"` + Balance int64 `json:"balance"` + Commission int64 `json:"commission"` + ReferralPercentage uint8 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` + GiftAmount int64 `json:"gift_amount"` + Telegram int64 `json:"telegram"` + ReferCode string `json:"refer_code"` + RefererId int64 `json:"referer_id"` + Enable bool `json:"enable"` + IsAdmin bool `json:"is_admin"` } type UpdateUserNotifyRequest struct { @@ -2055,6 +2468,8 @@ type User struct { Avatar string `json:"avatar"` Balance int64 `json:"balance"` Commission int64 `json:"commission"` + ReferralPercentage uint8 `json:"referral_percentage"` + OnlyFirstPurchase bool `json:"only_first_purchase"` GiftAmount int64 `json:"gift_amount"` Telegram int64 `json:"telegram"` ReferCode string `json:"refer_code"` @@ -2086,16 +2501,6 @@ type UserAuthMethod struct { Verified bool `json:"verified"` } -type UserBalanceLog struct { - Id int64 `json:"id"` - UserId int64 `json:"user_id"` - Amount int64 `json:"amount"` - Type uint8 `json:"type"` - OrderId int64 `json:"order_id"` - Balance int64 `json:"balance"` - CreatedAt int64 `json:"created_at"` -} - type UserDevice struct { Id int64 `json:"id"` Ip string `json:"ip"` @@ -2107,44 +2512,35 @@ type UserDevice struct { UpdatedAt int64 `json:"updated_at"` } -type UserInfoResponse struct { - Id int64 `json:"id"` - Balance int64 `json:"balance"` - Email string `json:"email"` - RefererId int64 `json:"referer_id"` - ReferCode string `json:"refer_code"` - Avatar string `json:"avatar"` - AreaCode string `json:"area_code"` - Telephone string `json:"telephone"` - Devices []UserDevice `json:"devices"` - AuthMethods []UserAuthMethod `json:"auth_methods"` -} - type UserLoginLog struct { Id int64 `json:"id"` UserId int64 `json:"user_id"` LoginIP string `json:"login_ip"` UserAgent string `json:"user_agent"` Success bool `json:"success"` - CreatedAt int64 `json:"created_at"` + Timestamp int64 `json:"timestamp"` } type UserLoginRequest struct { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } type UserRegisterRequest struct { - Email string `json:"email" validate:"required"` - Password string `json:"password" validate:"required"` - Invite string `json:"invite,optional"` - Code string `json:"code,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` + Invite string `json:"invite,optional"` + Code string `json:"code,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } type UserStatistics struct { @@ -2180,11 +2576,6 @@ type UserSubscribe struct { UpdatedAt int64 `json:"updated_at"` } -type UserSubscribeData struct { - SubscribeId int64 `json:"subscribe_id"` - UserSubscribeId int64 `json:"user_subscribe_id"` -} - type UserSubscribeDetail struct { Id int64 `json:"id"` UserId int64 `json:"user_id"` @@ -2211,15 +2602,17 @@ type UserSubscribeLog struct { Token string `json:"token"` IP string `json:"ip"` UserAgent string `json:"user_agent"` - CreatedAt int64 `json:"created_at"` + Timestamp int64 `json:"timestamp"` } -type UserSubscribeResetPeriodRequest struct { - UserSubscribeId int64 `json:"user_subscribe_id"` -} - -type UserSubscribeResetPeriodResponse struct { - Status bool `json:"status"` +type UserSubscribeTrafficLog struct { + SubscribeId int64 `json:"subscribe_id"` // Subscribe ID + UserId int64 `json:"user_id"` // User ID + Upload int64 `json:"upload"` // Upload traffic in bytes + Download int64 `json:"download"` // Download traffic in bytes + Total int64 `json:"total"` // Total traffic in bytes (Upload + Download) + Date string `json:"date"` // Date in YYYY-MM-DD format + Details bool `json:"details"` // Whether to show detailed traffic } type UserTraffic struct { @@ -2260,6 +2653,10 @@ type VerifyEmailRequest struct { Code string `json:"code" validate:"required"` } +type VersionResponse struct { + Version string `json:"version"` +} + type Vless struct { Port int `json:"port" validate:"required"` Flow string `json:"flow" validate:"required"` @@ -2295,9 +2692,3 @@ type VmessProtocol struct { Network string `json:"network"` Transport string `json:"transport"` } - -type WeeklyStat struct { - Day int `json:"day"` - DayName string `json:"day_name"` - Hours float64 `json:"hours"` -} diff --git a/pkg/adapter/adapter.go b/pkg/adapter/adapter.go deleted file mode 100644 index aaec788..0000000 --- a/pkg/adapter/adapter.go +++ /dev/null @@ -1,71 +0,0 @@ -package adapter - -import ( - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/pkg/adapter/clash" - "github.com/perfect-panel/ppanel-server/pkg/adapter/general" - "github.com/perfect-panel/ppanel-server/pkg/adapter/loon" - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - "github.com/perfect-panel/ppanel-server/pkg/adapter/quantumultx" - "github.com/perfect-panel/ppanel-server/pkg/adapter/shadowrocket" - "github.com/perfect-panel/ppanel-server/pkg/adapter/singbox" - "github.com/perfect-panel/ppanel-server/pkg/adapter/surfboard" -) - -type Adapter struct { - proxy.Adapter -} - -func NewAdapter(nodes []*server.Server, rules []*server.RuleGroup) *Adapter { - // 转换服务器列表 - proxies := adapterProxies(nodes) - // 生成代理组 - proxyGroup, region := generateProxyGroup(proxies) - // 转换规则组 - g, r := adapterRules(rules) - // 加入兜底节点 - for i, group := range g { - if len(group.Proxies) == 0 { - g[i].Proxies = append([]string{"DIRECT"}, region...) - } - } - // 合并代理组 - proxyGroup = append(proxyGroup, g...) - return &Adapter{ - Adapter: proxy.Adapter{ - Proxies: proxies, - Group: proxyGroup, - Rules: r, - Region: region, - }, - } -} - -func (m *Adapter) BuildClash(uuid string) ([]byte, error) { - client := clash.NewClash(m.Adapter) - return client.Build(uuid) -} - -func (m *Adapter) BuildGeneral(uuid string) []byte { - return general.GenerateBase64General(m.Proxies, uuid) -} - -func (m *Adapter) BuildLoon(uuid string) []byte { - return loon.BuildLoon(m.Proxies, uuid) -} - -func (m *Adapter) BuildQuantumultX(uuid string) string { - return quantumultx.BuildQuantumultX(m.Proxies, uuid) -} - -func (m *Adapter) BuildSingbox(uuid string) ([]byte, error) { - return singbox.BuildSingbox(m.Adapter, uuid) -} - -func (m *Adapter) BuildShadowrocket(uuid string, userInfo shadowrocket.UserInfo) []byte { - return shadowrocket.BuildShadowrocket(m.Proxies, uuid, userInfo) -} - -func (m *Adapter) BuildSurfboard(siteName string, user surfboard.UserInfo) []byte { - return surfboard.BuildSurfboard(m.Adapter, siteName, user) -} diff --git a/pkg/adapter/adapter_test.go b/pkg/adapter/adapter_test.go deleted file mode 100644 index 25c5a64..0000000 --- a/pkg/adapter/adapter_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package adapter - -import ( - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/pkg/adapter/surfboard" -) - -func createTestServer() []*server.Server { - c := server.Shadowsocks{ - Method: "aes-256-gcm", - Port: 10301, - ServerKey: "", - } - data, _ := json.Marshal(c) - - relays := creatRelayNode() - relay, _ := json.Marshal(relays) - enable := true - // 创建一个测试用的服务器列表 - return []*server.Server{ - { - Id: 1, - Name: "Test Server 1", - Tags: "", - Country: "CN", - City: "", - Latitude: "", - Longitude: "", - ServerAddr: "test1.example.com", - RelayMode: "random", - RelayNode: string(relay), - SpeedLimit: 0, - TrafficRatio: 0, - GroupId: 0, - Protocol: "shadowsocks", - Config: string(data), - Enable: &enable, - Sort: 0, - }, - } -} -func creatRelayNode() []*server.NodeRelay { - var nodes []*server.NodeRelay - for i := 0; i < 10; i++ { - port := 10301 + i - c := server.NodeRelay{ - Host: fmt.Sprintf("192.168.1.%d", i), - Port: port, - Prefix: fmt.Sprintf("relay-%d", i), - } - nodes = append(nodes, &c) - } - return nodes -} - -func TestNewAdapter(t *testing.T) { - nodes := createTestServer() - - rules := []*server.RuleGroup{ - { - Name: "Test Rule Group 1", - Tags: "", - Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1", - }, - } - - adapter := NewAdapter(nodes, rules) - bytes, err := adapter.BuildClash("some-uuid") - if err != nil { - t.Errorf("Failed to build adapter: %v", err) - return - } - t.Logf("Adapter built successfully: %s", string(bytes)) -} - -func TestAdapter_BuildSingbox(t *testing.T) { - nodes := createTestServer() - - rules := []*server.RuleGroup{ - { - Name: "Test Rule Group 1", - Tags: "", - Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1", - }, - } - - adapter := NewAdapter(nodes, rules) - bytes, err := adapter.BuildSingbox("some-uuid") - if err != nil { - t.Errorf("Failed to build adapter: %v", err) - return - } - var pretty map[string]interface{} - _ = json.Unmarshal(bytes, &pretty) - - if pretty == nil { - t.Errorf("Failed to parse Singbox config") - return - } - - prettyStr, err := json.MarshalIndent(pretty, "", " ") - if err != nil { - t.Errorf("Failed to format Singbox config: %v", err) - return - } - t.Logf("Adapter built successfully: \n %s", string(prettyStr)) -} - -func TestAdapter_BuildSurfboard(t *testing.T) { - nodes := createTestServer() - rules := []*server.RuleGroup{ - { - Name: "Test Rule Group 1", - Tags: "", - Rules: "DOMAIN-SUFFIX,example.com,Test Rule Group 1", - }, - } - adapter := NewAdapter(nodes, rules) - user := surfboard.UserInfo{ - UUID: "some-uuid", - Upload: 200, - Download: 13012, - TotalTraffic: 1024000, - ExpiredDate: time.Now().Add(24 * time.Hour), - SubscribeURL: "", - } - bytes := adapter.BuildSurfboard("test-site", user) - if bytes == nil { - t.Errorf("Failed to build adapter") - return - } - t.Logf("Adapter built successfully: %s", string(bytes)) -} diff --git a/pkg/adapter/clash/clash.go b/pkg/adapter/clash/clash.go deleted file mode 100644 index 52bcf61..0000000 --- a/pkg/adapter/clash/clash.go +++ /dev/null @@ -1,68 +0,0 @@ -package clash - -import ( - "fmt" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "gopkg.in/yaml.v3" -) - -type Clash struct { - proxy.Adapter -} - -func NewClash(adapter proxy.Adapter) *Clash { - return &Clash{ - Adapter: adapter, - } -} - -func (c *Clash) Build(uuid string) ([]byte, error) { - var proxies []Proxy - for _, v := range c.Proxies { - p, err := c.parseProxy(v, uuid) - if err != nil { - logger.Errorf("Failed to parse proxy for %s: %s", v.Name, err.Error()) - continue - } - proxies = append(proxies, *p) - } - var rawConfig RawConfig - if err := yaml.Unmarshal([]byte(DefaultTemplate), &rawConfig); err != nil { - return nil, fmt.Errorf("failed to unmarshal template: %w", err) - } - rawConfig.Proxies = proxies - // generate proxy groups - var groups []ProxyGroup - for _, group := range c.Group { - groups = append(groups, ProxyGroup{ - Name: group.Name, - Type: string(group.Type), - Proxies: group.Proxies, - Url: group.URL, - Interval: group.Interval, - }) - } - rawConfig.ProxyGroups = groups - rawConfig.Rules = append(c.Rules, "# 最终规则", "MATCH,手动选择") - return yaml.Marshal(&rawConfig) -} - -func (c *Clash) parseProxy(p proxy.Proxy, uuid string) (*Proxy, error) { - parseFuncs := map[string]func(proxy.Proxy, string) (*Proxy, error){ - "shadowsocks": parseShadowsocks, - "trojan": parseTrojan, - "vless": parseVless, - "vmess": parseVmess, - "hysteria2": parseHysteria2, - "tuic": parseTuic, - } - - if parseFunc, exists := parseFuncs[p.Protocol]; exists { - return parseFunc(p, uuid) - } - - logger.Errorw("Unknown protocol", logger.Field("protocol", p.Protocol), logger.Field("server", p.Name)) - return nil, fmt.Errorf("unknown protocol: %s", p.Protocol) -} diff --git a/pkg/adapter/clash/clash_test.go b/pkg/adapter/clash/clash_test.go deleted file mode 100644 index 1ab8156..0000000 --- a/pkg/adapter/clash/clash_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package clash - -import ( - "testing" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - "github.com/stretchr/testify/assert" -) - -func TestClash_Build(t *testing.T) { - adapter := proxy.Adapter{ - Proxies: []proxy.Proxy{ - { - Name: "test-proxy", - Protocol: "shadowsocks", - Server: "1.2.3.4", - Port: 8388, - Option: proxy.Shadowsocks{ - Method: "aes-256-gcm", - }, - }, - }, - Group: []proxy.Group{ - { - Name: "test-group", - Type: "select", - Proxies: []string{"test-proxy"}, - }, - }, - Rules: []string{ - "DOMAIN-SUFFIX,example.com,DIRECT", - "GEOIP,CN,DIRECT", - "MATCH,DIRECT", - }, - } - clash := NewClash(adapter) - result, err := clash.Build("test-uuid") - assert.NoError(t, err) - assert.NotNil(t, result) - -} diff --git a/pkg/adapter/clash/default.go b/pkg/adapter/clash/default.go deleted file mode 100644 index 4766053..0000000 --- a/pkg/adapter/clash/default.go +++ /dev/null @@ -1,35 +0,0 @@ -package clash - -const DefaultTemplate = ` -mixed-port: 7890 -allow-lan: true -bind-address: "*" -mode: rule -log-level: info -external-controller: 127.0.0.1:9090 -global-client-fingerprint: chrome -unified-delay: true -geox-url: - mmdb: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@release/geoip.metadb" -dns: - enable: true - ipv6: true - enhanced-mode: fake-ip - fake-ip-range: 198.18.0.1/16 - use-hosts: true - default-nameserver: - - 120.53.53.53 - - 1.12.12.12 - nameserver: - - https://120.53.53.53/dns-query#skip-cert-verify=true - - tls://1.12.12.12#skip-cert-verify=true - proxy-server-nameserver: - - https://120.53.53.53/dns-query#skip-cert-verify=true - - tls://1.12.12.12#skip-cert-verify=true - -proxies: - -proxy-groups: - -rules: -` diff --git a/pkg/adapter/clash/model.go b/pkg/adapter/clash/model.go deleted file mode 100644 index 9f9925d..0000000 --- a/pkg/adapter/clash/model.go +++ /dev/null @@ -1,131 +0,0 @@ -package clash - -type RawConfig struct { - Port int `yaml:"port" json:"port"` - SocksPort int `yaml:"socks-port" json:"socks-port"` - RedirPort int `yaml:"redir-port" json:"redir-port"` - TProxyPort int `yaml:"tproxy-port" json:"tproxy-port"` - MixedPort int `yaml:"mixed-port" json:"mixed-port"` - AllowLan bool `yaml:"allow-lan" json:"allow-lan"` - Mode string `yaml:"mode" json:"mode"` - LogLevel string `yaml:"log-level" json:"log-level"` - ExternalController string `yaml:"external-controller" json:"external-controller"` - Secret string `yaml:"secret" json:"secret"` - Proxies []Proxy `yaml:"proxies" json:"proxies"` - ProxyGroups []ProxyGroup `yaml:"proxy-groups" json:"proxy-groups"` - Rules []string `yaml:"rules" json:"rule"` -} - -type Proxy struct { - // 基础数据 - Name string `yaml:"name"` - Type string `yaml:"type"` - Server string `yaml:"server"` - Port int `yaml:"port,omitempty"` - // Shadowsocks - Password string `yaml:"password,omitempty"` - Cipher string `yaml:"cipher,omitempty"` - UDP bool `yaml:"udp,omitempty"` - Plugin string `yaml:"plugin,omitempty"` - PluginOpts map[string]any `yaml:"plugin-opts,omitempty"` - UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"` - UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"` - ClientFingerprint string `yaml:"client-fingerprint,omitempty"` - // Vmess - UUID string `yaml:"uuid,omitempty"` - AlterID *int `yaml:"alterId,omitempty"` - Network string `yaml:"network,omitempty"` - TLS bool `yaml:"tls,omitempty"` - ALPN []string `yaml:"alpn,omitempty"` - SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"` - Fingerprint string `yaml:"fingerprint,omitempty"` - ServerName string `yaml:"servername,omitempty"` - RealityOpts RealityOptions `yaml:"reality-opts,omitempty"` - HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"` - HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"` - GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"` - WSOpts WSOptions `yaml:"ws-opts,omitempty"` - PacketAddr bool `yaml:"packet-addr,omitempty"` - XUDP bool `yaml:"xudp,omitempty"` - PacketEncoding string `yaml:"packet-encoding,omitempty"` - GlobalPadding bool `yaml:"global-padding,omitempty"` - AuthenticatedLength bool `yaml:"authenticated-length,omitempty"` - // Vless - Flow string `yaml:"flow,omitempty"` - WSPath string `yaml:"ws-path,omitempty"` - WSHeaders map[string]string `yaml:"ws-headers,omitempty"` - // Trojan - SNI string `yaml:"sni,omitempty"` - SSOpts TrojanSSOption `yaml:"ss-opts,omitempty"` - // Hysteria2 - Ports string `yaml:"ports,omitempty"` - HopInterval int `yaml:"hop-interval,omitempty"` - Up string `yaml:"up,omitempty"` - Down string `yaml:"down,omitempty"` - Obfs string `yaml:"obfs,omitempty"` - ObfsPassword string `yaml:"obfs-password,omitempty"` - CustomCA string `yaml:"ca,omitempty"` - CustomCAString string `yaml:"ca-str,omitempty"` - CWND int `yaml:"cwnd,omitempty"` - UdpMTU int `yaml:"udp-mtu,omitempty"` - // Tuic - Token string `yaml:"token,omitempty"` - Ip string `yaml:"ip,omitempty"` - HeartbeatInterval int `yaml:"heartbeat-interval,omitempty"` - ReduceRtt bool `yaml:"reduce-rtt,omitempty"` - RequestTimeout int `yaml:"request-timeout,omitempty"` - UdpRelayMode string `yaml:"udp-relay-mode,omitempty"` - CongestionController string `yaml:"congestion-controller,omitempty"` - DisableSni bool `yaml:"disable-sni,omitempty"` - MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size,omitempty"` - FastOpen bool `yaml:"fast-open,omitempty"` - MaxOpenStreams int `yaml:"max-open-streams,omitempty"` - ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"` - ReceiveWindow int `yaml:"recv-window,omitempty"` - DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"` - MaxDatagramFrameSize int `yaml:"max-datagram-frame-size,omitempty"` - UDPOverStream bool `yaml:"udp-over-stream,omitempty"` - UDPOverStreamVersion int `yaml:"udp-over-stream-version,omitempty"` -} -type ProxyGroup struct { - Name string `yaml:"name"` - Type string `yaml:"type"` - Proxies []string `yaml:"proxies"` - Url string `yaml:"url,omitempty"` - Interval int `yaml:"interval,omitempty"` -} - -type TrojanSSOption struct { - Enabled bool `yaml:"enabled,omitempty"` - Method string `yaml:"method,omitempty"` - Password string `yaml:"password,omitempty"` -} - -type RealityOptions struct { - PublicKey string `yaml:"public-key"` - ShortID string `yaml:"short-id"` -} - -type HTTPOptions struct { - Method string `yaml:"method,omitempty"` - Path []string `yaml:"path,omitempty"` - Headers map[string][]string `yaml:"headers,omitempty"` -} - -type HTTP2Options struct { - Host []string `yaml:"host,omitempty"` - Path string `yaml:"path,omitempty"` -} - -type GrpcOptions struct { - GrpcServiceName string `yaml:"grpc-service-name,omitempty"` -} - -type WSOptions struct { - Path string `yaml:"path,omitempty"` - Headers map[string]string `yaml:"headers,omitempty"` - MaxEarlyData int `yaml:"max-early-data,omitempty"` - EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"` - V2rayHttpUpgrade bool `yaml:"v2ray-http-upgrade,omitempty"` - V2rayHttpUpgradeFastOpen bool `yaml:"v2ray-http-upgrade-fast-open,omitempty"` -} diff --git a/pkg/adapter/clash/parse.go b/pkg/adapter/clash/parse.go deleted file mode 100644 index a52c331..0000000 --- a/pkg/adapter/clash/parse.go +++ /dev/null @@ -1,165 +0,0 @@ -package clash - -import ( - "fmt" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func parseShadowsocks(s proxy.Proxy, uuid string) (*Proxy, error) { - config, ok := s.Option.(proxy.Shadowsocks) - if !ok { - return nil, fmt.Errorf("invalid type for Shadowsocks") - } - p := &Proxy{ - Name: s.Name, - Type: "ss", - Server: s.Server, - Port: s.Port, - Cipher: config.Method, - Password: uuid, - UDP: true, - } - - return p, nil -} - -func parseTrojan(data proxy.Proxy, password string) (*Proxy, error) { - trojan, ok := data.Option.(proxy.Trojan) - if !ok { - return nil, fmt.Errorf("invalid type for Trojan") - } - p := &Proxy{ - Name: data.Name, - Type: "trojan", - Server: data.Server, - Port: data.Port, - Password: password, - SNI: trojan.SecurityConfig.SNI, - SkipCertVerify: trojan.SecurityConfig.AllowInsecure, - } - setTransportOptions(p, trojan.Transport, trojan.TransportConfig) - return p, nil -} - -func parseVless(data proxy.Proxy, uuid string) (*Proxy, error) { - vless, ok := data.Option.(proxy.Vless) - if !ok { - return nil, fmt.Errorf("invalid type for Vless") - } - p := &Proxy{ - Name: data.Name, - Type: "vless", - Server: data.Server, - Port: data.Port, - UUID: uuid, - Flow: vless.Flow, - } - setSecurityOptions(p, vless.Security, vless.SecurityConfig) - clashTransport(p, vless.Transport, vless.TransportConfig) - return p, nil -} - -func parseVmess(data proxy.Proxy, uuid string) (*Proxy, error) { - vmess, ok := data.Option.(proxy.Vmess) - if !ok { - return nil, fmt.Errorf("invalid type for Vmess") - } - alterID := 0 - p := &Proxy{ - Name: data.Name, - Type: "vmess", - Server: data.Server, - Port: data.Port, - UUID: uuid, - AlterID: &alterID, - Cipher: "auto", - } - setSecurityOptions(p, vmess.Security, vmess.SecurityConfig) - clashTransport(p, vmess.Transport, vmess.TransportConfig) - return p, nil -} - -func parseHysteria2(data proxy.Proxy, uuid string) (*Proxy, error) { - hysteria2, ok := data.Option.(proxy.Hysteria2) - if !ok { - return nil, fmt.Errorf("invalid type for Hysteria2") - } - p := &Proxy{ - Name: data.Name, - Type: "hysteria2", - Server: data.Server, - Port: data.Port, - Ports: hysteria2.HopPorts, - Password: uuid, - HeartbeatInterval: hysteria2.HopInterval, - SkipCertVerify: hysteria2.SecurityConfig.AllowInsecure, - SNI: hysteria2.SecurityConfig.SNI, - } - if hysteria2.ObfsPassword != "" { - p.Obfs = "salamander" - p.ObfsPassword = hysteria2.ObfsPassword - } - - return p, nil -} - -func parseTuic(data proxy.Proxy, uuid string) (*Proxy, error) { - tuic, ok := data.Option.(proxy.Tuic) - if !ok { - return nil, fmt.Errorf("invalid type for Tuic") - } - p := &Proxy{ - Name: data.Name, - Type: "tuic", - Server: data.Server, - Port: data.Port, - UUID: uuid, - Password: uuid, - SNI: tuic.SecurityConfig.SNI, - SkipCertVerify: tuic.SecurityConfig.AllowInsecure, - } - - return p, nil -} - -func setSecurityOptions(p *Proxy, security string, config proxy.SecurityConfig) { - switch security { - case "tls": - p.TLS = true - p.ServerName = config.SNI - p.ClientFingerprint = config.Fingerprint - p.SkipCertVerify = config.AllowInsecure - case "reality": - p.TLS = true - p.ServerName = config.SNI - p.ClientFingerprint = config.Fingerprint - p.RealityOpts = RealityOptions{ - PublicKey: config.RealityPublicKey, - ShortID: config.RealityShortId, - } - p.SkipCertVerify = config.AllowInsecure - default: - p.TLS = false - } -} - -func setTransportOptions(p *Proxy, transport string, config proxy.TransportConfig) { - switch transport { - case "websocket": - p.Network = "ws" - p.WSOpts = WSOptions{ - Path: config.Path, - Headers: map[string]string{ - "Host": config.Host, - }, - } - case "grpc": - p.Network = "grpc" - p.GrpcOpts = GrpcOptions{ - GrpcServiceName: config.ServiceName, - } - default: - p.Network = "tcp" - } -} diff --git a/pkg/adapter/clash/tool.go b/pkg/adapter/clash/tool.go deleted file mode 100644 index e4c989e..0000000 --- a/pkg/adapter/clash/tool.go +++ /dev/null @@ -1,33 +0,0 @@ -package clash - -import "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - -func clashTransport(c *Proxy, transportType string, transportConfig proxy.TransportConfig) { - - switch transportType { - case "websocket", "httpupgrade": - if transportType == "websocket" { - c.Network = "ws" - } else { - c.Network = transportType - } - c.WSOpts = WSOptions{ - Path: transportConfig.Path, - Headers: map[string]string{}, - } - if transportConfig.Host != "" { - c.WSOpts.Headers["host"] = transportConfig.Host - } - if transportType == "httpupgrade" { - c.WSOpts.V2rayHttpUpgrade = true - } - case "grpc": - c.Network = "grpc" - c.GrpcOpts = GrpcOptions{ - GrpcServiceName: transportConfig.ServiceName, - } - case "tcp": - c.Network = "tcp" - } - -} diff --git a/pkg/adapter/general/uri.go b/pkg/adapter/general/uri.go deleted file mode 100644 index a5871fb..0000000 --- a/pkg/adapter/general/uri.go +++ /dev/null @@ -1,245 +0,0 @@ -package general - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "net" - "net/url" - "strconv" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -type v2rayShareLink struct { - Ps string `json:"ps"` - Add string `json:"add"` - Port string `json:"port"` - ID string `json:"id"` - Aid string `json:"aid"` - Net string `json:"net"` - Type string `json:"type"` - Host string `json:"host"` - SNI string `json:"sni"` - Path string `json:"path"` - TLS string `json:"tls"` - Flow string `json:"flow,omitempty"` - Alpn string `json:"alpn,omitempty"` - AllowInsecure bool `json:"allowInsecure"` - Fingerprint string `json:"fp,omitempty"` - PublicKey string `json:"pbk,omitempty"` - ShortId string `json:"sid,omitempty"` - SpiderX string `json:"spx,omitempty"` - V string `json:"v"` -} - -// GenerateBase64General will output node URLs split by '\n' and then encode into base64 -func GenerateBase64General(data []proxy.Proxy, uuid string) []byte { - var links []string - for _, v := range data { - p := buildProxy(v, uuid) - if p == "" { - continue - } - links = append(links, p) - } - var rsp []byte - rsp = base64.RawStdEncoding.AppendEncode(rsp, []byte(strings.Join(links, "\n"))) - return rsp -} - -func buildProxy(data proxy.Proxy, uuid string) string { - switch data.Protocol { - case "shadowsocks": - return ShadowsocksUri(data, uuid) - case "vmess": - return VmessUri(data, uuid) - case "vless": - return VlessUri(data, uuid) - case "trojan": - return TrojanUri(data, uuid) - case "hysteria2": - return Hysteria2Uri(data, uuid) - case "tuic": - return TuicUri(data, uuid) - default: - return "" - } -} - -func ShadowsocksUri(data proxy.Proxy, uuid string) string { - ss := data.Option.(proxy.Shadowsocks) - // sip002 - u := &url.URL{ - Scheme: "ss", - // 还没有写 2022 的 - User: url.User(strings.TrimSuffix(base64.URLEncoding.EncodeToString([]byte(ss.Method+":"+uuid)), "=")), - Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), - Fragment: data.Name, - } - return u.String() -} - -func VmessUri(data proxy.Proxy, uuid string) string { - vmess := data.Option.(proxy.Vmess) - - transport := vmess.TransportConfig - - securityConfig := vmess.SecurityConfig - - var s = v2rayShareLink{ - V: "2", - Add: data.Server, - Port: fmt.Sprint(data.Port), - ID: uuid, - Aid: "0", - Net: vmess.Transport, - // Type: "?", - Host: transport.Host, - Path: transport.Path, - } - - if vmess.Security == "tls" { - s.TLS = "tls" - s.SNI = securityConfig.SNI - s.AllowInsecure = securityConfig.AllowInsecure - s.Fingerprint = securityConfig.Fingerprint - } - b, _ := json.Marshal(s) - return "vmess://" + strings.TrimSuffix(base64.StdEncoding.EncodeToString(b), "=") -} - -func VlessUri(data proxy.Proxy, uuid string) string { - vless := data.Option.(proxy.Vless) - transportConfig := vless.TransportConfig - securityConfig := vless.SecurityConfig - - var query = make(url.Values) - setQuery(&query, "flow", vless.Flow) - setQuery(&query, "type", vless.Transport) - setQuery(&query, "security", vless.Security) - - switch vless.Transport { - case "ws", "http", "httpupgrade": - setQuery(&query, "path", transportConfig.Path) - setQuery(&query, "host", transportConfig.Host) - case "grpc": - setQuery(&query, "serviceName", transportConfig.ServiceName) - case "meek": - setQuery(&query, "url", transportConfig.Host) - } - - setQuery(&query, "sni", securityConfig.SNI) - setQuery(&query, "fp", securityConfig.Fingerprint) - setQuery(&query, "pbk", securityConfig.RealityPublicKey) - setQuery(&query, "sid", securityConfig.RealityShortId) - - u := url.URL{ - Scheme: "vless", - User: url.User(uuid), - Host: net.JoinHostPort(data.Server, fmt.Sprint(data.Port)), - RawQuery: query.Encode(), - Fragment: data.Name, - } - return u.String() -} - -func TrojanUri(data proxy.Proxy, uuid string) string { - trojan := data.Option.(proxy.Trojan) - transportConfig := trojan.TransportConfig - securityConfig := trojan.SecurityConfig - - var query = make(url.Values) - setQuery(&query, "type", trojan.Transport) - setQuery(&query, "security", trojan.Security) - - switch trojan.Transport { - case "ws", "http", "httpupgrade": - setQuery(&query, "path", transportConfig.Path) - setQuery(&query, "host", transportConfig.Host) - case "grpc": - setQuery(&query, "serviceName", transportConfig.ServiceName) - case "meek": - setQuery(&query, "url", transportConfig.Host) - } - - setQuery(&query, "sni", securityConfig.SNI) - setQuery(&query, "fp", securityConfig.Fingerprint) - setQuery(&query, "pbk", securityConfig.RealityPublicKey) - setQuery(&query, "sid", securityConfig.RealityShortId) - - if securityConfig.AllowInsecure { - setQuery(&query, "allowInsecure", "1") - } - - u := &url.URL{ - Scheme: "trojan", - User: url.User(uuid), - Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), - RawQuery: query.Encode(), - Fragment: data.Name, - } - return u.String() -} - -func Hysteria2Uri(data proxy.Proxy, uuid string) string { - hysteria2 := data.Option.(proxy.Hysteria2) - - var query = make(url.Values) - - setQuery(&query, "sni", hysteria2.SecurityConfig.SNI) - - if hysteria2.SecurityConfig.AllowInsecure { - setQuery(&query, "insecure", "1") - } - - if hp := strings.TrimSpace(hysteria2.HopPorts); hp != "" { - setQuery(&query, "mport", hp) - } - - if hysteria2.ObfsPassword != "" { - setQuery(&query, "obfs", "salamander") - setQuery(&query, "obfs-password", hysteria2.ObfsPassword) - } - - u := &url.URL{ - Scheme: "hysteria2", - User: url.User(uuid), - Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), - RawQuery: query.Encode(), - Fragment: data.Name, - } - return u.String() -} - -func TuicUri(data proxy.Proxy, uuid string) string { - tuic := data.Option.(proxy.Tuic) - var query = make(url.Values) - - setQuery(&query, "congestion_control", "bbr") - - if tuic.SecurityConfig.SNI == "" { - setQuery(&query, "sni", tuic.SecurityConfig.SNI) - } else { - setQuery(&query, "disable_sni", "1") - } - if tuic.SecurityConfig.AllowInsecure { - setQuery(&query, "allow_insecure", "1") - } - - u := &url.URL{ - Scheme: "tuic", - User: url.User(uuid + ":" + uuid), - Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), - RawQuery: query.Encode(), - Fragment: data.Name, - } - return u.String() -} - -func setQuery(q *url.Values, k, v string) { - if v != "" { - q.Set(k, v) - } -} diff --git a/pkg/adapter/general/uri_test.go b/pkg/adapter/general/uri_test.go deleted file mode 100644 index a33ef10..0000000 --- a/pkg/adapter/general/uri_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package general - -import ( - "testing" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func createServer() proxy.Proxy { - return proxy.Proxy{ - Name: "Meta", - Server: "127.0.0.1", - Port: 13092, - Protocol: "shadowsocks", - Option: proxy.Shadowsocks{ - Method: "aes-256-gcm", - ServerKey: "", - }, - } -} - -func TestGenerateBase64General(t *testing.T) { - s := createServer() - p := buildProxy(s, "935b33c7-e128-49f2-816b-71070469cac2") - t.Log(p) -} diff --git a/pkg/adapter/loon/build.go b/pkg/adapter/loon/build.go deleted file mode 100644 index 3256385..0000000 --- a/pkg/adapter/loon/build.go +++ /dev/null @@ -1,27 +0,0 @@ -package loon - -import ( - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func BuildLoon(servers []proxy.Proxy, uuid string) []byte { - uri := "" - for _, s := range servers { - switch s.Protocol { - case "vmess": - uri += buildVMess(s, uuid) - case "shadowsocks": - uri += buildShadowsocks(s, uuid) - case "trojan": - uri += buildTrojan(s, uuid) - case "vless": - uri += buildVless(s, uuid) - case "hysteria2": - uri += buildHysteria2(s, uuid) - default: - continue - } - } - - return []byte(uri) -} diff --git a/pkg/adapter/loon/hysteria2.go b/pkg/adapter/loon/hysteria2.go deleted file mode 100644 index 5b1547f..0000000 --- a/pkg/adapter/loon/hysteria2.go +++ /dev/null @@ -1,34 +0,0 @@ -package loon - -import ( - "fmt" - "strconv" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func buildHysteria2(data proxy.Proxy, password string) string { - hysteria2 := data.Option.(proxy.Hysteria2) - - configs := []string{ - fmt.Sprintf("%s=Hysteria2", data.Name), - data.Server, - strconv.Itoa(data.Port), - password, - "udp=true", - } - if hysteria2.ObfsPassword != "" { - configs = append(configs, "obfs=salamander", fmt.Sprintf("salamander-password=%s", hysteria2.ObfsPassword)) - } - if hysteria2.SecurityConfig.SNI != "" { - configs = append(configs, fmt.Sprintf("sni=%s", hysteria2.SecurityConfig.SNI)) - if hysteria2.SecurityConfig.AllowInsecure { - configs = append(configs, "skip-cert-verify=true") - } else { - configs = append(configs, "skip-cert-verify=false") - } - } - uri := strings.Join(configs, ",") - return uri + "\r\n" -} diff --git a/pkg/adapter/loon/loon_test.go b/pkg/adapter/loon/loon_test.go deleted file mode 100644 index 281cd5a..0000000 --- a/pkg/adapter/loon/loon_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package loon - -import ( - "testing" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func createSS() proxy.Proxy { - return proxy.Proxy{ - Name: "Shadowsocks", - Server: "127.0.0.1", - Port: 10301, - Protocol: "shadowsocks", - Option: proxy.Shadowsocks{ - Method: "aes-256-gcm", - ServerKey: "", - }, - } - -} - -func TestBuildSS(t *testing.T) { - s := createSS() - - password := "f0d0237d-193a-4cf5-99dd-b02207beaea6" - uri := buildShadowsocks(s, password) - t.Log(uri) -} diff --git a/pkg/adapter/loon/shadowsocks.go b/pkg/adapter/loon/shadowsocks.go deleted file mode 100644 index ffd7494..0000000 --- a/pkg/adapter/loon/shadowsocks.go +++ /dev/null @@ -1,49 +0,0 @@ -package loon - -import ( - "fmt" - "strconv" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" -) - -func buildShadowsocks(data proxy.Proxy, password string) string { - shadowsocks := data.Option.(proxy.Shadowsocks) - // If the method is 2022-blake3-chacha20-poly1305, it means that the server is a relay server - if shadowsocks.Method == "2022-blake3-chacha20-poly1305" { - return "" - } - - if strings.Contains(shadowsocks.Method, "2022") { - serverKey, userKey := generateShadowsocks2022Password(shadowsocks, password) - password = fmt.Sprintf("%s:%s", serverKey, userKey) - } - - configs := []string{ - fmt.Sprintf("%s=Shadowsocks", data.Name), - data.Server, - strconv.Itoa(data.Port), - shadowsocks.Method, - password, - "fast-open=false", - "udp=true", - } - uri := strings.Join(configs, ",") - return uri + "\r\n" -} - -func generateShadowsocks2022Password(ss proxy.Shadowsocks, password string) (string, string) { - // server key - var serverKey string - if ss.Method == "2022-blake3-aes-128-gcm" { - serverKey = tool.GenerateCipher(ss.ServerKey, 16) - password = uuidx.UUIDToBase64(password, 16) - } else { - serverKey = tool.GenerateCipher(ss.ServerKey, 32) - password = uuidx.UUIDToBase64(password, 32) - } - return serverKey, password -} diff --git a/pkg/adapter/loon/trojan.go b/pkg/adapter/loon/trojan.go deleted file mode 100644 index 3017d91..0000000 --- a/pkg/adapter/loon/trojan.go +++ /dev/null @@ -1,44 +0,0 @@ -package loon - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func buildTrojan(data proxy.Proxy, password string) string { - trojan := data.Option.(proxy.Trojan) - - configs := []string{ - fmt.Sprintf("%s=trojan", data.Name), - data.Server, - fmt.Sprintf("%d", data.Port), - "auto", - password, - "fast-open=false", - "udp=true", - } - - if trojan.SecurityConfig.SNI != "" { - configs = append(configs, fmt.Sprintf("sni=%s", trojan.SecurityConfig.SNI)) - } - if trojan.SecurityConfig.AllowInsecure { - configs = append(configs, "skip-cert-verify=true") - } else { - configs = append(configs, "skip-cert-verify=false") - } - - if trojan.Transport == "websocket" { - configs = append(configs, "transport=ws") - if trojan.TransportConfig.Path != "" { - configs = append(configs, fmt.Sprintf("path=%s", trojan.TransportConfig.Path)) - } - if trojan.TransportConfig.Host != "" { - configs = append(configs, fmt.Sprintf("host=%s", trojan.TransportConfig.Host)) - } - } - - uri := strings.Join(configs, ",") - return uri + "\r\n" -} diff --git a/pkg/adapter/loon/vless.go b/pkg/adapter/loon/vless.go deleted file mode 100644 index f14bbcb..0000000 --- a/pkg/adapter/loon/vless.go +++ /dev/null @@ -1,62 +0,0 @@ -package loon - -import ( - "fmt" - "strconv" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -func buildVless(data proxy.Proxy, password string) string { - vless := data.Option.(proxy.Vless) - // If flow is not empty, it means that the server is a relay server - if vless.Flow != "" { - return "" - } - - configs := []string{ - fmt.Sprintf("%s=vless", data.Name), - data.Server, - strconv.Itoa(data.Port), - "auto", - password, - "fast-open=false", - "udp=true", - "alterId=0", - } - - switch vless.Transport { - case "tcp": - configs = append(configs, "transport=tcp") - case "websocket": - configs = append(configs, "transport=ws") - if vless.TransportConfig.Path != "" { - configs = append(configs, fmt.Sprintf("path=%s", vless.TransportConfig.Path)) - } - if vless.TransportConfig.Host != "" { - configs = append(configs, fmt.Sprintf("host=%s", vless.TransportConfig.Host)) - } - default: - logger.Info("Loon Unknown transport type: ", logger.Field("transport", vless.Transport)) - return "" - } - - if vless.Security == "tls" { - configs = append(configs, "over-tls=true", fmt.Sprintf("tls-name=%s", vless.SecurityConfig.SNI)) - if vless.SecurityConfig.AllowInsecure { - configs = append(configs, "skip-cert-verify=true") - } else { - configs = append(configs, "skip-cert-verify=false") - } - } else if vless.Security == "reality" { - // Loon does not support reality security - logger.Info("Loon Unknown security type: ", logger.Field("security", vless.Security)) - return "" - } - - uri := strings.Join(configs, ",") - return uri + "\r\n" -} diff --git a/pkg/adapter/loon/vmess.go b/pkg/adapter/loon/vmess.go deleted file mode 100644 index 5d693b1..0000000 --- a/pkg/adapter/loon/vmess.go +++ /dev/null @@ -1,53 +0,0 @@ -package loon - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -func buildVMess(data proxy.Proxy, password string) string { - vmess := data.Option.(proxy.Vmess) - - configs := []string{ - fmt.Sprintf("%s=vmess", data.Name), - data.Server, - fmt.Sprintf("%d", data.Port), - "auto", - password, - "fast-open=false", - "udp=true", - "alterId=0", - } - - switch vmess.Transport { - case "tcp": - configs = append(configs, "transport=tcp") - case "websocket": - configs = append(configs, "transport=ws") - if vmess.TransportConfig.Path != "" { - configs = append(configs, fmt.Sprintf("path=%s", vmess.TransportConfig.Path)) - } - if vmess.TransportConfig.Host != "" { - configs = append(configs, fmt.Sprintf("host=%s", vmess.TransportConfig.Host)) - } - default: - logger.Info("Loon Unknown transport type: ", logger.Field("transport", vmess.Transport)) - return "" - } - - if vmess.Security == "tls" { - configs = append(configs, "over-tls=true", fmt.Sprintf("tls-name=%s", vmess.SecurityConfig.SNI)) - if vmess.SecurityConfig.AllowInsecure { - configs = append(configs, "skip-cert-verify=true") - } else { - configs = append(configs, "skip-cert-verify=false") - } - - } - - uri := strings.Join(configs, ",") - return uri + "\r\n" -} diff --git a/pkg/adapter/proxy/proxy.go b/pkg/adapter/proxy/proxy.go deleted file mode 100644 index d2cf603..0000000 --- a/pkg/adapter/proxy/proxy.go +++ /dev/null @@ -1,114 +0,0 @@ -package proxy - -// Adapter represents a proxy adapter -type Adapter struct { - Proxies []Proxy - Group []Group - Rules []string - Region []string -} - -// Proxy represents a proxy server -type Proxy struct { - Name string - Server string - Port int - Protocol string - Country string - Option any -} - -// Group represents a group of proxies -type Group struct { - Name string - Type GroupType - Proxies []string - URL string - Interval int -} - -type GroupType string - -const ( - GroupTypeSelect GroupType = "select" - GroupTypeURLTest GroupType = "url-test" - GroupTypeFallback GroupType = "fallback" -) - -// Shadowsocks represents a Shadowsocks proxy configuration -type Shadowsocks struct { - Port int `json:"port"` - Method string `json:"method"` - ServerKey string `json:"server_key"` -} - -// Vless represents a Vless proxy configuration -type Vless struct { - Port int `json:"port"` - Flow string `json:"flow"` - Transport string `json:"transport"` - TransportConfig TransportConfig `json:"transport_config"` - Security string `json:"security"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -// Vmess represents a Vmess proxy configuration -type Vmess struct { - Port int `json:"port"` - Flow string `json:"flow"` - Transport string `json:"transport"` - TransportConfig TransportConfig `json:"transport_config"` - Security string `json:"security"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -// Trojan represents a Trojan proxy configuration -type Trojan struct { - Port int `json:"port"` - Flow string `json:"flow"` - Transport string `json:"transport"` - TransportConfig TransportConfig `json:"transport_config"` - Security string `json:"security"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -// Hysteria2 represents a Hysteria2 proxy configuration -type Hysteria2 struct { - Port int `json:"port"` - HopPorts string `json:"hop_ports"` - HopInterval int `json:"hop_interval"` - ObfsPassword string `json:"obfs_password"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -// Tuic represents a Tuic proxy configuration -type Tuic struct { - Port int `json:"port"` - SecurityConfig SecurityConfig `json:"security_config"` -} - -// TransportConfig represents the transport configuration for a proxy -type TransportConfig struct { - Path string `json:"path,omitempty"` // ws/httpupgrade - Host string `json:"host,omitempty"` - ServiceName string `json:"service_name"` // grpc -} - -// SecurityConfig represents the security configuration for a proxy -type SecurityConfig struct { - SNI string `json:"sni"` - AllowInsecure bool `json:"allow_insecure"` - Fingerprint string `json:"fingerprint"` - RealityServerAddr string `json:"reality_server_addr"` - RealityServerPort int `json:"reality_server_port"` - RealityPrivateKey string `json:"reality_private_key"` - RealityPublicKey string `json:"reality_public_key"` - RealityShortId string `json:"reality_short_id"` -} - -// Relay represents a relay configuration -type Relay struct { - RelayHost string - DispatchMode string - Prefix string -} diff --git a/pkg/adapter/quantumultx/build.go b/pkg/adapter/quantumultx/build.go deleted file mode 100644 index a5b02cd..0000000 --- a/pkg/adapter/quantumultx/build.go +++ /dev/null @@ -1,22 +0,0 @@ -package quantumultx - -import ( - "encoding/base64" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func BuildQuantumultX(servers []proxy.Proxy, uuid string) string { - var uri string - for _, s := range servers { - switch s.Protocol { - case "vmess": - uri += buildVmess(s, uuid) - case "shadowsocks": - uri += buildShadowsocks(s, uuid) - case "trojan": - uri += buildTrojan(s, uuid) - } - } - return base64.StdEncoding.EncodeToString([]byte(uri)) -} diff --git a/pkg/adapter/quantumultx/quantumux_test.go b/pkg/adapter/quantumultx/quantumux_test.go deleted file mode 100644 index 79524bb..0000000 --- a/pkg/adapter/quantumultx/quantumux_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package quantumultx - -import ( - "testing" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func createVMess() proxy.Proxy { - - return proxy.Proxy{ - Name: "Vmess", - Server: "test.xxxx.com", - Port: 13002, - Protocol: "vmess", - Option: proxy.Vmess{ - Port: 13002, - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "test.xx.com", - }, - Security: "none", - }, - } -} - -func createSS() proxy.Proxy { - return proxy.Proxy{ - Name: "Shadowsocks", - Server: "test.xxxx.com", - Port: 10301, - Protocol: "shadowsocks", - Option: proxy.Shadowsocks{ - Port: 10301, - Method: "aes-256-gcm", - ServerKey: "123456", - }, - } -} - -func createTrojan() proxy.Proxy { - - return proxy.Proxy{ - Name: "Trojan", - Server: "test.xxxx.com", - Port: 13002, - Protocol: "trojan", - Option: proxy.Trojan{ - Port: 13002, - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "baidu.com", - }, - SecurityConfig: proxy.SecurityConfig{ - SNI: "baidu.com", - AllowInsecure: true, - }, - }, - } -} -func TestVmess(t *testing.T) { - s := createVMess() - vmess := buildVmess(s, "uuid") - t.Log(vmess) - // output: - // vmess=127.0.0.1:13002,method=chacha20-poly1305,password=uuid,fast-open=true,udp-relay=true,tag=Vmess,tls-verification=true,obfs-uri=/ws,obfs-host=baidu.com -} - -func TestShadowsocks(t *testing.T) { - s := createSS() - shadowsocks := buildShadowsocks(s, "uuid") - t.Log(shadowsocks) - // output: - // shadowsocks=127.0.0.1:10301,method=aes-256-gcm,password=uuid,fast-open=true,udp-relay=true,tag=Shadowsocks -} - -func TestTrojan(t *testing.T) { - s := createTrojan() - trojan := buildTrojan(s, "password") - t.Log(trojan) - // output: - // trojan=192.168.0.1:13002,password=password,fast-open=true,udp-relay=true,tag=Trojan,obfs=wss,obfs-uri=ws,obfs-host=baidu.com -} - -func TestBuildQuantumultX(t *testing.T) { - var servers []proxy.Proxy - uri := BuildQuantumultX(servers, "uuid") - t.Log(uri) - - // output: - // c2hhZG93c29ja3M9MTI3LjAuMC4xOjEwMzAxLG1ldGhvZD1hZXMtMjU2LWdjbSxwYXNzd29yZD11dWlkLGZhc3Qtb3Blbj10cnVlLHVkcC1yZWxheT10cnVlLHRhZz1TaGFkb3dzb2Nrcw0KdHJvamFuPTE5Mi4xNjguMC4xOjEzMDAyLHBhc3N3b3JkPXV1aWQsZmFzdC1vcGVuPXRydWUsdWRwLXJlbGF5PXRydWUsdGFnPVRyb2phbixvYmZzPXdzcyxvYmZzLXVyaT13cyxvYmZzLWhvc3Q9YmFpZHUuY29tDQp2bWVzcz0xMjcuMC4wLjE6MTMwMDIsbWV0aG9kPWNoYWNoYTIwLXBvbHkxMzA1LHBhc3N3b3JkPXV1aWQsZmFzdC1vcGVuPXRydWUsdWRwLXJlbGF5PXRydWUsdGFnPVZtZXNzLHRscy12ZXJpZmljYXRpb249dHJ1ZSxvYmZzLXVyaT0vd3Msb2Jmcy1ob3N0PWJhaWR1LmNvbQ0K -} diff --git a/pkg/adapter/quantumultx/shadowsocks.go b/pkg/adapter/quantumultx/shadowsocks.go deleted file mode 100644 index a03703c..0000000 --- a/pkg/adapter/quantumultx/shadowsocks.go +++ /dev/null @@ -1,23 +0,0 @@ -package quantumultx - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func buildShadowsocks(data proxy.Proxy, uuid string) string { - ss := data.Option.(proxy.Shadowsocks) - addr := fmt.Sprintf("%s:%d", data.Server, data.Port) - - config := []string{ - addr, - fmt.Sprintf("method=%s", ss.Method), - fmt.Sprintf("password=%s", uuid), - "fast-open=true", - "udp-relay=true", - fmt.Sprintf("tag=%s", data.Name), - } - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/quantumultx/trojan.go b/pkg/adapter/quantumultx/trojan.go deleted file mode 100644 index 0ebcd96..0000000 --- a/pkg/adapter/quantumultx/trojan.go +++ /dev/null @@ -1,39 +0,0 @@ -package quantumultx - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -// 生成 Trojan 配置 -func buildTrojan(data proxy.Proxy, password string) string { - trojan := data.Option.(proxy.Trojan) - - addr := fmt.Sprintf("trojan=%s:%d", data.Server, data.Port) - config := []string{ - addr, - fmt.Sprintf("password=%s", password), - "fast-open=true", - "udp-relay=true", - fmt.Sprintf("tag=%s", data.Name), - } - - if trojan.Transport == "websocket" { - config = append(config, "obfs=wss") - if trojan.TransportConfig.Path != "" { - config = append(config, fmt.Sprintf("obfs-uri=%s", trojan.TransportConfig.Path)) - } - if trojan.TransportConfig.Host != "" { - config = append(config, fmt.Sprintf("obfs-host=%s", trojan.TransportConfig.Host)) - } - } else { - config = append(config, "over-tls=true") - if trojan.SecurityConfig.SNI != "" { - config = append(config, fmt.Sprintf("tls-host=%s", trojan.SecurityConfig.SNI)) - } - } - - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/quantumultx/vmess.go b/pkg/adapter/quantumultx/vmess.go deleted file mode 100644 index d3cf13e..0000000 --- a/pkg/adapter/quantumultx/vmess.go +++ /dev/null @@ -1,45 +0,0 @@ -package quantumultx - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func buildVmess(data proxy.Proxy, uuid string) string { - - vmess := data.Option.(proxy.Vmess) - addr := fmt.Sprintf("vmess=%s:%d", data.Server, data.Port) - var host string - uriConfig := []string{ - addr, - "method=chacha20-poly1305", - fmt.Sprintf("password=%s", uuid), - "fast-open=true", - "udp-relay=true", - fmt.Sprintf("tag=%s", data.Name), - } - if vmess.Security == "tls" { - if vmess.Transport == "tcp" { - uriConfig = append(uriConfig, "obfs=over-tls") - } - if vmess.SecurityConfig.AllowInsecure { - uriConfig = append(uriConfig, "tls-verification=true") - } else { - uriConfig = append(uriConfig, "tls-verification=false") - } - if vmess.SecurityConfig.SNI != "" { - host = vmess.SecurityConfig.SNI - } - } - - if vmess.Transport == "websocket" { - uriConfig = append(uriConfig, fmt.Sprintf("obfs-uri=%s", vmess.TransportConfig.Path)) - host = vmess.TransportConfig.Host - } - if host != "" { - uriConfig = append(uriConfig, fmt.Sprintf("obfs-host=%s", host)) - } - return strings.Join(uriConfig, ",") + "\r\n" -} diff --git a/pkg/adapter/shadowrocket/build.go b/pkg/adapter/shadowrocket/build.go deleted file mode 100644 index 33143a7..0000000 --- a/pkg/adapter/shadowrocket/build.go +++ /dev/null @@ -1,48 +0,0 @@ -package shadowrocket - -import ( - "fmt" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/general" - - "encoding/base64" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - "github.com/perfect-panel/ppanel-server/pkg/traffic" -) - -type UserInfo struct { - Upload int64 - Download int64 - TotalTraffic int64 - ExpiredDate time.Time -} - -func BuildShadowrocket(servers []proxy.Proxy, uuid string, userinfo UserInfo) []byte { - upload := traffic.AutoConvert(userinfo.Upload, false) - download := traffic.AutoConvert(userinfo.Download, false) - total := traffic.AutoConvert(userinfo.TotalTraffic, false) - expiredAt := userinfo.ExpiredDate.Format("2006-01-02 15:04:05") - uri := fmt.Sprintf("STATUS=🚀↑:%s,↓:%s,TOT:%s💡Expires:%s\r\n", upload, download, total, expiredAt) - for _, s := range servers { - switch s.Protocol { - case "vmess": - uri += buildVmess(s, uuid) - case "shadowsocks": - uri += general.ShadowsocksUri(s, uuid) + "\r\n" - case "trojan": - uri += general.TrojanUri(s, uuid) + "\r\n" - case "vless": - uri += general.VlessUri(s, uuid) + "\r\n" - case "hysteria2": - uri += general.Hysteria2Uri(s, uuid) + "\r\n" - case "tuic": - uri += general.TuicUri(s, uuid) + "\r\n" - default: - continue - } - } - - return []byte(base64.StdEncoding.EncodeToString([]byte(uri))) -} diff --git a/pkg/adapter/shadowrocket/shadowrocket_test.go b/pkg/adapter/shadowrocket/shadowrocket_test.go deleted file mode 100644 index 50b8932..0000000 --- a/pkg/adapter/shadowrocket/shadowrocket_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package shadowrocket - -import ( - "testing" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func createVMess() proxy.Proxy { - return proxy.Proxy{ - Name: "Vmess", - Server: "test.xxxx.com", - Port: 13002, - Protocol: "vmess", - Option: proxy.Vmess{ - Port: 13002, - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "test.xx.com", - }, - Security: "none", - }, - } -} - -func createSS() proxy.Proxy { - return proxy.Proxy{ - Name: "Shadowsocks", - Server: "test.xxxx.com", - Port: 10301, - Protocol: "shadowsocks", - Option: proxy.Shadowsocks{ - Port: 10301, - Method: "aes-256-gcm", - ServerKey: "123456", - }, - } -} - -func createTrojan() proxy.Proxy { - - return proxy.Proxy{ - Name: "Trojan", - Server: "test.xxxx.com", - Port: 13002, - Protocol: "trojan", - Option: proxy.Trojan{ - Port: 13002, - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "baidu.com", - }, - SecurityConfig: proxy.SecurityConfig{ - SNI: "baidu.com", - AllowInsecure: true, - }, - }, - } -} -func TestBuildShadowrocket(t *testing.T) { - s := []proxy.Proxy{ - createVMess(), - createSS(), - createTrojan(), - } - uri := BuildShadowrocket(s, "uuid", UserInfo{ - Upload: 1024, - Download: 1024, - TotalTraffic: 2048, - ExpiredDate: time.Now().AddDate(0, 0, 1), - }) - t.Log(string(uri)) -} diff --git a/pkg/adapter/shadowrocket/vmess.go b/pkg/adapter/shadowrocket/vmess.go deleted file mode 100644 index f309566..0000000 --- a/pkg/adapter/shadowrocket/vmess.go +++ /dev/null @@ -1,57 +0,0 @@ -package shadowrocket - -import ( - "fmt" - "strings" - - "encoding/base64" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func buildVmess(data proxy.Proxy, uuid string) string { - vmess := data.Option.(proxy.Vmess) - - userinfo := fmt.Sprintf("auto:%s@%s:%d", uuid, data.Server, data.Port) - // 准备 config,使用默认值 - config := map[string]interface{}{ - "tfo": 1, - "remark": data.Name, - "alterId": 0, - } - - // tls 配置 - if vmess.Security == "tls" { - config["tls"] = 1 - if vmess.SecurityConfig.AllowInsecure { - config["allowInsecure"] = 1 - } - if vmess.SecurityConfig.SNI != "" { - config["peer"] = vmess.SecurityConfig.SNI - } - } - - // transport 配置 - switch vmess.Transport { - case "websocket": - config["obfs"] = "websocket" - if vmess.TransportConfig.Path != "" { - config["path"] = vmess.TransportConfig.Path - } - if vmess.TransportConfig.Host != "" { - config["obfsParam"] = vmess.TransportConfig.Host - } - case "grpc": - config["obfs"] = "grpc" - if vmess.TransportConfig.ServiceName != "" { - config["path"] = vmess.TransportConfig.ServiceName - } - } - query := make([]string, 0) - for k, v := range config { - query = append(query, fmt.Sprintf("%s=%v", k, v)) - } - queryStr := strings.Join(query, "&") - uri := fmt.Sprintf("vmess://%s?%s\r\n", base64.StdEncoding.EncodeToString([]byte(userinfo)), queryStr) - return uri -} diff --git a/pkg/adapter/singbox/build.go b/pkg/adapter/singbox/build.go deleted file mode 100644 index 30fbd61..0000000 --- a/pkg/adapter/singbox/build.go +++ /dev/null @@ -1,201 +0,0 @@ -package singbox - -import ( - "encoding/json" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - "github.com/perfect-panel/ppanel-server/pkg/logger" -) - -func BuildSingbox(adapter proxy.Adapter, uuid string) ([]byte, error) { - // build outbounds type is Proxy - var proxies []Proxy - // build outbound group - for _, group := range adapter.Group { - if group.Type == proxy.GroupTypeSelect { - selector := Proxy{ - Type: Selector, - Tag: group.Name, - SelectorOptions: &SelectorOutboundOptions{ - OutboundOptions: OutboundOptions{ - Tag: group.Name, - Type: Selector, - }, - Outbounds: group.Proxies, - Default: group.Proxies[0], - InterruptExistConnections: false, - }, - } - proxies = append(proxies, selector) - } else if group.Type == proxy.GroupTypeURLTest { - selector := Proxy{ - Type: URLTest, - Tag: group.Name, - URLTestOptions: &URLTestOutboundOptions{ - OutboundOptions: OutboundOptions{ - Tag: group.Name, - Type: URLTest, - }, - Outbounds: group.Proxies, - URL: group.URL, - }, - } - proxies = append(proxies, selector) - } else { - logger.Errorf("[sing-box] Unknown group type: %s, group name: %s", group.Type, group.Name) - } - } - - // build outbounds - for _, data := range adapter.Proxies { - p := buildProxy(data, uuid) - if p == nil { - continue - } - proxies = append(proxies, *p) - } - - // add direct outbound - direct := Proxy{ - Type: Direct, - Tag: "DIRECT", - } - // add block outbound - block := Proxy{ - Type: Block, - Tag: "block", - } - // add dns outbound - dns := Proxy{ - Type: DNS, - Tag: "dns-out", - } - proxies = append(proxies, direct, block, dns) - - var rawConfig map[string]any - if err := json.Unmarshal([]byte(DefaultTemplate), &rawConfig); err != nil { - return nil, err - } - - rawConfig["outbounds"] = proxies - route := RouteOptions{ - Final: "手动选择", - Rules: []Rule{ - { - Inbound: []string{ - "tun-in", - "mixed-in", - }, - Action: "sniff", - }, - { - Type: "logical", - Mode: "or", - Rules: []Rule{ - { - Port: []uint16{53}, - }, - { - Protocol: []string{"dns"}, - }, - }, - Action: "hijack-dns", - }, - { - RuleSet: []string{ - "geosite-category-ads-all", - }, - ClashMode: "rule", - Action: "reject", - }, - { - ClashMode: "direct", - Outbound: "DIRECT", - }, - { - ClashMode: "global", - Outbound: "手动选择", - }, - { - IPIsPrivate: true, - Outbound: "DIRECT", - }, - { - RuleSet: []string{ - "geosite-private", - }, - Outbound: "DIRECT", - }, - }, - RuleSet: []RuleSet{ - { - Tag: "geoip-cn", - Type: "remote", - Format: "binary", - URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs", - DownloadDetour: "DIRECT", - }, - { - Tag: "geosite-cn", - Type: "remote", - Format: "binary", - URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs", - DownloadDetour: "DIRECT", - }, - { - Tag: "geosite-private", - Type: "remote", - Format: "binary", - URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/private.srs", - DownloadDetour: "DIRECT", - }, - { - Tag: "geosite-category-ads-all", - Type: "remote", - Format: "binary", - URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ads-all.srs", - DownloadDetour: "DIRECT", - }, - { - Tag: "geosite-geolocation-!cn", - Type: "remote", - Format: "binary", - URL: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/geolocation-!cn.srs", - DownloadDetour: "DIRECT", - }, - }, - AutoDetectInterface: true, - } - route.Rules = append(route.Rules, adapterToSingboxRule(adapter.Rules)...) - rawConfig["route"] = route - return json.Marshal(rawConfig) -} - -func buildProxy(data proxy.Proxy, uuid string) *Proxy { - var p *Proxy - var err error - switch data.Protocol { - case VLESS: - p, err = ParseVless(data, uuid) - case Shadowsocks: - p, err = ParseShadowsocks(data, uuid) - case Trojan: - p, err = ParseTrojan(data, uuid) - case VMess: - p, err = ParseVMess(data, uuid) - - case Hysteria2: - p, err = ParseHysteria2(data, uuid) - - case TUIC: - p, err = ParseTUIC(data, uuid) - - default: - logger.Error("Unknown protocol", logger.Field("protocol", data.Protocol), logger.Field("server", data.Name)) - } - if err != nil { - logger.Error("ParseVless", logger.Field("error", err.Error()), logger.Field("server", data.Name), logger.Field("protocol", data.Protocol)) - return nil - } - return p -} diff --git a/pkg/adapter/singbox/default.go b/pkg/adapter/singbox/default.go deleted file mode 100644 index d73b236..0000000 --- a/pkg/adapter/singbox/default.go +++ /dev/null @@ -1,100 +0,0 @@ -package singbox - -const DefaultTemplate = ` -{ - "log": { - "level": "info", - "timestamp": true - }, - "experimental": { - "clash_api": { - "external_controller": "127.0.0.1:9090", - "external_ui": "ui", - "secret": "", - "external_ui_download_url": "https://mirror.ghproxy.com/https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip", - "external_ui_download_detour": "direct", - "default_mode": "rule" - }, - "cache_file": { - "enabled": true, - "store_fakeip": false - } - }, - "dns": { - "servers": [ - { - "tag": "dns_proxy", - "address": "tls://8.8.8.8", - "detour": "手动选择" - }, - { - "tag": "dns_direct", - "address": "https://223.5.5.5/dns-query", - "detour": "DIRECT" - } - ], - "rules": [ - { - "outbound": "any", - "server": "dns_direct", - "disable_cache": true - }, - { - "rule_set": "geosite-cn", - "server": "dns_direct" - }, - { - "clash_mode": "direct", - "server": "dns_direct" - }, - { - "clash_mode": "global", - "server": "dns_proxy" - }, - { - "rule_set": "geosite-geolocation-!cn", - "server": "dns_proxy" - } - ], - "final": "dns_direct", - "strategy": "ipv4_only" - }, - "route": { - "rules": [ - { - "action": "sniff" - }, - { - "protocol": "dns", - "action": "hijack-dns" - } - ] - }, - "inbounds": [ - { - "tag": "tun-in", - "type": "tun", - "address": [ - "172.18.0.1/30", - "fdfe:dcba:9876::1/126" - ], - "auto_route": true, - "strict_route": true, - "stack": "system", - "platform": { - "http_proxy": { - "enabled": true, - "server": "127.0.0.1", - "server_port": 7890 - } - } - }, - { - "tag": "mixed-in", - "type": "mixed", - "listen": "127.0.0.1", - "listen_port": 7890 - } - ] -} -` diff --git a/pkg/adapter/singbox/hysteria2.go b/pkg/adapter/singbox/hysteria2.go deleted file mode 100644 index e7b9f55..0000000 --- a/pkg/adapter/singbox/hysteria2.go +++ /dev/null @@ -1,76 +0,0 @@ -package singbox - -import ( - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -type Hysteria2Obfs struct { - Type string `json:"type,omitempty"` - Password string `json:"password,omitempty"` -} - -type Hysteria2OutboundOptions struct { - ServerOptions - ServerPorts []string `json:"server_ports,omitempty"` - HopInterval int `json:"hop_interval,omitempty"` - UpMbps int `json:"up_mbps,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs *Hysteria2Obfs `json:"obfs,omitempty"` - Password string `json:"password,omitempty"` - Network string `json:"network,omitempty"` - OutboundTLSOptionsContainer - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` -} - -func ParseHysteria2(data proxy.Proxy, password string) (*Proxy, error) { - hysteria2 := data.Option.(proxy.Hysteria2) - - p := &Proxy{ - Tag: data.Name, - Type: Hysteria2, - Hysteria2Options: &Hysteria2OutboundOptions{ - ServerOptions: ServerOptions{ - Tag: data.Name, - Type: Hysteria2, - Server: data.Server, - }, - Password: password, - }, - } - - var ports []string - - if hysteria2.HopPorts != "" { - ps := strings.Split(hysteria2.HopPorts, ",") - for _, port := range ps { - // 舍弃单个端口,只保留端口范围 - if len(strings.Split(port, "-")) > 1 { - tmp := strings.Split(port, "-") - ports = append(ports, strings.Join(tmp, ":")) - } - } - - } - if len(ports) > 0 { - p.Hysteria2Options.ServerPorts = ports - p.Hysteria2Options.HopInterval = hysteria2.HopInterval - } else { - p.Hysteria2Options.ServerPort = data.Port - } - - if hysteria2.ObfsPassword != "" { - p.Hysteria2Options.Obfs = &Hysteria2Obfs{ - Type: "salamander", - Password: hysteria2.ObfsPassword, - } - } - var tls *OutboundTLSOptions - if hysteria2.SecurityConfig.SNI != "" { - tls = NewOutboundTLSOptions("tls", hysteria2.SecurityConfig) - } - p.Hysteria2Options.TLS = tls - return p, nil -} diff --git a/pkg/adapter/singbox/multiplex.go b/pkg/adapter/singbox/multiplex.go deleted file mode 100644 index 7188f95..0000000 --- a/pkg/adapter/singbox/multiplex.go +++ /dev/null @@ -1,17 +0,0 @@ -package singbox - -type OutboundMultiplexOptions struct { - Enabled bool `json:"enabled,omitempty"` - Protocol string `json:"protocol,omitempty"` - MaxConnections int `json:"max_connections,omitempty"` - MinStreams int `json:"min_streams,omitempty"` - MaxStreams int `json:"max_streams,omitempty"` - Padding bool `json:"padding,omitempty"` - Brutal *BrutalOptions `json:"brutal,omitempty"` -} - -type BrutalOptions struct { - Enabled bool `json:"enabled,omitempty"` - UpMbps int `json:"up_mbps,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` -} diff --git a/pkg/adapter/singbox/rule.go b/pkg/adapter/singbox/rule.go deleted file mode 100644 index af00e56..0000000 --- a/pkg/adapter/singbox/rule.go +++ /dev/null @@ -1,130 +0,0 @@ -package singbox - -import ( - "strconv" - - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/rules" -) - -type Rule struct { - Outbound string `json:"outbound,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - RuleSet []string `json:"rule_set,omitempty"` - Domain []string `json:"domain,omitempty"` - DomainSuffix []string `json:"domain_suffix,omitempty"` - DomainKeyword []string `json:"domain_keyword,omitempty"` - DomainRegex []string `json:"domain_regex,omitempty"` - GeoIP []string `json:"geoip,omitempty"` - IPCIDR []string `json:"ip_cidr,omitempty"` - IPIsPrivate bool `json:"ip_is_private,omitempty"` - SourceIPCIDR []string `json:"source_ip_cidr,omitempty"` - ProcessName []string `json:"process_name,omitempty"` - ProcessPath []string `json:"process_path,omitempty"` - SourcePort []uint16 `json:"source_port,omitempty"` - Protocol []string `json:"protocol,omitempty"` - Port []uint16 `json:"port,omitempty"` - Action string `json:"action,omitempty"` - Inbound []string `json:"inbound,omitempty"` - Rules []Rule `json:"rules,omitempty"` - Type string `json:"type,omitempty"` - Mode string `json:"mode,omitempty"` -} - -type RuleSet struct { - Tag string `json:"tag,omitempty"` - Type string `json:"type,omitempty"` - Format string `json:"format,omitempty"` - URL string `json:"url,omitempty"` - DownloadDetour string `json:"download_detour,omitempty"` -} - -func adapterToSingboxRule(texts []string) []Rule { - var rulesList []Rule - for _, rule := range texts { - r := rules.NewRule(rule, "") - if r == nil { - continue - } - rulesList = addRuleToItem(rulesList, r.Target, *r) - } - return rulesList -} - -func addRuleToItem(group []Rule, outbound string, rule rules.Rule) []Rule { - for i := range group { - if group[i].Outbound == outbound { - switch rules.ParseRuleType(rule.Type) { - case rules.Domain: - group[i].Domain = append(group[i].Domain, rule.Payload) - return group - case rules.DomainSuffix: - group[i].DomainSuffix = append(group[i].DomainSuffix, rule.Payload) - return group - case rules.DomainKeyword: - group[i].DomainKeyword = append(group[i].DomainKeyword, rule.Payload) - return group - case rules.IPCIDR: - group[i].IPCIDR = append(group[i].IPCIDR, rule.Payload) - return group - case rules.SrcIPCIDR: - group[i].SourceIPCIDR = append(group[i].SourceIPCIDR, rule.Payload) - return group - case rules.SrcPort: - port, err := strconv.ParseUint(rule.Payload, 10, 16) - if err != nil { - logger.Errorf("[adapterToSingboxRule] failed to parse port %s to uint16", rule.Payload) - return group - } - group[i].SourcePort = append(group[i].SourcePort, uint16(port)) - return group - case rules.GEOIP: - group[i].GeoIP = append(group[i].GeoIP, rule.Payload) - return group - case rules.Process: - group[i].ProcessName = append(group[i].ProcessName, rule.Payload) - return group - case rules.ProcessPath: - group[i].ProcessPath = append(group[i].ProcessPath, rule.Payload) - return group - default: - logger.Errorf("[adapterToSingboxRule] unknown rule type %s", rule.Type) - return group - } - } - } - newRule := Rule{ - Outbound: outbound, - } - - switch rules.ParseRuleType(rule.Type) { - case rules.Domain: - newRule.Domain = []string{rule.Payload} - case rules.DomainSuffix: - newRule.DomainSuffix = []string{rule.Payload} - case rules.DomainKeyword: - newRule.DomainKeyword = []string{rule.Payload} - case rules.IPCIDR: - newRule.IPCIDR = []string{rule.Payload} - case rules.SrcIPCIDR: - newRule.SourceIPCIDR = []string{rule.Payload} - case rules.SrcPort: - port, err := strconv.ParseUint(rule.Payload, 10, 16) - if err != nil { - logger.Errorf("[adapterToSingboxRule] failed to parse port %s to uint16", rule.Payload) - return group - } - newRule.SourcePort = []uint16{uint16(port)} - case rules.GEOIP: - newRule.GeoIP = []string{rule.Payload} - case rules.Process: - newRule.ProcessName = []string{rule.Payload} - case rules.ProcessPath: - newRule.ProcessPath = []string{rule.Payload} - default: - logger.Errorf("[adapterToSingboxRule] unknown rule type %s", rule.Type) - return group - } - group = append(group, newRule) - return group -} diff --git a/pkg/adapter/singbox/rule_test.go b/pkg/adapter/singbox/rule_test.go deleted file mode 100644 index 95f8ce3..0000000 --- a/pkg/adapter/singbox/rule_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package singbox - -import ( - "fmt" - "testing" -) - -func TestAdapterToSingboxRule(t *testing.T) { - rules := []string{ - "DOMAIN,example.com,DIRECT", - "DOMAIN-SUFFIX,google.com,智能线路", - } - result := adapterToSingboxRule(rules) - fmt.Printf("TestAdapterToSingboxRule: result: %+v\n", result) -} diff --git a/pkg/adapter/singbox/shadowsocks.go b/pkg/adapter/singbox/shadowsocks.go deleted file mode 100644 index e0d59b0..0000000 --- a/pkg/adapter/singbox/shadowsocks.go +++ /dev/null @@ -1,34 +0,0 @@ -package singbox - -import ( - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -type ShadowsocksOptions struct { - ServerOptions - Method string `json:"method,omitempty"` - Password string `json:"password,omitempty"` - Plugin string `json:"plugin,omitempty"` - PluginOptions string `json:"plugin_opts,omitempty"` - Network string `json:"network,omitempty"` -} - -func ParseShadowsocks(data proxy.Proxy, uuid string) (*Proxy, error) { - config := data.Option.(proxy.Shadowsocks) - p := &Proxy{ - Tag: data.Name, - Type: Shadowsocks, - ShadowsocksOptions: &ShadowsocksOptions{ - ServerOptions: ServerOptions{ - Tag: data.Name, - Type: Shadowsocks, - Server: data.Server, - ServerPort: data.Port, - }, - Method: config.Method, - Password: uuid, - Network: "tcp", - }, - } - return p, nil -} diff --git a/pkg/adapter/singbox/singbox.go b/pkg/adapter/singbox/singbox.go deleted file mode 100644 index 0cb40f5..0000000 --- a/pkg/adapter/singbox/singbox.go +++ /dev/null @@ -1,98 +0,0 @@ -package singbox - -import ( - "encoding/json" - "fmt" -) - -const ( - Trojan = "trojan" - VLESS = "vless" - VMess = "vmess" - TUIC = "tuic" - Hysteria2 = "hysteria2" - Shadowsocks = "shadowsocks" - Selector = "selector" - URLTest = "urltest" - Direct = "direct" - Block = "block" - DNS = "dns" -) - -type Proxy struct { - Tag string `json:"tag,omitempty"` - Type string `json:"type"` - ShadowsocksOptions *ShadowsocksOptions `json:"-"` - TUICOptions *TUICOutboundOptions `json:"-"` - TrojanOptions *TrojanOutboundOptions `json:"-"` - VLESSOptions *VLESSOutboundOptions `json:"-"` - VMessOptions *VMessOutboundOptions `json:"-"` - Hysteria2Options *Hysteria2OutboundOptions `json:"-"` - SelectorOptions *SelectorOutboundOptions `json:"-"` - URLTestOptions *URLTestOutboundOptions `json:"-"` -} - -type ServerOptions struct { - Tag string `json:"tag"` - Type string `json:"type"` - Server string `json:"server"` - ServerPort int `json:"server_port,omitempty"` -} -type OutboundOptions struct { - Tag string `json:"tag"` - Type string `json:"type"` -} -type SelectorOutboundOptions struct { - OutboundOptions - Outbounds []string `json:"outbounds"` - Default string `json:"default,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} - -type URLTestOutboundOptions struct { - OutboundOptions - Outbounds []string `json:"outbounds"` - URL string `json:"url,omitempty"` - Interval Duration `json:"interval,omitempty"` - Tolerance uint16 `json:"tolerance,omitempty"` - IdleTimeout Duration `json:"idle_timeout,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` -} - -type RouteOptions struct { - Rules []Rule `json:"rules,omitempty"` - Final string `json:"final,omitempty"` - RuleSet []RuleSet `json:"rule_set,omitempty"` - AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` -} - -func (p Proxy) MarshalJSON() ([]byte, error) { - type Alias Proxy - aux := struct { - Alias - }{ - Alias: (Alias)(p), - } - switch p.Type { - case Shadowsocks: - return json.Marshal(p.ShadowsocksOptions) - case TUIC: - return json.Marshal(p.TUICOptions) - case Trojan: - return json.Marshal(p.TrojanOptions) - case VLESS: - return json.Marshal(p.VLESSOptions) - case VMess: - return json.Marshal(p.VMessOptions) - case Hysteria2: - return json.Marshal(p.Hysteria2Options) - case Selector: - return json.Marshal(p.SelectorOptions) - case URLTest: - return json.Marshal(p.URLTestOptions) - case Direct, Block, DNS: - return json.Marshal(aux.Alias) - default: - return nil, fmt.Errorf("[sing-box] MarshalJSON unknown type: %s", p.Type) - } -} diff --git a/pkg/adapter/singbox/singbox_test.go b/pkg/adapter/singbox/singbox_test.go deleted file mode 100644 index 0d9c335..0000000 --- a/pkg/adapter/singbox/singbox_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package singbox - -import ( - "testing" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - - "github.com/stretchr/testify/assert" -) - -func createSS() proxy.Proxy { - c := proxy.Shadowsocks{ - Method: "aes-256-gcm", - Port: 10301, - ServerKey: "", - } - return proxy.Proxy{ - Name: "Shadowsocks", - Server: "127.0.0.1", - Port: 10301, - Protocol: "shadowsocks", - Option: c, - } -} - -func createVLESS() proxy.Proxy { - c := proxy.Vless{ - Port: 10301, - Flow: "xtls-rprx-direct", - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "baidu.com", - }, - Security: "tls", - SecurityConfig: proxy.SecurityConfig{ - SNI: "baidu.com", - Fingerprint: "chrome", - AllowInsecure: true, - }, - } - s := proxy.Proxy{ - Name: "VLESS", - Server: "test.xxx.com", - Port: 10301, - Protocol: "vless", - Option: c, - } - return s -} - -func TestSingboxShadowsocks(t *testing.T) { - s := createSS() - p, err := ParseShadowsocks(s, "uuid") - if err != nil { - t.Fatal(err) - } - data, err := p.MarshalJSON() - if err != nil { - t.Fatal(err) - } - assert.NotEqual(t, 0, len(data)) - - // Output: - // proxy: proxy: {"tag":"Shadowsocks","type":"shadowsocks","server":"127.0.0.1","server_port":10301,"method":"aes-256-gcm","password":"uuid","network":"tcp"} - -} - -func TestSingboxVless(t *testing.T) { - s := createVLESS() - p, err := ParseVless(s, "uuid") - if err != nil { - t.Fatal(err) - } - data, err := p.MarshalJSON() - if err != nil { - t.Fatal(err) - } - assert.NotEqual(t, 0, len(data)) -} diff --git a/pkg/adapter/singbox/tls.go b/pkg/adapter/singbox/tls.go deleted file mode 100644 index fa7c1f9..0000000 --- a/pkg/adapter/singbox/tls.go +++ /dev/null @@ -1,87 +0,0 @@ -package singbox - -import ( - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -type OutboundTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - DisableSNI bool `json:"disable_sni,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - ALPN Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites Listable[string] `json:"cipher_suites,omitempty"` - Certificate Listable[string] `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - ECH *OutboundECHOptions `json:"ech,omitempty"` - UTLS *OutboundUTLSOptions `json:"utls,omitempty"` - Reality *OutboundRealityOptions `json:"reality,omitempty"` -} - -func NewOutboundTLSOptions(security string, cfg proxy.SecurityConfig) *OutboundTLSOptions { - var tls = &OutboundTLSOptions{} - switch security { - case "none": - return nil - case "tls": - tls.Enabled = true - if cfg.SNI != "" { - tls.ServerName = cfg.SNI - } else { - tls.DisableSNI = true - } - tls.Insecure = cfg.AllowInsecure - if cfg.Fingerprint != "" { - tls.UTLS = &OutboundUTLSOptions{ - Enabled: true, - Fingerprint: cfg.Fingerprint, - } - } - case "reality": - tls.Enabled = true - if cfg.SNI != "" { - tls.ServerName = cfg.SNI - } else { - tls.DisableSNI = true - } - tls.Insecure = cfg.AllowInsecure - if cfg.Fingerprint != "" { - tls.UTLS = &OutboundUTLSOptions{ - Enabled: true, - Fingerprint: cfg.Fingerprint, - } - } - tls.Reality = &OutboundRealityOptions{ - Enabled: true, - PublicKey: cfg.RealityPublicKey, - ShortID: cfg.RealityShortId, - } - } - return tls -} - -type OutboundECHOptions struct { - Enabled bool `json:"enabled,omitempty"` - PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"` - DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"` - Config Listable[string] `json:"config,omitempty"` - ConfigPath string `json:"config_path,omitempty"` -} - -type OutboundRealityOptions struct { - Enabled bool `json:"enabled,omitempty"` - PublicKey string `json:"public_key,omitempty"` - ShortID string `json:"short_id,omitempty"` -} - -type OutboundUTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - Fingerprint string `json:"fingerprint,omitempty"` -} -type Listable[T any] []T - -type OutboundTLSOptionsContainer struct { - TLS *OutboundTLSOptions `json:"tls,omitempty"` -} diff --git a/pkg/adapter/singbox/tool.go b/pkg/adapter/singbox/tool.go deleted file mode 100644 index 535dbe8..0000000 --- a/pkg/adapter/singbox/tool.go +++ /dev/null @@ -1,11 +0,0 @@ -package singbox - -import "encoding/json" - -func mergeOptions(target map[string]any, options any) error { - optionsJSON, err := json.Marshal(options) - if err != nil { - return err - } - return json.Unmarshal(optionsJSON, &target) -} diff --git a/pkg/adapter/singbox/trojan.go b/pkg/adapter/singbox/trojan.go deleted file mode 100644 index 9233ff1..0000000 --- a/pkg/adapter/singbox/trojan.go +++ /dev/null @@ -1,39 +0,0 @@ -package singbox - -import ( - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -type TrojanOutboundOptions struct { - ServerOptions - Password string `json:"password"` - Network string `json:"network,omitempty"` - OutboundTLSOptionsContainer - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` -} - -func ParseTrojan(data proxy.Proxy, uuid string) (*Proxy, error) { - trojan := data.Option.(proxy.Trojan) - p := &Proxy{ - Tag: data.Name, - Type: Trojan, - TrojanOptions: &TrojanOutboundOptions{ - ServerOptions: ServerOptions{ - Tag: data.Name, - Type: Trojan, - Server: data.Server, - ServerPort: data.Port, - }, - Password: uuid, - }, - } - // Transport options - transport := NewV2RayTransportOptions(trojan.Transport, trojan.TransportConfig) - - p.TrojanOptions.Transport = transport - // Security options - p.TrojanOptions.TLS = NewOutboundTLSOptions(trojan.Security, trojan.SecurityConfig) - return p, nil - -} diff --git a/pkg/adapter/singbox/tuic.go b/pkg/adapter/singbox/tuic.go deleted file mode 100644 index 9abdab2..0000000 --- a/pkg/adapter/singbox/tuic.go +++ /dev/null @@ -1,40 +0,0 @@ -package singbox - -import ( - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -type TUICOutboundOptions struct { - ServerOptions - UUID string `json:"uuid,omitempty"` - Password string `json:"password,omitempty"` - CongestionControl string `json:"congestion_control,omitempty"` - UDPRelayMode string `json:"udp_relay_mode,omitempty"` - UDPOverStream bool `json:"udp_over_stream,omitempty"` - ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` - Heartbeat string `json:"heartbeat,omitempty"` - Network string `json:"network,omitempty"` - OutboundTLSOptionsContainer -} - -func ParseTUIC(data proxy.Proxy, uuid string) (*Proxy, error) { - tuic := data.Option.(proxy.Tuic) - p := &Proxy{ - Tag: data.Name, - Type: TUIC, - TUICOptions: &TUICOutboundOptions{ - ServerOptions: ServerOptions{ - Tag: data.Name, - Type: TUIC, - Server: data.Server, - ServerPort: data.Port, - }, - UUID: uuid, - Password: uuid, - CongestionControl: "bbr", - }, - } - // Security options - p.TUICOptions.TLS = NewOutboundTLSOptions("tls", tuic.SecurityConfig) - return p, nil -} diff --git a/pkg/adapter/singbox/v2rayTransport.go b/pkg/adapter/singbox/v2rayTransport.go deleted file mode 100644 index 9ba3f10..0000000 --- a/pkg/adapter/singbox/v2rayTransport.go +++ /dev/null @@ -1,114 +0,0 @@ -package singbox - -import ( - "encoding/json" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -type V2RayTransportOptions struct { - Type string `json:"type"` - HTTPOptions V2RayHTTPOptions `json:"-"` - WebsocketOptions V2RayWebsocketOptions `json:"-"` - QUICOptions V2RayQUICOptions `json:"-"` - GRPCOptions V2RayGRPCOptions `json:"-"` - HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"` -} - -func (v V2RayTransportOptions) MarshalJSON() ([]byte, error) { - var v2rayTransportOptions any - data := map[string]any{ - "type": v.Type, - } - switch v.Type { - case "http": - v2rayTransportOptions = v.HTTPOptions - case "ws": - v2rayTransportOptions = v.WebsocketOptions - case "quic": - v2rayTransportOptions = v.QUICOptions - case "grpc": - v2rayTransportOptions = v.GRPCOptions - case "httpupgrade": - v2rayTransportOptions = v.HTTPUpgradeOptions - } - if err := mergeOptions(data, v2rayTransportOptions); err != nil { - return nil, err - } - return json.Marshal(data) -} - -func NewV2RayTransportOptions(network string, transport proxy.TransportConfig) *V2RayTransportOptions { - var t *V2RayTransportOptions = nil - switch network { - case "websocket": - t = &V2RayTransportOptions{ - Type: "ws", - WebsocketOptions: V2RayWebsocketOptions{ - Path: transport.Path, - Headers: map[string]Listable[string]{ - "Host": []string{transport.Host}, - }, - MaxEarlyData: 2048, - EarlyDataHeaderName: "Sec-WebSocket-Protocol", - }, - } - case "httpupgrade": - t = &V2RayTransportOptions{ - Type: "httpupgrade", - HTTPOptions: V2RayHTTPOptions{ - Path: transport.Path, - Host: []string{transport.Host}, - Headers: map[string]Listable[string]{ - "Host": []string{transport.Host}, - }, - }, - } - - case "grpc": - t = &V2RayTransportOptions{ - Type: "grpc", - GRPCOptions: V2RayGRPCOptions{ - ServiceName: transport.ServiceName, - }, - } - } - return t -} - -type V2RayHTTPOptions struct { - Host Listable[string] `json:"host,omitempty"` - Path string `json:"path,omitempty"` - Method string `json:"method,omitempty"` - Headers HTTPHeader `json:"headers,omitempty"` - IdleTimeout Duration `json:"idle_timeout,omitempty"` - PingTimeout Duration `json:"ping_timeout,omitempty"` -} - -type V2RayWebsocketOptions struct { - Path string `json:"path,omitempty"` - Headers HTTPHeader `json:"headers,omitempty"` - MaxEarlyData uint32 `json:"max_early_data,omitempty"` - EarlyDataHeaderName string `json:"early_data_header_name,omitempty"` -} - -type V2RayQUICOptions struct{} - -type V2RayGRPCOptions struct { - ServiceName string `json:"service_name,omitempty"` - IdleTimeout string `json:"idle_timeout,omitempty"` - PingTimeout string `json:"ping_timeout,omitempty"` - PermitWithoutStream bool `json:"permit_without_stream,omitempty"` - ForceLite bool `json:"-"` // for test -} - -type V2RayHTTPUpgradeOptions struct { - Host string `json:"host,omitempty"` - Path string `json:"path,omitempty"` - Headers HTTPHeader `json:"headers,omitempty"` -} - -type HTTPHeader map[string]Listable[string] - -type Duration time.Duration diff --git a/pkg/adapter/singbox/vless.go b/pkg/adapter/singbox/vless.go deleted file mode 100644 index e1038ed..0000000 --- a/pkg/adapter/singbox/vless.go +++ /dev/null @@ -1,44 +0,0 @@ -package singbox - -import ( - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -type VLESSOutboundOptions struct { - ServerOptions - OutboundTLSOptionsContainer - UUID string `json:"uuid"` - Flow string `json:"flow,omitempty"` - Network string `json:"network,omitempty"` - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` - PacketEncoding *string `json:"packet_encoding,omitempty"` -} - -func ParseVless(data proxy.Proxy, uuid string) (*Proxy, error) { - vless := data.Option.(proxy.Vless) - packetEncoding := "xudp" - p := &Proxy{ - Tag: data.Name, - Type: VLESS, - VLESSOptions: &VLESSOutboundOptions{ - ServerOptions: ServerOptions{ - Tag: data.Name, - Type: VLESS, - Server: data.Server, - ServerPort: data.Port, - }, - UUID: uuid, - Flow: vless.Flow, - PacketEncoding: &packetEncoding, - }, - } - // Transport options - transport := NewV2RayTransportOptions(vless.Transport, vless.TransportConfig) - p.VLESSOptions.Transport = transport - - // Security options - p.VLESSOptions.TLS = NewOutboundTLSOptions(vless.Security, vless.SecurityConfig) - - return p, nil -} diff --git a/pkg/adapter/singbox/vmess.go b/pkg/adapter/singbox/vmess.go deleted file mode 100644 index daf062f..0000000 --- a/pkg/adapter/singbox/vmess.go +++ /dev/null @@ -1,43 +0,0 @@ -package singbox - -import ( - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -type VMessOutboundOptions struct { - ServerOptions - UUID string `json:"uuid"` - Security string `json:"security"` - AlterId int `json:"alter_id,omitempty"` - GlobalPadding bool `json:"global_padding,omitempty"` - AuthenticatedLength bool `json:"authenticated_length,omitempty"` - Network string `json:"network,omitempty"` - PacketEncoding string `json:"packet_encoding,omitempty"` - Multiplex *OutboundMultiplexOptions `json:"multiplex,omitempty"` - Transport *V2RayTransportOptions `json:"transport,omitempty"` - OutboundTLSOptionsContainer -} - -func ParseVMess(data proxy.Proxy, uuid string) (*Proxy, error) { - vmess := data.Option.(proxy.Vmess) - p := &Proxy{ - Type: VMess, - VMessOptions: &VMessOutboundOptions{ - ServerOptions: ServerOptions{ - Tag: data.Name, - Type: VMess, - Server: data.Server, - ServerPort: data.Port, - }, - UUID: uuid, - Security: "auto", - AlterId: 0, - }, - } - // Transport options - p.VMessOptions.Transport = NewV2RayTransportOptions(vmess.Transport, vmess.TransportConfig) - // Security options - p.VMessOptions.TLS = NewOutboundTLSOptions(vmess.Security, vmess.SecurityConfig) - - return p, nil -} diff --git a/pkg/adapter/surfboard/build.go b/pkg/adapter/surfboard/build.go deleted file mode 100644 index 879a602..0000000 --- a/pkg/adapter/surfboard/build.go +++ /dev/null @@ -1,111 +0,0 @@ -package surfboard - -import ( - "bytes" - "embed" - "fmt" - "net/url" - "strings" - "text/template" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/traffic" -) - -//go:embed *.tpl -var configFiles embed.FS -var shadowsocksSupportMethod = []string{"aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305"} - -func BuildSurfboard(servers proxy.Adapter, siteName string, user UserInfo) []byte { - var proxies, proxyGroup string - for _, node := range servers.Proxies { - if uri := buildProxy(node, user.UUID); uri != "" { - proxies += uri - } - } - - for _, group := range servers.Group { - if group.Type == proxy.GroupTypeSelect { - proxyGroup += fmt.Sprintf("%s = select, %s", group.Name, strings.Join(group.Proxies, ", ")) + "\r\n" - } else if group.Type == proxy.GroupTypeURLTest { - proxyGroup += fmt.Sprintf("%s = url-test, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n" - } else if group.Type == proxy.GroupTypeFallback { - proxyGroup += fmt.Sprintf("%s = fallback, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n" - } else { - logger.Errorf("[BuildSurfboard] unknown group type: %s", group.Type) - } - } - - var rules string - for _, rule := range servers.Rules { - if rule == "" { - continue - } - rules += rule + "\r\n" - } - - //final rule - rules += "# 最终规则" + "\r\n" + "FINAL, 手动选择" - - file, err := configFiles.ReadFile("default.tpl") - if err != nil { - logger.Errorf("read default surfboard config error: %v", err.Error()) - return nil - } - // replace template - tpl, err := template.New("default").Parse(string(file)) - if err != nil { - logger.Errorf("read default surfboard config error: %v", err.Error()) - return nil - } - var buf bytes.Buffer - - var expiredAt string - if user.ExpiredDate.Before(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)) { - expiredAt = "长期有效" - } else { - expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05") - } - // convert traffic - upload := traffic.AutoConvert(user.Upload, false) - download := traffic.AutoConvert(user.Download, false) - total := traffic.AutoConvert(user.TotalTraffic, false) - unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false) - // query Host - urlParse, err := url.Parse(user.SubscribeURL) - if err != nil { - return nil - } - if err := tpl.Execute(&buf, map[string]interface{}{ - "Proxies": proxies, - "ProxyGroup": proxyGroup, - "SubscribeURL": user.SubscribeURL, - "SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt), - "SubscribeDomain": urlParse.Host, - "Rules": rules, - }); err != nil { - logger.Errorf("build surfboard config error: %v", err.Error()) - return nil - } - return buf.Bytes() -} - -func buildProxy(data proxy.Proxy, uuid string) string { - var p string - switch data.Protocol { - case "vmess": - p = buildVMess(data, uuid) - case "shadowsocks": - if !tool.Contains(shadowsocksSupportMethod, data.Option.(proxy.Shadowsocks).Method) { - return "" - } - p = buildShadowsocks(data, uuid) - case "trojan": - p = buildTrojan(data, uuid) - } - return p -} diff --git a/pkg/adapter/surfboard/build_test.go b/pkg/adapter/surfboard/build_test.go deleted file mode 100644 index f3773b1..0000000 --- a/pkg/adapter/surfboard/build_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package surfboard - -import ( - "testing" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - - "github.com/perfect-panel/ppanel-server/pkg/uuidx" -) - -func TestBuildSurfboard(t *testing.T) { - siteName := "test" - user := UserInfo{ - UUID: uuidx.NewUUID().String(), - Upload: 0, - Download: 0, - TotalTraffic: 0, - ExpiredDate: time.Now().AddDate(0, 1, 1), - SubscribeURL: "https://test.com", - } - conf := BuildSurfboard(proxy.Adapter{}, siteName, user) - t.Log(string(conf)) -} diff --git a/pkg/adapter/surfboard/default.tpl b/pkg/adapter/surfboard/default.tpl deleted file mode 100644 index e30aac0..0000000 --- a/pkg/adapter/surfboard/default.tpl +++ /dev/null @@ -1,29 +0,0 @@ -#!MANAGED-CONFIG {{ .SubscribeURL }} interval=43200 strict=true - -[General] -loglevel = notify -ipv6 = false -skip-proxy = localhost, *.local, injections.adguard.org, local.adguard.org, 0.0.0.0/8, 10.0.0.0/8, 17.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32 -tls-provider = default -show-error-page-for-reject = true -dns-server = 223.6.6.6, 119.29.29.29, 119.28.28.28 -test-timeout = 5 -internet-test-url = http://bing.com -proxy-test-url = http://bing.com - -[Panel] -SubscribeInfo = {{ .SubscribeInfo }}, style=info - -# Surfboard 配置文档:https://manual.getsurfboard.com/ - -[Proxy] -# 代理列表 -{{ .Proxies }} - -[Proxy Group] -# 代理组列表 -{{ .ProxyGroup }} - -[Rule] -# 规则列表 -{{ .Rules }} diff --git a/pkg/adapter/surfboard/model.go b/pkg/adapter/surfboard/model.go deleted file mode 100644 index 29d53ba..0000000 --- a/pkg/adapter/surfboard/model.go +++ /dev/null @@ -1,12 +0,0 @@ -package surfboard - -import "time" - -type UserInfo struct { - UUID string - Upload int64 - Download int64 - TotalTraffic int64 - ExpiredDate time.Time - SubscribeURL string -} diff --git a/pkg/adapter/surfboard/shadowsocks.go b/pkg/adapter/surfboard/shadowsocks.go deleted file mode 100644 index a8d03b4..0000000 --- a/pkg/adapter/surfboard/shadowsocks.go +++ /dev/null @@ -1,24 +0,0 @@ -package surfboard - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func buildShadowsocks(data proxy.Proxy, uuid string) string { - ss, ok := data.Option.(proxy.Shadowsocks) - if !ok { - return "" - } - addr := fmt.Sprintf("%s=ss, %s, %d", data.Name, data.Server, data.Port) - config := []string{ - addr, - fmt.Sprintf("encrypt-method=%s", ss.Method), - fmt.Sprintf("password=%s", uuid), - "tfo=true", - "udp-relay=true", - } - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/surfboard/shadowsocks_test.go b/pkg/adapter/surfboard/shadowsocks_test.go deleted file mode 100644 index a07e2b6..0000000 --- a/pkg/adapter/surfboard/shadowsocks_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package surfboard - -import ( - "testing" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func createSS() proxy.Proxy { - return proxy.Proxy{ - Name: "Shadowsocks", - Server: "test.xxxx.com", - Port: 10301, - Protocol: "shadowsocks", - Option: proxy.Shadowsocks{ - Port: 10301, - Method: "aes-256-gcm", - ServerKey: "123456", - }, - } -} - -func TestShadowsocks(t *testing.T) { - node := createSS() - uuid := "123456" - shadowsocks := buildShadowsocks(node, uuid) - t.Log(shadowsocks) -} diff --git a/pkg/adapter/surfboard/trojan.go b/pkg/adapter/surfboard/trojan.go deleted file mode 100644 index 86884b3..0000000 --- a/pkg/adapter/surfboard/trojan.go +++ /dev/null @@ -1,41 +0,0 @@ -package surfboard - -import ( - "strconv" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func buildTrojan(data proxy.Proxy, uuid string) string { - // $config = [ - // "{$server['name']}=trojan", - // "{$server['host']}", - // "{$server['port']}", - // "password={$password}", - // $protocol_settings['server_name'] ? "sni={$protocol_settings['server_name']}" : "", - // 'tfo=true', - // 'udp-relay=true' - //]; - trojan, ok := data.Option.(proxy.Trojan) - if !ok { - return "" - } - config := []string{ - data.Name + "=trojan", - data.Server, - strconv.Itoa(data.Port), - "password=" + uuid, - "tfo=true", - "udp-relay=true", - } - if trojan.SecurityConfig.SNI != "" { - config = append(config, "sni="+trojan.SecurityConfig.SNI) - } - if trojan.SecurityConfig.AllowInsecure { - config = append(config, "skip-cert-verify=true") - } else { - config = append(config, "skip-cert-verify=false") - } - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/surfboard/trojan_test.go b/pkg/adapter/surfboard/trojan_test.go deleted file mode 100644 index b1938d9..0000000 --- a/pkg/adapter/surfboard/trojan_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package surfboard - -import ( - "testing" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func createTrojan() proxy.Proxy { - - return proxy.Proxy{ - Name: "Trojan", - Server: "test.xxxx.com", - Port: 13002, - Protocol: "trojan", - Option: proxy.Trojan{ - Port: 13002, - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "baidu.com", - }, - SecurityConfig: proxy.SecurityConfig{ - SNI: "baidu.com", - AllowInsecure: true, - }, - }, - } -} - -func TestTrojan(t *testing.T) { - node := createTrojan() - uuid := "123456" - trojan := buildTrojan(node, uuid) - t.Log(trojan) -} diff --git a/pkg/adapter/surfboard/vmess.go b/pkg/adapter/surfboard/vmess.go deleted file mode 100644 index 0d8f57a..0000000 --- a/pkg/adapter/surfboard/vmess.go +++ /dev/null @@ -1,45 +0,0 @@ -package surfboard - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func buildVMess(data proxy.Proxy, uuid string) string { - vmess, ok := data.Option.(proxy.Vmess) - if !ok { - return "" - } - addr := fmt.Sprintf("%s=vmess, %s, %d", data.Name, data.Server, data.Port) - uriConfig := []string{ - addr, - fmt.Sprintf("username=%s", uuid), - "vmess-aead=true", - "tfo=true", - "udp-relay=true", - } - if vmess.Security == "tls" { - uriConfig = append(uriConfig, "tls=true") - if vmess.SecurityConfig.AllowInsecure { - uriConfig = append(uriConfig, "skip-cert-verify=true") - } else { - uriConfig = append(uriConfig, "skip-cert-verify=false") - } - if vmess.SecurityConfig.SNI != "" { - uriConfig = append(uriConfig, fmt.Sprintf("sni=%s", vmess.SecurityConfig.SNI)) - } - } - if vmess.Transport == "websocket" { - uriConfig = append(uriConfig, "ws=true") - if vmess.TransportConfig.Path != "" { - uriConfig = append(uriConfig, fmt.Sprintf("ws-path=%s", vmess.TransportConfig.Path)) - } - if vmess.TransportConfig.Host != "" { - uriConfig = append(uriConfig, fmt.Sprintf("ws-headers=Host:%s", vmess.TransportConfig.Host)) - } - } - - return strings.Join(uriConfig, ",") + "\r\n" -} diff --git a/pkg/adapter/surfboard/vmess_test.go b/pkg/adapter/surfboard/vmess_test.go deleted file mode 100644 index 4ccb912..0000000 --- a/pkg/adapter/surfboard/vmess_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package surfboard - -import ( - "testing" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func createVMess() proxy.Proxy { - - return proxy.Proxy{ - Name: "Vmess", - Server: "test.xxxx.com", - Port: 13002, - Protocol: "vmess", - Option: proxy.Vmess{ - Port: 13002, - Transport: "websocket", - TransportConfig: proxy.TransportConfig{ - Path: "/ws", - Host: "test.xx.com", - }, - Security: "none", - }, - } -} - -func TestVMess(t *testing.T) { - node := createVMess() - uuid := "123456" - p := buildVMess(node, uuid) - t.Log(p) -} diff --git a/pkg/adapter/surge/default.tpl b/pkg/adapter/surge/default.tpl deleted file mode 100644 index 7375b37..0000000 --- a/pkg/adapter/surge/default.tpl +++ /dev/null @@ -1,61 +0,0 @@ -#!MANAGED-CONFIG {{ .SubscribeURL }} interval=43200 strict=true -# Surge 的规则配置手册: https://manual.nssurge.com/ - -[General] -loglevel = notify -# 从 Surge iOS 4 / Surge Mac 3.3.0 起,工具开始支持 DoH -doh-server = https://doh.pub/dns-query -# https://dns.alidns.com/dns-query, https://13800000000.rubyfish.cn/, https://dns.google/dns-query -dns-server = 223.5.5.5, 114.114.114.114 -tun-excluded-routes = 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 255.255.255.255/32 -skip-proxy = localhost, *.local, injections.adguard.org, local.adguard.org, captive.apple.com, guzzoni.apple.com, 0.0.0.0/8, 10.0.0.0/8, 17.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.168.0.0/16, 192.88.99.0/24, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4, 255.255.255.255/32 - -wifi-assist = true -allow-wifi-access = true -wifi-access-http-port = 6152 -wifi-access-socks5-port = 6153 -http-listen = 0.0.0.0:6152 -socks5-listen = 0.0.0.0:6153 - -external-controller-access = surgepasswd@0.0.0.0:6170 -replica = false - -tls-provider = openssl -network-framework = false -exclude-simple-hostnames = true -ipv6 = true - -test-timeout = 4 -proxy-test-url = http://www.gstatic.com/generate_204 -geoip-maxmind-url = https://unpkg.zhimg.com/rulestatic@1.0.1/Country.mmdb - -[Replica] -hide-apple-request = true -hide-crashlytics-request = true -use-keyword-filter = false -hide-udp = false - -[Panel] -SubscribeInfo = {{ .SubscribeInfo }}, style=info - -# ----------------------------- -# Surge 的几种策略配置规范,请参考 https://manual.nssurge.com/policy/proxy.html -# 不同的代理策略有*很多*可选参数,请参考上方连接的 Parameters 一段,根据需求自行添加参数。 -# -# Surge 现已支持 UDP 转发功能,请参考: https://trello.com/c/ugOMxD3u/53-udp-%E8%BD%AC%E5%8F%91 -# Surge 现已支持 TCP-Fast-Open 技术,请参考: https://trello.com/c/ij65BU6Q/48-tcp-fast-open-troubleshooting-guide -# Surge 现已支持 ss-libev 的全部加密方式和混淆,请参考: https://trello.com/c/BTr0vG1O/47-ss-libev-%E7%9A%84%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5 -# ----------------------------- - -[Proxy] -{{ .Proxies }} - -[Proxy Group] -# 代理组列表 -{{ .ProxyGroup }} - -[Rule] -{{ .Rules }} - -[URL Rewrite] -^https?://(www.)?(g|google).cn https://www.google.com 302 \ No newline at end of file diff --git a/pkg/adapter/surge/hysteria2.go b/pkg/adapter/surge/hysteria2.go deleted file mode 100644 index d2205e0..0000000 --- a/pkg/adapter/surge/hysteria2.go +++ /dev/null @@ -1,43 +0,0 @@ -package surge - -import ( - "fmt" - "strconv" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func buildHysteria2(data proxy.Proxy, uuid string) string { - hysteria2, ok := data.Option.(proxy.Hysteria2) - if !ok { - return "" - } - - var port int - if hysteria2.HopPorts != "" { - ports := strings.Split(hysteria2.HopPorts, ",") - p := ports[0] - if len(strings.Split(p, "-")) > 1 { - p = strings.Split(p, "-")[0] - } - port, _ = strconv.Atoi(p) - } else { - port = data.Port - } - - config := []string{ - fmt.Sprintf("%s=hysteria2,%s,%d", data.Name, data.Server, port), - "password=" + uuid, - "udp-relay=true", - } - if hysteria2.SecurityConfig.SNI != "" { - config = append(config, "sni="+hysteria2.SecurityConfig.SNI) - } - if hysteria2.SecurityConfig.AllowInsecure { - config = append(config, "skip-cert-verify=true") - } else { - config = append(config, "skip-cert-verify=false") - } - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/surge/hysteria2_test.go b/pkg/adapter/surge/hysteria2_test.go deleted file mode 100644 index 73d4306..0000000 --- a/pkg/adapter/surge/hysteria2_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package surge - -import ( - "testing" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func TestBuildHysteria2(t *testing.T) { - tests := []struct { - name string - data proxy.Proxy - uuid string - expected string - }{ - { - name: "Valid Hysteria2 with HopPorts", - data: proxy.Proxy{ - Name: "test", - Server: "server.com", - Port: 443, - Option: proxy.Hysteria2{ - HopPorts: "1000-2000", - SecurityConfig: proxy.SecurityConfig{ - SNI: "example.com", - AllowInsecure: true, - }, - }, - }, - uuid: "test-uuid", - expected: "test=hysteria2,server.com,1000,password=test-uuid,udp-relay=true,sni=example.com,skip-cert-verify=true\r\n", - }, - { - name: "Valid Hysteria2 without HopPorts", - data: proxy.Proxy{ - Name: "test", - Server: "server.com", - Port: 443, - Option: proxy.Hysteria2{ - SecurityConfig: proxy.SecurityConfig{ - SNI: "example.com", - AllowInsecure: false, - }, - }, - }, - uuid: "test-uuid", - expected: "test=hysteria2,server.com,443,password=test-uuid,udp-relay=true,sni=example.com,skip-cert-verify=false\r\n", - }, - { - name: "Invalid Hysteria2 Option", - data: proxy.Proxy{ - Name: "test", - Server: "server.com", - Port: 443, - Option: nil, - }, - uuid: "test-uuid", - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := buildHysteria2(tt.data, tt.uuid) - if result != tt.expected { - t.Errorf("expected %s, got %s", tt.expected, result) - } - }) - } -} diff --git a/pkg/adapter/surge/shadowsocks.go b/pkg/adapter/surge/shadowsocks.go deleted file mode 100644 index 25eef9c..0000000 --- a/pkg/adapter/surge/shadowsocks.go +++ /dev/null @@ -1,24 +0,0 @@ -package surge - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func buildShadowsocks(data proxy.Proxy, uuid string) string { - ss, ok := data.Option.(proxy.Shadowsocks) - if !ok { - return "" - } - addr := fmt.Sprintf("%s=ss, %s, %d", data.Name, data.Server, data.Port) - config := []string{ - addr, - fmt.Sprintf("encrypt-method=%s", ss.Method), - fmt.Sprintf("password=%s", uuid), - "tfo=true", - "udp-relay=true", - } - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/surge/surge.go b/pkg/adapter/surge/surge.go deleted file mode 100644 index c6565a5..0000000 --- a/pkg/adapter/surge/surge.go +++ /dev/null @@ -1,117 +0,0 @@ -package surge - -import ( - "bytes" - "embed" - "fmt" - "net/url" - "strings" - "text/template" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/traffic" -) - -//go:embed *.tpl -var configFiles embed.FS - -type UserInfo struct { - UUID string - Upload int64 - Download int64 - TotalTraffic int64 - ExpiredDate time.Time - SubscribeURL string -} - -type Surge struct { - Adapter proxy.Adapter - UUID string - User UserInfo -} - -func NewSurge(adapter proxy.Adapter) *Surge { - return &Surge{ - Adapter: adapter, - } -} - -func (m *Surge) Build(uuid, siteName string, user UserInfo) []byte { - var proxies, proxyGroup, rules string - - for _, p := range m.Adapter.Proxies { - switch p.Protocol { - case "shadowsocks": - proxies += buildShadowsocks(p, uuid) - case "trojan": - proxies += buildTrojan(p, uuid) - case "hysteria2": - proxies += buildHysteria2(p, uuid) - case "vmess": - proxies += buildVMess(p, uuid) - } - } - for _, group := range m.Adapter.Group { - if group.Type == proxy.GroupTypeSelect { - proxyGroup += fmt.Sprintf("%s = select, %s", group.Name, strings.Join(group.Proxies, ", ")) + "\r\n" - } else if group.Type == proxy.GroupTypeURLTest { - proxyGroup += fmt.Sprintf("%s = url-test, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n" - } else if group.Type == proxy.GroupTypeFallback { - proxyGroup += fmt.Sprintf("%s = fallback, %s, url=%s, interval=%d", group.Name, strings.Join(group.Proxies, ", "), group.URL, group.Interval) + "\r\n" - } else { - logger.Errorf("[BuildSurfboard] unknown group type: %s", group.Type) - } - } - for _, rule := range m.Adapter.Rules { - if rule == "" { - continue - } - rules += rule + "\r\n" - } - //final rule - rules += "# 最终规则" + "\r\n" + "FINAL,手动选择,dns-failed" - - file, err := configFiles.ReadFile("default.tpl") - if err != nil { - logger.Errorf("read default surfboard config error: %v", err.Error()) - return nil - } - // replace template - tpl, err := template.New("default").Parse(string(file)) - if err != nil { - logger.Errorf("read default surfboard config error: %v", err.Error()) - return nil - } - var buf bytes.Buffer - - var expiredAt string - if user.ExpiredDate.Before(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)) { - expiredAt = "长期有效" - } else { - expiredAt = user.ExpiredDate.Format("2006-01-02 15:04:05") - } - // convert traffic - upload := traffic.AutoConvert(user.Upload, false) - download := traffic.AutoConvert(user.Download, false) - total := traffic.AutoConvert(user.TotalTraffic, false) - unusedTraffic := traffic.AutoConvert(user.TotalTraffic-user.Upload-user.Download, false) - // query Host - urlParse, err := url.Parse(user.SubscribeURL) - if err != nil { - return nil - } - if err := tpl.Execute(&buf, map[string]interface{}{ - "Proxies": proxies, - "ProxyGroup": proxyGroup, - "SubscribeURL": user.SubscribeURL, - "SubscribeInfo": fmt.Sprintf("title=%s订阅信息, content=上传流量:%s\\n下载流量:%s\\n剩余流量: %s\\n套餐流量:%s\\n到期时间:%s", siteName, upload, download, unusedTraffic, total, expiredAt), - "SubscribeDomain": urlParse.Host, - "Rules": rules, - }); err != nil { - logger.Errorf("build Surge config error: %v", err.Error()) - return nil - } - return buf.Bytes() -} diff --git a/pkg/adapter/surge/surge_test.go b/pkg/adapter/surge/surge_test.go deleted file mode 100644 index efec1d5..0000000 --- a/pkg/adapter/surge/surge_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package surge - -import ( - "strings" - "testing" - "time" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func TestSurgeBuild(t *testing.T) { - adapter := proxy.Adapter{ - Proxies: []proxy.Proxy{ - { - Name: "test-shadowsocks", - Protocol: "shadowsocks", - Server: "1.2.3.4", - Port: 8388, - Option: proxy.Shadowsocks{ - Method: "aes-256-gcm", - }, - }, - { - Name: "test-trojan", - Protocol: "trojan", - Server: "5.6.7.8", - Port: 443, - Option: proxy.Trojan{ - SecurityConfig: proxy.SecurityConfig{ - SNI: "example.com", - AllowInsecure: true, - }, - }, - }, - { - Name: "test-hysteria", - Protocol: "hysteria2", - Server: "1.1.1.1", - Port: 443, - Option: proxy.Hysteria2{ - HopPorts: "8080-8090", - HopInterval: 320, - SecurityConfig: proxy.SecurityConfig{ - SNI: "example.com", - AllowInsecure: true, - }, - }, - }, - }, - Group: []proxy.Group{ - { - Name: "test-group", - Type: proxy.GroupTypeSelect, - Proxies: []string{"test-shadowsocks", "test-trojan", "test-hysteria"}, - }, - { - Name: "手动选择", - Type: proxy.GroupTypeSelect, - Proxies: []string{"test-shadowsocks", "test-trojan", "test-hysteria"}, - }, - }, - Rules: []string{ - "DOMAIN-SUFFIX,example.com,DIRECT", - }, - } - - user := UserInfo{ - UUID: "test-uuid", - Upload: 1024, - Download: 2048, - TotalTraffic: 4096, - ExpiredDate: time.Now().Add(24 * time.Hour), - SubscribeURL: "http://example.com/subscribe", - } - - surge := NewSurge(adapter) - config := surge.Build("test-uuid", "TestSite", user) - - if config == nil { - t.Fatal("Expected non-nil config") - } - - configStr := string(config) - t.Logf("configStr: %v", configStr) - if !strings.Contains(configStr, "test-shadowsocks=ss") { - t.Errorf("Expected config to contain test-shadowsocks proxy") - } - if !strings.Contains(configStr, "test-trojan=trojan") { - t.Errorf("Expected config to contain test-trojan proxy") - } - if !strings.Contains(configStr, "test-group = select") { - t.Errorf("Expected config to contain test-group proxy group") - } - if !strings.Contains(configStr, "DOMAIN-SUFFIX,example.com,DIRECT") { - t.Errorf("Expected config to contain rule for example.com") - } -} diff --git a/pkg/adapter/surge/trojan.go b/pkg/adapter/surge/trojan.go deleted file mode 100644 index 4d755c7..0000000 --- a/pkg/adapter/surge/trojan.go +++ /dev/null @@ -1,32 +0,0 @@ -package surge - -import ( - "strconv" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func buildTrojan(data proxy.Proxy, uuid string) string { - trojan, ok := data.Option.(proxy.Trojan) - if !ok { - return "" - } - config := []string{ - data.Name + "=trojan", - data.Server, - strconv.Itoa(data.Port), - "password=" + uuid, - "tfo=true", - "udp-relay=true", - } - if trojan.SecurityConfig.SNI != "" { - config = append(config, "sni="+trojan.SecurityConfig.SNI) - } - if trojan.SecurityConfig.AllowInsecure { - config = append(config, "skip-cert-verify=true") - } else { - config = append(config, "skip-cert-verify=false") - } - return strings.Join(config, ",") + "\r\n" -} diff --git a/pkg/adapter/surge/vmess.go b/pkg/adapter/surge/vmess.go deleted file mode 100644 index 0b2c95d..0000000 --- a/pkg/adapter/surge/vmess.go +++ /dev/null @@ -1,44 +0,0 @@ -package surge - -import ( - "fmt" - "strings" - - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" -) - -func buildVMess(data proxy.Proxy, uuid string) string { - vmess, ok := data.Option.(proxy.Vmess) - if !ok { - return "" - } - addr := fmt.Sprintf("%s=vmess, %s, %d", data.Name, data.Server, data.Port) - uriConfig := []string{ - addr, - fmt.Sprintf("username=%s", uuid), - "vmess-aead=true", - "tfo=true", - "udp-relay=true", - } - if vmess.Security == "tls" { - uriConfig = append(uriConfig, "tls=true") - if vmess.SecurityConfig.AllowInsecure { - uriConfig = append(uriConfig, "skip-cert-verify=true") - } else { - uriConfig = append(uriConfig, "skip-cert-verify=false") - } - if vmess.SecurityConfig.SNI != "" { - uriConfig = append(uriConfig, fmt.Sprintf("sni=%s", vmess.SecurityConfig.SNI)) - } - } - if vmess.Transport == "websocket" { - uriConfig = append(uriConfig, "ws=true") - if vmess.TransportConfig.Path != "" { - uriConfig = append(uriConfig, fmt.Sprintf("ws-path=%s", vmess.TransportConfig.Path)) - } - if vmess.TransportConfig.Host != "" { - uriConfig = append(uriConfig, fmt.Sprintf("ws-headers=Host:%s", vmess.TransportConfig.Host)) - } - } - return strings.Join(uriConfig, ",") + "\r\n" -} diff --git a/pkg/adapter/uilts.go b/pkg/adapter/uilts.go deleted file mode 100644 index 698c1ba..0000000 --- a/pkg/adapter/uilts.go +++ /dev/null @@ -1,197 +0,0 @@ -package adapter - -import ( - "encoding/json" - "strings" - - "github.com/perfect-panel/ppanel-server/internal/model/server" - "github.com/perfect-panel/ppanel-server/pkg/adapter/proxy" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/random" - "github.com/perfect-panel/ppanel-server/pkg/tool" -) - -func addNode(data *server.Server, host string, port int) *proxy.Proxy { - var option any - node := proxy.Proxy{ - Name: data.Name, - Server: host, - Port: port, - Country: data.Country, - Protocol: data.Protocol, - } - switch data.Protocol { - case "shadowsocks": - var ss proxy.Shadowsocks - if err := json.Unmarshal([]byte(data.Config), &ss); err != nil { - return nil - } - if port == 0 { - node.Port = ss.Port - } - option = ss - case "vless": - var vless proxy.Vless - if err := json.Unmarshal([]byte(data.Config), &vless); err != nil { - return nil - } - if port == 0 { - node.Port = vless.Port - } - option = vless - case "vmess": - var vmess proxy.Vmess - if err := json.Unmarshal([]byte(data.Config), &vmess); err != nil { - return nil - } - if port == 0 { - node.Port = vmess.Port - } - option = vmess - case "trojan": - var trojan proxy.Trojan - if err := json.Unmarshal([]byte(data.Config), &trojan); err != nil { - return nil - } - if port == 0 { - node.Port = trojan.Port - } - option = trojan - case "hysteria2": - var hysteria2 proxy.Hysteria2 - if err := json.Unmarshal([]byte(data.Config), &hysteria2); err != nil { - return nil - } - if port == 0 { - node.Port = hysteria2.Port - } - option = hysteria2 - case "tuic": - var tuic proxy.Tuic - if err := json.Unmarshal([]byte(data.Config), &tuic); err != nil { - return nil - } - if port == 0 { - node.Port = tuic.Port - } - option = tuic - default: - return nil - } - node.Option = option - return &node -} - -func addProxyToGroup(proxyName, groupName string, groups []proxy.Group) []proxy.Group { - for i, group := range groups { - if group.Name == groupName { - groups[i].Proxies = tool.RemoveDuplicateElements(append(group.Proxies, proxyName)...) - return groups - } - } - groups = append(groups, proxy.Group{ - Name: groupName, - Type: "select", - Proxies: []string{proxyName}, - }) - return groups -} - -func adapterRules(groups []*server.RuleGroup) (proxyGroup []proxy.Group, rules []string) { - for _, group := range groups { - proxyGroup = append(proxyGroup, proxy.Group{ - Name: group.Name, - Type: "select", - Proxies: RemoveEmptyString(strings.Split(group.Tags, ",")), - }) - rules = append(rules, strings.Split(group.Rules, "/n")...) - } - return -} - -func generateProxyGroup(servers []proxy.Proxy) (proxyGroup []proxy.Group, region []string) { - // 设置手动选择分组 - proxyGroup = append(proxyGroup, []proxy.Group{ - { - Name: "智能线路", - Type: "url-test", - Proxies: make([]string, 0), - URL: "https://www.gstatic.com/generate_204", - Interval: 300, - }, - { - Name: "手动选择", - Type: "select", - Proxies: []string{"智能线路"}, - }, - }...) - - for _, node := range servers { - if node.Country != "" { - proxyGroup = addProxyToGroup(node.Name, node.Country, proxyGroup) - region = append(region, node.Country) - proxyGroup = addProxyToGroup(node.Country, "智能线路", proxyGroup) - } - proxyGroup = addProxyToGroup(node.Name, "手动选择", proxyGroup) - } - proxyGroup = addProxyToGroup("DIRECT", "手动选择", proxyGroup) - return proxyGroup, tool.RemoveDuplicateElements(region...) -} - -func adapterProxies(servers []*server.Server) []proxy.Proxy { - var proxies []proxy.Proxy - for _, node := range servers { - switch node.RelayMode { - case server.RelayModeAll: - var relays []server.NodeRelay - if err := json.Unmarshal([]byte(node.RelayNode), &relays); err != nil { - logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", node.Name), logger.Field("relayNode", node.RelayNode)) - continue - } - for _, relay := range relays { - n := addNode(node, relay.Host, relay.Port) - if n == nil { - continue - } - if relay.Prefix != "" { - n.Name = relay.Prefix + "-" + n.Name - } - proxies = append(proxies, *n) - } - case server.RelayModeRandom: - var relays []server.NodeRelay - if err := json.Unmarshal([]byte(node.RelayNode), &relays); err != nil { - logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", node.Name), logger.Field("relayNode", node.RelayNode)) - continue - } - randNum := random.RandomInRange(0, len(relays)-1) - relay := relays[randNum] - n := addNode(node, relay.Host, relay.Port) - if n == nil { - continue - } - if relay.Prefix != "" { - n.Name = relay.Prefix + " - " + node.Name - } - proxies = append(proxies, *n) - default: - logger.Info("Not Relay Mode", logger.Field("node", node.Name), logger.Field("relayMode", node.RelayMode)) - n := addNode(node, node.ServerAddr, 0) - if n != nil { - proxies = append(proxies, *n) - } - } - } - return proxies -} - -// RemoveEmptyString 切片去除空值 -func RemoveEmptyString(arr []string) []string { - var result []string - for _, str := range arr { - if str != "" { - result = append(result, str) - } - } - return result -} diff --git a/pkg/cache/cacheopt.go b/pkg/cache/cacheopt.go new file mode 100644 index 0000000..836c12a --- /dev/null +++ b/pkg/cache/cacheopt.go @@ -0,0 +1,49 @@ +package cache + +import "time" + +const ( + defaultExpiry = time.Hour * 24 * 7 + defaultNotFoundExpiry = time.Minute +) + +type ( + // Options is used to store the cache options. + Options struct { + Expiry time.Duration + NotFoundExpiry time.Duration + } + + // Option defines the method to customize an Options. + Option func(o *Options) +) + +func newOptions(opts ...Option) Options { + var o Options + for _, opt := range opts { + opt(&o) + } + + if o.Expiry <= 0 { + o.Expiry = defaultExpiry + } + if o.NotFoundExpiry <= 0 { + o.NotFoundExpiry = defaultNotFoundExpiry + } + + return o +} + +// WithExpiry returns a func to customize an Options with given expiry. +func WithExpiry(expiry time.Duration) Option { + return func(o *Options) { + o.Expiry = expiry + } +} + +// WithNotFoundExpiry returns a func to customize an Options with given not found expiry. +func WithNotFoundExpiry(expiry time.Duration) Option { + return func(o *Options) { + o.NotFoundExpiry = expiry + } +} diff --git a/pkg/cache/cacheopt_test.go b/pkg/cache/cacheopt_test.go new file mode 100644 index 0000000..7b7d82c --- /dev/null +++ b/pkg/cache/cacheopt_test.go @@ -0,0 +1,28 @@ +package cache + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestCacheOptions(t *testing.T) { + t.Run("default options", func(t *testing.T) { + o := newOptions() + assert.Equal(t, defaultExpiry, o.Expiry) + assert.Equal(t, defaultNotFoundExpiry, o.NotFoundExpiry) + }) + + t.Run("with expiry", func(t *testing.T) { + o := newOptions(WithExpiry(time.Second)) + assert.Equal(t, time.Second, o.Expiry) + assert.Equal(t, defaultNotFoundExpiry, o.NotFoundExpiry) + }) + + t.Run("with not found expiry", func(t *testing.T) { + o := newOptions(WithNotFoundExpiry(time.Second)) + assert.Equal(t, defaultExpiry, o.Expiry) + assert.Equal(t, time.Second, o.NotFoundExpiry) + }) +} diff --git a/pkg/cache/gorm.go b/pkg/cache/gorm.go index 23552de..0fd2805 100644 --- a/pkg/cache/gorm.go +++ b/pkg/cache/gorm.go @@ -5,12 +5,16 @@ import ( "database/sql" "encoding/json" "errors" + "time" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) -var ErrNotFound = redis.Nil +var ( + // ErrNotFound is the error when cache not found. + ErrNotFound = redis.Nil +) type ( // ExecCtxFn defines the sql exec method. @@ -23,16 +27,21 @@ type ( QueryCtxFn func(conn *gorm.DB, v interface{}) error CachedConn struct { - db *gorm.DB - cache *redis.Client + db *gorm.DB + cache *redis.Client + expiry time.Duration + notFoundExpiry time.Duration } ) // NewConn returns a CachedConn with a redis cluster cache. -func NewConn(db *gorm.DB, c *redis.Client) CachedConn { +func NewConn(db *gorm.DB, c *redis.Client, opts ...Option) CachedConn { + o := newOptions(opts...) return CachedConn{ - db: db, - cache: c, + db: db, + cache: c, + expiry: o.Expiry, + notFoundExpiry: o.NotFoundExpiry, } } @@ -65,7 +74,7 @@ func (cc CachedConn) SetCache(key string, v interface{}) error { return err } // set redis key - return cc.cache.Set(context.Background(), key, val, 0).Err() + return cc.cache.Set(context.Background(), key, val, cc.expiry).Err() } // ExecCtx runs given exec on given keys, and returns execution result. diff --git a/pkg/cache/gorm_test.go b/pkg/cache/gorm_test.go index 76bf62e..0078884 100644 --- a/pkg/cache/gorm_test.go +++ b/pkg/cache/gorm_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/perfect-panel/ppanel-server/pkg/orm" + "github.com/perfect-panel/server/pkg/orm" "github.com/redis/go-redis/v9" "gorm.io/gorm" "gorm.io/plugin/soft_delete" diff --git a/pkg/constant/context.go b/pkg/constant/context.go index 4023cd1..45c7f86 100644 --- a/pkg/constant/context.go +++ b/pkg/constant/context.go @@ -8,4 +8,5 @@ const ( CtxKeyRequestHost CtxKey = "requestHost" CtxKeyPlatform CtxKey = "platform" CtxKeyPayment CtxKey = "payment" + LoginType CtxKey = "loginType" ) diff --git a/pkg/constant/types.go b/pkg/constant/types.go index 20238a9..a2db39b 100644 --- a/pkg/constant/types.go +++ b/pkg/constant/types.go @@ -1,8 +1,6 @@ package constant -import ( - "encoding/json" -) +import "encoding/json" // Used for type cloning conversion const ( @@ -43,9 +41,20 @@ type TemporaryOrderInfo struct { Identifier string `json:"identifier"` AuthType string `json:"auth_type"` Password string `json:"password"` + InviteCode string `json:"invite_code,omitempty"` } -func (t TemporaryOrderInfo) Marshal() string { - value, _ := json.Marshal(t) - return string(value) +func (t *TemporaryOrderInfo) Unmarshal(data []byte) error { + type Alias TemporaryOrderInfo + aux := (*Alias)(t) + return json.Unmarshal(data, aux) +} + +func (t *TemporaryOrderInfo) Marshal() ([]byte, error) { + type Alias TemporaryOrderInfo + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(t), + }) } diff --git a/pkg/constant/version.go b/pkg/constant/version.go index a964009..5370107 100644 --- a/pkg/constant/version.go +++ b/pkg/constant/version.go @@ -1,4 +1,7 @@ package constant // Version PPanel version -const Version = "0.3.0(3002)" +var ( + Version = "unknown version" + BuildTime = "unknown time" +) diff --git a/pkg/countryCenter/county_center.go b/pkg/countryCenter/county_center.go deleted file mode 100644 index 549b762..0000000 --- a/pkg/countryCenter/county_center.go +++ /dev/null @@ -1,1175 +0,0 @@ -package countryCenter - -import ( - "fmt" - "strings" -) - -var countryCenter = map[string][2]float64{ - "Afghanistan": {33.93911, 67.709953}, - "Albania": {41.153332, 20.168331}, - "Algeria": {28.033886, 1.659626}, - "Andorra": {42.546245, 1.601554}, - "Angola": {-11.202692, 17.873887}, - "Antigua and Barbuda": {17.060816, -61.796428}, - "Argentina": {-38.416097, -63.616672}, - "Armenia": {40.069099, 45.038189}, - "Australia": {-25.274398, 133.775136}, - "Azerbaijan": {40.143105, 47.576927}, - "Bahamas": {25.03428, -77.39628}, - "Bahrain": {26.0667, 50.5577}, - "Bangladesh": {23.684994, 90.356331}, - "Barbados": {13.193887, -59.543198}, - "Belarus": {53.709807, 27.953389}, - "Belgium": {50.503887, 4.469936}, - "Belize": {17.189877, -88.49765}, - "Benin": {9.30769, 2.315834}, - "Bhutan": {27.514162, 90.433601}, - "Bolivia": {-16.290154, -63.588653}, - "Bosnia and Herzegovina": {43.915886, 17.679076}, - "Botswana": {-22.328474, 24.684866}, - "Brazil": {-14.235004, -51.92528}, - "Brunei": {4.535277, 114.727669}, - "Bulgaria": {42.733883, 25.48583}, - "Burkina Faso": {12.238333, -1.561593}, - "Burundi": {-3.373056, 29.918886}, - "Cabo Verde": {16.5388, -23.0418}, - "Cambodia": {12.565679, 104.990963}, - "Cameroon": {7.369722, 12.354722}, - "Canada": {56.130366, -106.346771}, - "Central African Republic": {6.611111, 20.939444}, - "Chad": {15.454166, 18.732207}, - "Chile": {-35.675147, -71.542969}, - "China": {35.86166, 104.195397}, - "Colombia": {4.570868, -74.297333}, - "Comoros": {-11.6455, 43.3333}, - "Congo": {-0.228021, 15.827659}, - "Costa Rica": {9.748917, -83.753428}, - "Croatia": {45.1, 15.2}, - "Cuba": {21.521757, -77.781167}, - "Cyprus": {35.126413, 33.429859}, - "Czechia": {49.817492, 15.472962}, - "Denmark": {56.26392, 9.501785}, - "Djibouti": {11.825138, 42.590275}, - "Dominica": {15.414999, -61.370976}, - "Dominican Republic": {18.735693, -70.162651}, - "Ecuador": {-1.831239, -78.183406}, - "Egypt": {26.820553, 30.802498}, - "El Salvador": {13.794185, -88.89653}, - "Equatorial Guinea": {1.650801, 10.267895}, - "Eritrea": {15.179384, 39.782334}, - "Estonia": {58.595272, 25.013607}, - "Eswatini": {-26.522503, 31.465866}, - "Ethiopia": {9.145, 40.489673}, - "Fiji": {-17.713371, 178.065032}, - "Finland": {61.92411, 25.748151}, - "France": {46.603354, 1.888334}, - "Gabon": {-0.803689, 11.609444}, - "Gambia": {13.443182, -15.310139}, - "Georgia": {32.165622, -82.900075}, - "Germany": {51.165691, 10.451526}, - "Ghana": {7.946527, -1.023194}, - "Greece": {39.074208, 21.824312}, - "Grenada": {12.262776, -61.604171}, - "Guatemala": {15.783471, -90.230759}, - "Guinea": {9.945587, -9.696645}, - "Guinea-Bissau": {11.803749, -15.180413}, - "Guyana": {4.860416, -58.93018}, - "Haiti": {18.971187, -72.285215}, - "Honduras": {15.199999, -86.241905}, - "Hungary": {47.162494, 19.503304}, - "Iceland": {64.963051, -19.020835}, - "India": {20.593684, 78.96288}, - "Indonesia": {-0.789275, 113.921327}, - "Iran": {32.427908, 53.688046}, - "Iraq": {33.223191, 43.679291}, - "Ireland": {53.41291, -8.24389}, - "Israel": {31.046051, 34.851612}, - "Italy": {41.87194, 12.56738}, - "Jamaica": {18.109581, -77.297508}, - "Japan": {36.204824, 138.252924}, - "Jordan": {30.585164, 36.238414}, - "Kazakhstan": {48.019573, 66.923684}, - "Kenya": {-1.292066, 36.821946}, - "Kiribati": {-3.370417, -168.734039}, - "Kuwait": {29.31166, 47.481766}, - "Kyrgyzstan": {41.20438, 74.766098}, - "Laos": {19.85627, 102.495496}, - "Latvia": {56.879635, 24.603189}, - "Lebanon": {33.854721, 35.862285}, - "Lesotho": {-29.609988, 28.233608}, - "Liberia": {6.428055, -9.429499}, - "Libya": {26.3351, 17.228331}, - "Liechtenstein": {47.166, 9.555373}, - "Lithuania": {55.169438, 23.881275}, - "Luxembourg": {49.815273, 6.129583}, - "Madagascar": {-18.766947, 46.869107}, - "Malawi": {-13.254308, 34.301525}, - "Malaysia": {4.210484, 101.975766}, - "Maldives": {3.202778, 73.22068}, - "Mali": {17.570692, -3.996166}, - "Malta": {35.937496, 14.375416}, - "Marshall Islands": {7.131474, 171.184478}, - "Mauritania": {21.00789, -10.940835}, - "Mauritius": {-20.348404, 57.552152}, - "Mexico": {23.634501, -102.552784}, - "Micronesia": {7.425554, 150.550812}, - "Moldova": {47.411631, 28.369885}, - "Monaco": {43.738417, 7.424616}, - "Mongolia": {46.862496, 103.846656}, - "Montenegro": {42.708678, 19.37439}, - "Morocco": {31.791702, -7.09262}, - "Mozambique": {-18.665695, 35.529562}, - "Myanmar": {21.916221, 95.955974}, - "Namibia": {-22.95764, 18.49041}, - "Nauru": {-0.522778, 166.931503}, - "Nepal": {28.394857, 84.124008}, - "Netherlands": {52.132633, 5.291266}, - "New Zealand": {-40.900557, 174.885971}, - "Nicaragua": {12.865416, -85.207229}, - "Niger": {17.607789, 8.081666}, - "Nigeria": {9.081999, 8.675277}, - "North Korea": {40.339852, 127.510093}, - "North Macedonia": {41.608635, 21.745275}, - "Norway": {60.472024, 8.468946}, - "Oman": {21.512583, 55.923255}, - "Pakistan": {30.375321, 69.345116}, - "Palau": {7.51498, 134.58252}, - "Palestine": {31.952162, 35.233154}, - "Panama": {8.537981, -80.782127}, - "Papua New Guinea": {-6.314993, 143.95555}, - "Paraguay": {-23.442503, -58.443832}, - "Peru": {-9.189967, -75.015152}, - "Philippines": {12.879721, 121.774017}, - "Poland": {51.919438, 19.145136}, - "Portugal": {39.399872, -8.224454}, - "Qatar": {25.354826, 51.183884}, - "Romania": {45.943161, 24.96676}, - "Russia": {61.52401, 105.318756}, - "Rwanda": {-1.940278, 29.873888}, - "Saint Kitts and Nevis": {17.357822, -62.782998}, - "Saint Lucia": {13.909444, -60.978893}, - "Saint Vincent and the Grenadines": {12.984305, -61.287228}, - "Samoa": {-13.759029, -172.104629}, - "San Marino": {43.94236, 12.457777}, - "Sao Tome and Principe": {0.18636, 6.613081}, - "Saudi Arabia": {23.885942, 45.079162}, - "Senegal": {14.497401, -14.452362}, - "Serbia": {44.016521, 21.005859}, - "Seychelles": {-4.679574, 55.491977}, - "Sierra Leone": {8.460555, -11.779889}, - "Singapore": {1.352083, 103.819836}, - "Slovakia": {48.669026, 19.699024}, - "Slovenia": {46.151241, 14.995463}, - "Solomon Islands": {-9.64571, 160.156194}, - "Somalia": {5.152149, 46.199616}, - "South Africa": {-30.559482, 22.937506}, - "South Korea": {35.907757, 127.766922}, - "South Sudan": {6.8769919, 31.3069788}, - "Spain": {40.463667, -3.74922}, - "Sri Lanka": {7.873054, 80.771797}, - "Sudan": {12.862807, 30.217636}, - "Suriname": {3.919305, -56.027783}, - "Sweden": {60.128161, 18.643501}, - "Switzerland": {46.818188, 8.227512}, - "Syria": {34.802075, 38.996815}, - "Tajikistan": {38.861034, 71.276093}, - "Tanzania": {-6.369028, 34.888822}, - "Thailand": {15.870032, 100.992541}, - "Timor-Leste": {-8.874217, 125.727539}, - "Togo": {8.619543, 0.824782}, - "Tonga": {-21.178986, -175.198242}, - "Trinidad and Tobago": {10.691803, -61.222503}, - "Tunisia": {33.886917, 9.537499}, - "Turkey": {38.963745, 35.243322}, - "Turkmenistan": {38.969719, 59.556278}, - "Tuvalu": {-7.109535, 177.64933}, - "Uganda": {1.373333, 32.290275}, - "Ukraine": {48.379433, 31.16558}, - "United Arab Emirates": {23.424076, 53.847818}, - "United Kingdom": {55.378051, -3.435973}, - "United States": {37.09024, -95.712891}, - "Uruguay": {-32.522779, -55.765835}, - "Uzbekistan": {41.377491, 64.585262}, - "Vanuatu": {-15.376706, 166.959158}, - "Vatican City": {41.902916, 12.453389}, - "Venezuela": {6.42375, -66.58973}, - "Vietnam": {14.058324, 108.277199}, - "Yemen": {15.552727, 48.516388}, - "Zambia": {-13.133897, 27.849332}, - "Zimbabwe": {-19.015438, 29.154857}, -} - -// 国家简称到全称的映射表 -var countryAbbr = map[string]string{ - // ISO 3166-1 alpha-2 codes - "AD": "Andorra", - "AE": "United Arab Emirates", - "AF": "Afghanistan", - "AG": "Antigua and Barbuda", - "AI": "Anguilla", - "AL": "Albania", - "AM": "Armenia", - "AO": "Angola", - "AQ": "Antarctica", - "AR": "Argentina", - "AS": "American Samoa", - "AT": "Austria", - "AU": "Australia", - "AW": "Aruba", - "AX": "Aland Islands", - "AZ": "Azerbaijan", - "BA": "Bosnia and Herzegovina", - "BB": "Barbados", - "BD": "Bangladesh", - "BE": "Belgium", - "BF": "Burkina Faso", - "BG": "Bulgaria", - "BH": "Bahrain", - "BI": "Burundi", - "BJ": "Benin", - "BL": "Saint Barthelemy", - "BM": "Bermuda", - "BN": "Brunei", - "BO": "Bolivia", - "BQ": "Bonaire", - "BR": "Brazil", - "BS": "Bahamas", - "BT": "Bhutan", - "BV": "Bouvet Island", - "BW": "Botswana", - "BY": "Belarus", - "BZ": "Belize", - "CA": "Canada", - "CC": "Cocos Islands", - "CD": "Congo", - "CF": "Central African Republic", - "CG": "Congo", - "CH": "Switzerland", - "CI": "Cote D'Ivoire", - "CK": "Cook Islands", - "CL": "Chile", - "CM": "Cameroon", - "CN": "China", - "CO": "Colombia", - "CR": "Costa Rica", - "CU": "Cuba", - "CV": "Cabo Verde", - "CW": "Curacao", - "CX": "Christmas Island", - "CY": "Cyprus", - "CZ": "Czechia", - "DE": "Germany", - "DJ": "Djibouti", - "DK": "Denmark", - "DM": "Dominica", - "DO": "Dominican Republic", - "DZ": "Algeria", - "EC": "Ecuador", - "EE": "Estonia", - "EG": "Egypt", - "EH": "Western Sahara", - "ER": "Eritrea", - "ES": "Spain", - "ET": "Ethiopia", - "FI": "Finland", - "FJ": "Fiji", - "FK": "Falkland Islands", - "FM": "Micronesia", - "FO": "Faroe Islands", - "FR": "France", - "GA": "Gabon", - "GB": "United Kingdom", - "GD": "Grenada", - "GE": "Georgia", - "GF": "French Guiana", - "GG": "Guernsey", - "GH": "Ghana", - "GI": "Gibraltar", - "GL": "Greenland", - "GM": "Gambia", - "GN": "Guinea", - "GP": "Guadeloupe", - "GQ": "Equatorial Guinea", - "GR": "Greece", - "GS": "South Georgia", - "GT": "Guatemala", - "GU": "Guam", - "GW": "Guinea-Bissau", - "GY": "Guyana", - "HK": "Hong Kong", - "HM": "Heard Island", - "HN": "Honduras", - "HR": "Croatia", - "HT": "Haiti", - "HU": "Hungary", - "ID": "Indonesia", - "IE": "Ireland", - "IL": "Israel", - "IM": "Isle of Man", - "IN": "India", - "IO": "British Indian Ocean Territory", - "IQ": "Iraq", - "IR": "Iran", - "IS": "Iceland", - "IT": "Italy", - "JE": "Jersey", - "JM": "Jamaica", - "JO": "Jordan", - "JP": "Japan", - "KE": "Kenya", - "KG": "Kyrgyzstan", - "KH": "Cambodia", - "KI": "Kiribati", - "KM": "Comoros", - "KN": "Saint Kitts and Nevis", - "KP": "North Korea", - "KR": "South Korea", - "KW": "Kuwait", - "KY": "Cayman Islands", - "KZ": "Kazakhstan", - "LA": "Laos", - "LB": "Lebanon", - "LC": "Saint Lucia", - "LI": "Liechtenstein", - "LK": "Sri Lanka", - "LR": "Liberia", - "LS": "Lesotho", - "LT": "Lithuania", - "LU": "Luxembourg", - "LV": "Latvia", - "LY": "Libya", - "MA": "Morocco", - "MC": "Monaco", - "MD": "Moldova", - "ME": "Montenegro", - "MF": "Saint Martin", - "MG": "Madagascar", - "MH": "Marshall Islands", - "MK": "North Macedonia", - "ML": "Mali", - "MM": "Myanmar", - "MN": "Mongolia", - "MO": "Macao", - "MP": "Northern Mariana Islands", - "MQ": "Martinique", - "MR": "Mauritania", - "MS": "Montserrat", - "MT": "Malta", - "MU": "Mauritius", - "MV": "Maldives", - "MW": "Malawi", - "MX": "Mexico", - "MY": "Malaysia", - "MZ": "Mozambique", - "NA": "Namibia", - "NC": "New Caledonia", - "NE": "Niger", - "NF": "Norfolk Island", - "NG": "Nigeria", - "NI": "Nicaragua", - "NL": "Netherlands", - "NO": "Norway", - "NP": "Nepal", - "NR": "Nauru", - "NU": "Niue", - "NZ": "New Zealand", - "OM": "Oman", - "PA": "Panama", - "PE": "Peru", - "PF": "French Polynesia", - "PG": "Papua New Guinea", - "PH": "Philippines", - "PK": "Pakistan", - "PL": "Poland", - "PM": "Saint Pierre and Miquelon", - "PN": "Pitcairn", - "PR": "Puerto Rico", - "PS": "Palestine", - "PT": "Portugal", - "PW": "Palau", - "PY": "Paraguay", - "QA": "Qatar", - "RE": "Reunion", - "RO": "Romania", - "RS": "Serbia", - "RU": "Russia", - "RW": "Rwanda", - "SA": "Saudi Arabia", - "SB": "Solomon Islands", - "SC": "Seychelles", - "SD": "Sudan", - "SE": "Sweden", - "SG": "Singapore", - "SH": "Saint Helena", - "SI": "Slovenia", - "SJ": "Svalbard and Jan Mayen", - "SK": "Slovakia", - "SL": "Sierra Leone", - "SM": "San Marino", - "SN": "Senegal", - "SO": "Somalia", - "SR": "Suriname", - "SS": "South Sudan", - "ST": "Sao Tome and Principe", - "SV": "El Salvador", - "SX": "Sint Maarten", - "SY": "Syria", - "SZ": "Eswatini", - "TC": "Turks and Caicos Islands", - "TD": "Chad", - "TF": "French Southern Territories", - "TG": "Togo", - "TH": "Thailand", - "TJ": "Tajikistan", - "TK": "Tokelau", - "TL": "Timor-Leste", - "TM": "Turkmenistan", - "TN": "Tunisia", - "TO": "Tonga", - "TR": "Turkey", - "TT": "Trinidad and Tobago", - "TV": "Tuvalu", - "TW": "Taiwan", - "TZ": "Tanzania", - "UA": "Ukraine", - "UG": "Uganda", - "UM": "United States Minor Outlying Islands", - "US": "United States", - "UY": "Uruguay", - "UZ": "Uzbekistan", - "VA": "Vatican City", - "VC": "Saint Vincent and the Grenadines", - "VE": "Venezuela", - "VG": "British Virgin Islands", - "VI": "U.S. Virgin Islands", - "VN": "Vietnam", - "VU": "Vanuatu", - "WF": "Wallis and Futuna", - "WS": "Samoa", - "YE": "Yemen", - "YT": "Mayotte", - "ZA": "South Africa", - "ZM": "Zambia", - "ZW": "Zimbabwe", -} - -// 主要城市到国家的映射表 -var cityToCountry = map[string]string{ - // China - "Beijing": "China", "Shanghai": "China", "Guangzhou": "China", "Shenzhen": "China", "HK": "China", "Tsuen Wan": "China", - "Chengdu": "China", "Hangzhou": "China", "Wuhan": "China", "Xi'an": "China", - "Nanjing": "China", "Tianjin": "China", "Chongqing": "China", "Shenyang": "China", - "Dalian": "China", "Qingdao": "China", "Jinan": "China", "Harbin": "China", - "Changchun": "China", "Shijiazhuang": "China", "Taiyuan": "China", "Hohhot": "China", - "Yinchuan": "China", "Lanzhou": "China", "Xining": "China", "Urumqi": "China", - "Lhasa": "China", "Kunming": "China", "Guiyang": "China", "Nanning": "China", - "Haikou": "China", "Changsha": "China", "Zhengzhou": "China", "Nanchang": "China", - "Hefei": "China", "Fuzhou": "China", "Taipei": "China", "Hong Kong": "China", "Macao": "China", - - // United States - "New York": "United States", "Chicago": "United States", - "Houston": "United States", "Phoenix": "United States", "Philadelphia": "United States", - "San Antonio": "United States", "San Diego": "United States", "Dallas": "United States", - "San Jose": "United States", "Austin": "United States", "Jacksonville": "United States", - "Fort Worth": "United States", "Columbus": "United States", "Indianapolis": "United States", - "Charlotte": "United States", "San Francisco": "United States", "Seattle": "United States", - "Denver": "United States", "Washington": "United States", "Boston": "United States", - "El Paso": "United States", "Detroit": "United States", "Nashville": "United States", - "Portland Oregon": "United States", "Memphis": "United States", "Oklahoma City": "United States", - "Las Vegas": "United States", "Louisville": "United States", "Baltimore": "United States", - "Milwaukee": "United States", "Albuquerque": "United States", "Tucson": "United States", - "Fresno": "United States", "Sacramento": "United States", "Mesa": "United States", - "Kansas City": "United States", "Atlanta": "United States", "Long Beach": "United States", - "Colorado Springs": "United States", "Raleigh": "United States", "Miami": "United States", - "Virginia Beach": "United States", "Omaha": "United States", "Oakland": "United States", - "Minneapolis": "United States", "Tulsa": "United States", "Arlington": "United States", - - // Japan - "Tokyo": "Japan", "Osaka": "Japan", "Yokohama": "Japan", "Nagoya": "Japan", - "Sapporo": "Japan", "Fukuoka": "Japan", "Kobe": "Japan", "Kawasaki": "Japan", - "Kyoto": "Japan", "Saitama": "Japan", "Hiroshima": "Japan", "Sendai": "Japan", - "Kitakyushu": "Japan", "Chiba": "Japan", "Sakai": "Japan", "Niigata": "Japan", - "Hamamatsu": "Japan", "Okayama": "Japan", "Sagamihara": "Japan", "Kumamoto": "Japan", - - // United Kingdom - "London": "United Kingdom", "Birmingham": "United Kingdom", "Manchester": "United Kingdom", - "Glasgow": "United Kingdom", "Liverpool": "United Kingdom", "Edinburgh": "United Kingdom", - "Leeds": "United Kingdom", "Sheffield": "United Kingdom", "Bristol": "United Kingdom", - "Cardiff": "United Kingdom", "Belfast": "United Kingdom", "Leicester": "United Kingdom", - "Coventry": "United Kingdom", "Bradford": "United Kingdom", "Nottingham": "United Kingdom", - "Kingston upon Hull": "United Kingdom", "Newcastle upon Tyne": "United Kingdom", - "Stoke-on-Trent": "United Kingdom", "Southampton": "United Kingdom", "Derby": "United Kingdom", - - // Germany - "Berlin": "Germany", "Hamburg": "Germany", "Munich": "Germany", "Cologne": "Germany", - "Frankfurt": "Germany", "Stuttgart": "Germany", "Düsseldorf": "Germany", "Dortmund": "Germany", - "Essen": "Germany", "Leipzig": "Germany", "Bremen": "Germany", "Dresden": "Germany", - "Hanover": "Germany", "Nuremberg": "Germany", "Duisburg": "Germany", "Bochum": "Germany", - "Wuppertal": "Germany", "Bielefeld": "Germany", "Bonn": "Germany", "Münster": "Germany", - - // France - "Paris": "France", "Marseille": "France", "Lyon": "France", "Toulouse": "France", - "Nice": "France", "Nantes": "France", "Strasbourg": "France", "Montpellier": "France", - "Bordeaux": "France", "Lille": "France", "Rennes": "France", "Reims": "France", - "Le Havre": "France", "Saint-Étienne": "France", "Toulon": "France", "Grenoble": "France", - "Dijon": "France", "Angers": "France", "Nîmes": "France", "Villeurbanne": "France", - - // Italy - "Rome": "Italy", "Milan": "Italy", "Naples": "Italy", "Turin": "Italy", - "Palermo": "Italy", "Genoa": "Italy", "Bologna": "Italy", "Florence": "Italy", - "Bari": "Italy", "Catania": "Italy", "Venice": "Italy", "Verona": "Italy", - "Messina": "Italy", "Padua": "Italy", "Trieste": "Italy", "Taranto": "Italy", - - // Spain - "Madrid": "Spain", "Seville": "Spain", - "Zaragoza": "Spain", "Málaga": "Spain", "Murcia": "Spain", "Palma": "Spain", - "Las Palmas": "Spain", "Bilbao": "Spain", "Alicante": "Spain", - "Valladolid": "Spain", "Vigo": "Spain", "Gijón": "Spain", "Hospitalet": "Spain", - "A Coruña": "Spain", "Vitoria-Gasteiz": "Spain", "Granada": "Spain", "Elche": "Spain", - - // Canada - "Toronto": "Canada", "Montreal": "Canada", "Vancouver": "Canada", "Calgary": "Canada", - "Edmonton": "Canada", "Ottawa": "Canada", "Winnipeg": "Canada", "Quebec City": "Canada", - "Hamilton Canada": "Canada", "Kitchener": "Canada", "London Ontario": "Canada", "Victoria Canada": "Canada", - "Halifax": "Canada", "Oshawa": "Canada", "Windsor Canada": "Canada", "Saskatoon": "Canada", - "Regina": "Canada", "St. John's": "Canada", "Kelowna": "Canada", "Barrie": "Canada", - - // Australia - "Sydney": "Australia", "Melbourne": "Australia", "Brisbane": "Australia", "Perth": "Australia", - "Adelaide": "Australia", "Gold Coast": "Australia", "Newcastle Australia": "Australia", "Canberra": "Australia", - "Sunshine Coast": "Australia", "Wollongong": "Australia", "Hobart": "Australia", "Geelong": "Australia", - "Townsville": "Australia", "Cairns": "Australia", "Toowoomba": "Australia", "Darwin": "Australia", - - // India - "Mumbai": "India", "Delhi": "India", "Bangalore": "India", "Hyderabad": "India", - "Ahmedabad": "India", "Chennai": "India", "Kolkata": "India", "Surat": "India", - "Pune": "India", "Jaipur": "India", "Lucknow": "India", "Kanpur": "India", - "Nagpur": "India", "Indore": "India", "Thane": "India", "Bhopal": "India", - "Visakhapatnam": "India", "Pimpri": "India", "Patna": "India", "Vadodara": "India", - "Ludhiana": "India", "Agra": "India", "Nashik": "India", "Faridabad": "India", - "Meerut": "India", "Rajkot": "India", "Kalyan": "India", "Vasai": "India", - - // Brazil - "São Paulo": "Brazil", "Rio de Janeiro": "Brazil", "Brasília": "Brazil", "Salvador": "Brazil", - "Fortaleza": "Brazil", "Belo Horizonte": "Brazil", "Manaus": "Brazil", "Curitiba": "Brazil", - "Recife": "Brazil", "Porto Alegre": "Brazil", "Belém": "Brazil", "Goiânia": "Brazil", - "Guarulhos": "Brazil", "Campinas": "Brazil", "São Luís": "Brazil", "São Gonçalo": "Brazil", - - // Russia - "Moscow": "Russia", "Saint Petersburg": "Russia", "Novosibirsk": "Russia", "Yekaterinburg": "Russia", - "Nizhny Novgorod": "Russia", "Kazan": "Russia", "Chelyabinsk": "Russia", "Omsk": "Russia", - "Samara": "Russia", "Rostov-on-Don": "Russia", "Ufa": "Russia", "Krasnoyarsk": "Russia", - "Perm": "Russia", "Voronezh": "Russia", "Volgograd": "Russia", "Krasnodar": "Russia", - - // South Korea - "Seoul": "South Korea", "Busan": "South Korea", "Incheon": "South Korea", "Daegu": "South Korea", - "Daejeon": "South Korea", "Gwangju": "South Korea", "Suwon": "South Korea", "Ulsan": "South Korea", - "Changwon": "South Korea", "Goyang": "South Korea", "Yongin": "South Korea", "Bucheon": "South Korea", - - // Mexico - "Mexico City": "Mexico", "Guadalajara": "Mexico", "Monterrey": "Mexico", "Puebla": "Mexico", - "Tijuana": "Mexico", "León": "Mexico", "Juárez": "Mexico", "Torreón": "Mexico", - "Querétaro": "Mexico", "San Luis Potosí": "Mexico", "Mexicali": "Mexico", - - // Indonesia - "Jakarta": "Indonesia", "Surabaya": "Indonesia", "Bandung": "Indonesia", "Bekasi": "Indonesia", - "Medan": "Indonesia", "Tangerang": "Indonesia", "Depok": "Indonesia", "Semarang": "Indonesia", - "Palembang": "Indonesia", "Makassar": "Indonesia", "Batam": "Indonesia", "Bogor": "Indonesia", - - // Turkey - "Istanbul": "Turkey", "Ankara": "Turkey", "Izmir": "Turkey", "Bursa": "Turkey", - "Adana": "Turkey", "Gaziantep": "Turkey", "Konya": "Turkey", "Antalya": "Turkey", - "Kayseri": "Turkey", "Mersin": "Turkey", "Eskişehir": "Turkey", "Diyarbakır": "Turkey", - - // Netherlands - "Amsterdam": "Netherlands", "Rotterdam": "Netherlands", "The Hague": "Netherlands", "Utrecht": "Netherlands", - "Eindhoven": "Netherlands", "Tilburg": "Netherlands", "Groningen": "Netherlands", "Almere": "Netherlands", - "Breda": "Netherlands", "Nijmegen": "Netherlands", "Enschede": "Netherlands", "Haarlem": "Netherlands", - - // Saudi Arabia - "Riyadh": "Saudi Arabia", "Jeddah": "Saudi Arabia", "Mecca": "Saudi Arabia", "Medina": "Saudi Arabia", - "Dammam": "Saudi Arabia", "Khobar": "Saudi Arabia", "Tabuk": "Saudi Arabia", "Buraidah": "Saudi Arabia", - "Khamis Mushait": "Saudi Arabia", "Hafar Al-Batin": "Saudi Arabia", "Jubail": "Saudi Arabia", "Taif": "Saudi Arabia", - - // Argentina - "Buenos Aires": "Argentina", "Rosario": "Argentina", "Mendoza": "Argentina", - "Tucumán": "Argentina", "La Plata": "Argentina", "Mar del Plata": "Argentina", "Salta": "Argentina", - "Santa Fe": "Argentina", "San Juan": "Argentina", "Resistencia": "Argentina", "Santiago del Estero": "Argentina", - - // Poland - "Warsaw": "Poland", "Kraków": "Poland", "Łódź": "Poland", "Wrocław": "Poland", - "Poznań": "Poland", "Gdańsk": "Poland", "Szczecin": "Poland", "Bydgoszcz": "Poland", - "Lublin": "Poland", "Katowice": "Poland", "Białystok": "Poland", "Gdynia": "Poland", - - // Ukraine - "Kiev": "Ukraine", "Kharkiv": "Ukraine", "Odessa": "Ukraine", "Dnipro": "Ukraine", - "Donetsk": "Ukraine", "Zaporizhzhia": "Ukraine", "Lviv": "Ukraine", "Kryvyi Rih": "Ukraine", - "Mykolaiv": "Ukraine", "Mariupol": "Ukraine", "Luhansk": "Ukraine", "Vinnytsia": "Ukraine", - - // Egypt - "Cairo": "Egypt", "Alexandria": "Egypt", "Giza": "Egypt", "Shubra El Kheima": "Egypt", - "Port Said": "Egypt", "Suez": "Egypt", "Luxor": "Egypt", "Mansoura": "Egypt", - "El Mahalla El Kubra": "Egypt", "Tanta": "Egypt", "Asyut": "Egypt", "Ismailia": "Egypt", - - // Nigeria - "Lagos": "Nigeria", "Kano": "Nigeria", "Ibadan": "Nigeria", "Abuja": "Nigeria", - "Port Harcourt": "Nigeria", "Benin City": "Nigeria", "Maiduguri": "Nigeria", "Zaria": "Nigeria", - "Aba": "Nigeria", "Jos": "Nigeria", "Ilorin": "Nigeria", "Oyo": "Nigeria", - - // South Africa - "Johannesburg": "South Africa", "Cape Town": "South Africa", "Durban": "South Africa", "Pretoria": "South Africa", - "Soweto": "South Africa", "Port Elizabeth": "South Africa", "Pietermaritzburg": "South Africa", "Benoni": "South Africa", - "Tembisa": "South Africa", "East London": "South Africa", "Vereeniging": "South Africa", "Bloemfontein": "South Africa", - - // Iran - "Tehran": "Iran", "Mashhad": "Iran", "Isfahan": "Iran", "Karaj": "Iran", - "Shiraz": "Iran", "Tabriz": "Iran", "Qom": "Iran", "Ahvaz": "Iran", - "Kermanshah": "Iran", "Urmia": "Iran", "Rasht": "Iran", "Zahedan": "Iran", - - // Thailand - "Bangkok": "Thailand", "Nonthaburi": "Thailand", "Pak Kret": "Thailand", "Hat Yai": "Thailand", - "Chiang Mai": "Thailand", "Phuket": "Thailand", "Pattaya": "Thailand", "Nakhon Ratchasima": "Thailand", - "Khon Kaen": "Thailand", "Udon Thani": "Thailand", "Surat Thani": "Thailand", "Nakhon Si Thammarat": "Thailand", - - // Vietnam - "Ho Chi Minh City": "Vietnam", "Hanoi": "Vietnam", "Haiphong": "Vietnam", "Da Nang": "Vietnam", - "Bien Hoa": "Vietnam", "Hue": "Vietnam", "Nha Trang": "Vietnam", "Can Tho": "Vietnam", - "Rach Gia": "Vietnam", "Qui Nhon": "Vietnam", "Vung Tau": "Vietnam", "Nam Dinh": "Vietnam", - - // Philippines - "Manila": "Philippines", "Quezon City": "Philippines", "Davao": "Philippines", "Caloocan": "Philippines", - "Cebu City": "Philippines", "Zamboanga": "Philippines", "Antipolo": "Philippines", "Taguig": "Philippines", - "Pasig": "Philippines", "Cagayan de Oro": "Philippines", "Paranaque": "Philippines", "Makati": "Philippines", - - // Malaysia - "Kuala Lumpur": "Malaysia", "George Town": "Malaysia", "Ipoh": "Malaysia", "Shah Alam": "Malaysia", - "Petaling Jaya": "Malaysia", "Johor Bahru": "Malaysia", "Seremban": "Malaysia", "Kuching": "Malaysia", - "Kota Kinabalu": "Malaysia", "Klang": "Malaysia", "Kajang": "Malaysia", "Subang Jaya": "Malaysia", - - // Bangladesh - "Dhaka": "Bangladesh", "Chittagong": "Bangladesh", "Sylhet": "Bangladesh", "Khulna": "Bangladesh", - "Rajshahi": "Bangladesh", "Rangpur": "Bangladesh", "Barisal": "Bangladesh", "Comilla": "Bangladesh", - "Mymensingh": "Bangladesh", "Narayanganj": "Bangladesh", "Gazipur": "Bangladesh", "Tongi": "Bangladesh", - - // Pakistan - "Karachi": "Pakistan", "Lahore": "Pakistan", "Faisalabad": "Pakistan", "Rawalpindi": "Pakistan", - "Gujranwala": "Pakistan", "Peshawar": "Pakistan", "Multan": "Pakistan", "Islamabad": "Pakistan", - "Quetta": "Pakistan", "Bahawalpur": "Pakistan", "Sargodha": "Pakistan", "Sialkot": "Pakistan", - - // Afghanistan - "Kabul": "Afghanistan", "Kandahar": "Afghanistan", "Herat": "Afghanistan", "Mazar-i-Sharif": "Afghanistan", - "Jalalabad": "Afghanistan", "Kunduz": "Afghanistan", "Ghazni": "Afghanistan", "Bamyan": "Afghanistan", - - // Israel - "Jerusalem": "Israel", "Tel Aviv": "Israel", "Haifa": "Israel", "Rishon LeZion": "Israel", - "Petah Tikva": "Israel", "Ashdod": "Israel", "Netanya": "Israel", "Beer Sheva": "Israel", - "Holon": "Israel", "Bnei Brak": "Israel", "Ramat Gan": "Israel", "Ashkelon": "Israel", - - // Iraq - "Baghdad": "Iraq", "Basra": "Iraq", "Mosul": "Iraq", "Erbil": "Iraq", - "Sulaymaniyah": "Iraq", "Najaf": "Iraq", "Karbala": "Iraq", "Nasiriyah": "Iraq", - "Amarah": "Iraq", "Duhok": "Iraq", "Ramadi": "Iraq", "Fallujah": "Iraq", - - // Morocco - "Casablanca": "Morocco", "Rabat": "Morocco", "Fes": "Morocco", "Marrakech": "Morocco", - "Tangier": "Morocco", "Meknes": "Morocco", "Oujda": "Morocco", "Kenitra": "Morocco", - "Tetouan": "Morocco", "Safi": "Morocco", "El Jadida": "Morocco", "Nador": "Morocco", - - // Algeria - "Algiers": "Algeria", "Oran": "Algeria", "Constantine": "Algeria", "Batna": "Algeria", - "Djelfa": "Algeria", "Setif": "Algeria", "Annaba": "Algeria", "Sidi Bel Abbes": "Algeria", - "Biskra": "Algeria", "Tebessa": "Algeria", "El Oued": "Algeria", "Skikda": "Algeria", - - // Kenya - "Nairobi": "Kenya", "Mombasa": "Kenya", "Kisumu": "Kenya", "Nakuru": "Kenya", - "Eldoret": "Kenya", "Kitale": "Kenya", "Malindi": "Kenya", "Garissa": "Kenya", - "Kakamega": "Kenya", "Nyeri": "Kenya", "Machakos": "Kenya", "Meru": "Kenya", - - // Ethiopia - "Addis Ababa": "Ethiopia", "Dire Dawa": "Ethiopia", "Mek'ele": "Ethiopia", "Gondar": "Ethiopia", - "Adama": "Ethiopia", "Awasa": "Ethiopia", "Bahir Dar": "Ethiopia", "Dessie": "Ethiopia", - "Jimma": "Ethiopia", "Jijiga": "Ethiopia", "Shashamane": "Ethiopia", "Nekemte": "Ethiopia", - - // Ghana - "Accra": "Ghana", "Kumasi": "Ghana", "Tamale": "Ghana", "Sekondi-Takoradi": "Ghana", - "Ashaiman": "Ghana", "Cape Coast": "Ghana", "Obuasi": "Ghana", "Teshie": "Ghana", - "Madina": "Ghana", "Koforidua": "Ghana", "Wa": "Ghana", "Techiman": "Ghana", - - // Chile - "Santiago": "Chile", "Valparaíso": "Chile", "Concepción": "Chile", "La Serena": "Chile", - "Antofagasta": "Chile", "Temuco": "Chile", "Rancagua": "Chile", "Talca": "Chile", - "Arica": "Chile", "Chillán": "Chile", "Iquique": "Chile", - - // Colombia - "Bogotá": "Colombia", "Medellín": "Colombia", "Cali": "Colombia", "Barranquilla": "Colombia", - "Cartagena": "Colombia", "Cúcuta": "Colombia", "Bucaramanga": "Colombia", "Pereira": "Colombia", - "Santa Marta": "Colombia", "Ibagué": "Colombia", "Pasto": "Colombia", "Manizales": "Colombia", - - // Peru - "Lima": "Peru", "Arequipa": "Peru", "Trujillo": "Peru", "Chiclayo": "Peru", - "Piura": "Peru", "Iquitos": "Peru", "Cusco": "Peru", "Chimbote": "Peru", - "Huancayo": "Peru", "Tacna": "Peru", "Juliaca": "Peru", "Ica": "Peru", - - // Venezuela - "Caracas": "Venezuela", "Maracaibo": "Venezuela", "Barquisimeto": "Venezuela", - "Maracay": "Venezuela", "Ciudad Guayana": "Venezuela", "San Cristóbal": "Venezuela", "Maturín": "Venezuela", - "Ciudad Bolívar": "Venezuela", "Cumana": "Venezuela", - - // Ecuador - "Guayaquil": "Ecuador", "Quito": "Ecuador", "Cuenca": "Ecuador", "Santo Domingo": "Ecuador", - "Machala": "Ecuador", "Durán": "Ecuador", "Manta": "Ecuador", "Portoviejo": "Ecuador", - "Ambato": "Ecuador", "Riobamba": "Ecuador", "Loja": "Ecuador", "Esmeraldas": "Ecuador", - - // Bolivia - "Santa Cruz": "Bolivia", "La Paz": "Bolivia", "Cochabamba": "Bolivia", "Oruro": "Bolivia", - "Sucre": "Bolivia", "Tarija": "Bolivia", "Potosí": "Bolivia", "Trinidad": "Bolivia", - - // Uruguay - "Montevideo": "Uruguay", "Salto": "Uruguay", "Paysandú": "Uruguay", "Las Piedras": "Uruguay", - "Rivera": "Uruguay", "Maldonado": "Uruguay", "Tacuarembó": "Uruguay", "Melo": "Uruguay", - - // Paraguay - "Asunción": "Paraguay", "Ciudad del Este": "Paraguay", "San Lorenzo": "Paraguay", "Luque": "Paraguay", - "Capiatá": "Paraguay", "Lambaré": "Paraguay", "Fernando de la Mora": "Paraguay", "Limpio": "Paraguay", - - // Norway - "Oslo": "Norway", "Bergen": "Norway", "Trondheim": "Norway", "Stavanger": "Norway", - "Bærum": "Norway", "Kristiansand": "Norway", "Fredrikstad": "Norway", "Sandnes": "Norway", - "Tromsø": "Norway", "Drammen": "Norway", "Asker": "Norway", "Lillestrøm": "Norway", - - // Sweden - "Stockholm": "Sweden", "Gothenburg": "Sweden", "Malmö": "Sweden", "Uppsala": "Sweden", - "Västerås": "Sweden", "Örebro": "Sweden", "Linköping": "Sweden", "Helsingborg": "Sweden", - "Jönköping": "Sweden", "Norrköping": "Sweden", "Lund": "Sweden", "Umeå": "Sweden", - - // Finland - "Helsinki": "Finland", "Espoo": "Finland", "Tampere": "Finland", "Vantaa": "Finland", - "Oulu": "Finland", "Turku": "Finland", "Jyväskylä": "Finland", "Lahti": "Finland", - "Kuopio": "Finland", "Pori": "Finland", "Joensuu": "Finland", "Lappeenranta": "Finland", - - // Denmark - "Copenhagen": "Denmark", "Aarhus": "Denmark", "Odense": "Denmark", "Aalborg": "Denmark", - "Esbjerg": "Denmark", "Randers": "Denmark", "Kolding": "Denmark", "Horsens": "Denmark", - "Vejle": "Denmark", "Roskilde": "Denmark", "Herning": "Denmark", "Silkeborg": "Denmark", - - // Switzerland - "Zurich": "Switzerland", "Geneva": "Switzerland", "Basel": "Switzerland", "Bern": "Switzerland", - "Lausanne": "Switzerland", "Winterthur": "Switzerland", "Lucerne": "Switzerland", "St. Gallen": "Switzerland", - "Lugano": "Switzerland", "Biel": "Switzerland", "Thun": "Switzerland", "Köniz": "Switzerland", - - // Austria - "Innsbruck": "Austria", "Klagenfurt": "Austria", "Villach": "Austria", "Wels": "Austria", - "Sankt Pölten": "Austria", "Dornbirn": "Austria", "Steyr": "Austria", "Wiener Neustadt": "Austria", - "Feldkirch": "Austria", "Bregenz": "Austria", "Leonding": "Austria", "Klosterneuburg": "Austria", - - // Belgium - "Brussels": "Belgium", "Antwerp": "Belgium", "Ghent": "Belgium", "Charleroi": "Belgium", - "Liège": "Belgium", "Bruges": "Belgium", "Namur": "Belgium", "Leuven": "Belgium", - "Mons": "Belgium", "Aalst": "Belgium", "Mechelen": "Belgium", "La Louvière": "Belgium", - - // Czech Republic (Czechia) - "Prague": "Czechia", "Brno": "Czechia", "Ostrava": "Czechia", "Plzen": "Czechia", - "Liberec": "Czechia", "Olomouc": "Czechia", "Budweis": "Czechia", "Hradec Králové": "Czechia", - "Ústí nad Labem": "Czechia", "Pardubice": "Czechia", "Zlín": "Czechia", "Havířov": "Czechia", - - // Hungary - "Budapest": "Hungary", "Debrecen": "Hungary", "Szeged": "Hungary", "Miskolc": "Hungary", - "Pécs": "Hungary", "Győr": "Hungary", "Nyíregyháza": "Hungary", "Kecskemét": "Hungary", - "Székesfehérvár": "Hungary", "Szombathely": "Hungary", "Érd": "Hungary", "Tatabánya": "Hungary", - - // Romania - "Bucharest": "Romania", "Cluj-Napoca": "Romania", "Timișoara": "Romania", "Iași": "Romania", - "Constanța": "Romania", "Craiova": "Romania", "Brașov": "Romania", "Galați": "Romania", - "Ploiești": "Romania", "Oradea": "Romania", "Brăila": "Romania", "Arad": "Romania", - - // Serbia - "Belgrade": "Serbia", "Novi Sad": "Serbia", "Niš": "Serbia", "Kragujevac": "Serbia", - "Subotica": "Serbia", "Zrenjanin": "Serbia", "Pančevo": "Serbia", "Čačak": "Serbia", - "Novi Pazar": "Serbia", "Kraljevo": "Serbia", "Smederevo": "Serbia", "Leskovac": "Serbia", - - // Croatia - "Zagreb": "Croatia", "Split": "Croatia", "Rijeka": "Croatia", "Osijek": "Croatia", - "Zadar": "Croatia", "Pula": "Croatia", "Slavonski Brod": "Croatia", "Karlovac": "Croatia", - "Varaždin": "Croatia", "Šibenik": "Croatia", "Sisak": "Croatia", "Velika Gorica": "Croatia", - - // Greece - "Athens": "Greece", "Thessaloniki": "Greece", "Patras": "Greece", "Heraklion": "Greece", - "Larissa": "Greece", "Volos": "Greece", "Rhodes": "Greece", "Ioannina": "Greece", - "Chania": "Greece", "Chalcis": "Greece", "Serres": "Greece", "Alexandroupoli": "Greece", - - // Portugal - "Lisbon": "Portugal", "Porto": "Portugal", "Vila Nova de Gaia": "Portugal", "Amadora": "Portugal", - "Braga": "Portugal", "Funchal": "Portugal", "Coimbra": "Portugal", "Setúbal": "Portugal", - "Almada": "Portugal", "Agualva-Cacém": "Portugal", "Queluz": "Portugal", "Rio Tinto": "Portugal", - - // Bulgaria - "Sofia": "Bulgaria", "Plovdiv": "Bulgaria", "Varna": "Bulgaria", "Burgas": "Bulgaria", - "Ruse": "Bulgaria", "Stara Zagora": "Bulgaria", "Pleven": "Bulgaria", "Sliven": "Bulgaria", - "Dobrich": "Bulgaria", "Shumen": "Bulgaria", "Pernik": "Bulgaria", "Haskovo": "Bulgaria", - - // Slovakia - "Bratislava": "Slovakia", "Košice": "Slovakia", "Prešov": "Slovakia", "Žilina": "Slovakia", - "Banská Bystrica": "Slovakia", "Nitra": "Slovakia", "Trnava": "Slovakia", "Martin": "Slovakia", - "Trenčín": "Slovakia", "Poprad": "Slovakia", "Prievidza": "Slovakia", "Zvolen": "Slovakia", - - // Slovenia - "Ljubljana": "Slovenia", "Maribor": "Slovenia", "Celje": "Slovenia", "Kranj": "Slovenia", - "Velenje": "Slovenia", "Koper": "Slovenia", "Novo Mesto": "Slovenia", "Ptuj": "Slovenia", - "Trbovlje": "Slovenia", "Kamnik": "Slovenia", "Jesenice": "Slovenia", "Nova Gorica": "Slovenia", - - // Lithuania - "Vilnius": "Lithuania", "Kaunas": "Lithuania", "Klaipėda": "Lithuania", "Šiauliai": "Lithuania", - "Panevėžys": "Lithuania", "Alytus": "Lithuania", "Marijampolė": "Lithuania", "Mažeikiai": "Lithuania", - "Jonava": "Lithuania", "Utena": "Lithuania", "Kėdainiai": "Lithuania", "Telšiai": "Lithuania", - - // Latvia - "Riga": "Latvia", "Daugavpils": "Latvia", "Liepāja": "Latvia", "Jelgava": "Latvia", - "Jūrmala": "Latvia", "Ventspils": "Latvia", "Rēzekne": "Latvia", "Valmiera": "Latvia", - "Jēkabpils": "Latvia", "Ogre": "Latvia", "Tukums": "Latvia", "Salaspils": "Latvia", - - // Estonia - "Tallinn": "Estonia", "Tartu": "Estonia", "Narva": "Estonia", "Pärnu": "Estonia", - "Kohtla-Järve": "Estonia", "Viljandi": "Estonia", "Rakvere": "Estonia", "Maardu": "Estonia", - "Sillamäe": "Estonia", "Kuressaare": "Estonia", "Võru": "Estonia", "Valga": "Estonia", - - // New Zealand - "Auckland": "New Zealand", "Wellington": "New Zealand", "Christchurch": "New Zealand", "Hamilton": "New Zealand", - "Tauranga": "New Zealand", "Napier-Hastings": "New Zealand", "Dunedin": "New Zealand", "Palmerston North": "New Zealand", - "Nelson": "New Zealand", "Rotorua": "New Zealand", "New Plymouth": "New Zealand", "Whangarei": "New Zealand", - - // Singapore - "Singapore": "Singapore", - - // United Arab Emirates - "Dubai": "United Arab Emirates", "Abu Dhabi": "United Arab Emirates", "Sharjah": "United Arab Emirates", "Al Ain": "United Arab Emirates", - "Ajman": "United Arab Emirates", "Ras Al Khaimah": "United Arab Emirates", "Fujairah": "United Arab Emirates", "Umm Al Quwain": "United Arab Emirates", - - // Lebanon - "Beirut": "Lebanon", "Sidon": "Lebanon", "Tyre": "Lebanon", - "Nabatieh": "Lebanon", "Jounieh": "Lebanon", "Zahle": "Lebanon", "Baalbek": "Lebanon", - - // Jordan - "Amman": "Jordan", "Zarqa": "Jordan", "Irbid": "Jordan", "Russeifa": "Jordan", - "Wadi as-Sir": "Jordan", "Aqaba": "Jordan", "Madaba": "Jordan", "Salt": "Jordan", - - // Yemen - "Sanaa": "Yemen", "Aden": "Yemen", "Taiz": "Yemen", "Hodeidah": "Yemen", - "Mukalla": "Yemen", "Ibb": "Yemen", "Dhamar": "Yemen", "Zinjibar": "Yemen", - - // Syria - "Damascus": "Syria", "Aleppo": "Syria", "Homs": "Syria", "Latakia": "Syria", - "Hama": "Syria", "Raqqa": "Syria", "Deir ez-Zor": "Syria", "Hasakah": "Syria", - - // Oman - "Muscat": "Oman", "Seeb": "Oman", "Salalah": "Oman", "Bawshar": "Oman", - "Sohar": "Oman", "Sur": "Oman", "Ibra": "Oman", "Nizwa": "Oman", - - // Qatar - "Doha": "Qatar", "Al Rayyan": "Qatar", "Umm Salal": "Qatar", "Al Wakrah": "Qatar", - "Al Khor": "Qatar", "Dukhan": "Qatar", "Lusail": "Qatar", "Mesaieed": "Qatar", - - // Kuwait - "Kuwait City": "Kuwait", "Al Ahmadi": "Kuwait", "Hawally": "Kuwait", "As Salimiyah": "Kuwait", - "Sabah as-Salim": "Kuwait", "Al Farwaniyah": "Kuwait", "Al Fahahil": "Kuwait", "Ar Riqqah": "Kuwait", - - // Bahrain - "Manama": "Bahrain", "Riffa": "Bahrain", "Muharraq": "Bahrain", "Hamad Town": "Bahrain", - "A'ali": "Bahrain", "Isa Town": "Bahrain", "Sitra": "Bahrain", "Budaiya": "Bahrain", - - // Cyprus - "Nicosia": "Cyprus", "Limassol": "Cyprus", "Larnaca": "Cyprus", "Famagusta": "Cyprus", - "Paphos": "Cyprus", "Kyrenia": "Cyprus", "Protaras": "Cyprus", "Paralimni": "Cyprus", - - // Malta - "Valletta": "Malta", "Birkirkara": "Malta", "Mosta": "Malta", "Qormi": "Malta", - "Zabbar": "Malta", "San Pawl il-Bahar": "Malta", "Tarxien": "Malta", "Naxxar": "Malta", - - // Iceland - "Reykjavik": "Iceland", "Kopavogur": "Iceland", "Hafnarfjordur": "Iceland", "Akureyri": "Iceland", - "Reykjanesbaer": "Iceland", "Gardabaer": "Iceland", "Mosfellsbaer": "Iceland", "Arborg": "Iceland", - - // Luxembourg - "Luxembourg City": "Luxembourg", "Esch-sur-Alzette": "Luxembourg", "Dudelange": "Luxembourg", "Schifflange": "Luxembourg", - "Bettembourg": "Luxembourg", "Petange": "Luxembourg", "Ettelbruck": "Luxembourg", "Diekirch": "Luxembourg", - - // Belarus - "Minsk": "Belarus", "Gomel": "Belarus", "Mogilev": "Belarus", "Vitebsk": "Belarus", - "Grodno": "Belarus", "Brest": "Belarus", "Bobruisk": "Belarus", "Baranovichi": "Belarus", - - // Moldova - "Chisinau": "Moldova", "Tiraspol": "Moldova", "Balti": "Moldova", "Bender": "Moldova", - "Rybnitsa": "Moldova", "Cahul": "Moldova", "Ungheni": "Moldova", "Soroca": "Moldova", - - // North Macedonia - "Skopje": "North Macedonia", "Bitola": "North Macedonia", "Kumanovo": "North Macedonia", "Prilep": "North Macedonia", - "Tetovo": "North Macedonia", "Veles": "North Macedonia", "Shtip": "North Macedonia", "Ohrid": "North Macedonia", - - // Albania - "Tirana": "Albania", "Durres": "Albania", "Vlore": "Albania", "Elbasan": "Albania", - "Shkoder": "Albania", "Fier": "Albania", "Korce": "Albania", "Berat": "Albania", - - // Montenegro - "Podgorica": "Montenegro", "Niksic": "Montenegro", "Pljevlja": "Montenegro", "Bijelo Polje": "Montenegro", - "Cetinje": "Montenegro", "Bar": "Montenegro", "Herceg Novi": "Montenegro", "Berane": "Montenegro", - - // Bosnia and Herzegovina - "Sarajevo": "Bosnia and Herzegovina", "Banja Luka": "Bosnia and Herzegovina", "Tuzla": "Bosnia and Herzegovina", "Zenica": "Bosnia and Herzegovina", - "Mostar": "Bosnia and Herzegovina", "Prijedor": "Bosnia and Herzegovina", "Brčko": "Bosnia and Herzegovina", "Bijeljina": "Bosnia and Herzegovina", - - // Armenia - "Yerevan": "Armenia", "Gyumri": "Armenia", "Vanadzor": "Armenia", "Vagharshapat": "Armenia", - "Hrazdan": "Armenia", "Abovyan": "Armenia", "Kapan": "Armenia", "Armavir": "Armenia", - - // Georgia - "Tbilisi": "Georgia", "Kutaisi": "Georgia", "Batumi": "Georgia", "Rustavi": "Georgia", - "Gori": "Georgia", "Zugdidi": "Georgia", "Poti": "Georgia", "Kobuleti": "Georgia", - - // Azerbaijan - "Baku": "Azerbaijan", "Ganja": "Azerbaijan", "Sumqayit": "Azerbaijan", "Mingachevir": "Azerbaijan", - "Quba": "Azerbaijan", "Lankaran": "Azerbaijan", "Shaki": "Azerbaijan", "Yevlax": "Azerbaijan", - - // Kazakhstan - "Almaty": "Kazakhstan", "Nur-Sultan": "Kazakhstan", "Shymkent": "Kazakhstan", "Aktobe": "Kazakhstan", - "Taraz": "Kazakhstan", "Pavlodar": "Kazakhstan", "Ust-Kamenogorsk": "Kazakhstan", "Semey": "Kazakhstan", - "Atyrau": "Kazakhstan", "Kostanay": "Kazakhstan", "Kyzylorda": "Kazakhstan", "Oral": "Kazakhstan", - - // Uzbekistan - "Tashkent": "Uzbekistan", "Namangan": "Uzbekistan", "Samarkand": "Uzbekistan", "Andijan": "Uzbekistan", - "Nukus": "Uzbekistan", "Fergana": "Uzbekistan", "Bukhara": "Uzbekistan", "Qarshi": "Uzbekistan", - "Kokand": "Uzbekistan", "Margilan": "Uzbekistan", "Chirchiq": "Uzbekistan", "Termez": "Uzbekistan", - - // Kyrgyzstan - "Bishkek": "Kyrgyzstan", "Osh": "Kyrgyzstan", "Jalal-Abad": "Kyrgyzstan", "Karakol": "Kyrgyzstan", - "Tokmok": "Kyrgyzstan", "Uzgen": "Kyrgyzstan", "Naryn": "Kyrgyzstan", "Talas": "Kyrgyzstan", - - // Tajikistan - "Dushanbe": "Tajikistan", "Khujand": "Tajikistan", "Kulob": "Tajikistan", "Qurghonteppa": "Tajikistan", - "Istaravshan": "Tajikistan", "Isfara": "Tajikistan", "Panjakent": "Tajikistan", "Tursunzoda": "Tajikistan", - - // Turkmenistan - "Ashgabat": "Turkmenistan", "Turkmenbashi": "Turkmenistan", "Dasoguz": "Turkmenistan", "Mary": "Turkmenistan", - "Balkanabat": "Turkmenistan", "Bayramaly": "Turkmenistan", "Tejen": "Turkmenistan", "Serdar": "Turkmenistan", - - // Mongolia - "Ulaanbaatar": "Mongolia", "Erdenet": "Mongolia", "Darkhan": "Mongolia", "Choibalsan": "Mongolia", - "Murun": "Mongolia", "Bayankhongor": "Mongolia", "Ulgii": "Mongolia", "Khovd": "Mongolia", - - // Myanmar - "Yangon": "Myanmar", "Mandalay": "Myanmar", "Naypyidaw": "Myanmar", "Mawlamyine": "Myanmar", - "Bago": "Myanmar", "Pathein": "Myanmar", "Monywa": "Myanmar", "Meiktila": "Myanmar", - "Sittwe": "Myanmar", "Myitkyina": "Myanmar", "Dawei": "Myanmar", "Pyay": "Myanmar", - - // Laos - "Vientiane": "Laos", "Pakse": "Laos", "Savannakhet": "Laos", "Luang Prabang": "Laos", - "Thakhek": "Laos", "Muang Xay": "Laos", "Phonsavan": "Laos", "Muang Pakbeng": "Laos", - - // Cambodia - "Phnom Penh": "Cambodia", "Siem Reap": "Cambodia", "Battambang": "Cambodia", "Sihanoukville": "Cambodia", - "Poipet": "Cambodia", "Kampong Cham": "Cambodia", "Pursat": "Cambodia", "Kampong Speu": "Cambodia", - - // Sri Lanka - "Colombo": "Sri Lanka", "Dehiwala-Mount Lavinia": "Sri Lanka", "Moratuwa": "Sri Lanka", "Negombo": "Sri Lanka", - "Kandy": "Sri Lanka", "Kalmunai": "Sri Lanka", "Galle": "Sri Lanka", "Trincomalee": "Sri Lanka", - "Batticaloa": "Sri Lanka", "Jaffna": "Sri Lanka", "Katunayake": "Sri Lanka", "Dambulla": "Sri Lanka", - - // Nepal - "Kathmandu": "Nepal", "Pokhara": "Nepal", "Lalitpur": "Nepal", "Bharatpur": "Nepal", - "Biratnagar": "Nepal", "Birgunj": "Nepal", "Dharan": "Nepal", "Bhim Datta": "Nepal", - "Butwal": "Nepal", "Hetauda": "Nepal", "Dhangadhi": "Nepal", "Itahari": "Nepal", - - // Bhutan - "Thimphu": "Bhutan", "Phuntsholing": "Bhutan", "Punakha": "Bhutan", "Wangdue": "Bhutan", - "Samdrup Jongkhar": "Bhutan", "Gelephu": "Bhutan", "Trongsa": "Bhutan", "Mongar": "Bhutan", - - // Maldives - "Male": "Maldives", "Addu City": "Maldives", "Fuvahmulah": "Maldives", "Kulhudhuffushi": "Maldives", - "Thinadhoo": "Maldives", "Ungoofaaru": "Maldives", "Naifaru": "Maldives", "Dhidhdhoo": "Maldives", - - // Madagascar - "Antananarivo": "Madagascar", "Toamasina": "Madagascar", "Antsirabe": "Madagascar", "Fianarantsoa": "Madagascar", - "Mahajanga": "Madagascar", "Toliara": "Madagascar", "Antsiranana": "Madagascar", "Ambovombe": "Madagascar", - "Morondava": "Madagascar", "Sambava": "Madagascar", "Manakara": "Madagascar", "Farafangana": "Madagascar", - - // Mauritius - "Port Louis": "Mauritius", "Beau Bassin-Rose Hill": "Mauritius", "Vacoas-Phoenix": "Mauritius", "Curepipe": "Mauritius", - "Quatre Bornes": "Mauritius", "Triolet": "Mauritius", "Goodlands": "Mauritius", "Centre de Flacq": "Mauritius", - - // Seychelles - "Victoria": "Seychelles", "Anse Boileau": "Seychelles", "Beau Vallon": "Seychelles", "Cascade": "Seychelles", - "Anse Royale": "Seychelles", "Takamaka": "Seychelles", "Port Glaud": "Seychelles", "Grand Anse Mahe": "Seychelles", - - // Comoros - "Moroni": "Comoros", "Mutsamudu": "Comoros", "Fomboni": "Comoros", "Domoni": "Comoros", - "Sima": "Comoros", "Mitsoudje": "Comoros", "Adda-Doueni": "Comoros", "Ouani": "Comoros", - - // Djibouti - "Djibouti City": "Djibouti", "Ali Sabieh": "Djibouti", "Dikhil": "Djibouti", "Tadjoura": "Djibouti", - "Obock": "Djibouti", "Arta": "Djibouti", "Holhol": "Djibouti", "Yoboki": "Djibouti", - - // Eritrea - "Asmara": "Eritrea", "Assab": "Eritrea", "Massawa": "Eritrea", "Keren": "Eritrea", - "Mendefera": "Eritrea", "Barentu": "Eritrea", "Adi Keih": "Eritrea", "Adi Quala": "Eritrea", - - // Somalia - "Mogadishu": "Somalia", "Hargeisa": "Somalia", "Bosaso": "Somalia", "Kismayo": "Somalia", - "Merca": "Somalia", "Galcayo": "Somalia", "Berbera": "Somalia", "Baidoa": "Somalia", - "Garowe": "Somalia", "Jowhar": "Somalia", "Borama": "Somalia", "Las Anod": "Somalia", - - // Sudan - "Khartoum": "Sudan", "Omdurman": "Sudan", "Port Sudan": "Sudan", "Kassala": "Sudan", - "Obeid": "Sudan", "Nyala": "Sudan", "Gedaref": "Sudan", "Wad Medani": "Sudan", - "El Fasher": "Sudan", "Kosti": "Sudan", "Sennar": "Sudan", "Dongola": "Sudan", - - // South Sudan - "Juba": "South Sudan", "Malakal": "South Sudan", "Wau": "South Sudan", "Bentiu": "South Sudan", - "Yei": "South Sudan", "Aweil": "South Sudan", "Kuacjok": "South Sudan", "Bor": "South Sudan", - - // Chad - "N'Djamena": "Chad", "Moundou": "Chad", "Sarh": "Chad", "Abéché": "Chad", - "Kelo": "Chad", "Koumra": "Chad", "Pala": "Chad", "Am Timan": "Chad", - - // Central African Republic - "Bangui": "Central African Republic", "Bimbo": "Central African Republic", "Berbérati": "Central African Republic", "Carnot": "Central African Republic", - "Bambari": "Central African Republic", "Bouar": "Central African Republic", "Bossangoa": "Central African Republic", "Bria": "Central African Republic", - - // Democratic Republic of Congo - "Kinshasa": "Congo", "Lubumbashi": "Congo", "Mbuji-Mayi": "Congo", "Kisangani": "Congo", - "Masina": "Congo", "Kananga": "Congo", "Likasi": "Congo", "Kolwezi": "Congo", - "Tshikapa": "Congo", "Beni": "Congo", "Bukavu": "Congo", "Mwene-Ditu": "Congo", - - // Republic of Congo - "Brazzaville": "Congo", "Pointe-Noire": "Congo", "Dolisie": "Congo", "Nkayi": "Congo", - "Impfondo": "Congo", "Ouesso": "Congo", "Madingou": "Congo", "Owando": "Congo", - - // Gabon - "Libreville": "Gabon", "Port-Gentil": "Gabon", "Franceville": "Gabon", "Oyem": "Gabon", - "Moanda": "Gabon", "Mouila": "Gabon", "Lambaréné": "Gabon", "Tchibanga": "Gabon", - - // Equatorial Guinea - "Malabo": "Equatorial Guinea", "Bata": "Equatorial Guinea", "Ebebiyin": "Equatorial Guinea", "Aconibe": "Equatorial Guinea", - "Añisoc": "Equatorial Guinea", "Luba": "Equatorial Guinea", "Evinayong": "Equatorial Guinea", "Mengomeyén": "Equatorial Guinea", - - // Cameroon - "Yaoundé": "Cameroon", "Douala": "Cameroon", "Bamenda": "Cameroon", "Bafoussam": "Cameroon", - "Garoua": "Cameroon", "Maroua": "Cameroon", "Nkongsamba": "Cameroon", "Bertoua": "Cameroon", - "Edéa": "Cameroon", "Loum": "Cameroon", "Kumba": "Cameroon", "Foumban": "Cameroon", - - // Angola - "Luanda": "Angola", "Huambo": "Angola", "Lobito": "Angola", "Benguela": "Angola", - "Kuito": "Angola", "Lubango": "Angola", "Malanje": "Angola", "Namibe": "Angola", - "Soyo": "Angola", "Cabinda": "Angola", "Uíge": "Angola", "Saurimo": "Angola", - - // Zambia - "Lusaka": "Zambia", "Kitwe": "Zambia", "Ndola": "Zambia", "Kabwe": "Zambia", - "Chingola": "Zambia", "Mufulira": "Zambia", "Livingstone": "Zambia", "Luanshya": "Zambia", - "Kasama": "Zambia", "Chipata": "Zambia", "Mazabuka": "Zambia", "Mongu": "Zambia", - - // Zimbabwe - "Harare": "Zimbabwe", "Bulawayo": "Zimbabwe", "Chitungwiza": "Zimbabwe", "Mutare": "Zimbabwe", - // Namibia - "Windhoek": "Namibia", "Rundu": "Namibia", "Walvis Bay": "Namibia", "Oshakati": "Namibia", - "Swakopmund": "Namibia", "Katima Mulilo": "Namibia", "Grootfontein": "Namibia", "Rehoboth": "Namibia", - "Otjiwarongo": "Namibia", "Okahandja": "Namibia", "Ondangwa": "Namibia", "Outapi": "Namibia", - "Conakry": "Guinea", "Nzérékoré": "Guinea", "Kankan": "Guinea", "Kindia": "Guinea", - // Botswana - "Gaborone": "Botswana", "Francistown": "Botswana", "Molepolole": "Botswana", "Maun": "Botswana", - "Serowe": "Botswana", "Selibe Phikwe": "Botswana", "Kanye": "Botswana", "Mochudi": "Botswana", - "Mahalapye": "Botswana", "Palapye": "Botswana", "Lobatse": "Botswana", "Kasane": "Botswana", - // Guinea-Bissau (补充缺失) - // Lesotho - "Maseru": "Lesotho", "Teyateyaneng": "Lesotho", "Mafeteng": "Lesotho", "Hlotse": "Lesotho", - "Mohale's Hoek": "Lesotho", "Maputsoe": "Lesotho", "Qacha's Nek": "Lesotho", "Quthing": "Lesotho", - "Freetown": "Sierra Leone", "Bo": "Sierra Leone", "Kenema": "Sierra Leone", "Koidu": "Sierra Leone", - // Eswatini (Swaziland) - "Mbabane": "Eswatini", "Manzini": "Eswatini", "Big Bend": "Eswatini", "Malkerns": "Eswatini", - "Nhlangano": "Eswatini", "Siteki": "Eswatini", "Pigg's Peak": "Eswatini", "Lobamba": "Eswatini", - "Monrovia": "Liberia", "Gbarnga": "Liberia", "Kakata": "Liberia", "Bensonville": "Liberia", - // Malawi - "Lilongwe": "Malawi", "Blantyre": "Malawi", "Mzuzu": "Malawi", "Zomba": "Malawi", - "Kasungu": "Malawi", "Mangochi": "Malawi", "Karonga": "Malawi", "Salima": "Malawi", - "Liwonde": "Malawi", "Nkhotakota": "Malawi", "Chiradzulu": "Malawi", "Nsanje": "Malawi", - "Abidjan": "Cote D'Ivoire", "Bouaké": "Cote D'Ivoire", "Daloa": "Cote D'Ivoire", "Yamoussoukro": "Cote D'Ivoire", - // Mozambique - "Maputo": "Mozambique", "Matola": "Mozambique", "Beira": "Mozambique", "Nampula": "Mozambique", - "Chimoio": "Mozambique", "Nacala": "Mozambique", "Quelimane": "Mozambique", "Tete": "Mozambique", - "Xai-Xai": "Mozambique", "Maxixe": "Mozambique", "Inhambane": "Mozambique", "Pemba": "Mozambique", - "Lomé": "Togo", "Sokodé": "Togo", "Kara": "Togo", "Palimé": "Togo", - // Tanzania - "Dar es Salaam": "Tanzania", "Mwanza": "Tanzania", "Arusha": "Tanzania", "Dodoma": "Tanzania", - "Mbeya": "Tanzania", "Morogoro": "Tanzania", "Tanga": "Tanzania", "Kahama": "Tanzania", - "Tabora": "Tanzania", "Zanzibar City": "Tanzania", "Kigoma": "Tanzania", "Sumbawanga": "Tanzania", - "Cotonou": "Benin", "Porto-Novo": "Benin", "Parakou": "Benin", "Djougou": "Benin", - // Rwanda - "Kigali": "Rwanda", "Butare": "Rwanda", "Gitarama": "Rwanda", "Ruhengeri": "Rwanda", - "Gisenyi": "Rwanda", "Byumba": "Rwanda", "Cyangugu": "Rwanda", "Kibuye": "Rwanda", - "Banjul": "Gambia", "Serekunda": "Gambia", "Brikama": "Gambia", "Bakau": "Gambia", - // Burundi - "Gitega": "Burundi", "Bujumbura": "Burundi", "Muyinga": "Burundi", "Ruyigi": "Burundi", - "Ngozi": "Burundi", "Rutana": "Burundi", "Kayanza": "Burundi", "Makamba": "Burundi", - "Nouakchott": "Mauritania", "Nouadhibou": "Mauritania", "Néma": "Mauritania", "Kaédi": "Mauritania", - // Uganda - "Kampala": "Uganda", "Gulu": "Uganda", "Lira": "Uganda", "Mbarara": "Uganda", - "Jinja": "Uganda", "Bwizibwera": "Uganda", "Mbale": "Uganda", "Mukono": "Uganda", - "Kasese": "Uganda", "Masaka": "Uganda", "Entebbe": "Uganda", "Njeru": "Uganda", - // Cabo Verde (补充缺失) - // Tunisia - "Tunis": "Tunisia", "Sfax": "Tunisia", "Sousse": "Tunisia", "Ettadhamen": "Tunisia", - "Kairouan": "Tunisia", "Bizerte": "Tunisia", "Gabès": "Tunisia", "Aryanah": "Tunisia", - "Gafsa": "Tunisia", "El Mourouj": "Tunisia", "Kasserine": "Tunisia", "Ben Arous": "Tunisia", - // São Tomé and Príncipe (补充缺失) - // Libya - "Benghazi": "Libya", "Misrata": "Libya", "Tarhuna": "Libya", - "Al Bayda": "Libya", "Zawiya": "Libya", "Zuwara": "Libya", "Ajdabiya": "Libya", - "Tobruk": "Libya", "Sabha": "Libya", "Sirte": "Libya", "Marj": "Libya", - "Dublin": "Ireland", "Cork": "Ireland", "Limerick": "Ireland", "Galway": "Ireland", - "Waterford": "Ireland", "Drogheda": "Ireland", "Dundalk": "Ireland", "Swords": "Ireland", - "Bray": "Ireland", "Navan": "Ireland", "Ennis": "Ireland", "Kilkenny": "Ireland", - - // More countries and cities can be added here... -} - -// GetCountryCenterByCountryOrCity 根据国家名称或城市名称获取国家中心经纬度 -// countryOrAbbr: 国家名称(全称或简称) -// city: 城市名称 -// 返回: 纬度, 经度, 是否找到 -func GetCountryCenterByCountryOrCity(countryOrAbbr, city string) (lat, lon string, found bool) { - // 1. 首先尝试国家简称转全称(大小写不敏感) - countryOrAbbr = strings.TrimSpace(countryOrAbbr) - if countryOrAbbr != "" { - // 尝试作为简称查找 - if fullName, ok := countryAbbr[strings.ToUpper(countryOrAbbr)]; ok { - countryOrAbbr = fullName - } - // 2. 直接查找国家中心点(大小写不敏感) - for country, center := range countryCenter { - if strings.EqualFold(country, countryOrAbbr) { - return fmt.Sprintf("%f", center[0]), fmt.Sprintf("%f", center[1]), true - } - } - } - - // 3. 通过城市查找国家(大小写不敏感) - city = strings.TrimSpace(city) - if city != "" { - for cityName, country := range cityToCountry { - if strings.EqualFold(cityName, city) { - if center, ok := countryCenter[country]; ok { - return fmt.Sprintf("%f", center[0]), fmt.Sprintf("%f", center[1]), true - } - } - } - } - - return "", "", false -} - -// GetCountryCenter 根据国家名称获取中心经纬度(兼容旧接口) -func GetCountryCenter(countryName string) (lat, lon string, found bool) { - return GetCountryCenterByCountryOrCity(countryName, "") -} - -// GetCountryCenterByCity 根据城市名称获取所在国家的中心经纬度 -func GetCountryCenterByCity(cityName string) (lat, lon string, found bool) { - return GetCountryCenterByCountryOrCity("", cityName) -} diff --git a/pkg/countryCenter/county_center_test.go b/pkg/countryCenter/county_center_test.go deleted file mode 100644 index ef1f8b1..0000000 --- a/pkg/countryCenter/county_center_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package countryCenter - -import ( - "testing" -) - -func TestGetCountryCenter(t *testing.T) { - lat, lon, found := GetCountryCenterByCountryOrCity("SG", "Singapore") - if !found { - t.Error("GetCountryCenter('HK') should return found = true") - } - t.Logf("lat = %v, lon = %v", lat, lon) - -} diff --git a/pkg/deduction/deduction.go b/pkg/deduction/deduction.go index 467b90b..46f32c8 100644 --- a/pkg/deduction/deduction.go +++ b/pkg/deduction/deduction.go @@ -1,95 +1,302 @@ +// Package deduction provides functionality for calculating remaining amounts +// in subscription billing systems, supporting various time units and traffic-based calculations. package deduction import ( - "log" + "errors" + "fmt" + "math" "time" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/pkg/tool" ) const ( - UnitTimeNoLimit = "NoLimit" - UnitTimeYear = "Year" - UnitTimeMonth = "Month" - UnitTimeDay = "Day" - UintTimeHour = "Hour" - UintTimeMinute = "Minute" + // Time unit constants for subscription billing + UnitTimeNoLimit = "NoLimit" // Unlimited time subscription + UnitTimeYear = "Year" // Annual subscription + UnitTimeMonth = "Month" // Monthly subscription + UnitTimeDay = "Day" // Daily subscription + UnitTimeHour = "Hour" // Hourly subscription + UnitTimeMinute = "Minute" // Per-minute subscription - ResetCycleNone = 0 - ResetCycle1st = 1 - ResetCycleMonthly = 2 - ResetCycleYear = 3 + // Reset cycle constants for traffic resets + ResetCycleNone = 0 // No reset cycle + ResetCycle1st = 1 // Reset on 1st of each month + ResetCycleMonthly = 2 // Reset monthly based on start date + ResetCycleYear = 3 // Reset yearly based on start date + + // Safety limits for overflow protection + maxInt64 = math.MaxInt64 + minInt64 = math.MinInt64 ) +// Error definitions for validation and calculation failures +var ( + ErrInvalidQuantity = errors.New("order quantity cannot be zero or negative") + ErrInvalidAmount = errors.New("order amount cannot be negative") + ErrInvalidTraffic = errors.New("traffic values cannot be negative") + ErrInvalidTimeRange = errors.New("expire time must be after start time") + ErrInvalidUnitTime = errors.New("invalid unit time") + ErrInvalidDeductionRatio = errors.New("deduction ratio must be between 0 and 100") + ErrOverflow = errors.New("calculation overflow") +) + +// Subscribe represents a subscription with time and traffic limits type Subscribe struct { - StartTime time.Time - ExpireTime time.Time - Traffic int64 - Download int64 - Upload int64 - UnitTime string - UnitPrice int64 - ResetCycle int64 - DeductionRatio int64 + StartTime time.Time // Subscription start time + ExpireTime time.Time // Subscription expiration time + Traffic int64 // Total traffic allowance in bytes + Download int64 // Downloaded traffic in bytes + Upload int64 // Uploaded traffic in bytes + UnitTime string // Time unit for billing (Year, Month, Day, etc.) + UnitPrice int64 // Price per unit time + ResetCycle int64 // Traffic reset cycle + DeductionRatio int64 // Deduction ratio for weighted calculations (0-100) } +// Order represents a purchase order for subscription calculation type Order struct { - Amount int64 - Quantity int64 + Amount int64 // Total order amount + Quantity int64 // Order quantity } -func CalculateRemainingAmount(sub Subscribe, order Order) int64 { - if sub.UnitTime == UnitTimeNoLimit && sub.ResetCycle != 0 { - return 0 +// Validate checks if the Subscribe struct contains valid data +func (s *Subscribe) Validate() error { + if s.Traffic < 0 || s.Download < 0 || s.Upload < 0 { + return ErrInvalidTraffic } - log.Printf("开始计算订单剩余价值") - // 实际单价 - sub.UnitPrice = order.Amount / order.Quantity - log.Printf("订阅实际单价: %d", sub.UnitPrice) - now := time.Now() + + if s.Download+s.Upload > s.Traffic { + return fmt.Errorf("download + upload (%d) cannot exceed total traffic (%d)", s.Download+s.Upload, s.Traffic) + } + + if !s.ExpireTime.After(s.StartTime) { + return ErrInvalidTimeRange + } + + if s.DeductionRatio < 0 || s.DeductionRatio > 100 { + return ErrInvalidDeductionRatio + } + + validUnitTimes := []string{UnitTimeNoLimit, UnitTimeYear, UnitTimeMonth, UnitTimeDay, UnitTimeHour, UnitTimeMinute} + valid := false + for _, ut := range validUnitTimes { + if s.UnitTime == ut { + valid = true + break + } + } + if !valid { + return ErrInvalidUnitTime + } + + return nil +} + +// Validate checks if the Order struct contains valid data +func (o *Order) Validate() error { + if o.Quantity <= 0 { + return ErrInvalidQuantity + } + if o.Amount < 0 { + return ErrInvalidAmount + } + return nil +} + +// safeMultiply performs multiplication with overflow protection +func safeMultiply(a, b int64) (int64, error) { + if a == 0 || b == 0 { + return 0, nil + } + + if a > 0 && b > 0 { + if a > maxInt64/b { + return 0, ErrOverflow + } + } else if a < 0 && b < 0 { + if a < maxInt64/b { + return 0, ErrOverflow + } + } else { + if (a > 0 && b < minInt64/a) || (a < 0 && b > minInt64/a) { + return 0, ErrOverflow + } + } + + return a * b, nil +} + +// safeAdd performs addition with overflow protection +func safeAdd(a, b int64) (int64, error) { + if (b > 0 && a > maxInt64-b) || (b < 0 && a < minInt64-b) { + return 0, ErrOverflow + } + return a + b, nil +} + +// safeDivide performs division with zero-division protection +func safeDivide(a, b int64) (int64, error) { + if b == 0 { + return 0, errors.New("division by zero") + } + return a / b, nil +} + +// CalculateRemainingAmount calculates the remaining refund amount for a subscription +// based on unused time and traffic. Returns the amount and any calculation errors. +func CalculateRemainingAmount(sub Subscribe, order Order) (int64, error) { + if err := sub.Validate(); err != nil { + return 0, fmt.Errorf("invalid subscription: %w", err) + } + + if err := order.Validate(); err != nil { + return 0, fmt.Errorf("invalid order: %w", err) + } + + if sub.UnitTime == UnitTimeNoLimit && sub.ResetCycle != 0 { + return 0, nil + } + + unitPrice, err := safeDivide(order.Amount, order.Quantity) + if err != nil { + return 0, fmt.Errorf("failed to calculate unit price: %w", err) + } + sub.UnitPrice = unitPrice + + loc, err := time.LoadLocation(sub.StartTime.Location().String()) + if err != nil { + loc = time.UTC + } + now := time.Now().In(loc) + switch sub.UnitTime { case UnitTimeNoLimit: - log.Printf("订阅不限时长") - usedTraffic := sub.Traffic - sub.Download - sub.Upload - unitPrice := float64(order.Amount) / float64(sub.Traffic) - return int64(float64(usedTraffic) * unitPrice) + return calculateNoLimitAmount(sub, order) case UnitTimeYear: - log.Printf("订阅时长为年") remainingYears := tool.YearDiff(now, sub.ExpireTime) - remainingUnitTimeAmount := calculateRemainingUnitTimeAmount(sub) - return int64(remainingYears)*sub.UnitPrice + remainingUnitTimeAmount + remainingUnitTimeAmount, err := calculateRemainingUnitTimeAmount(sub) + if err != nil { + return 0, err + } + + yearAmount, err := safeMultiply(int64(remainingYears), sub.UnitPrice) + if err != nil { + return 0, fmt.Errorf("year calculation overflow: %w", err) + } + + total, err := safeAdd(yearAmount, remainingUnitTimeAmount) + if err != nil { + return 0, fmt.Errorf("total calculation overflow: %w", err) + } + + return total, nil case UnitTimeMonth: - log.Printf("订阅时长为月") remainingMonths := tool.MonthDiff(now, sub.ExpireTime) - remainingUnitTimeAmount := calculateRemainingUnitTimeAmount(sub) - return int64(remainingMonths)*sub.UnitPrice + remainingUnitTimeAmount + remainingUnitTimeAmount, err := calculateRemainingUnitTimeAmount(sub) + if err != nil { + return 0, err + } + + monthAmount, err := safeMultiply(int64(remainingMonths), sub.UnitPrice) + if err != nil { + return 0, fmt.Errorf("month calculation overflow: %w", err) + } + + total, err := safeAdd(monthAmount, remainingUnitTimeAmount) + if err != nil { + return 0, fmt.Errorf("total calculation overflow: %w", err) + } + + return total, nil + + case UnitTimeDay: + remainingDays := tool.DayDiff(now, sub.ExpireTime) + remainingUnitTimeAmount, err := calculateRemainingUnitTimeAmount(sub) + if err != nil { + return 0, err + } + + dayAmount, err := safeMultiply(remainingDays, sub.UnitPrice) + if err != nil { + return 0, fmt.Errorf("day calculation overflow: %w", err) + } + + total, err := safeAdd(dayAmount, remainingUnitTimeAmount) + if err != nil { + return 0, fmt.Errorf("total calculation overflow: %w", err) + } + + return total, nil } - return 0 + return 0, nil } -func calculateRemainingUnitTimeAmount(sub Subscribe) int64 { +// calculateNoLimitAmount calculates refund amount for unlimited time subscriptions +// based on unused traffic only +func calculateNoLimitAmount(sub Subscribe, order Order) (int64, error) { + if sub.Traffic == 0 { + return 0, nil + } + + usedTraffic := sub.Traffic - sub.Download - sub.Upload + if usedTraffic < 0 { + usedTraffic = 0 + } + + unitPrice := float64(order.Amount) / float64(sub.Traffic) + result := float64(usedTraffic) * unitPrice + + if result > float64(maxInt64) || result < float64(minInt64) { + return 0, ErrOverflow + } + + return int64(result), nil +} + +// calculateRemainingUnitTimeAmount calculates the remaining amount based on +// both time and traffic usage, applying deduction ratios when specified +func calculateRemainingUnitTimeAmount(sub Subscribe) (int64, error) { now := time.Now() - log.Printf("开始计算订阅剩余时长价值") - log.Printf("订阅开始时间: %s, 订阅到期时间: %s,订阅流量: %d", sub.StartTime.Format("2006-01-02 15:04:05"), sub.ExpireTime.Format("2006-01-02 15:04:05"), sub.Traffic) trafficWeight, timeWeight := calculateWeights(sub.DeductionRatio) remainingDays, totalDays := getRemainingAndTotalDays(sub, now) - remainingTraffic := sub.Traffic - sub.Download - sub.Upload - remainingTimeAmount := calculateProportionalAmount(sub.UnitPrice, remainingDays, totalDays) - remainingTrafficAmount := calculateProportionalAmount(sub.UnitPrice, remainingTraffic, sub.Traffic) - log.Printf("订阅剩余天数: %d, 总天数: %d, 剩余流量: %d, 剩余时间价值: %d, 剩余流量价值: %d", remainingDays, totalDays, remainingTraffic, remainingTimeAmount, remainingTrafficAmount) - if sub.Traffic == 0 { - return remainingTimeAmount + + if totalDays == 0 { + return 0, nil } + + remainingTraffic := sub.Traffic - sub.Download - sub.Upload + if remainingTraffic < 0 { + remainingTraffic = 0 + } + + remainingTimeAmount, err := calculateProportionalAmount(sub.UnitPrice, remainingDays, totalDays) + if err != nil { + return 0, fmt.Errorf("time amount calculation failed: %w", err) + } + + if sub.Traffic == 0 { + return remainingTimeAmount, nil + } + + remainingTrafficAmount, err := calculateProportionalAmount(sub.UnitPrice, remainingTraffic, sub.Traffic) + if err != nil { + return 0, fmt.Errorf("traffic amount calculation failed: %w", err) + } + if sub.DeductionRatio != 0 { return calculateWeightedAmount(sub.UnitPrice, remainingTraffic, sub.Traffic, remainingDays, totalDays, trafficWeight, timeWeight) } - return min(remainingTimeAmount, remainingTrafficAmount) + return min(remainingTimeAmount, remainingTrafficAmount), nil } +// calculateWeights converts deduction ratio to traffic and time weights +// for weighted calculations func calculateWeights(deductionRatio int64) (float64, float64) { if deductionRatio == 0 { return 0, 0 @@ -99,22 +306,32 @@ func calculateWeights(deductionRatio int64) (float64, float64) { return trafficWeight, timeWeight } +// getRemainingAndTotalDays calculates remaining and total days based on +// the subscription's reset cycle configuration func getRemainingAndTotalDays(sub Subscribe, now time.Time) (int64, int64) { - log.Printf("开始计算订阅剩余天数") - log.Printf("重置周期: %d", sub.ResetCycle) switch sub.ResetCycle { case ResetCycleNone: - remaining := sub.ExpireTime.Sub(now).Hours() / 24 total := sub.ExpireTime.Sub(sub.StartTime).Hours() / 24 + if remaining < 0 { + remaining = 0 + } + if total < 0 { + total = 0 + } return int64(remaining), int64(total) case ResetCycle1st: return tool.DaysToNextMonth(now), tool.GetLastDayOfMonth(now) case ResetCycleMonthly: - // -1 to include the current day - return tool.DaysToMonthDay(now, sub.StartTime.Day()) - 1, tool.DaysToMonthDay(now, sub.StartTime.Day()) + remaining := tool.DaysToMonthDay(now, sub.StartTime.Day()) - 1 + total := tool.DaysToMonthDay(now, sub.StartTime.Day()) + if remaining < 0 { + remaining = 0 + } + return remaining, total + case ResetCycleYear: return tool.DaysToYearDay(now, int(sub.StartTime.Month()), sub.StartTime.Day()), tool.GetYearDays(now, int(sub.StartTime.Month()), sub.StartTime.Day()) @@ -122,13 +339,36 @@ func getRemainingAndTotalDays(sub Subscribe, now time.Time) (int64, int64) { return 0, 0 } -func calculateWeightedAmount(unitPrice, remainingTraffic, totalTraffic, remainingDays, totalDays int64, trafficWeight, timeWeight float64) int64 { +// calculateWeightedAmount applies weighted calculation combining both time and traffic +// remaining ratios based on the specified weights +func calculateWeightedAmount(unitPrice, remainingTraffic, totalTraffic, remainingDays, totalDays int64, trafficWeight, timeWeight float64) (int64, error) { + if totalDays == 0 || totalTraffic == 0 { + return 0, nil + } + remainingTimeRatio := float64(remainingDays) / float64(totalDays) remainingTrafficRatio := float64(remainingTraffic) / float64(totalTraffic) weightedRemainingRatio := (timeWeight * remainingTimeRatio) + (trafficWeight * remainingTrafficRatio) - return int64(float64(unitPrice) * weightedRemainingRatio) + + result := float64(unitPrice) * weightedRemainingRatio + if result > float64(maxInt64) || result < float64(minInt64) { + return 0, ErrOverflow + } + + return int64(result), nil } -func calculateProportionalAmount(unitPrice, remaining, total int64) int64 { - return int64(float64(unitPrice) * (float64(remaining) / float64(total))) +// calculateProportionalAmount calculates proportional amount based on +// remaining vs total ratio with overflow protection +func calculateProportionalAmount(unitPrice, remaining, total int64) (int64, error) { + if total == 0 { + return 0, nil + } + + result := float64(unitPrice) * (float64(remaining) / float64(total)) + if result > float64(maxInt64) || result < float64(minInt64) { + return 0, ErrOverflow + } + + return int64(result), nil } diff --git a/pkg/deduction/deduction_test.go b/pkg/deduction/deduction_test.go new file mode 100644 index 0000000..0e96555 --- /dev/null +++ b/pkg/deduction/deduction_test.go @@ -0,0 +1,665 @@ +package deduction + +import ( + "math" + "testing" + "time" +) + +func TestSubscribe_Validate(t *testing.T) { + tests := []struct { + name string + sub Subscribe + wantErr bool + errType error + }{ + { + name: "valid subscription", + sub: Subscribe{ + StartTime: time.Now(), + ExpireTime: time.Now().Add(24 * time.Hour), + Traffic: 1000, + Download: 100, + Upload: 200, + UnitTime: UnitTimeMonth, + DeductionRatio: 50, + }, + wantErr: false, + }, + { + name: "negative traffic", + sub: Subscribe{ + StartTime: time.Now(), + ExpireTime: time.Now().Add(24 * time.Hour), + Traffic: -1000, + Download: 100, + Upload: 200, + UnitTime: UnitTimeMonth, + DeductionRatio: 50, + }, + wantErr: true, + errType: ErrInvalidTraffic, + }, + { + name: "negative download", + sub: Subscribe{ + StartTime: time.Now(), + ExpireTime: time.Now().Add(24 * time.Hour), + Traffic: 1000, + Download: -100, + Upload: 200, + UnitTime: UnitTimeMonth, + DeductionRatio: 50, + }, + wantErr: true, + errType: ErrInvalidTraffic, + }, + { + name: "download + upload exceeds traffic", + sub: Subscribe{ + StartTime: time.Now(), + ExpireTime: time.Now().Add(24 * time.Hour), + Traffic: 1000, + Download: 600, + Upload: 500, + UnitTime: UnitTimeMonth, + DeductionRatio: 50, + }, + wantErr: true, + }, + { + name: "expire time before start time", + sub: Subscribe{ + StartTime: time.Now(), + ExpireTime: time.Now().Add(-24 * time.Hour), + Traffic: 1000, + Download: 100, + Upload: 200, + UnitTime: UnitTimeMonth, + DeductionRatio: 50, + }, + wantErr: true, + errType: ErrInvalidTimeRange, + }, + { + name: "invalid deduction ratio - negative", + sub: Subscribe{ + StartTime: time.Now(), + ExpireTime: time.Now().Add(24 * time.Hour), + Traffic: 1000, + Download: 100, + Upload: 200, + UnitTime: UnitTimeMonth, + DeductionRatio: -10, + }, + wantErr: true, + errType: ErrInvalidDeductionRatio, + }, + { + name: "invalid deduction ratio - over 100", + sub: Subscribe{ + StartTime: time.Now(), + ExpireTime: time.Now().Add(24 * time.Hour), + Traffic: 1000, + Download: 100, + Upload: 200, + UnitTime: UnitTimeMonth, + DeductionRatio: 150, + }, + wantErr: true, + errType: ErrInvalidDeductionRatio, + }, + { + name: "invalid unit time", + sub: Subscribe{ + StartTime: time.Now(), + ExpireTime: time.Now().Add(24 * time.Hour), + Traffic: 1000, + Download: 100, + Upload: 200, + UnitTime: "InvalidUnit", + DeductionRatio: 50, + }, + wantErr: true, + errType: ErrInvalidUnitTime, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.sub.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("Subscribe.Validate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.errType != nil && err != tt.errType { + t.Errorf("Subscribe.Validate() error = %v, want %v", err, tt.errType) + } + }) + } +} + +func TestOrder_Validate(t *testing.T) { + tests := []struct { + name string + order Order + wantErr bool + errType error + }{ + { + name: "valid order", + order: Order{Amount: 1000, Quantity: 2}, + wantErr: false, + }, + { + name: "zero quantity", + order: Order{Amount: 1000, Quantity: 0}, + wantErr: true, + errType: ErrInvalidQuantity, + }, + { + name: "negative quantity", + order: Order{Amount: 1000, Quantity: -1}, + wantErr: true, + errType: ErrInvalidQuantity, + }, + { + name: "negative amount", + order: Order{Amount: -1000, Quantity: 2}, + wantErr: true, + errType: ErrInvalidAmount, + }, + { + name: "zero amount is valid", + order: Order{Amount: 0, Quantity: 1}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.order.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("Order.Validate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.errType != nil && err != tt.errType { + t.Errorf("Order.Validate() error = %v, want %v", err, tt.errType) + } + }) + } +} + +func TestSafeMultiply(t *testing.T) { + tests := []struct { + name string + a, b int64 + want int64 + wantErr bool + }{ + { + name: "normal multiplication", + a: 10, + b: 20, + want: 200, + wantErr: false, + }, + { + name: "zero multiplication", + a: 10, + b: 0, + want: 0, + wantErr: false, + }, + { + name: "negative multiplication", + a: -10, + b: 20, + want: -200, + wantErr: false, + }, + { + name: "overflow case", + a: math.MaxInt64, + b: 2, + want: 0, + wantErr: true, + }, + { + name: "large numbers no overflow", + a: 1000000, + b: 1000000, + want: 1000000000000, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safeMultiply(tt.a, tt.b) + if (err != nil) != tt.wantErr { + t.Errorf("safeMultiply() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("safeMultiply() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSafeAdd(t *testing.T) { + tests := []struct { + name string + a, b int64 + want int64 + wantErr bool + }{ + { + name: "normal addition", + a: 10, + b: 20, + want: 30, + wantErr: false, + }, + { + name: "negative addition", + a: -10, + b: 5, + want: -5, + wantErr: false, + }, + { + name: "overflow case", + a: math.MaxInt64, + b: 1, + want: 0, + wantErr: true, + }, + { + name: "underflow case", + a: math.MinInt64, + b: -1, + want: 0, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safeAdd(tt.a, tt.b) + if (err != nil) != tt.wantErr { + t.Errorf("safeAdd() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("safeAdd() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSafeDivide(t *testing.T) { + tests := []struct { + name string + a, b int64 + want int64 + wantErr bool + }{ + { + name: "normal division", + a: 20, + b: 10, + want: 2, + wantErr: false, + }, + { + name: "division by zero", + a: 20, + b: 0, + want: 0, + wantErr: true, + }, + { + name: "negative division", + a: -20, + b: 10, + want: -2, + wantErr: false, + }, + { + name: "zero dividend", + a: 0, + b: 10, + want: 0, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := safeDivide(tt.a, tt.b) + if (err != nil) != tt.wantErr { + t.Errorf("safeDivide() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("safeDivide() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCalculateWeights(t *testing.T) { + tests := []struct { + name string + deductionRatio int64 + wantTrafficWeight float64 + wantTimeWeight float64 + }{ + { + name: "zero ratio", + deductionRatio: 0, + wantTrafficWeight: 0, + wantTimeWeight: 0, + }, + { + name: "50% ratio", + deductionRatio: 50, + wantTrafficWeight: 0.5, + wantTimeWeight: 0.5, + }, + { + name: "75% ratio", + deductionRatio: 75, + wantTrafficWeight: 0.75, + wantTimeWeight: 0.25, + }, + { + name: "100% ratio", + deductionRatio: 100, + wantTrafficWeight: 1.0, + wantTimeWeight: 0.0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotTrafficWeight, gotTimeWeight := calculateWeights(tt.deductionRatio) + if gotTrafficWeight != tt.wantTrafficWeight { + t.Errorf("calculateWeights() trafficWeight = %v, want %v", gotTrafficWeight, tt.wantTrafficWeight) + } + if gotTimeWeight != tt.wantTimeWeight { + t.Errorf("calculateWeights() timeWeight = %v, want %v", gotTimeWeight, tt.wantTimeWeight) + } + }) + } +} + +func TestCalculateProportionalAmount(t *testing.T) { + tests := []struct { + name string + unitPrice int64 + remaining int64 + total int64 + want int64 + wantErr bool + }{ + { + name: "normal calculation", + unitPrice: 100, + remaining: 50, + total: 100, + want: 50, + wantErr: false, + }, + { + name: "zero total", + unitPrice: 100, + remaining: 50, + total: 0, + want: 0, + wantErr: false, + }, + { + name: "zero remaining", + unitPrice: 100, + remaining: 0, + total: 100, + want: 0, + wantErr: false, + }, + { + name: "quarter remaining", + unitPrice: 200, + remaining: 25, + total: 100, + want: 50, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := calculateProportionalAmount(tt.unitPrice, tt.remaining, tt.total) + if (err != nil) != tt.wantErr { + t.Errorf("calculateProportionalAmount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("calculateProportionalAmount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCalculateNoLimitAmount(t *testing.T) { + tests := []struct { + name string + sub Subscribe + order Order + want int64 + wantErr bool + }{ + { + name: "normal no limit calculation", + sub: Subscribe{ + Traffic: 1000, + Download: 300, + Upload: 200, + }, + order: Order{ + Amount: 1000, + }, + want: 500, // (1000 - 300 - 200) / 1000 * 1000 = 500 + wantErr: false, + }, + { + name: "zero traffic", + sub: Subscribe{ + Traffic: 0, + Download: 0, + Upload: 0, + }, + order: Order{ + Amount: 1000, + }, + want: 0, + wantErr: false, + }, + { + name: "overused traffic", + sub: Subscribe{ + Traffic: 1000, + Download: 600, + Upload: 500, + }, + order: Order{ + Amount: 1000, + }, + want: 0, // usedTraffic would be negative, clamped to 0 + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := calculateNoLimitAmount(tt.sub, tt.order) + if (err != nil) != tt.wantErr { + t.Errorf("calculateNoLimitAmount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("calculateNoLimitAmount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCalculateRemainingAmount(t *testing.T) { + now := time.Now() + + tests := []struct { + name string + sub Subscribe + order Order + wantErr bool + }{ + { + name: "valid no limit subscription", + sub: Subscribe{ + StartTime: now.Add(-24 * time.Hour), + ExpireTime: now.Add(24 * time.Hour), + Traffic: 1000, + Download: 300, + Upload: 200, + UnitTime: UnitTimeNoLimit, + ResetCycle: ResetCycleNone, + DeductionRatio: 0, + }, + order: Order{ + Amount: 1000, + Quantity: 1, + }, + wantErr: false, + }, + { + name: "invalid subscription", + sub: Subscribe{ + StartTime: now, + ExpireTime: now.Add(-24 * time.Hour), // Invalid: expire before start + Traffic: 1000, + Download: 300, + Upload: 200, + UnitTime: UnitTimeMonth, + DeductionRatio: 0, + }, + order: Order{ + Amount: 1000, + Quantity: 1, + }, + wantErr: true, + }, + { + name: "invalid order", + sub: Subscribe{ + StartTime: now.Add(-24 * time.Hour), + ExpireTime: now.Add(24 * time.Hour), + Traffic: 1000, + Download: 300, + Upload: 200, + UnitTime: UnitTimeMonth, + DeductionRatio: 0, + }, + order: Order{ + Amount: 1000, + Quantity: 0, // Invalid: zero quantity + }, + wantErr: true, + }, + { + name: "no limit with reset cycle", + sub: Subscribe{ + StartTime: now.Add(-24 * time.Hour), + ExpireTime: now.Add(24 * time.Hour), + Traffic: 1000, + Download: 300, + Upload: 200, + UnitTime: UnitTimeNoLimit, + ResetCycle: ResetCycleMonthly, // Should return 0 + DeductionRatio: 0, + }, + order: Order{ + Amount: 1000, + Quantity: 1, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := CalculateRemainingAmount(tt.sub, tt.order) + if (err != nil) != tt.wantErr { + t.Errorf("CalculateRemainingAmount() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCalculateRemainingAmount_NoLimitWithResetCycle(t *testing.T) { + now := time.Now() + sub := Subscribe{ + StartTime: now.Add(-24 * time.Hour), + ExpireTime: now.Add(24 * time.Hour), + Traffic: 1000, + Download: 300, + Upload: 200, + UnitTime: UnitTimeNoLimit, + ResetCycle: ResetCycleMonthly, + DeductionRatio: 0, + } + order := Order{ + Amount: 1000, + Quantity: 1, + } + + got, err := CalculateRemainingAmount(sub, order) + if err != nil { + t.Errorf("CalculateRemainingAmount() error = %v", err) + return + } + if got != 0 { + t.Errorf("CalculateRemainingAmount() = %v, want 0", got) + } +} + +// Benchmark tests +func BenchmarkCalculateRemainingAmount(b *testing.B) { + now := time.Now() + sub := Subscribe{ + StartTime: now.Add(-24 * time.Hour), + ExpireTime: now.Add(24 * time.Hour), + Traffic: 1000, + Download: 300, + Upload: 200, + UnitTime: UnitTimeMonth, + ResetCycle: ResetCycleNone, + DeductionRatio: 50, + } + order := Order{ + Amount: 1000, + Quantity: 1, + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = CalculateRemainingAmount(sub, order) + } +} + +func BenchmarkSafeMultiply(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = safeMultiply(12345, 67890) + } +} diff --git a/pkg/email/manager.go b/pkg/email/manager.go new file mode 100644 index 0000000..8e4b8a3 --- /dev/null +++ b/pkg/email/manager.go @@ -0,0 +1,134 @@ +package email + +import ( + "context" + "sync" + "time" + + "github.com/perfect-panel/server/pkg/logger" + "gorm.io/gorm" +) + +var ( + Manager *WorkerManager // 全局调度器实例 + once sync.Once // 确保 Scheduler 只被初始化一次 + limit sync.RWMutex // 控制并发限制 +) + +type WorkerManager struct { + db *gorm.DB // 数据库连接 + sender Sender // 邮件发送器接口 + mutex sync.RWMutex // 读写互斥锁,确保线程安全 + workers map[int64]*Worker // 存储所有 Worker 实例 + cancels map[int64]context.CancelFunc // 存储每个 Worker 的取消函数 +} + +func NewWorkerManager(db *gorm.DB, sender Sender) *WorkerManager { + if Manager != nil { + return Manager + } + once.Do(func() { + Manager = &WorkerManager{ + db: db, + workers: make(map[int64]*Worker), + cancels: make(map[int64]context.CancelFunc), + sender: sender, + } + }) + // 设置定时检查任务 + go func() { + for { + // 每隔5分钟检查一次 + select { + case <-time.After(1 * time.Minute): + checkWorker() + continue + } + } + }() + return Manager +} + +// AddWorker 添加一个新的 Worker 实例 +func (m *WorkerManager) AddWorker(id int64) { + m.mutex.Lock() + defer m.mutex.Unlock() + if _, exists := m.workers[id]; !exists { + ctx, cancel := context.WithCancel(context.Background()) + worker := NewWorker(ctx, id, m.db, m.sender) + m.workers[id] = worker + m.cancels[id] = cancel + go worker.Start() + logger.Info("Batch Send Email", + logger.Field("message", "Added new worker"), + logger.Field("task_id", id), + ) + } else { + logger.Info("Batch Send Email", + logger.Field("message", "Worker already exists"), + logger.Field("task_id", id), + ) + } + +} + +// GetWorker 获取指定任务的 Worker 实例 +func (m *WorkerManager) GetWorker(id int64) *Worker { + m.mutex.RLock() + defer m.mutex.RUnlock() + if worker, exists := m.workers[id]; exists { + return worker + } else { + logger.Error("Batch Send Email", + logger.Field("message", "Worker not found"), + logger.Field("task_id", id), + ) + return nil + } +} + +// RemoveWorker 移除指定任务的 Worker 实例 +func (m *WorkerManager) RemoveWorker(id int64) { + m.mutex.Lock() + defer m.mutex.Unlock() + if _, exists := m.workers[id]; exists { + delete(m.workers, id) + if cancelFunc, ok := m.cancels[id]; ok { + cancelFunc() // 调用取消函数 + delete(m.cancels, id) + } + logger.Info("Batch Send Email", + logger.Field("message", "Removed worker"), + logger.Field("task_id", id), + ) + } else { + logger.Error("Batch Send Email", + logger.Field("message", "Worker not found for removal"), + logger.Field("task_id", id), + ) + } +} + +func checkWorker() { + if Manager == nil { + // 如果 Manager 未初始化,直接返回 + return + } + Manager.mutex.Lock() + defer Manager.mutex.Unlock() + for id, worker := range Manager.workers { + if worker.IsRunning() == 2 { + // 如果Worker已完成,移除它 + delete(Manager.workers, id) + if cancelFunc, ok := Manager.cancels[id]; ok { + cancelFunc() // 调用取消函数 + delete(Manager.cancels, id) + } + logger.Info("Batch Send Email", + logger.Field("message", "Removed completed worker"), + logger.Field("task_id", id), + ) + } + } + +} diff --git a/pkg/email/platform.go b/pkg/email/platform.go index 95d737c..515ef45 100644 --- a/pkg/email/platform.go +++ b/pkg/email/platform.go @@ -1,6 +1,6 @@ package email -import "github.com/perfect-panel/ppanel-server/internal/types" +import "github.com/perfect-panel/server/internal/types" type Platform int diff --git a/pkg/email/sender.go b/pkg/email/sender.go index d1e6570..7bbe1fc 100644 --- a/pkg/email/sender.go +++ b/pkg/email/sender.go @@ -4,8 +4,8 @@ import ( "encoding/json" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/email/smtp" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/email/smtp" + "github.com/perfect-panel/server/pkg/logger" ) type Sender interface { diff --git a/pkg/email/template.go b/pkg/email/template.go index f31b9c7..5d2bd5b 100644 --- a/pkg/email/template.go +++ b/pkg/email/template.go @@ -282,7 +282,6 @@ const ( ` - DefaultTrafficExceedEmailTemplate = ` diff --git a/pkg/email/worker.go b/pkg/email/worker.go new file mode 100644 index 0000000..fd5aba4 --- /dev/null +++ b/pkg/email/worker.go @@ -0,0 +1,192 @@ +package email + +import ( + "context" + "encoding/json" + "time" + + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "gorm.io/gorm" +) + +type ErrorInfo struct { + Error string `json:"error"` + Email string `json:"email"` + Time int64 `json:"time"` +} + +type Worker struct { + id int64 // 任务ID + db *gorm.DB // 数据库连接 + ctx context.Context // 上下文 + sender Sender // 邮件发送器接口 + status uint8 // 任务状态,0 表示未运行,1 表示运行中 2 表示已完成 +} + +func NewWorker(ctx context.Context, id int64, db *gorm.DB, sender Sender) *Worker { + return &Worker{ + id: id, + db: db, + ctx: ctx, + sender: sender, + } +} + +// GetID 获取Worker的任务ID +func (w *Worker) GetID() int64 { + return w.id +} + +// IsRunning 检查Worker是否正在运行 +func (w *Worker) IsRunning() uint8 { + return w.status +} + +// Start 启动Worker,开始处理任务 +func (w *Worker) Start() { + // 检查并发限制 + limit.Lock() + defer limit.Unlock() + tx := w.db.WithContext(w.ctx) + var taskInfo task.Task + if err := tx.Model(&task.Task{}).Where("id = ?", w.id).First(&taskInfo).Error; err != nil { + logger.Error("Batch Send Email", + logger.Field("message", "Failed to find task"), + logger.Field("error", err.Error()), + logger.Field("task_id", w.id), + ) + return + } + if taskInfo.Status != 0 { + logger.Error("Batch Send Email", + logger.Field("message", "Task already completed or in progress"), + logger.Field("task_id", w.id), + ) + return + } + + var scope task.EmailScope + if err := json.Unmarshal([]byte(taskInfo.Scope), &scope); err != nil { + logger.Error("Batch Send Email", + logger.Field("message", "Failed to parse task scope"), + logger.Field("error", err.Error()), + logger.Field("task_id", w.id), + ) + return + } + + if len(scope.Recipients) == 0 && len(scope.Additional) == 0 { + logger.Error("Batch Send Email", + logger.Field("message", "No recipients or additional emails provided"), + logger.Field("task_id", w.id), + ) + return + } + + var content task.EmailContent + if err := json.Unmarshal([]byte(taskInfo.Content), &content); err != nil { + logger.Error("Batch Send Email", + logger.Field("message", "Failed to parse task content"), + logger.Field("error", err.Error()), + logger.Field("task_id", w.id), + ) + return + } + + w.status = 1 // 设置状态为运行中 + var recipients []string + // 解析收件人 + if len(scope.Recipients) > 0 { + recipients = append(recipients, scope.Recipients...) + } + // 解析附加收件人 + if len(scope.Additional) > 0 { + recipients = append(recipients, scope.Additional...) + } + // 去重和清理空字符串 + recipients = tool.RemoveDuplicateElements(recipients...) + + if len(recipients) == 0 { + logger.Error("Batch Send Email", + logger.Field("message", "No valid recipients found"), + logger.Field("task_id", w.id), + ) + w.status = 2 // 设置状态为已完成 + return + } + + // 设置发送间隔时间 + var intervalTime time.Duration + if scope.Interval == 0 { + intervalTime = 1 * time.Second + } else { + intervalTime = time.Duration(scope.Interval) * time.Second + } + + var errors []ErrorInfo + var count uint64 + for _, recipient := range recipients { + select { + case <-w.ctx.Done(): + logger.Info("Batch Send Email", + logger.Field("message", "Worker stopped by context cancellation"), + logger.Field("task_id", w.id), + ) + return + default: + } + if taskInfo.Status == 0 { + taskInfo.Status = 1 // 1 表示任务进行中 + } + + if err := w.sender.Send([]string{recipient}, content.Subject, content.Content); err != nil { + logger.Error("Batch Send Email", + logger.Field("message", "Failed to send email"), + logger.Field("error", err.Error()), + logger.Field("recipient", recipient), + logger.Field("task_id", w.id), + ) + errors = append(errors, ErrorInfo{ + Error: err.Error(), + Email: recipient, + Time: time.Now().Unix(), + }) + text, _ := json.Marshal(errors) + taskInfo.Errors = string(text) + } + count++ + taskInfo.Current = count + if err := tx.Model(&task.Task{}).Where("`id` = ?", taskInfo.Id).Save(&taskInfo).Error; err != nil { + logger.Error("Batch Send Email", + logger.Field("message", "Failed to update task progress"), + logger.Field("error", err.Error()), + logger.Field("task_id", w.id), + ) + errors = append(errors, ErrorInfo{ + Error: err.Error(), + Email: recipient, + Time: time.Now().Unix(), + }) + w.status = 2 // 设置状态为已完成 + } + time.Sleep(intervalTime) + } + taskInfo.Status = 2 // 2 表示任务已完成 + w.status = 2 // 设置状态为已完成 + + if err := tx.Model(&task.Task{}).Where("`id` = ?", taskInfo.Id).Save(&taskInfo).Error; err != nil { + logger.Error("Batch Send Email", + logger.Field("message", "Failed to finalize task"), + logger.Field("error", err.Error()), + logger.Field("task_id", w.id), + ) + } else { + logger.Info("Batch Send Email", + logger.Field("message", "Task completed successfully"), + logger.Field("task_id", w.id), + logger.Field("total_sent", count), + ) + } +} diff --git a/pkg/fs/temps.go b/pkg/fs/temps.go index f1ead01..a7ad7b3 100644 --- a/pkg/fs/temps.go +++ b/pkg/fs/temps.go @@ -3,7 +3,7 @@ package fs import ( "os" - "github.com/perfect-panel/ppanel-server/pkg/hash" + "github.com/perfect-panel/server/pkg/hash" ) // TempFileWithText creates the temporary file with the given content, diff --git a/pkg/hash/consistenthash.go b/pkg/hash/consistenthash.go index 0e67126..6aae91d 100644 --- a/pkg/hash/consistenthash.go +++ b/pkg/hash/consistenthash.go @@ -6,7 +6,7 @@ import ( "strconv" "sync" - "github.com/perfect-panel/ppanel-server/pkg/lang" + "github.com/perfect-panel/server/pkg/lang" ) const ( diff --git a/pkg/limit/tokenlimit.go b/pkg/limit/tokenlimit.go index ad42241..19d9ed0 100644 --- a/pkg/limit/tokenlimit.go +++ b/pkg/limit/tokenlimit.go @@ -10,8 +10,8 @@ import ( "sync/atomic" "time" - "github.com/perfect-panel/ppanel-server/pkg/errorx" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/errorx" + "github.com/perfect-panel/server/pkg/logger" "github.com/redis/go-redis/v9" xrate "golang.org/x/time/rate" ) diff --git a/pkg/logger/color.go b/pkg/logger/color.go index fa33495..c56977d 100644 --- a/pkg/logger/color.go +++ b/pkg/logger/color.go @@ -3,7 +3,7 @@ package logger import ( "sync/atomic" - "github.com/perfect-panel/ppanel-server/pkg/color" + "github.com/perfect-panel/server/pkg/color" ) // WithColor is a helper function to add color to a string, only in plain encoding. diff --git a/pkg/logger/color_test.go b/pkg/logger/color_test.go index b6de740..b2bb45d 100644 --- a/pkg/logger/color_test.go +++ b/pkg/logger/color_test.go @@ -4,7 +4,7 @@ import ( "sync/atomic" "testing" - "github.com/perfect-panel/ppanel-server/pkg/color" + "github.com/perfect-panel/server/pkg/color" "github.com/stretchr/testify/assert" ) diff --git a/pkg/logger/config.go b/pkg/logger/config.go index 8f75b02..dceb82d 100644 --- a/pkg/logger/config.go +++ b/pkg/logger/config.go @@ -8,13 +8,13 @@ type LogConf struct { // console: log to console. // file: log to file. // volume: used in k8s, prepend the hostname to the log file name. - Mode string `yaml:"Mode" default:"console"` + Mode string `yaml:"Mode" default:"file"` // Encoding represents the encoding type, default is `json`. // json: json encoding. // plain: plain text encoding, typically used in development. Encoding string `yaml:"Encoding" default:"json"` // TimeFormat represents the time format, default is `2006-01-02T15:04:05.000Z07:00`. - TimeFormat string `yaml:"TimeFormat" default:"2006-01-02T15:04:05.000Z07:00"` + TimeFormat string `yaml:"TimeFormat" default:"2006-01-02 15:04:05.000"` // Path represents the log file path, default is `logs`. Path string `yaml:"Path" default:"logs"` // Level represents the log level, default is `info`. diff --git a/pkg/logger/limitedexecutor.go b/pkg/logger/limitedexecutor.go index 6db3271..cec494c 100644 --- a/pkg/logger/limitedexecutor.go +++ b/pkg/logger/limitedexecutor.go @@ -4,8 +4,8 @@ import ( "sync/atomic" "time" - "github.com/perfect-panel/ppanel-server/pkg/syncx" - "github.com/perfect-panel/ppanel-server/pkg/timex" + "github.com/perfect-panel/server/pkg/syncx" + "github.com/perfect-panel/server/pkg/timex" ) type limitedExecutor struct { diff --git a/pkg/logger/limitedexecutor_test.go b/pkg/logger/limitedexecutor_test.go index 34af915..eb365e9 100644 --- a/pkg/logger/limitedexecutor_test.go +++ b/pkg/logger/limitedexecutor_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/perfect-panel/ppanel-server/pkg/timex" + "github.com/perfect-panel/server/pkg/timex" "github.com/stretchr/testify/assert" ) diff --git a/pkg/logger/logtest/logtest.go b/pkg/logger/logtest/logtest.go index 779ab22..4f7c03f 100644 --- a/pkg/logger/logtest/logtest.go +++ b/pkg/logger/logtest/logtest.go @@ -6,7 +6,7 @@ import ( "io" "testing" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" ) type Buffer struct { diff --git a/pkg/logger/logtest/logtest_test.go b/pkg/logger/logtest/logtest_test.go index e98aed3..953fec2 100644 --- a/pkg/logger/logtest/logtest_test.go +++ b/pkg/logger/logtest/logtest_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "github.com/stretchr/testify/assert" ) diff --git a/pkg/logger/read.go b/pkg/logger/read.go new file mode 100644 index 0000000..fc81e56 --- /dev/null +++ b/pkg/logger/read.go @@ -0,0 +1,98 @@ +package logger + +import ( + "bufio" + "fmt" + "io" + "os" +) + +func ReadLastNLines(path string, n int) ([]string, error) { + // Open the file + file, err := os.Open(fmt.Sprintf("%s/%s", path, accessFilename)) + if err != nil { + return nil, err + } + defer file.Close() + + // Get file size + fileInfo, err := file.Stat() + if err != nil { + return nil, err + } + fileSize := fileInfo.Size() + + // If file is empty, return empty slice + if fileSize == 0 { + return []string{}, nil + } + + // Buffer for reading + bufferSize := int64(4096) + if bufferSize > fileSize { + bufferSize = fileSize + } + buffer := make([]byte, bufferSize) + + // Start reading from the end + position := fileSize + lines := make([]string, 0, n) + lineCount := 0 + + for lineCount < n && position > 0 { + // How much to read + readSize := bufferSize + if position < bufferSize { + readSize = position + } + position -= readSize + + // Read chunk from position + _, err := file.Seek(position, io.SeekStart) + if err != nil { + return nil, err + } + + _, err = file.Read(buffer[:readSize]) + if err != nil { + return nil, err + } + + // Count newlines in reverse + for i := readSize - 1; i >= 0; i-- { + if buffer[i] == '\n' { + lineCount++ + if lineCount > n { + // We found more than n lines + // Need to adjust position to read only last n lines + position += int64(i) + 1 + break + } + } + } + } + + // If we couldn't find n lines, start from beginning + if position < 0 { + position = 0 + } + + // Seek to the position where we want to start reading + _, err = file.Seek(position, io.SeekStart) + if err != nil { + return nil, err + } + + // Read lines from position to end + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + // Check if we need to trim + if len(lines) > n { + lines = lines[len(lines)-n:] + } + + return lines, scanner.Err() +} diff --git a/pkg/logger/read_test.go b/pkg/logger/read_test.go new file mode 100644 index 0000000..527d32d --- /dev/null +++ b/pkg/logger/read_test.go @@ -0,0 +1,16 @@ +package logger + +import ( + "testing" +) + +func TestReadLastNLines(t *testing.T) { + t.Skipf("skip this test until this test fails") + lines, err := ReadLastNLines("/Users/tension/code/ppanel/server/logs", 10) + if err != nil { + t.Fatalf("Error reading last N lines: %v", err) + } + for i, line := range lines { + t.Logf("Line %d: %s", i, line) + } +} diff --git a/pkg/logger/richlogger.go b/pkg/logger/richlogger.go index a949510..510a4a0 100644 --- a/pkg/logger/richlogger.go +++ b/pkg/logger/richlogger.go @@ -5,9 +5,9 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/internal/trace" + "github.com/perfect-panel/server/internal/trace" - "github.com/perfect-panel/ppanel-server/pkg/timex" + "github.com/perfect-panel/server/pkg/timex" ) // WithCallerSkip returns a Logger with given caller skip. diff --git a/pkg/logger/rotatelogger.go b/pkg/logger/rotatelogger.go index 94411f6..04dffb0 100644 --- a/pkg/logger/rotatelogger.go +++ b/pkg/logger/rotatelogger.go @@ -13,8 +13,8 @@ import ( "sync" "time" - "github.com/perfect-panel/ppanel-server/pkg/fs" - "github.com/perfect-panel/ppanel-server/pkg/lang" + "github.com/perfect-panel/server/pkg/fs" + "github.com/perfect-panel/server/pkg/lang" ) const ( diff --git a/pkg/logger/rotatelogger_test.go b/pkg/logger/rotatelogger_test.go index 763dce4..c2b5be1 100644 --- a/pkg/logger/rotatelogger_test.go +++ b/pkg/logger/rotatelogger_test.go @@ -11,9 +11,9 @@ import ( "testing" "time" - "github.com/perfect-panel/ppanel-server/pkg/random" + "github.com/perfect-panel/server/pkg/random" - "github.com/perfect-panel/ppanel-server/pkg/fs" + "github.com/perfect-panel/server/pkg/fs" "github.com/stretchr/testify/assert" ) diff --git a/pkg/logger/vars.go b/pkg/logger/vars.go index f43c526..f23f083 100644 --- a/pkg/logger/vars.go +++ b/pkg/logger/vars.go @@ -3,7 +3,7 @@ package logger import ( "errors" - "github.com/perfect-panel/ppanel-server/pkg/syncx" + "github.com/perfect-panel/server/pkg/syncx" ) const ( diff --git a/pkg/logger/writer.go b/pkg/logger/writer.go index 4f7f97d..6d7658b 100644 --- a/pkg/logger/writer.go +++ b/pkg/logger/writer.go @@ -12,8 +12,8 @@ import ( "sync/atomic" fatihcolor "github.com/fatih/color" - "github.com/perfect-panel/ppanel-server/pkg/color" - "github.com/perfect-panel/ppanel-server/pkg/errorx" + "github.com/perfect-panel/server/pkg/color" + "github.com/perfect-panel/server/pkg/errorx" ) type ( diff --git a/pkg/nodeMultiplier/manage_test.go b/pkg/nodeMultiplier/manage_test.go index 789dae9..881db55 100644 --- a/pkg/nodeMultiplier/manage_test.go +++ b/pkg/nodeMultiplier/manage_test.go @@ -8,10 +8,15 @@ import ( func TestNewNodeMultiplierManager(t *testing.T) { periods := []TimePeriod{ { - StartTime: "23:00", - EndTime: "1:59", + StartTime: "23:00.000", + EndTime: "1:59.000", Multiplier: 1.2, }, + { + StartTime: "12:00.000", + EndTime: "13:59.000", + Multiplier: 0.5, + }, } m := NewNodeMultiplierManager(periods) if len(m.Periods) != 1 { diff --git a/pkg/nodeMultiplier/manager.go b/pkg/nodeMultiplier/manager.go index 7f9e687..7cb6d02 100644 --- a/pkg/nodeMultiplier/manager.go +++ b/pkg/nodeMultiplier/manager.go @@ -28,8 +28,8 @@ func (m *Manager) GetMultiplier(current time.Time) float32 { } func (m *Manager) isInTimePeriod(current time.Time, start, end string) bool { - startTime, _ := time.Parse("15:04", start) - endTime, _ := time.Parse("15:04", end) + startTime, _ := time.Parse("15:04.000", start) + endTime, _ := time.Parse("15:04.000", end) currentTime := time.Date(0, 1, 1, current.Hour(), current.Minute(), 0, 0, time.UTC) startTimeFormatted := time.Date(0, 1, 1, startTime.Hour(), startTime.Minute(), 0, 0, time.UTC) diff --git a/pkg/oauth/apple/apple.html b/pkg/oauth/apple/apple.html index aa77fc1..2bfe37d 100644 --- a/pkg/oauth/apple/apple.html +++ b/pkg/oauth/apple/apple.html @@ -11,7 +11,7 @@ AppleID.auth.init({ clientId: 'web.jiashus.com', // 替换为你的服务 ID scope: 'name email', // 可选,授权范围 - redirectURI: 'https://test.muran.org:8443/auth/apple/callback', // 替换为你的回调 URL + redirectURI: 'https://test.ppanel.dev:8443/auth/apple/callback', // 替换为你的回调 URL state: 'optional-csrf-token', // 可选 usePopup: false // 可选,是否使用弹窗方式 }); diff --git a/pkg/oauth/apple/apple_test.go b/pkg/oauth/apple/apple_test.go index 511bc6f..eec19e5 100644 --- a/pkg/oauth/apple/apple_test.go +++ b/pkg/oauth/apple/apple_test.go @@ -39,7 +39,7 @@ func handleAppleCallBack(ctx context.Context, request CallbackRequest) { ClientID: ClientID, KeyID: KeyID, ClientSecret: ClientSecret, - RedirectURI: "https://test.muran.org:8443/auth/apple/callback", + RedirectURI: "https://test.ppanel.dev:8443/auth/apple/callback", }) if err != nil { fmt.Println("error creating apple client: " + err.Error()) diff --git a/pkg/oauth/google/google.go b/pkg/oauth/google/google.go index 1d5e724..e277b3f 100644 --- a/pkg/oauth/google/google.go +++ b/pkg/oauth/google/google.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "golang.org/x/oauth2" "golang.org/x/oauth2/google" ) diff --git a/pkg/orm/mysql.go b/pkg/orm/mysql.go index 8f7584f..07920e4 100644 --- a/pkg/orm/mysql.go +++ b/pkg/orm/mysql.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "gorm.io/driver/mysql" "gorm.io/gorm" diff --git a/pkg/orm/tool_test.go b/pkg/orm/tool_test.go index 6bdb5cd..d415bbd 100644 --- a/pkg/orm/tool_test.go +++ b/pkg/orm/tool_test.go @@ -1,6 +1,13 @@ package orm -import "testing" +import ( + "testing" + + "github.com/perfect-panel/server/internal/model/task" + + "gorm.io/driver/mysql" + "gorm.io/gorm" +) func TestParseDSN(t *testing.T) { dsn := "root:mylove520@tcp(localhost:3306)/vpnboard" @@ -16,3 +23,18 @@ func TestPing(t *testing.T) { status := Ping(dsn) t.Log(status) } + +func TestMysql(t *testing.T) { + db, err := gorm.Open(mysql.New(mysql.Config{ + DSN: "root:mylove520@tcp(localhost:3306)/vpnboard", + })) + if err != nil { + t.Fatalf("Failed to connect to MySQL: %v", err) + } + err = db.Migrator().AutoMigrate(&task.Task{}) + if err != nil { + t.Fatalf("Failed to auto migrate: %v", err) + return + } + t.Log("MySQL connection and migration successful") +} diff --git a/pkg/payment/alipay/alipay.go b/pkg/payment/alipay/alipay.go index ad6c40e..db34a22 100644 --- a/pkg/payment/alipay/alipay.go +++ b/pkg/payment/alipay/alipay.go @@ -4,8 +4,8 @@ import ( "context" "net/url" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" "github.com/pkg/errors" "github.com/smartwalle/alipay/v3" ) diff --git a/pkg/payment/epay/epay.go b/pkg/payment/epay/epay.go index 1a7e110..ae1315e 100644 --- a/pkg/payment/epay/epay.go +++ b/pkg/payment/epay/epay.go @@ -9,8 +9,8 @@ import ( "strings" "time" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" ) type Client struct { diff --git a/pkg/payment/payssion/payssion.go b/pkg/payment/payssion/payssion.go deleted file mode 100644 index fa30293..0000000 --- a/pkg/payment/payssion/payssion.go +++ /dev/null @@ -1,127 +0,0 @@ -package payssion - -import ( - "bytes" - "encoding/json" - "fmt" - "github.com/perfect-panel/ppanel-server/pkg/md5" - "github.com/pkg/errors" - "go.uber.org/zap" - "io" - "log" - "net/http" -) - -type Order struct { - Name string - OrderNo string - Amount float64 - NotifyUrl string - ReturnUrl string -} - -type Client struct { - Name string - ApiKey string - SecretKey string - QueryUrl string - CreateUrl string - PmId string - Currency string -} - -func NewClient(apiKey string, secretKey, pmId, currency, queryUrl, createUrl string) *Client { - return &Client{ - ApiKey: apiKey, - SecretKey: secretKey, - PmId: pmId, - Currency: currency, - QueryUrl: queryUrl, - CreateUrl: createUrl, - } -} - -func (c *Client) CreateOrder(order Order) (string, error) { - content := fmt.Sprintf("%s|%s|%.2f|%s|%s|%s", c.ApiKey, c.PmId, order.Amount, "USD", order.OrderNo, c.SecretKey) - sign := md5.Sign(content) - params := map[string]string{ - "api_key": c.ApiKey, - "pm_id": c.PmId, - "amount": fmt.Sprintf("%.2f", order.Amount), - "currency": "USD", - "description": "shop", - "order_id": order.OrderNo, - "api_sig": sign, - "return_url": order.ReturnUrl, - } - marshal, _ := json.Marshal(params) - resp, err := http.Post(c.CreateUrl, "application/json", bytes.NewBuffer(marshal)) - if err != nil { - return "", err - } - all, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - log.Println(string(all)) - result := make(map[string]interface{}) - err = json.Unmarshal(all, &result) - if err != nil { - return "", err - } - result_code := result["result_code"] - if fmt.Sprintf("%v", result_code) != "200" { - return "", errors.New(result["description"].(string)) - } - url := result["redirect_url"] - if url == nil { - return "", errors.New(string(all)) - } - return url.(string), nil -} - -func (c *Client) QueryOrder(orderNo string) (queryResult *QueryResult, err error) { - content := fmt.Sprintf("%s|%s|%s", c.ApiKey, orderNo, c.SecretKey) - sign := md5.Sign(content) - params := map[string]string{ - "api_key": c.ApiKey, - "order_id": orderNo, - "api_sig": sign, - } - marshal, _ := json.Marshal(params) - resp, err := http.Post(c.QueryUrl, "application/json", bytes.NewBuffer(marshal)) - if err != nil { - return nil, err - } - all, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - zap.S().Infof("Payssion QueryOrderDetail result: %s", string(all)) - err = json.Unmarshal(all, &queryResult) - return queryResult, err -} - -type QueryResult struct { - Transaction struct { - TransactionID string `json:"transaction_id"` - Description string `json:"description"` - AppName string `json:"app_name"` - PmID string `json:"pm_id"` - Amount string `json:"amount"` - Currency string `json:"currency"` - OrderID string `json:"order_id"` - Paid string `json:"paid"` - Net string `json:"net"` - State string `json:"state"` - Fee string `json:"fee"` - Refund string `json:"refund"` - RefundFee string `json:"refund_fee"` - Created int64 `json:"created"` - Updated int64 `json:"updated"` - Fees string `json:"fees"` - FeesAdd string `json:"fees_add"` - RefundFees string `json:"refund_fees"` - } `json:"transaction"` - ResultCode int64 `json:"result_code"` -} diff --git a/pkg/payment/payssion/payssion_test.go b/pkg/payment/payssion/payssion_test.go deleted file mode 100644 index 8f5ae34..0000000 --- a/pkg/payment/payssion/payssion_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package payssion - -import ( - "fmt" - "testing" -) - -func TestCreateOrder(t *testing.T) { - client := Client{ - ApiKey: "", - PmId: "", - SecretKey: "", - QueryUrl: "http://sandbox.payssion.com/api/v1/payment/getDetail", - CreateUrl: "http://sandbox.payssion.com/api/v1/payments", - } - order := Order{ - Name: "shop", - OrderNo: "123", - Amount: 1000, - } - createOrder, err := client.CreateOrder(order) - if err != nil { - t.Error(err) - return - } - fmt.Println(createOrder) - queryOrder, err := client.QueryOrder(order.OrderNo) - if err != nil { - t.Error(err) - } - fmt.Println(queryOrder.Transaction.State) -} diff --git a/pkg/payment/platform.go b/pkg/payment/platform.go index b749c83..42b8815 100644 --- a/pkg/payment/platform.go +++ b/pkg/payment/platform.go @@ -1,6 +1,6 @@ package payment -import "github.com/perfect-panel/ppanel-server/internal/types" +import "github.com/perfect-panel/server/internal/types" type Platform int @@ -9,15 +9,15 @@ const ( AlipayF2F EPay Balance - Payssion - UNSUPPORTED + CryptoSaaS + UNSUPPORTED Platform = -1 ) var platformNames = map[string]Platform{ + "CryptoSaaS": CryptoSaaS, "Stripe": Stripe, "AlipayF2F": AlipayF2F, "EPay": EPay, - "Payssion": Payssion, "balance": Balance, "unsupported": UNSUPPORTED, } @@ -71,15 +71,12 @@ func GetSupportedPlatforms() []types.PlatformInfo { }, }, { - Platform: Payssion.String(), - PlatformUrl: "", + Platform: CryptoSaaS.String(), + PlatformUrl: "https://t.me/CryptoSaaSBot", PlatformFieldDescription: map[string]string{ - "api_key": "api_key", - "secret_key": "secret_key", - "pm_id": "pm_id", - "currency": "currency", - "create_url": "Create URL", - "query_url": "Query URL", + "endpoint": "API Endpoint", + "account_id": "Account ID", + "secret_key": "Secret Key", }, }, } diff --git a/pkg/payment/stripe/stripe.go b/pkg/payment/stripe/stripe.go index 4bd5080..d0f4af6 100644 --- a/pkg/payment/stripe/stripe.go +++ b/pkg/payment/stripe/stripe.go @@ -5,7 +5,9 @@ import ( "fmt" "strconv" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/stripe/stripe-go/v81/webhookendpoint" + + "github.com/perfect-panel/server/pkg/logger" "github.com/stripe/stripe-go/v81" "github.com/stripe/stripe-go/v81/customer" "github.com/stripe/stripe-go/v81/ephemeralkey" @@ -193,3 +195,16 @@ func (c *Client) RetrievePaymentMethod(id string) (*stripe.PaymentMethod, error) stripe.Key = c.SecretKey return paymentmethod.Get(id, nil) } + +// CreateWebhookEndpoint 创建 webhook endpoint +func (c *Client) CreateWebhookEndpoint(url string) (*stripe.WebhookEndpoint, error) { + stripe.Key = c.SecretKey + params := &stripe.WebhookEndpointParams{ + URL: stripe.String(url), + EnabledEvents: []*string{ + stripe.String("payment_intent.succeeded"), + stripe.String("payment_intent.payment_failed"), + }, + } + return webhookendpoint.New(params) +} diff --git a/pkg/payment/stripe/stripe_test.go b/pkg/payment/stripe/stripe_test.go index 872b9e9..419b758 100644 --- a/pkg/payment/stripe/stripe_test.go +++ b/pkg/payment/stripe/stripe_test.go @@ -20,7 +20,7 @@ func TestStripeAlipay(t *testing.T) { } user := User{ UserId: 1, - Email: "tension@muran.org", + Email: "tension@ppanel.dev", } result, err := client.CreatePaymentSheet(&order, &user) if err != nil { @@ -45,7 +45,7 @@ func TestStripeWechat(t *testing.T) { } user := User{ UserId: 1, - Email: "tension@muran.org", + Email: "tension@ppanel.dev", } result, err := client.CreatePaymentSheet(&order, &user) if err != nil { diff --git a/pkg/proc/shutdown.go b/pkg/proc/shutdown.go index d08b466..c2283a3 100644 --- a/pkg/proc/shutdown.go +++ b/pkg/proc/shutdown.go @@ -9,7 +9,7 @@ import ( "syscall" "time" - "github.com/perfect-panel/ppanel-server/pkg/threading" + "github.com/perfect-panel/server/pkg/threading" ) const ( diff --git a/pkg/random/RandomKey_test.go b/pkg/random/RandomKey_test.go index a5f23fd..244054d 100644 --- a/pkg/random/RandomKey_test.go +++ b/pkg/random/RandomKey_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/perfect-panel/ppanel-server/pkg/snowflake" + "github.com/perfect-panel/server/pkg/snowflake" "github.com/stretchr/testify/assert" ) diff --git a/pkg/rescue/recover.go b/pkg/rescue/recover.go index 3138cb6..8e6ffd8 100644 --- a/pkg/rescue/recover.go +++ b/pkg/rescue/recover.go @@ -5,7 +5,7 @@ import ( "log" "runtime/debug" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" ) // Recover is used with defer to do cleanup on panics. diff --git a/pkg/result/httpResult.go b/pkg/result/httpResult.go index 728115a..f5a125a 100644 --- a/pkg/result/httpResult.go +++ b/pkg/result/httpResult.go @@ -6,7 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/pkg/errors" - "github.com/perfect-panel/ppanel-server/pkg/xerr" + "github.com/perfect-panel/server/pkg/xerr" ) // HttpResult HTTP Result diff --git a/pkg/service/service.go b/pkg/service/service.go index 668975b..58a7d94 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -3,8 +3,8 @@ package service import ( "sync" - "github.com/perfect-panel/ppanel-server/pkg/proc" - "github.com/perfect-panel/ppanel-server/pkg/threading" + "github.com/perfect-panel/server/pkg/proc" + "github.com/perfect-panel/server/pkg/threading" ) type ( diff --git a/pkg/service/servicegroup_test.go b/pkg/service/servicegroup_test.go index 8ade90a..aaf683e 100644 --- a/pkg/service/servicegroup_test.go +++ b/pkg/service/servicegroup_test.go @@ -4,7 +4,7 @@ import ( "sync" "testing" - "github.com/perfect-panel/ppanel-server/pkg/proc" + "github.com/perfect-panel/server/pkg/proc" "github.com/stretchr/testify/assert" ) diff --git a/pkg/sms/abosend/abosend.go b/pkg/sms/abosend/abosend.go index 4701b79..1291bf0 100644 --- a/pkg/sms/abosend/abosend.go +++ b/pkg/sms/abosend/abosend.go @@ -5,9 +5,9 @@ import ( "fmt" "github.com/go-resty/resty/v2" - "github.com/perfect-panel/ppanel-server/pkg/random" - "github.com/perfect-panel/ppanel-server/pkg/templatex" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/pkg/random" + "github.com/perfect-panel/server/pkg/templatex" + "github.com/perfect-panel/server/pkg/tool" ) const BaseURL = "https://smsapi.abosend.com" diff --git a/pkg/sms/alibabacloud/alibabacloud.go b/pkg/sms/alibabacloud/alibabacloud.go index 992772f..03e71f4 100644 --- a/pkg/sms/alibabacloud/alibabacloud.go +++ b/pkg/sms/alibabacloud/alibabacloud.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" openapi "github.com/alibabacloud-go/darabonba-openapi/client" dysmsapi "github.com/alibabacloud-go/dysmsapi-20170525/v2/client" diff --git a/pkg/sms/platform.go b/pkg/sms/platform.go index ef22cde..f06a7b9 100644 --- a/pkg/sms/platform.go +++ b/pkg/sms/platform.go @@ -1,6 +1,6 @@ package sms -import "github.com/perfect-panel/ppanel-server/internal/types" +import "github.com/perfect-panel/server/internal/types" type Platform int diff --git a/pkg/sms/sender.go b/pkg/sms/sender.go index 30c2a72..a46e34f 100644 --- a/pkg/sms/sender.go +++ b/pkg/sms/sender.go @@ -5,10 +5,10 @@ import ( "fmt" "log" - "github.com/perfect-panel/ppanel-server/pkg/sms/abosend" - "github.com/perfect-panel/ppanel-server/pkg/sms/alibabacloud" - "github.com/perfect-panel/ppanel-server/pkg/sms/smsbao" - "github.com/perfect-panel/ppanel-server/pkg/sms/twilio" + "github.com/perfect-panel/server/pkg/sms/abosend" + "github.com/perfect-panel/server/pkg/sms/alibabacloud" + "github.com/perfect-panel/server/pkg/sms/smsbao" + "github.com/perfect-panel/server/pkg/sms/twilio" ) type Sender interface { diff --git a/pkg/sms/smsbao/smsbao.go b/pkg/sms/smsbao/smsbao.go index 20a9f4b..0cff2c7 100644 --- a/pkg/sms/smsbao/smsbao.go +++ b/pkg/sms/smsbao/smsbao.go @@ -4,8 +4,8 @@ import ( "fmt" "github.com/go-resty/resty/v2" - "github.com/perfect-panel/ppanel-server/pkg/templatex" - "github.com/perfect-panel/ppanel-server/pkg/tool" + "github.com/perfect-panel/server/pkg/templatex" + "github.com/perfect-panel/server/pkg/tool" ) const BaseURL = "https://api.smsbao.com" diff --git a/pkg/sms/twilio/twilio.go b/pkg/sms/twilio/twilio.go index bb5252d..51ea6a3 100644 --- a/pkg/sms/twilio/twilio.go +++ b/pkg/sms/twilio/twilio.go @@ -3,8 +3,8 @@ package twilio import ( "fmt" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/templatex" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/templatex" "github.com/twilio/twilio-go" twilioApi "github.com/twilio/twilio-go/rest/api/v2010" ) diff --git a/pkg/syncx/cond.go b/pkg/syncx/cond.go index 3c82843..e35e455 100644 --- a/pkg/syncx/cond.go +++ b/pkg/syncx/cond.go @@ -3,8 +3,8 @@ package syncx import ( "time" - "github.com/perfect-panel/ppanel-server/pkg/lang" - "github.com/perfect-panel/ppanel-server/pkg/timex" + "github.com/perfect-panel/server/pkg/lang" + "github.com/perfect-panel/server/pkg/timex" ) // A Cond is used to wait for conditions. diff --git a/pkg/syncx/donechan.go b/pkg/syncx/donechan.go index c748f53..ebe7ef4 100644 --- a/pkg/syncx/donechan.go +++ b/pkg/syncx/donechan.go @@ -3,7 +3,7 @@ package syncx import ( "sync" - "github.com/perfect-panel/ppanel-server/pkg/lang" + "github.com/perfect-panel/server/pkg/lang" ) // A DoneChan is used as a channel that can be closed multiple times and wait for done. diff --git a/pkg/syncx/immutableresource.go b/pkg/syncx/immutableresource.go index d207d07..8122c44 100644 --- a/pkg/syncx/immutableresource.go +++ b/pkg/syncx/immutableresource.go @@ -4,7 +4,7 @@ import ( "sync" "time" - "github.com/perfect-panel/ppanel-server/pkg/timex" + "github.com/perfect-panel/server/pkg/timex" ) const defaultRefreshInterval = time.Second diff --git a/pkg/syncx/limit.go b/pkg/syncx/limit.go index 5c6ce9e..d3209ba 100644 --- a/pkg/syncx/limit.go +++ b/pkg/syncx/limit.go @@ -3,7 +3,7 @@ package syncx import ( "errors" - "github.com/perfect-panel/ppanel-server/pkg/lang" + "github.com/perfect-panel/server/pkg/lang" ) // ErrLimitReturn indicates that the more than borrowed elements were returned. diff --git a/pkg/syncx/pool.go b/pkg/syncx/pool.go index 2bccacb..489b98c 100644 --- a/pkg/syncx/pool.go +++ b/pkg/syncx/pool.go @@ -4,7 +4,7 @@ import ( "sync" "time" - "github.com/perfect-panel/ppanel-server/pkg/timex" + "github.com/perfect-panel/server/pkg/timex" ) type ( diff --git a/pkg/syncx/pool_test.go b/pkg/syncx/pool_test.go index 35ab59d..5bdb47c 100644 --- a/pkg/syncx/pool_test.go +++ b/pkg/syncx/pool_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/perfect-panel/ppanel-server/pkg/lang" + "github.com/perfect-panel/server/pkg/lang" "github.com/stretchr/testify/assert" ) diff --git a/pkg/syncx/resourcemanager.go b/pkg/syncx/resourcemanager.go index ae4ae47..0c8dc5b 100644 --- a/pkg/syncx/resourcemanager.go +++ b/pkg/syncx/resourcemanager.go @@ -4,7 +4,7 @@ import ( "io" "sync" - "github.com/perfect-panel/ppanel-server/pkg/errorx" + "github.com/perfect-panel/server/pkg/errorx" ) // A ResourceManager is a manager that used to manage resources. diff --git a/pkg/syncx/spinlock_test.go b/pkg/syncx/spinlock_test.go index 026617c..9bcff23 100644 --- a/pkg/syncx/spinlock_test.go +++ b/pkg/syncx/spinlock_test.go @@ -7,7 +7,7 @@ import ( "testing" "time" - "github.com/perfect-panel/ppanel-server/pkg/lang" + "github.com/perfect-panel/server/pkg/lang" "github.com/stretchr/testify/assert" ) diff --git a/pkg/threading/routines.go b/pkg/threading/routines.go index f7508a9..58352c7 100644 --- a/pkg/threading/routines.go +++ b/pkg/threading/routines.go @@ -6,7 +6,7 @@ import ( "runtime" "strconv" - "github.com/perfect-panel/ppanel-server/pkg/rescue" + "github.com/perfect-panel/server/pkg/rescue" ) // GoSafe runs the given fn using another goroutine, recovers if fn panics. diff --git a/pkg/timex/ticker.go b/pkg/timex/ticker.go index c0ddf96..df4913f 100644 --- a/pkg/timex/ticker.go +++ b/pkg/timex/ticker.go @@ -4,7 +4,7 @@ import ( "errors" "time" - "github.com/perfect-panel/ppanel-server/pkg/lang" + "github.com/perfect-panel/server/pkg/lang" ) // errTimeout indicates a timeout. diff --git a/pkg/tool/cipher_test.go b/pkg/tool/cipher_test.go index fb0f592..4bfc07e 100644 --- a/pkg/tool/cipher_test.go +++ b/pkg/tool/cipher_test.go @@ -5,6 +5,7 @@ import ( ) func TestGenerateCipher(t *testing.T) { - pwd := GenerateCipher("serverKey", 128) + pwd := GenerateCipher("", 16) t.Logf("pwd: %s", pwd) + t.Logf("pwd length: %d", len(pwd)) } diff --git a/pkg/tool/convert.go b/pkg/tool/convert.go index fa93055..0079620 100644 --- a/pkg/tool/convert.go +++ b/pkg/tool/convert.go @@ -1,6 +1,7 @@ package tool import ( + "encoding/json" "fmt" "reflect" "strconv" @@ -26,6 +27,16 @@ func ConvertValueToString(value reflect.Value) string { default: return "" } + case reflect.Struct, reflect.Map, reflect.Slice, reflect.Array: + bytes, err := json.Marshal(value.Interface()) + if err != nil { + fmt.Println("Error marshaling struct:", err.Error()) + return "" + } + if string(bytes) == "null" { + return "" + } + return string(bytes) default: return "" } diff --git a/pkg/tool/copy.go b/pkg/tool/copy.go index 6464426..7d707c1 100644 --- a/pkg/tool/copy.go +++ b/pkg/tool/copy.go @@ -10,20 +10,31 @@ import ( "github.com/jinzhu/copier" "github.com/pkg/errors" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" ) -func DeepCopy[T, K interface{}](destStruct T, srcStruct K) T { +// CopyOption 定义复制选项的函数类型 +type CopyOption func(*copier.Option) + +// CopyWithIgnoreEmpty 设置是否忽略空值 +func CopyWithIgnoreEmpty(ignoreEmpty bool) CopyOption { + return func(o *copier.Option) { + o.IgnoreEmpty = ignoreEmpty + } +} + +func DeepCopy[T, K any](destStruct T, srcStruct K, opts ...CopyOption) T { var dst = destStruct var src = srcStruct - _ = copier.CopyWithOption(dst, src, copier.Option{ + + option := copier.Option{ DeepCopy: true, IgnoreEmpty: true, Converters: []copier.TypeConverter{ { SrcType: time.Time{}, DstType: constant.Int64, - Fn: func(src interface{}) (interface{}, error) { + Fn: func(src any) (any, error) { s, ok := src.(time.Time) if !ok { return nil, errors.New("src type not matching") @@ -32,13 +43,21 @@ func DeepCopy[T, K interface{}](destStruct T, srcStruct K) T { }, }, }, - }) + } + + for _, opt := range opts { + opt(&option) + } + + _ = copier.CopyWithOption(dst, src, option) return dst } -func ShallowCopy[T, K interface{}](destStruct T, srcStruct K) T { + +func ShallowCopy[T, K interface{}](destStruct T, srcStruct K, opts ...CopyOption) T { var dst = destStruct var src = srcStruct - _ = copier.CopyWithOption(dst, src, copier.Option{ + + option := copier.Option{ IgnoreEmpty: true, Converters: []copier.TypeConverter{ { @@ -46,7 +65,6 @@ func ShallowCopy[T, K interface{}](destStruct T, srcStruct K) T { DstType: constant.Int64, Fn: func(src interface{}) (interface{}, error) { s, ok := src.(time.Time) - if !ok { return nil, errors.New("src type not matching") } @@ -54,7 +72,13 @@ func ShallowCopy[T, K interface{}](destStruct T, srcStruct K) T { }, }, }, - }) + } + + for _, opt := range opts { + opt(&option) + } + + _ = copier.CopyWithOption(dst, src, option) return dst } diff --git a/pkg/tool/encryption_test.go b/pkg/tool/encryption_test.go index 45e0a18..8841072 100644 --- a/pkg/tool/encryption_test.go +++ b/pkg/tool/encryption_test.go @@ -3,5 +3,5 @@ package tool import "testing" func TestEncodePassWord(t *testing.T) { - t.Logf("EncodePassWord: %v", EncodePassWord("")) + t.Logf("EncodePassWord: %v", EncodePassWord("password")) } diff --git a/pkg/tool/slice.go b/pkg/tool/slice.go index 337b357..3797878 100644 --- a/pkg/tool/slice.go +++ b/pkg/tool/slice.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" ) func Int64SliceToStringSlice(slice []int64) []string { @@ -59,6 +59,7 @@ func Int64SliceToString(intSlice []int64) string { // string slice to string func StringSliceToString(stringSlice []string) string { + stringSlice = RemoveDuplicateElements(stringSlice...) return strings.Join(stringSlice, ",") } @@ -128,3 +129,14 @@ func Contains[T comparable](slice []T, target T) bool { } return false } + +// RemoveStringElement 移除指定元素 +func RemoveStringElement(arr []string, element ...string) []string { + var result []string + for _, str := range arr { + if !Contains(element, str) { + result = append(result, str) + } + } + return result +} diff --git a/pkg/tool/sliceReflectToStruct.go b/pkg/tool/sliceReflectToStruct.go index 644e91b..8458a3c 100644 --- a/pkg/tool/sliceReflectToStruct.go +++ b/pkg/tool/sliceReflectToStruct.go @@ -5,7 +5,7 @@ import ( "strconv" "github.com/goccy/go-json" - "github.com/perfect-panel/ppanel-server/internal/model/system" + "github.com/perfect-panel/server/internal/model/system" ) func SystemConfigSliceReflectToStruct(slice []*system.System, structType any) { diff --git a/pkg/tool/time.go b/pkg/tool/time.go index 67a2535..31c6883 100644 --- a/pkg/tool/time.go +++ b/pkg/tool/time.go @@ -3,7 +3,7 @@ package tool import ( "time" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" ) func AddTime(unit string, quantity int64, baseTime ...time.Time) time.Time { @@ -138,3 +138,16 @@ func YearDiff(startTime, endTime time.Time) int { } return yearDiff } + +func DayDiff(startTime, endTime time.Time) int64 { + // 计算时间差 + duration := endTime.Sub(startTime) + return int64(duration.Hours() / 24) // 转换为整天数 +} + +// HourDiff 计算两个时间点之间的小时差 +func HourDiff(startTime, endTime time.Time) int64 { + // 计算时间差 + duration := endTime.Sub(startTime) + return int64(duration.Hours()) // 返回小时数,可能包含小数部分 +} diff --git a/pkg/tool/version_test.go b/pkg/tool/version_test.go index 2878371..02d8920 100644 --- a/pkg/tool/version_test.go +++ b/pkg/tool/version_test.go @@ -3,7 +3,7 @@ package tool import ( "testing" - "github.com/perfect-panel/ppanel-server/pkg/constant" + "github.com/perfect-panel/server/pkg/constant" ) func TestExtractVersionNumber(t *testing.T) { diff --git a/pkg/trace/agent.go b/pkg/trace/agent.go index c66f8c4..a8bf870 100644 --- a/pkg/trace/agent.go +++ b/pkg/trace/agent.go @@ -18,8 +18,8 @@ import ( sdktrace "go.opentelemetry.io/otel/sdk/trace" - "github.com/perfect-panel/ppanel-server/pkg/lang" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/lang" + "github.com/perfect-panel/server/pkg/logger" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" ) diff --git a/pkg/trace/agent_test.go b/pkg/trace/agent_test.go index 7176419..02e8e5f 100644 --- a/pkg/trace/agent_test.go +++ b/pkg/trace/agent_test.go @@ -3,7 +3,7 @@ package trace import ( "testing" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "github.com/stretchr/testify/assert" ) diff --git a/pkg/trace/utils.go b/pkg/trace/utils.go index 2756284..beb3a4f 100644 --- a/pkg/trace/utils.go +++ b/pkg/trace/utils.go @@ -5,7 +5,7 @@ import ( "net" "strings" - ptrace "github.com/perfect-panel/ppanel-server/internal/trace" + ptrace "github.com/perfect-panel/server/internal/trace" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" diff --git a/pkg/uuidx/uuid.go b/pkg/uuidx/uuid.go index baaaf75..5573ae8 100644 --- a/pkg/uuidx/uuid.go +++ b/pkg/uuidx/uuid.go @@ -8,7 +8,7 @@ import ( "time" "github.com/gofrs/uuid/v5" - "github.com/perfect-panel/ppanel-server/pkg/random" + "github.com/perfect-panel/server/pkg/random" ) // NewUUID returns a new UUID. diff --git a/pkg/uuidx/uuid_test.go b/pkg/uuidx/uuid_test.go index ca83b9a..4ec2493 100644 --- a/pkg/uuidx/uuid_test.go +++ b/pkg/uuidx/uuid_test.go @@ -20,8 +20,8 @@ import ( "testing" "time" - "github.com/perfect-panel/ppanel-server/pkg/random" - "github.com/perfect-panel/ppanel-server/pkg/snowflake" + "github.com/perfect-panel/server/pkg/random" + "github.com/perfect-panel/server/pkg/snowflake" "github.com/gofrs/uuid/v5" ) diff --git a/pkg/xerr/errCode.go b/pkg/xerr/errCode.go index 8b238a1..64cbd6a 100644 --- a/pkg/xerr/errCode.go +++ b/pkg/xerr/errCode.go @@ -52,9 +52,10 @@ const ( //coupon error const ( - CouponNotExist uint32 = 50001 - CouponUsed uint32 = 50002 - CouponNotMatch uint32 = 50003 + CouponNotExist uint32 = 50001 // Coupon does not exist + CouponAlreadyUsed uint32 = 50002 // Coupon has already been used + CouponNotApplicable uint32 = 50003 // Coupon does not match the order or conditions + CouponInsufficientUsage uint32 = 50004 // Coupon has insufficient remaining uses ) // Subscribe diff --git a/pkg/xerr/errMsg.go b/pkg/xerr/errMsg.go index b0fa9a8..a259e54 100644 --- a/pkg/xerr/errMsg.go +++ b/pkg/xerr/errMsg.go @@ -41,9 +41,10 @@ func init() { NodeGroupNotEmpty: "Node group is not empty", //coupon error - CouponNotExist: "Coupon does not exist", - CouponUsed: "Coupon has been used", - CouponNotMatch: "Coupon does not match", + CouponNotExist: "Coupon does not exist", + CouponAlreadyUsed: "Coupon has already been used", + CouponNotApplicable: "Coupon does not match the order or conditions", + CouponInsufficientUsage: "Coupon has insufficient remaining uses", // Subscribe SubscribeExpired: "Subscribe is expired", diff --git a/ppanel.api b/ppanel.api index ca98a2f..10c83c2 100644 --- a/ppanel.api +++ b/ppanel.api @@ -27,6 +27,8 @@ import ( "apis/admin/console.api" "apis/admin/log.api" "apis/admin/ads.api" + "apis/admin/marketing.api" + "apis/admin/application.api" "apis/public/user.api" "apis/public/subscribe.api" "apis/public/order.api" @@ -35,14 +37,5 @@ import ( "apis/public/payment.api" "apis/public/document.api" "apis/public/portal.api" - "apis/app/auth.api" - "apis/app/user.api" - "apis/app/node.api" - "apis/app/ws.api" - "apis/app/order.api" - "apis/app/announcement.api" - "apis/app/payment.api" - "apis/app/document.api" - "apis/app/subscribe.api" ) diff --git a/ppanel.go b/ppanel.go index 7c8624c..a2e8110 100644 --- a/ppanel.go +++ b/ppanel.go @@ -1,6 +1,6 @@ package main -import "github.com/perfect-panel/ppanel-server/cmd" +import "github.com/perfect-panel/server/cmd" func main() { cmd.Execute() diff --git a/queue/handler/routes.go b/queue/handler/routes.go index bb315d1..edf2293 100644 --- a/queue/handler/routes.go +++ b/queue/handler/routes.go @@ -2,15 +2,16 @@ package handler import ( "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/svc" - countrylogic "github.com/perfect-panel/ppanel-server/queue/logic/country" - orderLogic "github.com/perfect-panel/ppanel-server/queue/logic/order" - smslogic "github.com/perfect-panel/ppanel-server/queue/logic/sms" - "github.com/perfect-panel/ppanel-server/queue/logic/subscription" - "github.com/perfect-panel/ppanel-server/queue/logic/traffic" - "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/svc" + countrylogic "github.com/perfect-panel/server/queue/logic/country" + orderLogic "github.com/perfect-panel/server/queue/logic/order" + smslogic "github.com/perfect-panel/server/queue/logic/sms" + "github.com/perfect-panel/server/queue/logic/subscription" + "github.com/perfect-panel/server/queue/logic/task" + "github.com/perfect-panel/server/queue/logic/traffic" + "github.com/perfect-panel/server/queue/types" - emailLogic "github.com/perfect-panel/ppanel-server/queue/logic/email" + emailLogic "github.com/perfect-panel/server/queue/logic/email" ) func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) { @@ -20,7 +21,6 @@ func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) { mux.Handle(types.ForthwithSendEmail, emailLogic.NewSendEmailLogic(serverCtx)) // Send sms task mux.Handle(types.ForthwithSendSms, smslogic.NewSendSmsLogic(serverCtx)) - // Defer close order task mux.Handle(types.DeferCloseOrder, orderLogic.NewDeferCloseOrderLogic(serverCtx)) // Forthwith activate order task @@ -34,6 +34,16 @@ func RegisterHandlers(mux *asynq.ServeMux, serverCtx *svc.ServiceContext) { // Schedule total server data mux.Handle(types.SchedulerTotalServerData, traffic.NewServerDataLogic(serverCtx)) - //定时查单 - mux.Handle(types.SchedulerCheckOrder, orderLogic.NewCheckOrderLogic(serverCtx)) + + // Schedule reset traffic + mux.Handle(types.SchedulerResetTraffic, traffic.NewResetTrafficLogic(serverCtx)) + + // ScheduledBatchSendEmail + mux.Handle(types.ScheduledBatchSendEmail, emailLogic.NewBatchEmailLogic(serverCtx)) + + // ScheduledTrafficStat + mux.Handle(types.SchedulerTrafficStat, traffic.NewStatLogic(serverCtx)) + + // ForthwithQuotaTask + mux.Handle(types.ForthwithQuotaTask, task.NewQuotaTaskLogic(serverCtx)) } diff --git a/queue/logic/country/getCountryLogic.go b/queue/logic/country/getCountryLogic.go index d3e9236..75e0f6f 100644 --- a/queue/logic/country/getCountryLogic.go +++ b/queue/logic/country/getCountryLogic.go @@ -2,14 +2,9 @@ package countrylogic import ( "context" - "encoding/json" - - "github.com/perfect-panel/ppanel-server/pkg/logger" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/ip" - "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/svc" ) type GetNodeCountryLogic struct { @@ -22,39 +17,6 @@ func NewGetNodeCountryLogic(svcCtx *svc.ServiceContext) *GetNodeCountryLogic { } } func (l *GetNodeCountryLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { - var payload types.GetNodeCountry - if err := json.Unmarshal(task.Payload(), &payload); err != nil { - logger.WithContext(ctx).Error("[GetNodeCountryLogic] Unmarshal payload failed", - logger.Field("error", err.Error()), - logger.Field("payload", task.Payload()), - ) - return nil - } - serverAddr := payload.ServerAddr - resp, err := ip.GetRegionByIp(serverAddr) - if err != nil { - logger.WithContext(ctx).Error("[GetNodeCountryLogic] ", logger.Field("error", err.Error()), logger.Field("serverAddr", serverAddr)) - return nil - } - servers, err := l.svcCtx.ServerModel.FindNodeByServerAddrAndProtocol(ctx, payload.ServerAddr, payload.Protocol) - if err != nil { - logger.WithContext(ctx).Error("[GetNodeCountryLogic] FindNodeByServerAddrAnd", logger.Field("error", err.Error()), logger.Field("serverAddr", serverAddr)) - return err - } - if len(servers) == 0 { - return nil - } - for _, ser := range servers { - ser.Country = resp.Country - ser.City = resp.City - ser.Latitude = resp.Latitude - ser.Longitude = resp.Longitude - err := l.svcCtx.ServerModel.Update(ctx, ser) - if err != nil { - logger.WithContext(ctx).Error("[GetNodeCountryLogic] ", logger.Field("error", err.Error()), logger.Field("id", ser.Id)) - } - } - logger.WithContext(ctx).Info("[GetNodeCountryLogic] ", logger.Field("country", resp.Country), logger.Field("city", resp.Country)) return nil } diff --git a/queue/logic/email/batchEmailLogic.go b/queue/logic/email/batchEmailLogic.go new file mode 100644 index 0000000..2aa8123 --- /dev/null +++ b/queue/logic/email/batchEmailLogic.go @@ -0,0 +1,78 @@ +package emailLogic + +import ( + "context" + "strconv" + + "github.com/hibiken/asynq" + taskModel "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/email" + "github.com/perfect-panel/server/pkg/logger" +) + +type BatchEmailLogic struct { + svcCtx *svc.ServiceContext +} + +type ErrorInfo struct { + Error string `json:"error"` + Email string `json:"email"` + Time int64 `json:"time"` +} + +func NewBatchEmailLogic(svcCtx *svc.ServiceContext) *BatchEmailLogic { + return &BatchEmailLogic{ + svcCtx: svcCtx, + } +} + +func (l *BatchEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { + // 解析任务负载 + payload := task.Payload() + if len(payload) == 0 { + logger.Error("[BatchEmailLogic] ProcessTask failed: empty payload") + return asynq.SkipRetry + } + // 转换获取任务id + taskID, err := strconv.ParseInt(string(payload), 10, 64) + if err != nil { + logger.WithContext(ctx).Error("[BatchEmailLogic] ProcessTask failed: invalid task ID", + logger.Field("error", err.Error()), + logger.Field("payload", string(payload)), + ) + return asynq.SkipRetry + } + tx := l.svcCtx.DB.WithContext(ctx) + var taskInfo taskModel.Task + if err = tx.Model(&taskModel.Task{}).Where("id = ?", taskID).First(&taskInfo).Error; err != nil { + logger.WithContext(ctx).Error("[BatchEmailLogic] ProcessTask failed", + logger.Field("error", err.Error()), + logger.Field("taskID", taskID), + ) + return asynq.SkipRetry + } + + if taskInfo.Status != 0 { + logger.WithContext(ctx).Info("[BatchEmailLogic] ProcessTask skipped: task already processed", + logger.Field("taskID", taskID), + logger.Field("status", taskInfo.Status), + ) + return nil + } + + sender, err := email.NewSender(l.svcCtx.Config.Email.Platform, l.svcCtx.Config.Email.PlatformConfig, l.svcCtx.Config.Site.SiteName) + if err != nil { + logger.WithContext(ctx).Error("[BatchEmailLogic] NewSender failed", logger.Field("error", err.Error())) + return nil + } + manager := email.NewWorkerManager(l.svcCtx.DB, sender) + if manager == nil { + logger.WithContext(ctx).Error("[BatchEmailLogic] ProcessTask failed: worker manager is nil") + return asynq.SkipRetry + } + + // 添加或获取 Worker 实例 + manager.AddWorker(taskID) + return nil +} diff --git a/queue/logic/email/sendEmailLogic.go b/queue/logic/email/sendEmailLogic.go index 393a796..7a56350 100644 --- a/queue/logic/email/sendEmailLogic.go +++ b/queue/logic/email/sendEmailLogic.go @@ -1,16 +1,19 @@ package emailLogic import ( + "bytes" "context" "encoding/json" + "text/template" + "time" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/log" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/email" - "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/email" + "github.com/perfect-panel/server/queue/types" ) type SendEmailLogic struct { @@ -31,8 +34,7 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro ) return nil } - messageLog := log.MessageLog{ - Type: log.Email.String(), + messageLog := log.Message{ Platform: l.svcCtx.Config.Email.Platform, To: payload.Email, Subject: payload.Subject, @@ -43,18 +45,111 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro logger.WithContext(ctx).Error("[SendEmailLogic] NewSender failed", logger.Field("error", err.Error())) return nil } - err = sender.Send([]string{payload.Email}, payload.Subject, payload.Content) + var content string + switch payload.Type { + case types.EmailTypeVerify: + tpl, _ := template.New("verify").Parse(l.svcCtx.Config.Email.VerifyEmailTemplate) + var result bytes.Buffer + + payload.Content["Type"] = uint8(payload.Content["Type"].(float64)) + + err = tpl.Execute(&result, payload.Content) + if err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Execute template failed", + logger.Field("error", err.Error()), + logger.Field("data", payload.Content), + ) + return nil + } + content = result.String() + case types.EmailTypeMaintenance: + tpl, _ := template.New("maintenance").Parse(l.svcCtx.Config.Email.MaintenanceEmailTemplate) + var result bytes.Buffer + err = tpl.Execute(&result, payload.Content) + if err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Execute template failed", + logger.Field("error", err.Error()), + logger.Field("template", l.svcCtx.Config.Email.MaintenanceEmailTemplate), + logger.Field("data", payload.Content), + ) + return nil + } + content = result.String() + case types.EmailTypeExpiration: + tpl, _ := template.New("expiration").Parse(l.svcCtx.Config.Email.ExpirationEmailTemplate) + var result bytes.Buffer + err = tpl.Execute(&result, payload.Content) + if err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Execute template failed", + logger.Field("error", err.Error()), + logger.Field("template", l.svcCtx.Config.Email.ExpirationEmailTemplate), + logger.Field("data", payload.Content), + ) + return nil + } + content = result.String() + case types.EmailTypeTrafficExceed: + tpl, _ := template.New("traffic_exceed").Parse(l.svcCtx.Config.Email.TrafficExceedEmailTemplate) + var result bytes.Buffer + err = tpl.Execute(&result, payload.Content) + if err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Execute template failed", + logger.Field("error", err.Error()), + logger.Field("template", l.svcCtx.Config.Email.TrafficExceedEmailTemplate), + logger.Field("data", payload.Content), + ) + return nil + } + content = result.String() + case types.EmailTypeCustom: + if payload.Content == nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Custom email content is empty", + logger.Field("payload", payload), + ) + return nil + } + if tpl, ok := payload.Content["content"].(string); !ok { + logger.WithContext(ctx).Error("[SendEmailLogic] Custom email content is not a string", + logger.Field("payload", payload), + ) + return nil + } else { + content = tpl + } + default: + logger.WithContext(ctx).Error("[SendEmailLogic] Unsupported email type", + logger.Field("type", payload.Type), + logger.Field("payload", payload), + ) + return nil + } + + err = sender.Send([]string{payload.Email}, payload.Subject, content) if err != nil { logger.WithContext(ctx).Error("[SendEmailLogic] Send email failed", logger.Field("error", err.Error())) return nil } messageLog.Status = 1 - if err = l.svcCtx.LogModel.InsertMessageLog(ctx, &messageLog); err != nil { - logger.WithContext(ctx).Error("[SendEmailLogic] InsertMessageLog failed", + emailLog, err := messageLog.Marshal() + if err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Marshal message log failed", logger.Field("error", err.Error()), logger.Field("messageLog", messageLog), ) + return nil + } + + if err = l.svcCtx.LogModel.Insert(ctx, &log.SystemLog{ + Type: log.TypeEmailMessage.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: 0, + Content: string(emailLog), + }); err != nil { + logger.WithContext(ctx).Error("[SendEmailLogic] Insert email log failed", + logger.Field("error", err.Error()), + logger.Field("emailLog", string(emailLog)), + ) + return nil } - logger.WithContext(ctx).Info("[SendEmailLogic] Send email", logger.Field("email", payload.Email), logger.Field("content", payload.Content)) return nil } diff --git a/queue/logic/order/activateOrderLogic.go b/queue/logic/order/activateOrderLogic.go index 19b164d..f6c8f9c 100644 --- a/queue/logic/order/activateOrderLogic.go +++ b/queue/logic/order/activateOrderLogic.go @@ -1,3 +1,5 @@ +// Package orderLogic provides order processing logic for handling various types of orders +// including subscription purchases, renewals, traffic resets, and balance recharges. package orderLogic import ( @@ -7,230 +9,325 @@ import ( "strconv" "time" - "github.com/perfect-panel/ppanel-server/pkg/constant" - - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/logger" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/google/uuid" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/logic/telegram" - "github.com/perfect-panel/ppanel-server/internal/model/order" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/tool" - "github.com/perfect-panel/ppanel-server/pkg/uuidx" - "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/logic/telegram" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/queue/types" "gorm.io/gorm" ) +// Order type constants define the different types of orders that can be processed const ( - Subscribe = 1 - Renewal = 2 - ResetTraffic = 3 - Recharge = 4 + OrderTypeSubscribe = 1 // New subscription purchase + OrderTypeRenewal = 2 // Subscription renewal + OrderTypeResetTraffic = 3 // Traffic quota reset + OrderTypeRecharge = 4 // Balance recharge ) +// Order status constants define the lifecycle states of an order +const ( + OrderStatusPending = 1 // Order created but not paid + OrderStatusPaid = 2 // Order paid and ready for processing + OrderStatusClose = 3 // Order closed/cancelled + OrderStatusFailed = 4 // Order processing failed + OrderStatusFinished = 5 // Order successfully completed +) + +// Predefined error variables for common error conditions +var ( + ErrInvalidOrderStatus = fmt.Errorf("invalid order status") + ErrInvalidOrderType = fmt.Errorf("invalid order type") +) + +// ActivateOrderLogic handles the activation and processing of paid orders type ActivateOrderLogic struct { - svc *svc.ServiceContext + svc *svc.ServiceContext // Service context containing dependencies } +// NewActivateOrderLogic creates a new instance of ActivateOrderLogic func NewActivateOrderLogic(svc *svc.ServiceContext) *ActivateOrderLogic { return &ActivateOrderLogic{ svc: svc, } } +// ProcessTask is the main entry point for processing order activation tasks. +// It handles the complete workflow of activating a paid order including validation, +// processing based on order type, and finalization. func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { - payload := types.ForthwithActivateOrderPayload{} - if err := json.Unmarshal(task.Payload(), &payload); err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Unmarshal payload failed", - logger.Field("error", err.Error()), - logger.Field("payload", string(task.Payload())), - ) - return nil - } - // Find order by order no - orderInfo, err := l.svc.OrderModel.FindOneByOrderNo(ctx, payload.OrderNo) + payload, err := l.parsePayload(ctx, task.Payload()) if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Find order failed", - logger.Field("error", err.Error()), - logger.Field("order_no", payload.OrderNo), - ) - return nil + return nil // Log and continue } - if orderInfo.Status != 2 { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Order status error", - logger.Field("order_no", orderInfo.OrderNo), - logger.Field("status", orderInfo.Status), - ) - return nil - } - switch orderInfo.Type { - case Subscribe: - err = l.NewPurchase(ctx, orderInfo) - case Renewal: - err = l.Renewal(ctx, orderInfo) - case ResetTraffic: - err = l.ResetTraffic(ctx, orderInfo) - case Recharge: - err = l.Recharge(ctx, orderInfo) - default: - logger.WithContext(ctx).Error("[ActivateOrderLogic] Order type is invalid", logger.Field("type", orderInfo.Type)) - } + orderInfo, err := l.validateAndGetOrder(ctx, payload.OrderNo) if err != nil { + return nil // Log and continue + } + + if err = l.processOrderByType(ctx, orderInfo); err != nil { logger.WithContext(ctx).Error("[ActivateOrderLogic] Process task failed", logger.Field("error", err.Error())) return nil } - // if coupon is not empty + + l.finalizeCouponAndOrder(ctx, orderInfo) + return nil +} + +// parsePayload unMarshals the task payload into a structured format +func (l *ActivateOrderLogic) parsePayload(ctx context.Context, payload []byte) (*types.ForthwithActivateOrderPayload, error) { + var p types.ForthwithActivateOrderPayload + if err := json.Unmarshal(payload, &p); err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Unmarshal payload failed", + logger.Field("error", err.Error()), + logger.Field("payload", string(payload)), + ) + return nil, err + } + return &p, nil +} + +// validateAndGetOrder retrieves an order by order number and validates its status +// Returns error if order is not found or not in paid status +func (l *ActivateOrderLogic) validateAndGetOrder(ctx context.Context, orderNo string) (*order.Order, error) { + orderInfo, err := l.svc.OrderModel.FindOneByOrderNo(ctx, orderNo) + if err != nil { + logger.WithContext(ctx).Error("Find order failed", + logger.Field("error", err.Error()), + logger.Field("order_no", orderNo), + ) + return nil, err + } + + if orderInfo.Status != OrderStatusPaid { + logger.WithContext(ctx).Error("Order status error", + logger.Field("order_no", orderInfo.OrderNo), + logger.Field("status", orderInfo.Status), + ) + return nil, ErrInvalidOrderStatus + } + + return orderInfo, nil +} + +// processOrderByType routes order processing based on the order type +func (l *ActivateOrderLogic) processOrderByType(ctx context.Context, orderInfo *order.Order) error { + switch orderInfo.Type { + case OrderTypeSubscribe: + return l.NewPurchase(ctx, orderInfo) + case OrderTypeRenewal: + return l.Renewal(ctx, orderInfo) + case OrderTypeResetTraffic: + return l.ResetTraffic(ctx, orderInfo) + case OrderTypeRecharge: + return l.Recharge(ctx, orderInfo) + default: + logger.WithContext(ctx).Error("Order type is invalid", logger.Field("type", orderInfo.Type)) + return ErrInvalidOrderType + } +} + +// finalizeCouponAndOrder handles post-processing tasks including coupon updates +// and order status finalization +func (l *ActivateOrderLogic) finalizeCouponAndOrder(ctx context.Context, orderInfo *order.Order) { + // Update coupon if exists if orderInfo.Coupon != "" { - // update coupon status - err = l.svc.CouponModel.UpdateCount(ctx, orderInfo.Coupon) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Update coupon status failed", + if err := l.svc.CouponModel.UpdateCount(ctx, orderInfo.Coupon); err != nil { + logger.WithContext(ctx).Error("Update coupon status failed", logger.Field("error", err.Error()), logger.Field("coupon", orderInfo.Coupon), ) } } - // update order status - orderInfo.Status = 5 - err = l.svc.OrderModel.Update(ctx, orderInfo) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Update order status failed", + + // Update order status + orderInfo.Status = OrderStatusFinished + if err := l.svc.OrderModel.Update(ctx, orderInfo); err != nil { + logger.WithContext(ctx).Error("Update order status failed", logger.Field("error", err.Error()), logger.Field("order_no", orderInfo.OrderNo), ) } +} +// NewPurchase handles new subscription purchase including user creation, +// subscription setup, commission processing, cache updates, and notifications +func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.Order) error { + userInfo, err := l.getUserOrCreate(ctx, orderInfo) + if err != nil { + return err + } + + sub, err := l.getSubscribeInfo(ctx, orderInfo.SubscribeId) + if err != nil { + return err + } + + userSub, err := l.createUserSubscription(ctx, orderInfo, sub) + if err != nil { + return err + } + + // Handle commission in separate goroutine to avoid blocking + go l.handleCommission(context.Background(), userInfo, orderInfo) + + // Clear cache + l.clearServerCache(ctx, sub) + + // Send notifications + l.sendNotifications(ctx, orderInfo, userInfo, sub, userSub, telegram.PurchaseNotify) + + logger.WithContext(ctx).Info("Insert user subscribe success") return nil } -// NewPurchase New purchase -func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.Order) error { - var userInfo *user.User - var err error +// getUserOrCreate retrieves an existing user or creates a new guest user based on order details +func (l *ActivateOrderLogic) getUserOrCreate(ctx context.Context, orderInfo *order.Order) (*user.User, error) { if orderInfo.UserId != 0 { - // find user by user id - userInfo, err = l.svc.UserModel.FindOne(ctx, orderInfo.UserId) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed", - logger.Field("error", err.Error()), - logger.Field("user_id", orderInfo.UserId), - logger.Field("user_id", orderInfo.UserId), - ) - return err - } - } else { - // If User ID is 0, it means that the order is a guest order, need to create a new user - // query info with redis - cacheKey := fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo) - data, err := l.svc.Redis.Get(ctx, cacheKey).Result() - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Get temp order cache failed", - logger.Field("error", err.Error()), - logger.Field("cache_key", cacheKey), - ) - return err - } - var tempOrder constant.TemporaryOrderInfo - if err = json.Unmarshal([]byte(data), &tempOrder); err != nil { - logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Unmarshal temp order failed", - logger.Field("error", err.Error()), - ) - return err - } - // create user - - userInfo = &user.User{ - Password: tool.EncodePassWord(tempOrder.Password), - AuthMethods: []user.AuthMethods{ - { - AuthType: tempOrder.AuthType, - AuthIdentifier: tempOrder.Identifier, - }, - }, - } - err = l.svc.UserModel.Transaction(ctx, func(tx *gorm.DB) error { - // Save user information - if err := tx.Save(userInfo).Error; err != nil { - return err - } - // Generate ReferCode - userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) - // Update ReferCode - if err := tx.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { - return err - } - orderInfo.UserId = userInfo.Id - return tx.Model(&order.Order{}).Where("order_no = ?", orderInfo.OrderNo).Update("user_id", userInfo.Id).Error - }) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Create user failed", - logger.Field("error", err.Error()), - ) - return err - } - logger.WithContext(ctx).Info("[ActivateOrderLogic] Create guest user success", logger.Field("user_id", userInfo.Id), logger.Field("Identifier", tempOrder.Identifier), logger.Field("AuthType", tempOrder.AuthType)) + return l.getExistingUser(ctx, orderInfo.UserId) } - // find subscribe by id - sub, err := l.svc.SubscribeModel.FindOne(ctx, orderInfo.SubscribeId) + return l.createGuestUser(ctx, orderInfo) +} + +// getExistingUser retrieves user information by user ID +func (l *ActivateOrderLogic) getExistingUser(ctx context.Context, userId int64) (*user.User, error) { + userInfo, err := l.svc.UserModel.FindOne(ctx, userId) if err != nil { - logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Find subscribe failed", + logger.WithContext(ctx).Error("Find user failed", logger.Field("error", err.Error()), - logger.Field("subscribe_id", orderInfo.SubscribeId), + logger.Field("user_id", userId), ) - return err + return nil, err } - // create user subscribe - now := time.Now() + return userInfo, nil +} - //系统开启了试用订阅功能,并且当前存在试用订阅 - if l.svc.Config.Register.EnableTrial && l.svc.Config.Register.TrialSubscribe != 0 { - //查询使用订阅套餐 - subscribeDetails, subErr := l.svc.UserModel.QueryUserSubscribe(ctx, userInfo.Id, 1, 2) - if subErr != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] disable user try out subscribe failed", - logger.Field("error", subErr.Error()), - ) - } else { - for _, item := range subscribeDetails { - if item.Subscribe.Id == l.svc.Config.Register.TrialSubscribe { - err = l.svc.UserModel.UpdateSubscribe(ctx, &user.Subscribe{ - Id: item.Id, - UserId: item.UserId, - OrderId: item.OrderId, - SubscribeId: item.SubscribeId, - StartTime: item.StartTime, - ExpireTime: now, - Traffic: item.Traffic, - Download: item.Download, - Upload: item.Upload, - Token: item.Token, - UUID: item.UUID, - FinishedAt: now, - Status: 3, - }) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] disable user try out subscribe failed", - logger.Field("error", err.Error()), - ) - } else { - logger.WithContext(ctx).Info("[ActivateOrderLogic] disable user try out subscribe success", - logger.Field("user_id", userInfo.Id), - logger.Field("subscribe_id", item.SubscribeId), - ) - } - break - } - } +// createGuestUser creates a new user account for guest orders using temporary order information +// stored in Redis cache +func (l *ActivateOrderLogic) createGuestUser(ctx context.Context, orderInfo *order.Order) (*user.User, error) { + tempOrder, err := l.getTempOrderInfo(ctx, orderInfo.OrderNo) + if err != nil { + return nil, err + } + + userInfo := &user.User{ + Password: tool.EncodePassWord(tempOrder.Password), + AuthMethods: []user.AuthMethods{ + { + AuthType: tempOrder.AuthType, + AuthIdentifier: tempOrder.Identifier, + }, + }, + } + + err = l.svc.UserModel.Transaction(ctx, func(tx *gorm.DB) error { + if err := tx.Save(userInfo).Error; err != nil { + return err } + + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + if err := tx.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + return err + } + + orderInfo.UserId = userInfo.Id + return tx.Model(&order.Order{}).Where("order_no = ?", orderInfo.OrderNo).Update("user_id", userInfo.Id).Error + }) + + if err != nil { + logger.WithContext(ctx).Error("Create user failed", logger.Field("error", err.Error())) + return nil, err } - userSub := user.Subscribe{ - Id: 0, + // Handle referrer relationship + l.handleReferrer(ctx, userInfo, tempOrder.InviteCode) + + logger.WithContext(ctx).Info("Create guest user success", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", tempOrder.Identifier), + logger.Field("auth_type", tempOrder.AuthType), + ) + + return userInfo, nil +} + +// getTempOrderInfo retrieves temporary order information from Redis cache +func (l *ActivateOrderLogic) getTempOrderInfo(ctx context.Context, orderNo string) (*constant.TemporaryOrderInfo, error) { + cacheKey := fmt.Sprintf(constant.TempOrderCacheKey, orderNo) + data, err := l.svc.Redis.Get(ctx, cacheKey).Result() + if err != nil { + logger.WithContext(ctx).Error("Get temp order cache failed", + logger.Field("error", err.Error()), + logger.Field("cache_key", cacheKey), + ) + return nil, err + } + + var tempOrder constant.TemporaryOrderInfo + if err = tempOrder.Unmarshal([]byte(data)); err != nil { + logger.WithContext(ctx).Error("Unmarshal temp order cache failed", + logger.Field("error", err.Error()), + logger.Field("cache_key", cacheKey), + logger.Field("data", data), + ) + return nil, err + } + + return &tempOrder, nil +} + +// handleReferrer establishes referrer relationship if an invite code is provided +func (l *ActivateOrderLogic) handleReferrer(ctx context.Context, userInfo *user.User, inviteCode string) { + if inviteCode == "" { + return + } + + referer, err := l.svc.UserModel.FindOneByReferCode(ctx, inviteCode) + if err != nil { + logger.WithContext(ctx).Error("Find referer failed", + logger.Field("error", err.Error()), + logger.Field("refer_code", inviteCode), + ) + return + } + + userInfo.RefererId = referer.Id + if err = l.svc.UserModel.Update(ctx, userInfo); err != nil { + logger.WithContext(ctx).Error("Update user referer failed", + logger.Field("error", err.Error()), + logger.Field("user_id", userInfo.Id), + ) + } +} + +// getSubscribeInfo retrieves subscription plan details by subscription ID +func (l *ActivateOrderLogic) getSubscribeInfo(ctx context.Context, subscribeId int64) (*subscribe.Subscribe, error) { + sub, err := l.svc.SubscribeModel.FindOne(ctx, subscribeId) + if err != nil { + logger.WithContext(ctx).Error("Find subscribe failed", + logger.Field("error", err.Error()), + logger.Field("subscribe_id", subscribeId), + ) + return nil, err + } + return sub, nil +} + +// createUserSubscription creates a new user subscription record based on order and subscription plan details +func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderInfo *order.Order, sub *subscribe.Subscribe) (*user.Subscribe, error) { + now := time.Now() + userSub := &user.Subscribe{ UserId: orderInfo.UserId, OrderId: orderInfo.Id, SubscribeId: orderInfo.SubscribeId, @@ -243,383 +340,373 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O UUID: uuid.New().String(), Status: 1, } - err = l.svc.UserModel.InsertSubscribe(ctx, &userSub) + + if err := l.svc.UserModel.InsertSubscribe(ctx, userSub); err != nil { + logger.WithContext(ctx).Error("Insert user subscribe failed", logger.Field("error", err.Error())) + return nil, err + } + + return userSub, nil +} + +// handleCommission processes referral commission for the referrer if applicable. +// This runs asynchronously to avoid blocking the main order processing flow. +func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *user.User, orderInfo *order.Order) { + if !l.shouldProcessCommission(userInfo, orderInfo.IsNew) { + return + } + + referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId) + if err != nil { + logger.WithContext(ctx).Error("Find referer failed", + logger.Field("error", err.Error()), + logger.Field("referer_id", userInfo.RefererId), + ) + return + } + + var referralPercentage uint8 + if referer.ReferralPercentage != 0 { + referralPercentage = referer.ReferralPercentage + } else { + referralPercentage = uint8(l.svc.Config.Invite.ReferralPercentage) + } + + // Order commission calculation: (Order Amount - Order Fee) * Referral Percentage + amount := l.calculateCommission(orderInfo.Amount-orderInfo.FeeAmount, referralPercentage) + + // Use transaction for commission updates + err = l.svc.DB.Transaction(func(tx *gorm.DB) error { + referer.Commission += amount + if err = l.svc.UserModel.Update(ctx, referer, tx); err != nil { + return err + } + + var commissionType uint16 + switch orderInfo.Type { + case OrderTypeSubscribe: + commissionType = log.CommissionTypePurchase + case OrderTypeRenewal: + commissionType = log.CommissionTypeRenewal + } + + commissionLog := &log.Commission{ + Type: commissionType, + Amount: amount, + OrderNo: orderInfo.OrderNo, + Timestamp: orderInfo.CreatedAt.UnixMilli(), + } + + content, _ := commissionLog.Marshal() + return tx.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeCommission.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: referer.Id, + Content: string(content), + }).Error + }) if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert user subscribe failed", + logger.WithContext(ctx).Error("Update referer commission failed", logger.Field("error", err.Error())) + return + } + + // Update cache + if err = l.svc.UserModel.UpdateUserCache(ctx, referer); err != nil { + logger.WithContext(ctx).Error("Update referer cache failed", logger.Field("error", err.Error()), + logger.Field("user_id", referer.Id), ) + } +} + +// shouldProcessCommission determines if commission should be processed based on +// referrer existence, commission settings, and order type +func (l *ActivateOrderLogic) shouldProcessCommission(userInfo *user.User, isFirstPurchase bool) bool { + if userInfo == nil || userInfo.RefererId == 0 { + return false + } + + referer, err := l.svc.UserModel.FindOne(context.Background(), userInfo.RefererId) + if err != nil { + logger.Errorw("Find referer failed", + logger.Field("error", err.Error()), + logger.Field("referer_id", userInfo.RefererId)) + return false + } + if referer == nil { + return false + } + + // use referer's custom settings if set + if referer.ReferralPercentage > 0 { + if referer.OnlyFirstPurchase != nil && *referer.OnlyFirstPurchase && !isFirstPurchase { + return false + } + return true + } + + // use global settings + if l.svc.Config.Invite.ReferralPercentage == 0 { + return false + } + if l.svc.Config.Invite.OnlyFirstPurchase && !isFirstPurchase { + return false + } + + return true +} + +// calculateCommission computes the commission amount based on order price and referral percentage +func (l *ActivateOrderLogic) calculateCommission(price int64, percentage uint8) int64 { + return int64(float64(price) * (float64(percentage) / 100)) +} + +// clearServerCache clears user list cache for all servers associated with the subscription +func (l *ActivateOrderLogic) clearServerCache(ctx context.Context, sub *subscribe.Subscribe) { + if err := l.svc.SubscribeModel.ClearCache(ctx, sub.Id); err != nil { + logger.WithContext(ctx).Error("[Order Queue] Clear subscribe cache failed", logger.Field("error", err.Error())) + } +} + +// Renewal handles subscription renewal including subscription extension, +// traffic reset (if configured), commission processing, and notifications +func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order) error { + userInfo, err := l.getExistingUser(ctx, orderInfo.UserId) + if err != nil { return err } - // handler commission - if userInfo.RefererId != 0 && - l.svc.Config.Invite.ReferralPercentage != 0 && - (!l.svc.Config.Invite.OnlyFirstPurchase || orderInfo.IsNew) { - referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Find referer failed", - logger.Field("error", err.Error()), - logger.Field("referer_id", userInfo.RefererId), - ) - goto updateCache - } - // calculate commission - amount := float64(orderInfo.Price) * (float64(l.svc.Config.Invite.ReferralPercentage) / 100) - referer.Commission += int64(amount) - err = l.svc.UserModel.Update(ctx, referer) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Update referer commission failed", - logger.Field("error", err.Error()), - ) - goto updateCache - } - // create commission log - commissionLog := user.CommissionLog{ - UserId: referer.Id, - OrderNo: orderInfo.OrderNo, - Amount: int64(amount), - } - err = l.svc.UserModel.InsertCommissionLog(ctx, &commissionLog) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert commission log failed", - logger.Field("error", err.Error()), - ) - } - err = l.svc.UserModel.UpdateUserCache(ctx, referer) - if err != nil { - logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Update referer cache", logger.Field("error", err.Error()), logger.Field("user_id", referer.Id)) - } - } -updateCache: - for _, id := range tool.StringToInt64Slice(sub.Server) { - cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, id) - err = l.svc.Redis.Del(ctx, cacheKey).Err() - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Del server user list cache failed", - logger.Field("error", err.Error()), - logger.Field("cache_key", cacheKey), - ) - } - } - data, err := l.svc.ServerModel.FindServerListByGroupIds(ctx, tool.StringToInt64Slice(sub.ServerGroup)) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Find server list failed", logger.Field("error", err.Error())) - return nil - } - for _, item := range data { - cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, item.Id) - err = l.svc.Redis.Del(ctx, cacheKey).Err() - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Del server user list cache failed", - logger.Field("error", err.Error()), - logger.Field("cache_key", cacheKey), - ) - } - } - userTelegramChatId, ok := findTelegram(userInfo) - // sendMessage To Telegram - if ok { - text, err := tool.RenderTemplateToString(telegram.PurchaseNotify, map[string]string{ - "OrderNo": orderInfo.OrderNo, - "SubscribeName": sub.Name, - "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), - "ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"), - }) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed", - logger.Field("error", err.Error()), - ) - } - l.sendUserNotifyWithTelegram(userTelegramChatId, text) - } - // send message to admin - text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{ - "OrderNo": orderInfo.OrderNo, - "TradeNo": orderInfo.TradeNo, - "SubscribeName": sub.Name, - //"UserEmail": userInfo.Email, - "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), - "OrderStatus": "已支付", - "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), - "PaymentMethod": orderInfo.Method, - }) + userSub, err := l.getUserSubscription(ctx, orderInfo.SubscribeToken) if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed", + return err + } + + sub, err := l.getSubscribeInfo(ctx, orderInfo.SubscribeId) + if err != nil { + return err + } + + if err = l.updateSubscriptionForRenewal(ctx, userSub, sub, orderInfo); err != nil { + return err + } + + // Clear user subscription cache + err = l.svc.UserModel.ClearSubscribeCache(ctx, userSub) + if err != nil { + logger.WithContext(ctx).Error("Clear user subscribe cache failed", logger.Field("error", err.Error()), + logger.Field("subscribe_id", userSub.Id), + logger.Field("user_id", userInfo.Id), ) } - l.sendAdminNotifyWithTelegram(ctx, text) - logger.WithContext(ctx).Info("[ActivateOrderLogic] Insert user subscribe success") + + // Clear cache + l.clearServerCache(ctx, sub) + + // Handle commission + go l.handleCommission(context.Background(), userInfo, orderInfo) + + // Send notifications + l.sendNotifications(ctx, orderInfo, userInfo, sub, userSub, telegram.RenewalNotify) + return nil } -// Renewal Renewal -func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order) error { - // find user by user id - userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId) +// getUserSubscription retrieves user subscription by token +func (l *ActivateOrderLogic) getUserSubscription(ctx context.Context, token string) (*user.Subscribe, error) { + userSub, err := l.svc.UserModel.FindOneSubscribeByToken(ctx, token) if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed", - logger.Field("error", err.Error()), - logger.Field("user_id", orderInfo.UserId), - ) - return err - } - // find user subscribe by subscribe token - userSub, err := l.svc.UserModel.FindOneSubscribeByOrderId(ctx, orderInfo.ParentId) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user subscribe failed", - logger.Field("error", err.Error()), - logger.Field("order_id", orderInfo.Id), - ) - return err - } - // find subscribe by id - sub, err := l.svc.SubscribeModel.FindOne(ctx, orderInfo.SubscribeId) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Find subscribe failed", - logger.Field("error", err.Error()), - logger.Field("subscribe_id", orderInfo.SubscribeId), - logger.Field("order_id", orderInfo.Id), - ) - return err + logger.WithContext(ctx).Error("Find user subscribe failed", logger.Field("error", err.Error())) + return nil, err } + return userSub, nil +} + +// updateSubscriptionForRenewal updates subscription details for renewal including +// expiration time extension and traffic reset if configured +func (l *ActivateOrderLogic) updateSubscriptionForRenewal(ctx context.Context, userSub *user.Subscribe, sub *subscribe.Subscribe, orderInfo *order.Order) error { now := time.Now() if userSub.ExpireTime.Before(now) { userSub.ExpireTime = now - userSub.Status = 1 + } + today := time.Now().Day() + resetDay := userSub.ExpireTime.Day() + + // Reset traffic if enabled + if (sub.RenewalReset != nil && *sub.RenewalReset) || today == resetDay { + userSub.Download = 0 + userSub.Upload = 0 } - //fix bug:FinishedAt causes the update subscription to fail - if now.AddDate(-30, 0, 0).After(userSub.FinishedAt) { - userSub.FinishedAt = now + if userSub.FinishedAt != nil { + if userSub.FinishedAt.Before(now) && today > resetDay { + // reset user traffic if finished at is before now + userSub.Download = 0 + userSub.Upload = 0 + } + + userSub.FinishedAt = nil } userSub.ExpireTime = tool.AddTime(sub.UnitTime, orderInfo.Quantity, userSub.ExpireTime) - // update user subscribe - err = l.svc.UserModel.UpdateSubscribe(ctx, userSub) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user subscribe failed", - logger.Field("error", err.Error()), - ) + userSub.Status = 1 + + if err := l.svc.UserModel.UpdateSubscribe(ctx, userSub); err != nil { + logger.WithContext(ctx).Error("Update user subscribe failed", logger.Field("error", err.Error())) return err } - // handler commission - if userInfo.RefererId != 0 && - l.svc.Config.Invite.ReferralPercentage != 0 && - !l.svc.Config.Invite.OnlyFirstPurchase { - referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Find referer failed", - logger.Field("error", err.Error()), - logger.Field("referer_id", userInfo.RefererId), - ) - goto sendMessage - } - // calculate commission - amount := float64(orderInfo.Price) * (float64(l.svc.Config.Invite.ReferralPercentage) / 100) - referer.Commission += int64(amount) - err = l.svc.UserModel.Update(ctx, referer) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Update referer commission failed", - logger.Field("error", err.Error()), - ) - goto sendMessage - } - // create commission log - commissionLog := user.CommissionLog{ - UserId: referer.Id, - OrderNo: orderInfo.OrderNo, - Amount: int64(amount), - } - err = l.svc.UserModel.InsertCommissionLog(ctx, &commissionLog) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert commission log failed", - logger.Field("error", err.Error()), - ) - } - err = l.svc.UserModel.UpdateUserCache(ctx, referer) - if err != nil { - logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Update referer cache", logger.Field("error", err.Error()), logger.Field("user_id", referer.Id)) - } - } -sendMessage: - userTelegramChatId, ok := findTelegram(userInfo) - // SendMessage To Telegram - if ok { - text, err := tool.RenderTemplateToString(telegram.RenewalNotify, map[string]string{ - "OrderNo": orderInfo.OrderNo, - "SubscribeName": sub.Name, - "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), - "ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"), - }) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed", - logger.Field("error", err.Error()), - ) - } - l.sendUserNotifyWithTelegram(userTelegramChatId, text) - } - // send message to admin - text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{ - "OrderNo": orderInfo.OrderNo, - "TradeNo": orderInfo.TradeNo, - "SubscribeName": sub.Name, - //"UserEmail": userInfo.Email, - "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), - "OrderStatus": "已支付", - "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), - "PaymentMethod": orderInfo.Method, - }) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed", - logger.Field("error", err.Error()), - ) - } - l.sendAdminNotifyWithTelegram(ctx, text) return nil } -// ResetTraffic Reset traffic +// ResetTraffic handles traffic quota reset for existing subscriptions func (l *ActivateOrderLogic) ResetTraffic(ctx context.Context, orderInfo *order.Order) error { - // find user by user id - userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId) + userInfo, err := l.getExistingUser(ctx, orderInfo.UserId) if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed", - logger.Field("error", err.Error()), - logger.Field("user_id", orderInfo.UserId), - ) return err } - // Generate a Subscribe Token through orderNo - // find user subscribe by subscribe token - userSub, err := l.svc.UserModel.FindOneSubscribeByToken(ctx, orderInfo.SubscribeToken) + + userSub, err := l.getUserSubscription(ctx, orderInfo.SubscribeToken) if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user subscribe failed", - logger.Field("error", err.Error()), - logger.Field("order_id", orderInfo.Id), - ) return err } + + // Reset traffic userSub.Download = 0 userSub.Upload = 0 userSub.Status = 1 - // update user subscribe - err = l.svc.UserModel.UpdateSubscribe(ctx, userSub) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user subscribe failed", - logger.Field("error", err.Error()), - ) + + if err := l.svc.UserModel.UpdateSubscribe(ctx, userSub); err != nil { + logger.WithContext(ctx).Error("Update user subscribe failed", logger.Field("error", err.Error())) return err } - sub, err := l.svc.SubscribeModel.FindOne(ctx, userSub.SubscribeId) + + sub, err := l.getSubscribeInfo(ctx, userSub.SubscribeId) if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Find subscribe failed", - logger.Field("error", err.Error()), - logger.Field("subscribe_id", userSub.SubscribeId), - ) - return nil - } - userTelegramChatId, ok := findTelegram(userInfo) - // SendMessage To Telegram - if ok { - text, err := tool.RenderTemplateToString(telegram.ResetTrafficNotify, map[string]string{ - "OrderNo": orderInfo.OrderNo, - "SubscribeName": sub.Name, - "ResetTime": time.Now().Format("2006-01-02 15:04:05"), - "ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"), - }) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed", - logger.Field("error", err.Error()), - ) - } - l.sendUserNotifyWithTelegram(userTelegramChatId, text) + return err } - // send message to admin - text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{ - "OrderNo": orderInfo.OrderNo, - "TradeNo": orderInfo.TradeNo, - "SubscribeName": "流量重置", - "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), - "OrderStatus": "已支付", - "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), - "PaymentMethod": orderInfo.Method, - }) + // Clear user subscription cache + err = l.svc.UserModel.ClearSubscribeCache(ctx, userSub) if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed", + logger.WithContext(ctx).Error("Clear user subscribe cache failed", logger.Field("error", err.Error()), + logger.Field("subscribe_id", userSub.Id), + logger.Field("user_id", userInfo.Id), ) } - l.sendAdminNotifyWithTelegram(ctx, text) + + // Clear cache + l.clearServerCache(ctx, sub) + + // insert reset traffic log + resetLog := &log.ResetSubscribe{ + Type: log.ResetSubscribeTypePaid, + UserId: userInfo.Id, + OrderNo: orderInfo.OrderNo, + Timestamp: time.Now().UnixMilli(), + } + + content, _ := resetLog.Marshal() + if err = l.svc.LogModel.Insert(ctx, &log.SystemLog{ + Type: log.TypeResetSubscribe.Uint8(), + Date: time.Now().Format(time.DateOnly), + ObjectID: userSub.Id, + Content: string(content), + }); err != nil { + logger.WithContext(ctx).Error("[Order Queue]Insert reset subscribe log failed", logger.Field("error", err.Error())) + } + + // Send notifications + l.sendNotifications(ctx, orderInfo, userInfo, sub, userSub, telegram.ResetTrafficNotify) + return nil } -// Recharge Recharge to user +// Recharge handles balance recharge orders including balance updates, +// transaction logging, and notifications func (l *ActivateOrderLogic) Recharge(ctx context.Context, orderInfo *order.Order) error { - // find user by user id - userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId) + userInfo, err := l.getExistingUser(ctx, orderInfo.UserId) if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed", - logger.Field("error", err.Error()), - logger.Field("user_id", orderInfo.UserId), - ) return err } - userInfo.Balance += orderInfo.Price - // update user + + // Update balance in transaction err = l.svc.DB.Transaction(func(tx *gorm.DB) error { - err = l.svc.UserModel.Update(ctx, userInfo, tx) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user failed", - logger.Field("error", err.Error()), - ) - return err - } - // Create Balance Log - balanceLog := user.BalanceLog{ - UserId: orderInfo.UserId, - Amount: orderInfo.Price, - Type: 1, - OrderId: orderInfo.Id, - Balance: userInfo.Balance, - } - err = l.svc.UserModel.InsertBalanceLog(ctx, &balanceLog, tx) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert balance log failed", - logger.Field("error", err.Error()), - ) + userInfo.Balance += orderInfo.Price + if err = l.svc.UserModel.Update(ctx, userInfo, tx); err != nil { return err } - return nil + balanceLog := &log.Balance{ + Amount: orderInfo.Price, + Type: log.BalanceTypeRecharge, + OrderNo: orderInfo.OrderNo, + Balance: userInfo.Balance, + Timestamp: time.Now().UnixMilli(), + } + content, _ := balanceLog.Marshal() + + return tx.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeBalance.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: userInfo.Id, + Content: string(content), + }).Error }) + if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Database transaction failed", - logger.Field("error", err.Error()), - ) + logger.WithContext(ctx).Error("[Recharge] Database transaction failed", logger.Field("error", err.Error())) return err } - userTelegramChatId, ok := findTelegram(userInfo) - // SendMessage To Telegram - if ok { - text, err := tool.RenderTemplateToString(telegram.RechargeNotify, map[string]string{ + + // clear user cache + if err = l.svc.UserModel.UpdateUserCache(ctx, userInfo); err != nil { + logger.WithContext(ctx).Error("[Recharge] Update user cache failed", logger.Field("error", err.Error())) + return err + } + + // Send notifications + l.sendRechargeNotifications(ctx, orderInfo, userInfo) + + return nil +} + +// sendNotifications sends both user and admin notifications for order completion +func (l *ActivateOrderLogic) sendNotifications(ctx context.Context, orderInfo *order.Order, userInfo *user.User, sub *subscribe.Subscribe, userSub *user.Subscribe, notifyType string) { + // Send user notification + if telegramId, ok := findTelegram(userInfo); ok { + templateData := l.buildUserNotificationData(orderInfo, sub, userSub) + if text, err := tool.RenderTemplateToString(notifyType, templateData); err == nil { + l.sendUserNotifyWithTelegram(telegramId, text) + } + } + + // Send admin notification + adminData := l.buildAdminNotificationData(orderInfo, sub) + if text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, adminData); err == nil { + l.sendAdminNotifyWithTelegram(ctx, text) + } +} + +// sendRechargeNotifications sends specific notifications for balance recharge orders +func (l *ActivateOrderLogic) sendRechargeNotifications(ctx context.Context, orderInfo *order.Order, userInfo *user.User) { + // Send user notification + if telegramId, ok := findTelegram(userInfo); ok { + templateData := map[string]string{ "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), "PaymentMethod": orderInfo.Method, "Time": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), "Balance": fmt.Sprintf("%.2f", float64(userInfo.Balance)/100), - }) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed", - logger.Field("error", err.Error()), - ) } - l.sendUserNotifyWithTelegram(userTelegramChatId, text) + if text, err := tool.RenderTemplateToString(telegram.RechargeNotify, templateData); err == nil { + l.sendUserNotifyWithTelegram(telegramId, text) + } } - // send message to admin - text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{ + + // Send admin notification + adminData := map[string]string{ "OrderNo": orderInfo.OrderNo, "TradeNo": orderInfo.TradeNo, "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), @@ -627,65 +714,83 @@ func (l *ActivateOrderLogic) Recharge(ctx context.Context, orderInfo *order.Orde "OrderStatus": "已支付", "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), "PaymentMethod": orderInfo.Method, - }) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed", - logger.Field("error", err.Error()), - ) } - l.sendAdminNotifyWithTelegram(ctx, text) - return nil + if text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, adminData); err == nil { + l.sendAdminNotifyWithTelegram(ctx, text) + } } -// sendUserNotifyWithTelegram send message to user +// buildUserNotificationData creates template data for user notifications +func (l *ActivateOrderLogic) buildUserNotificationData(orderInfo *order.Order, sub *subscribe.Subscribe, userSub *user.Subscribe) map[string]string { + data := map[string]string{ + "OrderNo": orderInfo.OrderNo, + "SubscribeName": sub.Name, + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + } + + if userSub != nil { + data["ExpireTime"] = userSub.ExpireTime.Format("2006-01-02 15:04:05") + data["ResetTime"] = time.Now().Format("2006-01-02 15:04:05") + } + + return data +} + +// buildAdminNotificationData creates template data for admin notifications +func (l *ActivateOrderLogic) buildAdminNotificationData(orderInfo *order.Order, sub *subscribe.Subscribe) map[string]string { + subscribeName := sub.Name + if orderInfo.Type == OrderTypeResetTraffic { + subscribeName = "流量重置" + } + + return map[string]string{ + "OrderNo": orderInfo.OrderNo, + "TradeNo": orderInfo.TradeNo, + "SubscribeName": subscribeName, + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "OrderStatus": "已支付", + "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), + "PaymentMethod": orderInfo.Method, + } +} + +// sendUserNotifyWithTelegram sends a notification message to a user via Telegram func (l *ActivateOrderLogic) sendUserNotifyWithTelegram(chatId int64, text string) { msg := tgbotapi.NewMessage(chatId, text) msg.ParseMode = "markdown" - _, err := l.svc.TelegramBot.Send(msg) - if err != nil { - logger.Error("[ActivateOrderLogic] Send telegram user message failed", - logger.Field("error", err.Error()), - ) + if _, err := l.svc.TelegramBot.Send(msg); err != nil { + logger.Error("Send telegram user message failed", logger.Field("error", err.Error())) } } -// sendAdminNotifyWithTelegram send message to admin +// sendAdminNotifyWithTelegram sends a notification message to all admin users via Telegram func (l *ActivateOrderLogic) sendAdminNotifyWithTelegram(ctx context.Context, text string) { admins, err := l.svc.UserModel.QueryAdminUsers(ctx) if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Query admin users failed", - logger.Field("error", err.Error()), - ) + logger.WithContext(ctx).Error("Query admin users failed", logger.Field("error", err.Error())) return } + for _, admin := range admins { - telegramId, ok := findTelegram(admin) - if !ok { - continue - } - msg := tgbotapi.NewMessage(telegramId, text) - msg.ParseMode = "markdown" - _, err := l.svc.TelegramBot.Send(msg) - if err != nil { - logger.WithContext(ctx).Error("[ActivateOrderLogic] Send telegram admin message failed", - logger.Field("error", err.Error()), - ) + if telegramId, ok := findTelegram(admin); ok { + msg := tgbotapi.NewMessage(telegramId, text) + msg.ParseMode = "markdown" + if _, err := l.svc.TelegramBot.Send(msg); err != nil { + logger.WithContext(ctx).Error("Send telegram admin message failed", logger.Field("error", err.Error())) + } } } } -// findTelegram find user telegram id +// findTelegram extracts Telegram chat ID from user authentication methods. +// Returns the chat ID and a boolean indicating if Telegram auth was found. func findTelegram(u *user.User) (int64, bool) { for _, item := range u.AuthMethods { if item.AuthType == "telegram" { - // string to int64 - parseInt, err := strconv.ParseInt(item.AuthIdentifier, 10, 64) - if err != nil { - return 0, false + if telegramId, err := strconv.ParseInt(item.AuthIdentifier, 10, 64); err == nil { + return telegramId, true } - return parseInt, true } - } return 0, false } diff --git a/queue/logic/order/activateOrderLogic.go_bak b/queue/logic/order/activateOrderLogic.go_bak new file mode 100644 index 0000000..f57f57f --- /dev/null +++ b/queue/logic/order/activateOrderLogic.go_bak @@ -0,0 +1,675 @@ +package orderLogic + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "time" + + "github.com/perfect-panel/server/pkg/constant" + + "github.com/perfect-panel/server/pkg/logger" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/google/uuid" + "github.com/hibiken/asynq" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/logic/telegram" + "github.com/perfect-panel/server/internal/model/order" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/tool" + "github.com/perfect-panel/server/pkg/uuidx" + "github.com/perfect-panel/server/queue/types" + "gorm.io/gorm" +) + +const ( + Subscribe = 1 + Renewal = 2 + ResetTraffic = 3 + Recharge = 4 +) + +type ActivateOrderLogic struct { + svc *svc.ServiceContext +} + +func NewActivateOrderLogic(svc *svc.ServiceContext) *ActivateOrderLogic { + return &ActivateOrderLogic{ + svc: svc, + } +} + +func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { + payload := types.ForthwithActivateOrderPayload{} + if err := json.Unmarshal(task.Payload(), &payload); err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Unmarshal payload failed", + logger.Field("error", err.Error()), + logger.Field("payload", string(task.Payload())), + ) + return nil + } + // Find order by order no + orderInfo, err := l.svc.OrderModel.FindOneByOrderNo(ctx, payload.OrderNo) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find order failed", + logger.Field("error", err.Error()), + logger.Field("order_no", payload.OrderNo), + ) + return nil + } + // 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished + if orderInfo.Status != 2 { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Order status error", + logger.Field("order_no", orderInfo.OrderNo), + logger.Field("status", orderInfo.Status), + ) + return nil + } + switch orderInfo.Type { + case Subscribe: + err = l.NewPurchase(ctx, orderInfo) + case Renewal: + err = l.Renewal(ctx, orderInfo) + case ResetTraffic: + err = l.ResetTraffic(ctx, orderInfo) + case Recharge: + err = l.Recharge(ctx, orderInfo) + default: + logger.WithContext(ctx).Error("[ActivateOrderLogic] Order type is invalid", logger.Field("type", orderInfo.Type)) + return nil + } + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Process task failed", logger.Field("error", err.Error())) + return nil + } + // if coupon is not empty + if orderInfo.Coupon != "" { + // update coupon status + err = l.svc.CouponModel.UpdateCount(ctx, orderInfo.Coupon) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update coupon status failed", + logger.Field("error", err.Error()), + logger.Field("coupon", orderInfo.Coupon), + ) + } + } + // update order status + orderInfo.Status = 5 + err = l.svc.OrderModel.Update(ctx, orderInfo) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update order status failed", + logger.Field("error", err.Error()), + logger.Field("order_no", orderInfo.OrderNo), + ) + } + + return nil +} + +// NewPurchase New purchase +func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.Order) error { + var userInfo *user.User + var err error + if orderInfo.UserId != 0 { + // find user by user id + userInfo, err = l.svc.UserModel.FindOne(ctx, orderInfo.UserId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed", + logger.Field("error", err.Error()), + logger.Field("user_id", orderInfo.UserId), + ) + return err + } + } else { + // If User ID is 0, it means that the order is a guest order, need to create a new user + // query info with redis + cacheKey := fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo) + data, err := l.svc.Redis.Get(ctx, cacheKey).Result() + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Get temp order cache failed", + logger.Field("error", err.Error()), + logger.Field("cache_key", cacheKey), + ) + return err + } + var tempOrder constant.TemporaryOrderInfo + if err = json.Unmarshal([]byte(data), &tempOrder); err != nil { + logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Unmarshal temp order failed", + logger.Field("error", err.Error()), + ) + return err + } + // create user + + userInfo = &user.User{ + Password: tool.EncodePassWord(tempOrder.Password), + AuthMethods: []user.AuthMethods{ + { + AuthType: tempOrder.AuthType, + AuthIdentifier: tempOrder.Identifier, + }, + }, + } + err = l.svc.UserModel.Transaction(ctx, func(tx *gorm.DB) error { + // Save user information + if err := tx.Save(userInfo).Error; err != nil { + return err + } + // Generate ReferCode + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + // Update ReferCode + if err := tx.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + return err + } + orderInfo.UserId = userInfo.Id + return tx.Model(&order.Order{}).Where("order_no = ?", orderInfo.OrderNo).Update("user_id", userInfo.Id).Error + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Create user failed", + logger.Field("error", err.Error()), + ) + return err + } + + if tempOrder.InviteCode != "" { + // find referer by refer code + referer, err := l.svc.UserModel.FindOneByReferCode(ctx, tempOrder.InviteCode) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find referer failed", + logger.Field("error", err.Error()), + logger.Field("refer_code", tempOrder.InviteCode), + ) + } else { + userInfo.RefererId = referer.Id + err = l.svc.UserModel.Update(ctx, userInfo) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user referer failed", + logger.Field("error", err.Error()), + logger.Field("user_id", userInfo.Id), + ) + } + } + } + + logger.WithContext(ctx).Info("[ActivateOrderLogic] Create guest user success", logger.Field("user_id", userInfo.Id), logger.Field("Identifier", tempOrder.Identifier), logger.Field("AuthType", tempOrder.AuthType)) + } + // find subscribe by id + sub, err := l.svc.SubscribeModel.FindOne(ctx, orderInfo.SubscribeId) + if err != nil { + logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Find subscribe failed", + logger.Field("error", err.Error()), + logger.Field("subscribe_id", orderInfo.SubscribeId), + ) + return err + } + // create user subscribe + now := time.Now() + + userSub := user.Subscribe{ + Id: 0, + UserId: orderInfo.UserId, + OrderId: orderInfo.Id, + SubscribeId: orderInfo.SubscribeId, + StartTime: now, + ExpireTime: tool.AddTime(sub.UnitTime, orderInfo.Quantity, now), + Traffic: sub.Traffic, + Download: 0, + Upload: 0, + Token: uuidx.SubscribeToken(orderInfo.OrderNo), + UUID: uuid.New().String(), + Status: 1, + } + err = l.svc.UserModel.InsertSubscribe(ctx, &userSub) + + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert user subscribe failed", + logger.Field("error", err.Error()), + ) + return err + } + // handler commission + if userInfo.RefererId != 0 && + l.svc.Config.Invite.ReferralPercentage != 0 && + (!l.svc.Config.Invite.OnlyFirstPurchase || orderInfo.IsNew) { + referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find referer failed", + logger.Field("error", err.Error()), + logger.Field("referer_id", userInfo.RefererId), + ) + goto updateCache + } + // calculate commission + amount := float64(orderInfo.Price) * (float64(l.svc.Config.Invite.ReferralPercentage) / 100) + referer.Commission += int64(amount) + err = l.svc.UserModel.Update(ctx, referer) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update referer commission failed", + logger.Field("error", err.Error()), + ) + goto updateCache + } + // create commission log + commissionLog := user.CommissionLog{ + UserId: referer.Id, + OrderNo: orderInfo.OrderNo, + Amount: int64(amount), + } + err = l.svc.UserModel.InsertCommissionLog(ctx, &commissionLog) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert commission log failed", + logger.Field("error", err.Error()), + ) + } + err = l.svc.UserModel.UpdateUserCache(ctx, referer) + if err != nil { + logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Update referer cache", logger.Field("error", err.Error()), logger.Field("user_id", referer.Id)) + } + } +updateCache: + for _, id := range tool.StringToInt64Slice(sub.Server) { + cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, id) + err = l.svc.Redis.Del(ctx, cacheKey).Err() + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Del server user list cache failed", + logger.Field("error", err.Error()), + logger.Field("cache_key", cacheKey), + ) + } + } + data, err := l.svc.ServerModel.FindServerListByGroupIds(ctx, tool.StringToInt64Slice(sub.ServerGroup)) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find server list failed", logger.Field("error", err.Error())) + return err + } + for _, item := range data { + cacheKey := fmt.Sprintf("%s%d", config.ServerUserListCacheKey, item.Id) + err = l.svc.Redis.Del(ctx, cacheKey).Err() + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Del server user list cache failed", + logger.Field("error", err.Error()), + logger.Field("cache_key", cacheKey), + ) + } + } + userTelegramChatId, ok := findTelegram(userInfo) + + // sendMessage To Telegram + if ok { + text, err := tool.RenderTemplateToString(telegram.PurchaseNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "SubscribeName": sub.Name, + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"), + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed", + logger.Field("error", err.Error()), + ) + } + l.sendUserNotifyWithTelegram(userTelegramChatId, text) + } + // send message to admin + text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "TradeNo": orderInfo.TradeNo, + "SubscribeName": sub.Name, + //"UserEmail": userInfo.Email, + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "OrderStatus": "已支付", + "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), + "PaymentMethod": orderInfo.Method, + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed", + logger.Field("error", err.Error()), + ) + } + l.sendAdminNotifyWithTelegram(ctx, text) + logger.WithContext(ctx).Info("[ActivateOrderLogic] Insert user subscribe success") + return nil +} + +// Renewal Renewal +func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order) error { + // find user by user id + userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed", + logger.Field("error", err.Error()), + logger.Field("user_id", orderInfo.UserId), + ) + return err + } + // find user subscribe by subscribe token + userSub, err := l.svc.UserModel.FindOneSubscribeByToken(ctx, orderInfo.SubscribeToken) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user subscribe failed", + logger.Field("error", err.Error()), + logger.Field("order_id", orderInfo.Id), + ) + return err + } + // find subscribe by id + sub, err := l.svc.SubscribeModel.FindOne(ctx, orderInfo.SubscribeId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find subscribe failed", + logger.Field("error", err.Error()), + logger.Field("subscribe_id", orderInfo.SubscribeId), + logger.Field("order_id", orderInfo.Id), + ) + return err + } + now := time.Now() + if userSub.ExpireTime.Before(now) { + userSub.ExpireTime = now + } + + // Check whether traffic reset on renewal is enabled + if sub.RenewalReset != nil && *sub.RenewalReset { + userSub.Download = 0 + userSub.Upload = 0 + } + if userSub.FinishedAt != nil { + userSub.FinishedAt = nil + } + + userSub.ExpireTime = tool.AddTime(sub.UnitTime, orderInfo.Quantity, userSub.ExpireTime) + userSub.Status = 1 + // update user subscribe + err = l.svc.UserModel.UpdateSubscribe(ctx, userSub) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user subscribe failed", + logger.Field("error", err.Error()), + ) + return err + } + // handler commission + if userInfo.RefererId != 0 && + l.svc.Config.Invite.ReferralPercentage != 0 && + !l.svc.Config.Invite.OnlyFirstPurchase { + referer, err := l.svc.UserModel.FindOne(ctx, userInfo.RefererId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find referer failed", + logger.Field("error", err.Error()), + logger.Field("referer_id", userInfo.RefererId), + ) + goto sendMessage + } + // calculate commission + amount := float64(orderInfo.Price) * (float64(l.svc.Config.Invite.ReferralPercentage) / 100) + referer.Commission += int64(amount) + err = l.svc.UserModel.Update(ctx, referer) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update referer commission failed", + logger.Field("error", err.Error()), + ) + goto sendMessage + } + // create commission log + commissionLog := user.CommissionLog{ + UserId: referer.Id, + OrderNo: orderInfo.OrderNo, + Amount: int64(amount), + } + err = l.svc.UserModel.InsertCommissionLog(ctx, &commissionLog) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert commission log failed", + logger.Field("error", err.Error()), + ) + } + err = l.svc.UserModel.UpdateUserCache(ctx, referer) + if err != nil { + logger.WithContext(ctx).Errorw("[ActivateOrderLogic] Update referer cache", logger.Field("error", err.Error()), logger.Field("user_id", referer.Id)) + } + } + +sendMessage: + userTelegramChatId, ok := findTelegram(userInfo) + // SendMessage To Telegram + if ok { + text, err := tool.RenderTemplateToString(telegram.RenewalNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "SubscribeName": sub.Name, + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"), + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed", + logger.Field("error", err.Error()), + ) + } + l.sendUserNotifyWithTelegram(userTelegramChatId, text) + } + + // send message to admin + text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "TradeNo": orderInfo.TradeNo, + "SubscribeName": sub.Name, + //"UserEmail": userInfo.Email, + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "OrderStatus": "已支付", + "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), + "PaymentMethod": orderInfo.Method, + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed", + logger.Field("error", err.Error()), + ) + } + l.sendAdminNotifyWithTelegram(ctx, text) + return nil +} + +// ResetTraffic Reset traffic +func (l *ActivateOrderLogic) ResetTraffic(ctx context.Context, orderInfo *order.Order) error { + // find user by user id + userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed", + logger.Field("error", err.Error()), + logger.Field("user_id", orderInfo.UserId), + ) + return err + } + // Generate a Subscribe Token through orderNo + // find user subscribe by subscribe token + userSub, err := l.svc.UserModel.FindOneSubscribeByToken(ctx, orderInfo.SubscribeToken) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user subscribe failed", + logger.Field("error", err.Error()), + logger.Field("order_id", orderInfo.Id), + ) + return err + } + userSub.Download = 0 + userSub.Upload = 0 + userSub.Status = 1 + // update user subscribe + err = l.svc.UserModel.UpdateSubscribe(ctx, userSub) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user subscribe failed", + logger.Field("error", err.Error()), + ) + return err + } + sub, err := l.svc.SubscribeModel.FindOne(ctx, userSub.SubscribeId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find subscribe failed", + logger.Field("error", err.Error()), + logger.Field("subscribe_id", userSub.SubscribeId), + ) + return err + } + userTelegramChatId, ok := findTelegram(userInfo) + // SendMessage To Telegram + if ok { + text, err := tool.RenderTemplateToString(telegram.ResetTrafficNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "SubscribeName": sub.Name, + "ResetTime": time.Now().Format("2006-01-02 15:04:05"), + "ExpireTime": userSub.ExpireTime.Format("2006-01-02 15:04:05"), + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed", + logger.Field("error", err.Error()), + ) + } + l.sendUserNotifyWithTelegram(userTelegramChatId, text) + } + + // send message to admin + text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "TradeNo": orderInfo.TradeNo, + "SubscribeName": "流量重置", + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "OrderStatus": "已支付", + "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), + "PaymentMethod": orderInfo.Method, + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed", + logger.Field("error", err.Error()), + ) + } + l.sendAdminNotifyWithTelegram(ctx, text) + return nil +} + +// Recharge Recharge to user +func (l *ActivateOrderLogic) Recharge(ctx context.Context, orderInfo *order.Order) error { + // find user by user id + userInfo, err := l.svc.UserModel.FindOne(ctx, orderInfo.UserId) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Find user failed", + logger.Field("error", err.Error()), + logger.Field("user_id", orderInfo.UserId), + ) + return err + } + userInfo.Balance += orderInfo.Price + // update user + err = l.svc.DB.Transaction(func(tx *gorm.DB) error { + err = l.svc.UserModel.Update(ctx, userInfo, tx) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Update user failed", + logger.Field("error", err.Error()), + ) + return err + } + // Create Balance Log + balanceLog := user.BalanceLog{ + UserId: orderInfo.UserId, + Amount: orderInfo.Price, + Type: 1, + OrderId: orderInfo.Id, + Balance: userInfo.Balance, + } + err = l.svc.UserModel.InsertBalanceLog(ctx, &balanceLog, tx) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Insert balance log failed", + logger.Field("error", err.Error()), + ) + return err + } + + return nil + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Database transaction failed", + logger.Field("error", err.Error()), + ) + return err + } + userTelegramChatId, ok := findTelegram(userInfo) + // SendMessage To Telegram + if ok { + text, err := tool.RenderTemplateToString(telegram.RechargeNotify, map[string]string{ + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "PaymentMethod": orderInfo.Method, + "Time": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), + "Balance": fmt.Sprintf("%.2f", float64(userInfo.Balance)/100), + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render template failed", + logger.Field("error", err.Error()), + ) + } + l.sendUserNotifyWithTelegram(userTelegramChatId, text) + } + // send message to admin + text, err := tool.RenderTemplateToString(telegram.AdminOrderNotify, map[string]string{ + "OrderNo": orderInfo.OrderNo, + "TradeNo": orderInfo.TradeNo, + "OrderAmount": fmt.Sprintf("%.2f", float64(orderInfo.Price)/100), + "SubscribeName": "余额充值", + "OrderStatus": "已支付", + "OrderTime": orderInfo.CreatedAt.Format("2006-01-02 15:04:05"), + "PaymentMethod": orderInfo.Method, + }) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Render AdminOrderNotify template failed", + logger.Field("error", err.Error()), + ) + } + l.sendAdminNotifyWithTelegram(ctx, text) + return nil +} + +// sendUserNotifyWithTelegram send message to user +func (l *ActivateOrderLogic) sendUserNotifyWithTelegram(chatId int64, text string) { + msg := tgbotapi.NewMessage(chatId, text) + msg.ParseMode = "markdown" + _, err := l.svc.TelegramBot.Send(msg) + if err != nil { + logger.Error("[ActivateOrderLogic] Send telegram user message failed", + logger.Field("error", err.Error()), + ) + } +} + +// sendAdminNotifyWithTelegram send message to admin +func (l *ActivateOrderLogic) sendAdminNotifyWithTelegram(ctx context.Context, text string) { + admins, err := l.svc.UserModel.QueryAdminUsers(ctx) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Query admin users failed", + logger.Field("error", err.Error()), + ) + return + } + for _, admin := range admins { + telegramId, ok := findTelegram(admin) + if !ok { + continue + } + msg := tgbotapi.NewMessage(telegramId, text) + msg.ParseMode = "markdown" + _, err := l.svc.TelegramBot.Send(msg) + if err != nil { + logger.WithContext(ctx).Error("[ActivateOrderLogic] Send telegram admin message failed", + logger.Field("error", err.Error()), + ) + } + } +} + +// findTelegram find user telegram id +func findTelegram(u *user.User) (int64, bool) { + for _, item := range u.AuthMethods { + if item.AuthType == "telegram" { + // string to int64 + parseInt, err := strconv.ParseInt(item.AuthIdentifier, 10, 64) + if err != nil { + return 0, false + } + return parseInt, true + } + + } + return 0, false +} diff --git a/queue/logic/order/checkOrderLogic.go b/queue/logic/order/checkOrderLogic.go deleted file mode 100644 index 6af678b..0000000 --- a/queue/logic/order/checkOrderLogic.go +++ /dev/null @@ -1,161 +0,0 @@ -package orderLogic - -import ( - "context" - "encoding/json" - "github.com/hibiken/asynq" - order2 "github.com/perfect-panel/ppanel-server/internal/logic/public/order" - "github.com/perfect-panel/ppanel-server/internal/model/payment" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/pkg/payment/alipay" - "github.com/perfect-panel/ppanel-server/pkg/payment/payssion" - "github.com/perfect-panel/ppanel-server/pkg/payment/stripe" - "github.com/perfect-panel/ppanel-server/queue/types" - "go.uber.org/zap" -) - -type CheckOrderLogic struct { - svc *svc.ServiceContext -} - -func NewCheckOrderLogic(svc *svc.ServiceContext) *CheckOrderLogic { - return &CheckOrderLogic{ - svc: svc, - } -} - -func (l *CheckOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { - - orderList, err := l.svc.OrderModel.QueryPendingOrders(ctx) - if err != nil { - logger.Errorf("query pending orders error: %v", zap.Error(err)) - return err - } - logger.Infof("查到订单数据: %v", orderList) - for _, order := range orderList { - paymentConfig, err := l.svc.PaymentModel.FindOne(ctx, order.PaymentId) - if err != nil { - logger.Errorw("[CheckOrder] Find payment config failed", logger.Field("error", err.Error()), logger.Field("paymentMark", order.Method)) - continue - } - logger.Infof("查到配置数据[%s]: %v", order.Method, orderList) - var flag bool - switch order.Method { - case order2.AlipayF2f: - if l.queryAlipay(paymentConfig, order.TradeNo) { - flag = true - } - break - case order2.Payssion: - logger.Infof("匹配配置类型: %v", order2.Payssion) - if l.queryPayssion(paymentConfig, order.OrderNo) { - flag = true - } - break - case order2.StripeWeChatPay: - if l.queryStripe(paymentConfig, order.TradeNo) { - flag = true - } - break - default: - logger.Infow("[CheckOrder] Unsupported payment method", logger.Field("paymentMethod", order.Method)) - continue - } - logger.Infof("[CheckOrder] Unsupported payment method[%v]", flag) - if flag { - err := l.svc.OrderModel.UpdateOrderStatus(ctx, order.OrderNo, 2) - if err != nil { - logger.Errorf("[CheckOrder] query order status error: %v", zap.Error(err)) - } - logger.Info("[CheckOrder] Notify status success", logger.Field("orderNo", order.TradeNo)) - payload := types.ForthwithActivateOrderPayload{ - OrderNo: order.OrderNo, - } - bytes, err := json.Marshal(&payload) - if err != nil { - logger.Error("[CheckOrder] Marshal payload failed", logger.Field("error", err.Error())) - return err - } - task := asynq.NewTask(types.ForthwithActivateOrder, bytes) - taskInfo, err := l.svc.Queue.EnqueueContext(ctx, task) - if err != nil { - logger.Error("[CheckOrder] Enqueue task failed", logger.Field("error", err.Error())) - return err - } - logger.Info("[CheckOrder] Enqueue task success", logger.Field("taskInfo", taskInfo)) - } - } - - return nil -} - -// queryAlipay Query Alipay payment status -// -//nolint:unused -func (l *CheckOrderLogic) queryAlipay(paymentConfig *payment.Payment, TradeNo string) bool { - config := payment.AlipayF2FConfig{} - if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil { - zap.S().Errorw("[CheckOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config)) - return false - } - client := alipay.NewClient(alipay.Config{ - AppId: config.AppId, - PrivateKey: config.PrivateKey, - PublicKey: config.PublicKey, - InvoiceName: config.InvoiceName, - }) - status, err := client.QueryTrade(context.Background(), TradeNo) - if err != nil { - zap.S().Errorw("[CheckOrder] Query trade failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) - return false - } - if status == alipay.Success || status == alipay.Finished { - return true - } - return false -} - -// queryStripe Query Stripe payment status -// -//nolint:unused -func (l *CheckOrderLogic) queryStripe(paymentConfig *payment.Payment, TradeNo string) bool { - config := payment.StripeConfig{} - if err := json.Unmarshal([]byte(paymentConfig.Config), &config); err != nil { - zap.S().Errorw("[CheckOrder] Unmarshal payment config failed", logger.Field("error", err.Error()), logger.Field("config", paymentConfig.Config)) - return false - } - client := stripe.NewClient(stripe.Config{ - PublicKey: config.PublicKey, - SecretKey: config.SecretKey, - WebhookSecret: config.WebhookSecret, - }) - status, err := client.QueryOrderStatus(TradeNo) - if err != nil { - zap.S().Errorw("[CheckOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) - return false - } - return status -} - -// queryPayssion Query Stripe payment status -// -//nolint:unused -func (l *CheckOrderLogic) queryPayssion(paymentConfig *payment.Payment, TradeNo string) bool { - zap.S().Infof("[CheckOrder]1 Query Payssion called") - payssionConfig := payment.PayssionConfig{} - if err := json.Unmarshal([]byte(paymentConfig.Config), &payssionConfig); err != nil { - zap.S().Errorw("[CheckOrder] Unmarshal error", logger.Field("error", err.Error())) - return false - } - zap.S().Infof("[CheckOrder]2 Query Payssion called") - client := payssion.NewClient(payssionConfig.ApiKey, payssionConfig.SecretKey, payssionConfig.PmId, payssionConfig.Currency, payssionConfig.QueryUrl, payssionConfig.CreateUrl) - // create payment - result, err := client.QueryOrder(TradeNo) - if err != nil { - zap.S().Errorw("[CheckOrder] Query order status failed", logger.Field("error", err.Error()), logger.Field("TradeNo", TradeNo)) - return false - } - zap.S().Infof("[CheckOrder]3 Query Payssion called") - return result.Transaction.State == "completed" -} diff --git a/queue/logic/order/deferCloseOrderLogic.go b/queue/logic/order/deferCloseOrderLogic.go index 16a446e..1dc91f3 100644 --- a/queue/logic/order/deferCloseOrderLogic.go +++ b/queue/logic/order/deferCloseOrderLogic.go @@ -4,13 +4,13 @@ import ( "context" "encoding/json" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/logic/public/order" - "github.com/perfect-panel/ppanel-server/internal/svc" - internal "github.com/perfect-panel/ppanel-server/internal/types" - "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/logic/public/order" + "github.com/perfect-panel/server/internal/svc" + internal "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/queue/types" ) type DeferCloseOrderLogic struct { diff --git a/queue/logic/sms/sendSmsLogic.go b/queue/logic/sms/sendSmsLogic.go index ad61b9d..76cef6d 100644 --- a/queue/logic/sms/sendSmsLogic.go +++ b/queue/logic/sms/sendSmsLogic.go @@ -4,15 +4,16 @@ import ( "context" "encoding/json" "fmt" + "time" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/log" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/constant" - "github.com/perfect-panel/ppanel-server/pkg/sms" - "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/constant" + "github.com/perfect-panel/server/pkg/sms" + "github.com/perfect-panel/server/queue/types" ) type SmsSendCount struct { @@ -43,17 +44,16 @@ func (l *SendSmsLogic) ProcessTask(ctx context.Context, task *asynq.Task) error logger.WithContext(ctx).Error("[SendSmsLogic] New send sms client failed", logger.Field("error", err.Error()), logger.Field("payload", payload)) return err } - createSms := &log.MessageLog{ - Type: log.Mobile.String(), + createSms := &log.Message{ Platform: l.svcCtx.Config.Mobile.Platform, To: fmt.Sprintf("+%s%s", payload.TelephoneArea, payload.Telephone), Subject: constant.ParseVerifyType(payload.Type).String(), - Content: "", + Content: map[string]interface{}{ + "content": client.GetSendCodeContent(payload.Content), + }, } err = client.SendCode(payload.TelephoneArea, payload.Telephone, payload.Content) - createSms.Content = client.GetSendCodeContent(payload.Content) - if err != nil { logger.WithContext(ctx).Error("[SendSmsLogic] Send sms failed", logger.Field("error", err.Error()), logger.Field("payload", payload)) if l.svcCtx.Config.Model != constant.DevMode { @@ -64,7 +64,14 @@ func (l *SendSmsLogic) ProcessTask(ctx context.Context, task *asynq.Task) error } createSms.Status = 1 logger.WithContext(ctx).Info("[SendSmsLogic] Send sms", logger.Field("telephone", payload.Telephone), logger.Field("content", createSms.Content)) - err = l.svcCtx.LogModel.InsertMessageLog(ctx, createSms) + + content, _ := createSms.Marshal() + err = l.svcCtx.LogModel.Insert(ctx, &log.SystemLog{ + Type: log.TypeMobileMessage.Uint8(), + Date: time.Now().Format("2006-01-02"), + ObjectID: 0, + Content: string(content), + }) if err != nil { logger.WithContext(ctx).Error("[SendSmsLogic] Send sms failed", logger.Field("error", err.Error()), logger.Field("payload", payload)) return nil diff --git a/queue/logic/subscription/checkSubscriptionLogic.go b/queue/logic/subscription/checkSubscriptionLogic.go index 8cfe566..81b86e7 100644 --- a/queue/logic/subscription/checkSubscriptionLogic.go +++ b/queue/logic/subscription/checkSubscriptionLogic.go @@ -1,19 +1,17 @@ package subscription import ( - "bytes" "context" "encoding/json" - "text/template" "time" - queue "github.com/perfect-panel/ppanel-server/queue/types" + queue "github.com/perfect-panel/server/queue/types" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/user" - "github.com/perfect-panel/ppanel-server/internal/svc" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" "gorm.io/gorm" ) @@ -32,7 +30,7 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task) // Check subscription traffic err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error { var list []*user.Subscribe - err := db.Model(&user.Subscribe{}).Where("upload + download >= traffic AND status = 1 AND traffic > 0 ").Find(&list).Error + err := db.Model(&user.Subscribe{}).Where("upload + download >= traffic AND status IN (0, 1) AND traffic > 0 ").Find(&list).Error if err != nil { logger.Errorw("[Check Subscription Traffic] Query subscribe failed", logger.Field("error", err.Error())) return err @@ -62,7 +60,7 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task) return err } } - + l.clearServerCache(ctx, list...) logger.Infow("[Check Subscription Traffic] Update subscribe status", logger.Field("user_ids", ids), logger.Field("count", int64(len(ids)))) } else { @@ -77,7 +75,7 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task) // Check subscription expire err = l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error { var list []*user.Subscribe - err = db.Model(&user.Subscribe{}).Where("`status` = 1 AND `expire_time` < ? AND `expire_time` != ? and `finished_at` IS NULL", time.Now(), time.UnixMilli(0)).Find(&list).Error + err = db.Model(&user.Subscribe{}).Where("`status` IN (0, 1) AND `expire_time` < ? AND `expire_time` != ? and `finished_at` IS NULL", time.Now(), time.UnixMilli(0)).Find(&list).Error if err != nil { logger.Error("[Check Subscription] Find subscribe failed", logger.Field("error", err.Error())) return err @@ -87,7 +85,10 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task) ids = append(ids, item.Id) } if len(ids) > 0 { - err = db.Model(&user.Subscribe{}).Where("id IN ?", ids).Update("status", 3).Error + err = db.Model(&user.Subscribe{}).Where("id IN ?", ids).Updates(map[string]interface{}{ + "status": 3, + "finished_at": time.Now(), + }).Error if err != nil { logger.Error("[Check Subscription Expire] Update subscribe status failed", logger.Field("error", err.Error())) return err @@ -97,17 +98,17 @@ func (l *CheckSubscriptionLogic) ProcessTask(ctx context.Context, _ *asynq.Task) logger.Error("[Check Subscription Expire] Send email failed", logger.Field("error", err.Error())) return nil } - if len(list) > 0 { - if err = l.svc.UserModel.ClearSubscribeCache(ctx, list...); err != nil { - logger.Errorw("[Check Subscription Traffic] Clear subscribe cache failed", logger.Field("error", err.Error())) - return err - } + if err = l.svc.UserModel.ClearSubscribeCache(ctx, list...); err != nil { + logger.Errorw("[Check Subscription Traffic] Clear subscribe cache failed", logger.Field("error", err.Error())) + return err } + l.clearServerCache(ctx, list...) + logger.Info("[Check Subscription Expire] Update subscribe status", logger.Field("user_ids", ids), logger.Field("count", int64(len(ids)))) } else { logger.Info("[Check Subscription Expire] No subscribe need to update") } - return l.svc.UserModel.ClearSubscribeCache(ctx, list...) + return nil }) if err != nil { logger.Info("[CheckSubscription] Transaction failed", logger.Field("error", err.Error())) @@ -128,24 +129,14 @@ func (l *CheckSubscriptionLogic) sendExpiredNotify(ctx context.Context, subs []i continue } var taskPayload queue.SendEmailPayload + taskPayload.Type = queue.EmailTypeExpiration taskPayload.Email = method.AuthIdentifier taskPayload.Subject = "Subscription Expired" - tpl, err := template.New("Expired").Parse(l.svc.Config.Email.ExpirationEmailTemplate) - if err != nil { - logger.Errorw("[CheckSubscription] Parse template failed", logger.Field("error", err.Error())) - continue - } - var result bytes.Buffer - err = tpl.Execute(&result, map[string]interface{}{ + taskPayload.Content = map[string]interface{}{ "SiteLogo": l.svc.Config.Site.SiteLogo, "SiteName": l.svc.Config.Site.SiteName, "ExpireDate": sub.ExpireTime.Format("2006-01-02 15:04:05"), - }) - if err != nil { - logger.Errorw("[CheckSubscription] Execute template failed", logger.Field("error", err.Error())) - continue } - taskPayload.Content = result.String() payloadBuy, err := json.Marshal(taskPayload) if err != nil { logger.Errorw("[CheckSubscription] Marshal payload failed", logger.Field("error", err.Error())) @@ -178,23 +169,13 @@ func (l *CheckSubscriptionLogic) sendTrafficNotify(ctx context.Context, subs []i continue } var taskPayload queue.SendEmailPayload + taskPayload.Type = queue.EmailTypeTrafficExceed taskPayload.Email = method.AuthIdentifier taskPayload.Subject = "Subscription Traffic Exceed" - tpl, err := template.New("Traffic").Parse(l.svc.Config.Email.TrafficExceedEmailTemplate) - if err != nil { - logger.Errorw("[CheckSubscription] Parse template failed", logger.Field("error", err.Error())) - continue - } - var result bytes.Buffer - err = tpl.Execute(&result, map[string]interface{}{ + taskPayload.Content = map[string]interface{}{ "SiteLogo": l.svc.Config.Site.SiteLogo, "SiteName": l.svc.Config.Site.SiteName, - }) - if err != nil { - logger.Errorw("[CheckSubscription] Execute template failed", logger.Field("error", err.Error())) - continue } - taskPayload.Content = result.String() payloadBuy, err := json.Marshal(taskPayload) if err != nil { logger.Errorw("[CheckSubscription] Marshal payload failed", logger.Field("error", err.Error())) @@ -213,3 +194,18 @@ func (l *CheckSubscriptionLogic) sendTrafficNotify(ctx context.Context, subs []i } return nil } + +func (l *CheckSubscriptionLogic) clearServerCache(ctx context.Context, userSubs ...*user.Subscribe) { + subs := make(map[int64]bool) + for _, sub := range userSubs { + if _, ok := subs[sub.SubscribeId]; !ok { + subs[sub.SubscribeId] = true + } + } + + for sub, _ := range subs { + if err := l.svc.SubscribeModel.ClearCache(ctx, sub); err != nil { + logger.Errorw("[CheckSubscription] ClearCache failed", logger.Field("error", err.Error()), logger.Field("subscribe_id", sub)) + } + } +} diff --git a/queue/logic/task/quotaLogic.go b/queue/logic/task/quotaLogic.go new file mode 100644 index 0000000..c2b1464 --- /dev/null +++ b/queue/logic/task/quotaLogic.go @@ -0,0 +1,407 @@ +package task + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "time" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/task" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/tool" + "gorm.io/gorm" +) + +const ( + UnitTimeNoLimit = "NoLimit" // Unlimited time subscription + UnitTimeYear = "Year" // Annual subscription + UnitTimeMonth = "Month" // Monthly subscription + UnitTimeDay = "Day" // Daily subscription + UnitTimeHour = "Hour" // Hourly subscription + UnitTimeMinute = "Minute" // Per-minute subscription + +) + +type QuotaTaskLogic struct { + svcCtx *svc.ServiceContext +} + +type ErrorInfo struct { + UserSubscribeId int64 `json:"user_subscribe_id"` + Error string `json:"error"` +} + +func NewQuotaTaskLogic(svcCtx *svc.ServiceContext) *QuotaTaskLogic { + return &QuotaTaskLogic{ + svcCtx: svcCtx, + } +} + +func (l *QuotaTaskLogic) ProcessTask(ctx context.Context, t *asynq.Task) error { + taskID, err := l.parseTaskID(ctx, t.Payload()) + if err != nil { + return err + } + + taskInfo, err := l.getTaskInfo(ctx, taskID) + if err != nil { + return err + } + + if taskInfo.Status != 0 { + logger.WithContext(ctx).Info("[QuotaTaskLogic.ProcessTask] task already processed", + logger.Field("taskID", taskID), + logger.Field("status", taskInfo.Status), + ) + return nil + } + + scope, content, err := l.parseTaskData(ctx, taskInfo) + if err != nil { + return err + } + + subscribes, err := l.getSubscribes(ctx, scope.Objects) + if err != nil { + return err + } + if err = l.processSubscribes(ctx, subscribes, content, taskInfo); err != nil { + return err + } + // 清理用户缓存(仅在有赠送金时清理) + if content.GiftValue != 0 { + var userIds []int64 + for _, sub := range subscribes { + userIds = append(userIds, sub.UserId) + } + userIds = tool.RemoveDuplicateElements(userIds...) + var users []*user.User + if err = l.svcCtx.DB.WithContext(ctx).Model(&user.User{}).Where("id IN ?", userIds).Find(&users).Error; err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.ProcessTask] find users error", + logger.Field("error", err.Error()), + logger.Field("userIDs", userIds)) + } + err = l.svcCtx.UserModel.ClearUserCache(ctx, users...) + if err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.ProcessTask] clear user cache error", + logger.Field("error", err.Error()), + logger.Field("userIDs", userIds)) + } + } + + // 清理用户订阅缓存 + err = l.svcCtx.UserModel.ClearSubscribeCache(ctx, subscribes...) + if err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.ProcessTask] clear subscribe cache error", + logger.Field("error", err.Error())) + } + + return nil +} + +func (l *QuotaTaskLogic) parseTaskID(ctx context.Context, payload []byte) (int64, error) { + if len(payload) == 0 { + logger.WithContext(ctx).Error("[QuotaTaskLogic.parseTaskID] empty payload") + return 0, asynq.SkipRetry + } + + taskID, err := strconv.ParseInt(string(payload), 10, 64) + if err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.parseTaskID] invalid task ID", + logger.Field("error", err.Error()), + logger.Field("payload", string(payload)), + ) + return 0, asynq.SkipRetry + } + return taskID, nil +} + +func (l *QuotaTaskLogic) getTaskInfo(ctx context.Context, taskID int64) (*task.Task, error) { + var taskInfo *task.Task + if err := l.svcCtx.DB.WithContext(ctx).Model(&task.Task{}).Where("id = ?", taskID).First(&taskInfo).Error; err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.getTaskInfo] find task error", + logger.Field("error", err.Error()), + logger.Field("taskID", taskID), + ) + return nil, asynq.SkipRetry + } + return taskInfo, nil +} + +func (l *QuotaTaskLogic) parseTaskData(ctx context.Context, taskInfo *task.Task) (task.QuotaScope, task.QuotaContent, error) { + var scope task.QuotaScope + if err := scope.Unmarshal([]byte(taskInfo.Scope)); err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.parseTaskData] unmarshal scope error", + logger.Field("error", err.Error()), + ) + return scope, task.QuotaContent{}, asynq.SkipRetry + } + + var content task.QuotaContent + if err := content.Unmarshal([]byte(taskInfo.Content)); err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.parseTaskData] unmarshal content error", + logger.Field("error", err.Error()), + ) + return scope, content, asynq.SkipRetry + } + return scope, content, nil +} + +func (l *QuotaTaskLogic) getSubscribes(ctx context.Context, subscriberIDs []int64) ([]*user.Subscribe, error) { + var subscribes []*user.Subscribe + if err := l.svcCtx.DB.WithContext(ctx).Model(&user.Subscribe{}).Where("id IN ?", subscriberIDs).Find(&subscribes).Error; err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.getSubscribes] find subscribes error", + logger.Field("error", err.Error()), + logger.Field("subscribers", subscriberIDs), + ) + return nil, asynq.SkipRetry + } + return subscribes, nil +} + +func (l *QuotaTaskLogic) processSubscribes(ctx context.Context, subscribes []*user.Subscribe, content task.QuotaContent, taskInfo *task.Task) error { + tx := l.svcCtx.DB.WithContext(ctx).Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] transaction panic", + logger.Field("panic", r), + ) + } + }() + + var errors []ErrorInfo + now := time.Now() + + for _, sub := range subscribes { + if err := l.processSubscription(tx, sub, content, now, &errors); err != nil { + tx.Rollback() + return err + } + } + + // 根据错误情况决定任务状态 + status := int8(2) // Completed + if len(errors) > 0 { + logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] some subscriptions failed", + logger.Field("total", len(subscribes)), + logger.Field("failed", len(errors)), + ) + // 如果所有订阅都失败,标记为失败状态 + if len(errors) == len(subscribes) { + status = 3 // Failed + } + errs, err := json.Marshal(errors) + if err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] marshal errors failed", + logger.Field("error", err.Error()), + ) + tx.Rollback() + return err + } + taskInfo.Errors = string(errs) + } + + taskInfo.Current = uint64(len(subscribes)) + taskInfo.Status = status + err := tx.Where("id = ?", taskInfo.Id).Save(taskInfo).Error + if err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] update task status error", + logger.Field("error", err.Error()), + logger.Field("taskID", taskInfo.Id), + ) + tx.Rollback() + return err + } + + if err = tx.Commit().Error; err != nil { + logger.WithContext(ctx).Error("[QuotaTaskLogic.processSubscribes] commit transaction error", + logger.Field("error", err.Error()), + ) + return err + } + + return nil +} + +func (l *QuotaTaskLogic) processSubscription(tx *gorm.DB, sub *user.Subscribe, content task.QuotaContent, now time.Time, errors *[]ErrorInfo) error { + // 验证订阅数据 + if sub == nil { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: 0, + Error: "subscription is nil", + }) + return nil + } + + updated := false + + // 处理时间延长 - 修复逻辑:只要Days不为0就处理,不管ExpireTime是否为0 + if content.Days != 0 { + if sub.ExpireTime.Unix() == 0 || sub.ExpireTime.Before(now) { + // 如果没有过期时间或已过期,从现在开始计算 + sub.ExpireTime = now.AddDate(0, 0, int(content.Days)) + } else { + // 在原有过期时间基础上延长 + sub.ExpireTime = sub.ExpireTime.AddDate(0, 0, int(content.Days)) + } + // 如果订阅延长到未来时间,设置为激活状态 + if sub.ExpireTime.After(now) && sub.Status != 1 { + sub.Status = 1 // Active + } + updated = true + } + + // 处理流量重置 + if content.ResetTraffic { + sub.Download = 0 + sub.Upload = 0 + updated = true + if err := l.createResetTrafficLog(tx, sub.Id, sub.UserId, now); err != nil { + // 记录错误但不阻断整个任务,日志失败不影响主流程 + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: "create reset traffic log error: " + err.Error(), + }) + } + } + + // 处理赠送金 + if content.GiftValue != 0 { + if err := l.processGift(tx, sub, content, now, errors); err != nil { + return err + } + } + + // 只有在有更新时才保存订阅信息 + if updated { + if err := tx.Where("id = ?", sub.Id).Save(sub).Error; err != nil { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: "update subscription error: " + err.Error(), + }) + return nil + } + } + + return nil +} + +func (l *QuotaTaskLogic) processGift(tx *gorm.DB, sub *user.Subscribe, content task.QuotaContent, now time.Time, errors *[]ErrorInfo) error { + // 验证赠送类型 + if content.GiftType != 1 && content.GiftType != 2 { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: fmt.Sprintf("invalid gift type: %d", content.GiftType), + }) + return nil + } + + var userInfo user.User + if err := tx.Model(&user.User{}).Where("id = ?", sub.UserId).First(&userInfo).Error; err != nil { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: "find user error: " + err.Error(), + }) + return nil + } + + var giftAmount int64 + switch content.GiftType { + case 1: + giftAmount = int64(content.GiftValue) + case 2: + // 获取订阅对应的套餐信息 + subscribeInfo, err := l.svcCtx.SubscribeModel.FindOne(context.Background(), sub.SubscribeId) + if err != nil { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: "find subscribe error: " + err.Error(), + }) + return nil + } + if subscribeInfo.UnitPrice > 0 { + giftAmount = int64(float64(subscribeInfo.UnitPrice) * (float64(content.GiftValue) / 100)) + } + } + + if giftAmount > 0 { + userInfo.GiftAmount += giftAmount + // 使用Update而不是Save,更精确地更新单个字段 + if err := tx.Model(&user.User{}).Where("id = ?", sub.UserId).Update("gift_amount", userInfo.GiftAmount).Error; err != nil { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: "update user gift amount error: " + err.Error(), + }) + return nil + } + + if err := l.createGiftLog(tx, sub.Id, userInfo.Id, giftAmount, userInfo.GiftAmount, now); err != nil { + *errors = append(*errors, ErrorInfo{ + UserSubscribeId: sub.Id, + Error: "create gift log error: " + err.Error(), + }) + // 回滚用户金额更新 + userInfo.GiftAmount -= giftAmount + tx.Model(&user.User{}).Where("id = ?", sub.UserId).Update("gift_amount", userInfo.GiftAmount) + return nil + } + } + + return nil +} + +func (l *QuotaTaskLogic) getStartTime(sub *user.Subscribe, now time.Time) time.Time { + if sub.StartTime.Unix() == 0 { + return now + } + return sub.StartTime +} + +func (l *QuotaTaskLogic) createGiftLog(tx *gorm.DB, subscribeId, userId, amount, balance int64, now time.Time) error { + giftLog := &log.Gift{ + Type: log.GiftTypeIncrease, + OrderNo: "", + SubscribeId: subscribeId, + Amount: amount, + Balance: balance, + Remark: "Quota task gift", + Timestamp: now.UnixMilli(), + } + + logString, err := giftLog.Marshal() + if err != nil { + return fmt.Errorf("marshal gift log error: %v", err) + } + return tx.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeGift.Uint8(), + Content: string(logString), + ObjectID: userId, + Date: now.Format(time.DateOnly), + }).Error +} + +func (l *QuotaTaskLogic) createResetTrafficLog(tx *gorm.DB, subscribeId, userId int64, now time.Time) error { + trafficLog := &log.ResetSubscribe{ + Type: log.ResetSubscribeTypeQuota, + UserId: userId, + OrderNo: "", + Timestamp: now.UnixMilli(), + } + + logString, err := trafficLog.Marshal() + if err != nil { + return fmt.Errorf("marshal traffic log error: %v", err) + } + return tx.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeResetSubscribe.Uint8(), + Content: string(logString), + ObjectID: subscribeId, + Date: now.Format(time.DateOnly), + }).Error +} diff --git a/queue/logic/traffic/resetTrafficLogic.go b/queue/logic/traffic/resetTrafficLogic.go new file mode 100644 index 0000000..bbfee15 --- /dev/null +++ b/queue/logic/traffic/resetTrafficLogic.go @@ -0,0 +1,625 @@ +package traffic + +import ( + "context" + "encoding/json" + "errors" + "strconv" + "strings" + "time" + + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/subscribe" + "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/queue/types" + + "github.com/hibiken/asynq" + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +// ResetTrafficLogic handles traffic reset logic for different subscription cycles +// Supports three reset modes: +// - reset_cycle = 1: Reset on 1st of every month +// - reset_cycle = 2: Reset monthly based on subscription start date +// - reset_cycle = 3: Reset yearly based on subscription start date +type ResetTrafficLogic struct { + svc *svc.ServiceContext +} + +// Cache and retry configuration constants +const ( + maxRetryAttempts = 3 + retryDelay = 30 * time.Minute + lockTimeout = 5 * time.Minute +) + +// Cache keys +var ( + cacheKey = "reset_traffic_cache" + retryCountKey = "reset_traffic_retry_count" + lockKey = "reset_traffic_lock" +) + +// resetTrafficCache stores the last reset time to prevent duplicate processing +type resetTrafficCache struct { + LastResetTime time.Time +} + +func NewResetTrafficLogic(svc *svc.ServiceContext) *ResetTrafficLogic { + return &ResetTrafficLogic{ + svc: svc, + } +} + +// ProcessTask executes the traffic reset task for all subscription types with enhanced retry mechanism +func (l *ResetTrafficLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error { + var err error + startTime := time.Now() + + // Get current retry count + retryCount := l.getRetryCount(ctx) + logger.Infow("[ResetTraffic] Starting task execution", + logger.Field("retryCount", retryCount), + logger.Field("startTime", startTime)) + + // Acquire distributed lock to prevent duplicate execution + lockAcquired := l.acquireLock(ctx) + if !lockAcquired { + logger.Infow("[ResetTraffic] Another task is already running, skipping execution") + return nil + } + defer l.releaseLock(ctx) + + defer func() { + if err != nil { + // Check if error is retryable and within retry limit + if l.isRetryableError(err) && retryCount < maxRetryAttempts { + // Increment retry count + l.setRetryCount(ctx, retryCount+1) + + // Schedule retry with delay + task := asynq.NewTask(types.SchedulerResetTraffic, nil) + _, retryErr := l.svc.Queue.Enqueue(task, asynq.ProcessIn(retryDelay)) + if retryErr != nil { + logger.Errorw("[ResetTraffic] Failed to enqueue retry task", + logger.Field("error", retryErr.Error()), + logger.Field("retryCount", retryCount)) + } else { + logger.Infow("[ResetTraffic] Task failed, retrying in 30 minutes", + logger.Field("error", err.Error()), + logger.Field("retryCount", retryCount+1), + logger.Field("maxRetryAttempts", maxRetryAttempts)) + } + } else { + // Max retries reached or non-retryable error + if retryCount >= maxRetryAttempts { + logger.Errorw("[ResetTraffic] Max retry attempts reached, giving up", + logger.Field("retryCount", retryCount), + logger.Field("maxRetryAttempts", maxRetryAttempts), + logger.Field("error", err.Error())) + } else { + logger.Errorw("[ResetTraffic] Non-retryable error, not retrying", + logger.Field("error", err.Error()), + logger.Field("retryCount", retryCount)) + } + // Reset retry count for next scheduled task + l.clearRetryCount(ctx) + } + } else { + // Task completed successfully, reset retry count + l.clearRetryCount(ctx) + logger.Infow("[ResetTraffic] Task completed successfully", + logger.Field("processingTime", time.Since(startTime)), + logger.Field("retryCount", retryCount)) + } + }() + + // Load last reset time from cache + var cache resetTrafficCache + cacheData, err := l.svc.Redis.Get(ctx, cacheKey).Result() + if err != nil { + if !errors.Is(err, redis.Nil) { + logger.Errorw("[ResetTraffic] Failed to get cache", logger.Field("error", err.Error())) + } + // Set default value if cache not found + cache = resetTrafficCache{ + LastResetTime: time.Now().Add(-10 * time.Minute), + } + logger.Infow("[ResetTraffic] Using default cache value", logger.Field("lastResetTime", cache.LastResetTime)) + } else { + // Parse JSON data + if err := json.Unmarshal([]byte(cacheData), &cache); err != nil { + logger.Errorw("[ResetTraffic] Failed to unmarshal cache", logger.Field("error", err.Error())) + cache = resetTrafficCache{ + LastResetTime: time.Now().Add(-10 * time.Minute), + } + } else { + logger.Infow("[ResetTraffic] Cache loaded successfully", logger.Field("lastResetTime", cache.LastResetTime)) + } + } + + // Execute reset operations in order: yearly -> monthly (1st) -> monthly (cycle) + err = l.resetYear(ctx) + if err != nil { + logger.Errorw("[ResetTraffic] Yearly reset failed", logger.Field("error", err.Error())) + return err + } + + err = l.reset1st(ctx, cache) + if err != nil { + logger.Errorw("[ResetTraffic] Monthly 1st reset failed", logger.Field("error", err.Error())) + return err + } + + err = l.resetMonth(ctx) + if err != nil { + logger.Errorw("[ResetTraffic] Monthly cycle reset failed", logger.Field("error", err.Error())) + return err + } + + // Update cache with current time after successful processing + updatedCache := resetTrafficCache{ + LastResetTime: startTime, + } + cacheDataBytes, marshalErr := json.Marshal(updatedCache) + if marshalErr != nil { + logger.Errorw("[ResetTraffic] Failed to marshal cache", logger.Field("error", marshalErr.Error())) + } else { + cacheErr := l.svc.Redis.Set(ctx, cacheKey, cacheDataBytes, 0).Err() + if cacheErr != nil { + logger.Errorw("[ResetTraffic] Failed to update cache", logger.Field("error", cacheErr.Error())) + // Don't return error here as the main task completed successfully + } else { + logger.Infow("[ResetTraffic] Cache updated successfully", logger.Field("newLastResetTime", startTime)) + } + } + + return nil +} + +// resetMonth handles monthly cycle reset based on subscription start date +// reset_cycle = 2: Reset monthly based on subscription start date +func (l *ResetTrafficLogic) resetMonth(ctx context.Context) error { + now := time.Now() + + err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error { + // Get all subscriptions that reset monthly based on start date + var resetMonthSubIds []int64 + err := db.Model(&subscribe.Subscribe{}).Select("`id`").Where("`reset_cycle` = ?", 2).Find(&resetMonthSubIds).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to query monthly subscriptions", logger.Field("error", err.Error())) + return err + } + + if len(resetMonthSubIds) == 0 { + logger.Infow("[ResetTraffic] No monthly cycle subscriptions found") + return nil + } + + // Query users for monthly reset based on subscription start date cycle + var monthlyResetUsers []int64 + + // Check if today is the last day of current month + isLastDayOfMonth := now.AddDate(0, 0, 1).Month() != now.Month() + + query := db.Model(&user.Subscribe{}).Select("`id`"). + Where("`subscribe_id` IN ?", resetMonthSubIds). + Where("`status` IN ?", []int64{1, 2}). // Only active subscriptions + Where("TIMESTAMPDIFF(MONTH, CURDATE(),DATE(expire_time)) >= 1") // At least 1 month passed + + if isLastDayOfMonth { + // Last day of month: handle subscription start dates >= today + query = query.Where("DAY(`expire_time`) >= ?", now.Day()) + } else { + // Normal case: exact day match + query = query.Where("DAY(`expire_time`) = ?", now.Day()) + } + + err = query.Find(&monthlyResetUsers).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to query monthly reset users", logger.Field("error", err.Error())) + return err + } + + if len(monthlyResetUsers) > 0 { + logger.Infow("[ResetTraffic] Found users for monthly reset", + logger.Field("count", len(monthlyResetUsers)), + logger.Field("userIds", monthlyResetUsers)) + + err = db.Model(&user.Subscribe{}).Where("`id` IN ?", monthlyResetUsers). + Updates(map[string]interface{}{ + "upload": 0, + "download": 0, + "status": 1, // Ensure status is active + "finished_at": nil, + }).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to update monthly reset users", logger.Field("error", err.Error())) + return err + } + // Find user subscriptions for these users + var userSubs []*user.Subscribe + err = db.Model(&user.Subscribe{}).Where("`id` IN ?", monthlyResetUsers).Find(&userSubs).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to find user subscriptions for 1st reset", logger.Field("error", err.Error())) + return err + } + // Clear cache for these subscriptions + l.clearCache(ctx, userSubs) + logger.Infow("[ResetTraffic] Monthly reset completed", logger.Field("count", len(monthlyResetUsers))) + } else { + logger.Infow("[ResetTraffic] No users found for monthly reset") + } + return l.svc.SubscribeModel.ClearCache(ctx, resetMonthSubIds...) + }) + if err != nil { + logger.Errorw("[ResetTraffic] Monthly reset transaction failed", logger.Field("error", err.Error())) + return err + } + + logger.Infow("[ResetTraffic] Monthly reset process completed") + return nil +} + +// reset1st handles reset on 1st of every month +// reset_cycle = 1: Reset on 1st of every month +func (l *ResetTrafficLogic) reset1st(ctx context.Context, cache resetTrafficCache) error { + now := time.Now() + + // Check if we already reset this month using cache + if cache.LastResetTime.Year() == now.Year() && cache.LastResetTime.Month() == now.Month() { + logger.Infow("[ResetTraffic] Already reset this month, skipping 1st reset", + logger.Field("lastResetTime", cache.LastResetTime), + logger.Field("currentTime", now)) + return nil + } + + // Only reset if it's the 1st day of the month + if now.Day() != 1 { + logger.Infow("[ResetTraffic] Not 1st day of month, skipping 1st reset", logger.Field("currentDay", now.Day())) + return nil + } + + err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error { + // Get all subscriptions that reset on 1st of month + var reset1stSubIds []int64 + err := db.Model(&subscribe.Subscribe{}).Select("`id`").Where("`reset_cycle` = ?", 1).Find(&reset1stSubIds).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to query 1st reset subscriptions", logger.Field("error", err.Error())) + return err + } + + if len(reset1stSubIds) == 0 { + logger.Infow("[ResetTraffic] No 1st reset subscriptions found") + return nil + } + + // Get all active users with these subscriptions + var users1stReset []int64 + err = db.Model(&user.Subscribe{}).Select("`id`"). + Where("`subscribe_id` IN ?", reset1stSubIds). + Where("`status` IN ?", []int64{1, 2}). // Only active subscriptions + Find(&users1stReset).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to query 1st reset users", logger.Field("error", err.Error())) + return err + } + + if len(users1stReset) > 0 { + logger.Infow("[ResetTraffic] Found users for 1st reset", + logger.Field("count", len(users1stReset)), + logger.Field("userIds", users1stReset)) + + // Reset upload and download traffic to zero + err = db.Model(&user.Subscribe{}).Where("`id` IN ?", users1stReset). + Updates(map[string]interface{}{ + "upload": 0, + "download": 0, + "status": 1, // Ensure status is active + "finished_at": nil, + }).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to update 1st reset users", logger.Field("error", err.Error())) + return err + } + var userSubs []*user.Subscribe + err = db.Model(&user.Subscribe{}).Where("`id` IN ?", users1stReset).Find(&userSubs).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to find user subscriptions for 1st reset", logger.Field("error", err.Error())) + return err + } + + // Clear cache for these subscriptions + l.clearCache(ctx, userSubs) + logger.Infow("[ResetTraffic] 1st reset completed", logger.Field("count", len(users1stReset))) + } else { + logger.Infow("[ResetTraffic] No users found for 1st reset") + } + + return l.svc.SubscribeModel.ClearCache(ctx, reset1stSubIds...) + }) + + if err != nil { + logger.Errorw("[ResetTraffic] 1st reset transaction failed", logger.Field("error", err.Error())) + return err + } + logger.Infow("[ResetTraffic] 1st reset process completed") + return nil +} + +// resetYear handles yearly reset based on subscription start date anniversary +// reset_cycle = 3: Reset yearly based on subscription start date +func (l *ResetTrafficLogic) resetYear(ctx context.Context) error { + now := time.Now() + + err := l.svc.UserModel.Transaction(ctx, func(db *gorm.DB) error { + // Get all subscriptions that reset yearly + var resetYearSubIds []int64 + err := db.Model(&subscribe.Subscribe{}).Select("`id`").Where("`reset_cycle` = ?", 3).Find(&resetYearSubIds).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to query yearly subscriptions", logger.Field("error", err.Error())) + return err + } + + if len(resetYearSubIds) == 0 { + logger.Infow("[ResetTraffic] No yearly reset subscriptions found") + return nil + } + + // Query users for yearly reset based on subscription start date anniversary + var usersYearReset []int64 + + // Check if today is February 28th (handle leap year case) + isLeapYearCase := now.Month() == 2 && now.Day() == 28 + + query := db.Model(&user.Subscribe{}).Select("`id`"). + Where("`subscribe_id` IN ?", resetYearSubIds). + Where("MONTH(expire_time) = ?", now.Month()). // Same month + Where("`status` IN ?", []int64{1, 2}). // Only active subscriptions + Where("TIMESTAMPDIFF(YEAR, CURDATE(),DATE(expire_time)) >= 1") // At least 1 year passed + if isLeapYearCase { + // February 28th: handle both Feb 28 and Feb 29 subscriptions + query = query.Where("DAY(expire_time) IN (28, 29)") + } else { + // Normal case: exact day match + query = query.Where("DAY(expire_time) = ?", now.Day()) + } + + err = query.Find(&usersYearReset).Error + if err != nil { + logger.Errorw("[ResetTraffic] Query yearly reset users failed", logger.Field("error", err.Error())) + return err + } + + if len(usersYearReset) > 0 { + logger.Infow("[ResetTraffic] Found users for yearly reset", + logger.Field("count", len(usersYearReset)), + logger.Field("userIds", usersYearReset)) + + // Reset upload and download traffic to zero + err = db.Model(&user.Subscribe{}).Where("`id` IN ?", usersYearReset). + Updates(map[string]interface{}{ + "upload": 0, + "download": 0, + "status": 1, // Ensure status is active + "finished_at": nil, + }).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to update yearly reset users", logger.Field("error", err.Error())) + return err + } + // Find user subscriptions for these users + var userSubs []*user.Subscribe + err = db.Model(&user.Subscribe{}).Where("`id` IN ?", usersYearReset).Find(&userSubs).Error + if err != nil { + logger.Errorw("[ResetTraffic] Failed to find user subscriptions for 1st reset", logger.Field("error", err.Error())) + return err + } + // Clear cache for these subscriptions + l.clearCache(ctx, userSubs) + logger.Infow("[ResetTraffic] Yearly reset completed", logger.Field("count", len(usersYearReset))) + } else { + logger.Infow("[ResetTraffic] No users found for yearly reset") + } + err = l.svc.SubscribeModel.ClearCache(ctx, resetYearSubIds...) + if err != nil { + logger.Errorw("[ResetTraffic] Failed to clear yearly reset subscription cache", logger.Field("error", err.Error())) + } + return nil + }) + + if err != nil { + logger.Errorw("[ResetTraffic] Yearly reset transaction failed", logger.Field("error", err.Error())) + return err + } + + logger.Infow("[ResetTraffic] Yearly reset process completed") + return nil +} + +// getRetryCount retrieves the current retry count from Redis +func (l *ResetTrafficLogic) getRetryCount(ctx context.Context) int { + countStr, err := l.svc.Redis.Get(ctx, retryCountKey).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + return 0 // No retry count found, start with 0 + } + logger.Errorw("[ResetTraffic] Failed to get retry count", logger.Field("error", err.Error())) + return 0 + } + + count, err := strconv.Atoi(countStr) + if err != nil { + logger.Errorw("[ResetTraffic] Invalid retry count format", logger.Field("value", countStr)) + return 0 + } + + return count +} + +// setRetryCount sets the retry count in Redis +func (l *ResetTrafficLogic) setRetryCount(ctx context.Context, count int) { + err := l.svc.Redis.Set(ctx, retryCountKey, count, 24*time.Hour).Err() + if err != nil { + logger.Errorw("[ResetTraffic] Failed to set retry count", + logger.Field("count", count), + logger.Field("error", err.Error())) + } +} + +// clearRetryCount removes the retry count from Redis +func (l *ResetTrafficLogic) clearRetryCount(ctx context.Context) { + err := l.svc.Redis.Del(ctx, retryCountKey).Err() + if err != nil { + logger.Errorw("[ResetTraffic] Failed to clear retry count", logger.Field("error", err.Error())) + } +} + +// acquireLock attempts to acquire a distributed lock +func (l *ResetTrafficLogic) acquireLock(ctx context.Context) bool { + result := l.svc.Redis.SetNX(ctx, lockKey, "locked", lockTimeout) + acquired, err := result.Result() + if err != nil { + logger.Errorw("[ResetTraffic] Failed to acquire lock", logger.Field("error", err.Error())) + return false + } + + if acquired { + logger.Infow("[ResetTraffic] Lock acquired successfully") + } else { + logger.Infow("[ResetTraffic] Lock already exists, another task is running") + } + + return acquired +} + +// releaseLock releases the distributed lock +func (l *ResetTrafficLogic) releaseLock(ctx context.Context) { + err := l.svc.Redis.Del(ctx, lockKey).Err() + if err != nil { + logger.Errorw("[ResetTraffic] Failed to release lock", logger.Field("error", err.Error())) + } else { + logger.Infow("[ResetTraffic] Lock released successfully") + } +} + +// isRetryableError determines if an error is retryable +func (l *ResetTrafficLogic) isRetryableError(err error) bool { + if err == nil { + return false + } + + errorMessage := strings.ToLower(err.Error()) + + // Network and connection errors (retryable) + retryableErrors := []string{ + "connection refused", + "connection reset", + "connection timeout", + "network", + "timeout", + "dial", + "context deadline exceeded", + "temporary failure", + "server error", + "service unavailable", + "internal server error", + "database is locked", + "too many connections", + "deadlock", + "lock wait timeout", + } + + // Database constraint errors (non-retryable) + nonRetryableErrors := []string{ + "foreign key constraint", + "unique constraint", + "check constraint", + "not null constraint", + "invalid input syntax", + "column does not exist", + "table does not exist", + "permission denied", + "access denied", + "authentication failed", + "invalid credentials", + } + + // Check for non-retryable errors first + for _, nonRetryable := range nonRetryableErrors { + if strings.Contains(errorMessage, nonRetryable) { + logger.Infow("[ResetTraffic] Non-retryable error detected", + logger.Field("error", err.Error()), + logger.Field("pattern", nonRetryable)) + return false + } + } + + // Check for retryable errors + for _, retryable := range retryableErrors { + if strings.Contains(errorMessage, retryable) { + logger.Infow("[ResetTraffic] Retryable error detected", + logger.Field("error", err.Error()), + logger.Field("pattern", retryable)) + return true + } + } + + // Default: treat unknown errors as retryable, but log for analysis + logger.Infow("[ResetTraffic] Unknown error type, treating as retryable", + logger.Field("error", err.Error())) + return true +} + +// clearCache clears the reset traffic cache +func (l *ResetTrafficLogic) clearCache(ctx context.Context, list []*user.Subscribe) { + if len(list) != 0 { + subs := make(map[int64]bool) + + for _, sub := range list { + if sub.SubscribeId > 0 { + err := l.svc.UserModel.ClearSubscribeCache(ctx, sub) + if err != nil { + logger.Errorw("[ResetTraffic] Failed to clear cache for subscription", + logger.Field("subscribeId", sub.SubscribeId), + logger.Field("error", err.Error())) + } + if _, ok := subs[sub.SubscribeId]; !ok { + subs[sub.SubscribeId] = true + } + } + // Insert traffic reset log + l.insertLog(ctx, sub.Id, sub.UserId) + } + + for sub, _ := range subs { + if err := l.svc.SubscribeModel.ClearCache(ctx, sub); err != nil { + logger.Errorw("[ResetTraffic] Failed to clear subscription cache", + logger.Field("subscribeId", sub), + logger.Field("error", err.Error()), + ) + } + } + } +} + +// insertLog inserts a reset traffic log entry +func (l *ResetTrafficLogic) insertLog(ctx context.Context, subId, userId int64) { + trafficLog := log.ResetSubscribe{ + Type: log.ResetSubscribeTypeAuto, + UserId: userId, + Timestamp: time.Now().UnixMilli(), + } + content, _ := trafficLog.Marshal() + if err := l.svc.DB.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeResetSubscribe.Uint8(), + ObjectID: subId, + Date: time.Now().Format(time.DateOnly), + Content: string(content), + }).Error; err != nil { + logger.Errorw("[ResetTraffic] Failed to create system log for subscription", logger.Field("error", err.Error())) + } +} diff --git a/queue/logic/traffic/serverDataLogic.go b/queue/logic/traffic/serverDataLogic.go index 83516de..f8ca675 100644 --- a/queue/logic/traffic/serverDataLogic.go +++ b/queue/logic/traffic/serverDataLogic.go @@ -5,12 +5,12 @@ import ( "encoding/json" "time" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/config" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/internal/types" + "github.com/perfect-panel/server/internal/config" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" ) type ServerDataLogic struct { @@ -73,7 +73,7 @@ func (l *ServerDataLogic) getRanking(ctx context.Context) (top10ServerToday, top if s.ServerId == 0 { continue } - serverInfo, err := l.svc.ServerModel.FindOne(ctx, s.ServerId) + serverInfo, err := l.svc.NodeModel.FindOneServer(ctx, s.ServerId) if err != nil { logger.Error("[ServerDataLogic] Find server failed", logger.Field("error", err.Error())) continue @@ -92,7 +92,7 @@ func (l *ServerDataLogic) getRanking(ctx context.Context) (top10ServerToday, top logger.Error("[ServerDataLogic] Get top servers traffic by day failed", logger.Field("error", err.Error())) } else { for _, s := range serverYesterday { - serverInfo, err := l.svc.ServerModel.FindOne(ctx, s.ServerId) + serverInfo, err := l.svc.NodeModel.FindOneServer(ctx, s.ServerId) if err != nil { logger.Error("[ServerDataLogic] Find server failed", logger.Field("error", err.Error())) continue diff --git a/queue/logic/traffic/trafficStatLogic.go b/queue/logic/traffic/trafficStatLogic.go new file mode 100644 index 0000000..81a422b --- /dev/null +++ b/queue/logic/traffic/trafficStatLogic.go @@ -0,0 +1,176 @@ +package traffic + +import ( + "context" + "time" + + "github.com/hibiken/asynq" + "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/model/traffic" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" +) + +type StatLogic struct { + svc *svc.ServiceContext +} + +func NewStatLogic(svc *svc.ServiceContext) *StatLogic { + return &StatLogic{ + svc: svc, + } +} + +func (l *StatLogic) ProcessTask(ctx context.Context, _ *asynq.Task) error { + now := time.Now() + tx := l.svc.DB.Begin() + var err error + defer func(err error) { + if err != nil { + logger.Errorf("[Traffic Stat Queue] Process task failed: %v", err.Error()) + tx.Rollback() + } else { + logger.Infof("[Traffic Stat Queue] Process task completed successfully, consuming: %s", time.Since(now).String()) + // 提交事务 + if err = tx.Commit().Error; err != nil { + logger.Errorf("[Traffic Stat Queue] Commit transaction failed: %v", err.Error()) + } + } + }(err) + + // 获取全部有效订阅 + var userTraffic []log.UserTraffic + // 获取统计时间范围 + start := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, time.Local) + end := start.Add(24 * time.Hour).Add(-time.Nanosecond) + + // 查询用户流量统计, 按用户和订阅分组 + err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}). + Select("user_id, subscribe_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Group("user_id, subscribe_id"). + Order("total DESC"). + Scan(&userTraffic).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Query user traffic failed: %v", err.Error()) + return err + } + + date := start.Format(time.DateOnly) + + userTop10 := log.UserTrafficRank{ + Rank: make(map[uint8]log.UserTraffic), + } + + // 更新用户流量统计 + for i, trafficData := range userTraffic { + if i < 10 { + userTop10.Rank[uint8(i+1)] = trafficData + } + // 更新用户流量统计日志 + content, _ := trafficData.Marshal() + err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeSubscribeTraffic.Uint8(), + Date: date, + ObjectID: trafficData.SubscribeId, + Content: string(content), + }).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Create user traffic log failed: %v", err.Error()) + return err + } + } + + userTop10Content, _ := userTop10.Marshal() + + // 更新用户排行榜 + err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeUserTrafficRank.Uint8(), + Date: date, + ObjectID: 0, // 0表示全局用户排行榜 + Content: string(userTop10Content), + }).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Create user traffic rank log failed: %v", err.Error()) + return err + } + + // 统计服务器流量 + var serverTraffic []log.ServerTraffic + err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}). + Select("server_id, SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Group("server_id"). + Order("total DESC"). + Scan(&serverTraffic).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Query server traffic failed: %v", err.Error()) + return err + } + + serverTop10 := log.ServerTrafficRank{ + Rank: make(map[uint8]log.ServerTraffic), + } + for i, trafficData := range serverTraffic { + if i < 10 { + serverTop10.Rank[uint8(i+1)] = trafficData + } + // 更新服务器流量统计日志 + content, _ := trafficData.Marshal() + err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeServerTraffic.Uint8(), + Date: date, + ObjectID: trafficData.ServerId, + Content: string(content), + }).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Create server traffic log failed: %v", err.Error()) + return err + } + } + serverTop10Content, _ := serverTop10.Marshal() + // 更新服务器排行榜 + err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeServerTrafficRank.Uint8(), + Date: date, + ObjectID: 0, // 0表示全局服务器排行榜 + Content: string(serverTop10Content), + }).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Create server traffic rank log failed: %v", err.Error()) + return err + } + + // traffic stat + var stat log.TrafficStat + err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}). + Select("SUM(download + upload) AS total, SUM(download) AS download, SUM(upload) AS upload"). + Where("timestamp BETWEEN ? AND ?", start, end). + Scan(&stat).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Query traffic stat failed: %v", err.Error()) + return err + } + + // 更新流量统计日志 + content, _ := stat.Marshal() + err = tx.WithContext(ctx).Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeTrafficStat.Uint8(), + Date: date, + ObjectID: 0, + Content: string(content), + }).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Create traffic stat log failed: %v", err.Error()) + return err + } + + // Delete old traffic logs + if l.svc.Config.Log.AutoClear { + err = tx.WithContext(ctx).Model(&traffic.TrafficLog{}).Where("created_at <= ?", end.AddDate(0, 0, int(-l.svc.Config.Log.ClearDays))).Delete(&traffic.TrafficLog{}).Error + if err != nil { + logger.Errorf("[Traffic Stat Queue] Delete server traffic log failed: %v", err.Error()) + } + } + return nil +} diff --git a/queue/logic/traffic/trafficStatisticsLogic.go b/queue/logic/traffic/trafficStatisticsLogic.go index d9b3a6d..ed98cd1 100644 --- a/queue/logic/traffic/trafficStatisticsLogic.go +++ b/queue/logic/traffic/trafficStatisticsLogic.go @@ -3,14 +3,16 @@ package traffic import ( "context" "encoding/json" + "strings" "time" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/internal/model/node" + "github.com/perfect-panel/server/pkg/logger" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/model/traffic" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/model/traffic" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/queue/types" ) //goland:noinspection GoNameStartsWithPackageName @@ -38,7 +40,7 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta return nil } // query server info - serverInfo, err := l.svc.ServerModel.FindOne(ctx, payload.ServerId) + serverInfo, err := l.svc.NodeModel.FindOneServer(ctx, payload.ServerId) if err != nil { logger.WithContext(ctx).Error("[TrafficStatistics] Find server info failed", logger.Field("serverId", payload.ServerId), @@ -46,27 +48,38 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta ) return nil } - if serverInfo.TrafficRatio == 0 { - logger.WithContext(ctx).Error("[TrafficStatistics] Server log ratio is 0", - logger.Field("serverId", payload.ServerId), - ) + // query protocol ratio + // default ratio is 1.0 + + protocols, err := serverInfo.UnmarshalProtocols() + if err != nil { + logger.Errorf("[TrafficStatistics] Unmarshal protocols failed: %s", err.Error()) return nil } + var protocol *node.Protocol + + var ratio float32 = 1.0 + + for _, p := range protocols { + if strings.ToLower(p.Type) == strings.ToLower(payload.Protocol) { + protocol = &p + break + } + } + + if protocol == nil { + logger.WithContext(ctx).Error("[TrafficStatistics] Protocol not found: %s", payload.Protocol) + return nil + } + + // use protocol ratio if it's greater than 0 + if protocol.Ratio > 0 { + ratio = float32(protocol.Ratio) + } + now := time.Now() realTimeMultiplier := l.svc.NodeMultiplierManager.GetMultiplier(now) for _, log := range payload.Logs { - // update user subscribe with log - d := int64(float32(log.Download) * serverInfo.TrafficRatio * realTimeMultiplier) - u := int64(float32(log.Upload) * serverInfo.TrafficRatio * realTimeMultiplier) - if err := l.svc.UserModel.UpdateUserSubscribeWithTraffic(ctx, log.SID, d, u); err != nil { - logger.WithContext(ctx).Error("[TrafficStatistics] Update user subscribe with log failed", - logger.Field("sid", log.SID), - logger.Field("download", float32(log.Download)*serverInfo.TrafficRatio), - logger.Field("upload", float32(log.Upload)*serverInfo.TrafficRatio), - logger.Field("error", err.Error()), - ) - continue - } // query user Subscribe Info sub, err := l.svc.UserModel.FindOneSubscribe(ctx, log.SID) if err != nil { @@ -77,8 +90,25 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta continue } + if log.Download+log.Upload <= l.svc.Config.Node.TrafficReportThreshold { + // no traffic, skip + continue + } + // update user subscribe with log + d := int64(float32(log.Download) * ratio * realTimeMultiplier) + u := int64(float32(log.Upload) * ratio * realTimeMultiplier) + if err := l.svc.UserModel.UpdateUserSubscribeWithTraffic(ctx, sub.Id, d, u); err != nil { + logger.WithContext(ctx).Error("[TrafficStatistics] Update user subscribe with log failed", + logger.Field("sid", log.SID), + logger.Field("download", float32(log.Download)*ratio), + logger.Field("upload", float32(log.Upload)*ratio), + logger.Field("error", err.Error()), + ) + continue + } + // create log log - if err := l.svc.TrafficLogModel.Insert(ctx, &traffic.TrafficLog{ + if err = l.svc.TrafficLogModel.Insert(ctx, &traffic.TrafficLog{ ServerId: payload.ServerId, SubscribeId: log.SID, UserId: sub.UserId, @@ -88,8 +118,8 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta }); err != nil { logger.WithContext(ctx).Error("[TrafficStatistics] Create log log failed", logger.Field("uid", log.SID), - logger.Field("download", float32(log.Download)*serverInfo.TrafficRatio), - logger.Field("upload", float32(log.Upload)*serverInfo.TrafficRatio), + logger.Field("download", float32(log.Download)*ratio), + logger.Field("upload", float32(log.Upload)*ratio), logger.Field("error", err.Error()), ) } diff --git a/queue/queue.go b/queue/queue.go index 6384412..abc5478 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -2,9 +2,9 @@ package queue import ( "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/pkg/logger" - "github.com/perfect-panel/ppanel-server/queue/handler" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/queue/handler" ) type Service struct { diff --git a/queue/types/email.go b/queue/types/email.go index 2cbdf2e..4fee979 100644 --- a/queue/types/email.go +++ b/queue/types/email.go @@ -5,10 +5,19 @@ const ( ForthwithSendEmail = "forthwith:email:send" ) +const ( + EmailTypeVerify = "verify" + EmailTypeMaintenance = "maintenance" + EmailTypeExpiration = "expiration" + EmailTypeTrafficExceed = "traffic_exceed" + EmailTypeCustom = "custom" +) + type ( SendEmailPayload struct { - Email string `json:"to"` - Subject string `json:"subject"` - Content string `json:"content"` + Type string `json:"type"` + Email string `json:"to"` + Subject string `json:"subject"` + Content map[string]interface{} `json:"content"` } ) diff --git a/queue/types/order.go b/queue/types/order.go index 5d05f9f..f35c570 100644 --- a/queue/types/order.go +++ b/queue/types/order.go @@ -6,9 +6,6 @@ const ( ) type ( - DeferCheckOrderLogic struct { - OrderNo string `json:"order_no"` - } DeferCloseOrderPayload struct { OrderNo string `json:"order_no"` } diff --git a/queue/types/scheduler.go b/queue/types/scheduler.go index 13b3028..51ef48c 100644 --- a/queue/types/scheduler.go +++ b/queue/types/scheduler.go @@ -3,5 +3,6 @@ package types const ( SchedulerCheckSubscription = "scheduler:check:subscription" SchedulerTotalServerData = "scheduler:total:server" - SchedulerCheckOrder = "scheduler:check:order" + SchedulerResetTraffic = "scheduler:reset:traffic" + SchedulerTrafficStat = "scheduler:traffic:stat" ) diff --git a/queue/types/server.go b/queue/types/server.go index 75ec89c..3202ed1 100644 --- a/queue/types/server.go +++ b/queue/types/server.go @@ -10,6 +10,7 @@ type UserTraffic struct { type TrafficStatistics struct { ServerId int64 `json:"server_id"` + Protocol string `json:"protocol"` Logs []UserTraffic `json:"logs"` } diff --git a/queue/types/sms.go b/queue/types/sms.go index 8d9f9e7..affb521 100644 --- a/queue/types/sms.go +++ b/queue/types/sms.go @@ -1,7 +1,7 @@ package types const ( - // ForthwithSendEmail forthwith send email + // ForthwithSendSms forthwith send email ForthwithSendSms = "forthwith:sms:send" ) diff --git a/queue/types/task.go b/queue/types/task.go new file mode 100644 index 0000000..0115f28 --- /dev/null +++ b/queue/types/task.go @@ -0,0 +1,9 @@ +package types + +const ( + // ScheduledBatchSendEmail scheduled batch send email + ScheduledBatchSendEmail = "scheduled:email:batch" + + // ForthwithQuotaTask create quota task immediately + ForthwithQuotaTask = "forthwith:quota:task" +) diff --git a/readme.md b/readme.md index e60f85a..975f5a7 100644 --- a/readme.md +++ b/readme.md @@ -1,71 +1,291 @@ -## Directory Structure +# PPanel Server -```text +

+ +[![License](https://img.shields.io/github/license/perfect-panel/server)](LICENSE) +[![Go Version](https://img.shields.io/badge/Go-1.21%2B-blue)](https://go.dev/) +[![Go Report Card](https://goreportcard.com/badge/github.com/perfect-panel/server)](https://goreportcard.com/report/github.com/perfect-panel/server) +[![Docker](https://img.shields.io/badge/Docker-Available-blue)](Dockerfile) +[![CI/CD](https://img.shields.io/github/actions/workflow/status/perfect-panel/server/release.yml)](.github/workflows/release.yml) + +**PPanel is a pure, professional, and perfect open-source proxy panel tool, designed for learning and practical use.** + +[English](README.md) | [中文](readme_zh.md) | [Report Bug](https://github.com/perfect-panel/server/issues/new) | [Request Feature](https://github.com/perfect-panel/server/issues/new) + +
+ +> **Article 1.** +> All human beings are born free and equal in dignity and rights. +> They are endowed with reason and conscience and should act towards one another in a spirit of brotherhood. +> +> **Article 12.** +> No one shall be subjected to arbitrary interference with his privacy, family, home or correspondence, nor to attacks upon his honour and reputation. +> Everyone has the right to the protection of the law against such interference or attacks. +> +> **Article 19.** +> Everyone has the right to freedom of opinion and expression; this right includes freedom to hold opinions without interference and to seek, receive and impart information and ideas through any media and regardless of frontiers. +> +> *Source: [United Nations – Universal Declaration of Human Rights (UN.org)](https://www.un.org/sites/un2.un.org/files/2021/03/udhr.pdf)* + +## 📋 Overview + +PPanel Server is the backend component of the PPanel project, providing robust APIs and core functionality for managing +proxy services. Built with Go, it emphasizes performance, security, and scalability. + +### Key Features + +- **Multi-Protocol Support**: Supports Shadowsocks, V2Ray, Trojan, and more. +- **Privacy First**: No user logs are collected, ensuring privacy and security. +- **Minimalist Design**: Simple yet powerful, with complete business logic. +- **User Management**: Full authentication and authorization system. +- **Subscription System**: Manage user subscriptions and service provisioning. +- **Payment Integration**: Supports multiple payment gateways. +- **Order Management**: Track and process user orders. +- **Ticket System**: Built-in customer support and issue tracking. +- **Node Management**: Monitor and control server nodes. +- **API Framework**: Comprehensive RESTful APIs for frontend integration. + +## 🚀 Quick Start + +### Prerequisites + +- **Go**: 1.21 or higher +- **Docker**: Optional, for containerized deployment +- **Git**: For cloning the repository + +### Installation from Source + +1. **Clone the repository**: + ```bash + git clone https://github.com/perfect-panel/ppanel-server.git + cd ppanel-server + ``` + +2. **Install dependencies**: + ```bash + go mod download + ``` + +3. **Generate code**: + ```bash + chmod +x script/generate.sh + ./script/generate.sh + ``` + +4. **Build the project**: + ```bash + make linux-amd64 + ``` + +5. **Run the server**: + ```bash + ./ppanel-server-linux-amd64 run --config etc/ppanel.yaml + ``` + +### 🐳 Docker Deployment + +1. **Build the Docker image**: + ```bash + docker buildx build --platform linux/amd64 -t ppanel-server:latest . + ``` + +2. **Run the container**: + ```bash + docker run --rm -p 8080:8080 -v $(pwd)/etc:/app/etc ppanel-server:latest + ``` + +3. **Use Docker Compose** (create `docker-compose.yml`): + ```yaml + version: '3.8' + services: + ppanel-server: + image: ppanel-server:latest + ports: + - "8080:8080" + volumes: + - ./etc:/app/etc + environment: + - TZ=Asia/Shanghai + ``` + Run: + ```bash + docker-compose up -d + ``` + +4. **Pull from Docker Hub** (after CI/CD publishes): + ```bash + docker pull ppanel/ppanel-server:latest + docker run --rm -p 8080:8080 ppanel/ppanel-server:latest + ``` + +## 📖 API Documentation + +Explore the full API documentation: + +- **Swagger**: [https://ppanel.dev/en-US/swagger/ppanel](https://ppanel.dev/swagger/ppanel) + +The documentation covers all endpoints, request/response formats, and authentication details. + +## 🔗 Related Projects + +| Project | Description | Link | +|------------------|----------------------------|-------------------------------------------------------| +| PPanel Web | Frontend for PPanel | [GitHub](https://github.com/perfect-panel/ppanel-web) | +| PPanel User Web | User interface for PPanel | [Preview](https://user.ppanel.dev) | +| PPanel Admin Web | Admin interface for PPanel | [Preview](https://admin.ppanel.dev) | + +## 🌐 Official Website + +Visit [ppanel.dev](https://ppanel.dev/) for more details. + +## 🏛 Architecture + +![Architecture Diagram](./doc/image/architecture-en.png) + +## 📁 Directory Structure + +``` . -├── etc -├── cmd -├── queue -├── generate -├── initialize -├── go.mod -├── internal -│ ├── config -│ ├── handler -│ ├── middleware -│ ├── logic -│ ├── svc -│ ├── types -│ └── model -├── scheduler -├── pkg -└── script -``` -- apis: API definition files -- etc: Directory for static configuration files -- cmd:Application entry point -- queue:Queue consumption service -- generate:Code generation tools -- initialize: Initialization system configuration -- internal:Internal modules - - config:Configuration file parsing - - handler:HTTP interface handling, with `handler` as the fixed suffix - - middleware:HTTP middleware - - logic:Business logic handling, with `logic` as the fixed suffix - - svc:Service layer encapsulation - - types:Type definitions - - model:Data models -- scheduler:Scheduled tasks -- pkg: Common utility code -- script:Build scripts - - -##### Generate Code - -```bash -$ chmod +x script/generate.sh -$ ./script/generate.sh +├── apis/ # API definition files +├── cmd/ # Application entry point +├── doc/ # Documentation +├── etc/ # Configuration files (e.g., ppanel.yaml) +├── generate/ # Code generation tools +├── initialize/ # System initialization +├── internal/ # Internal modules +│ ├── config/ # Configuration parsing +│ ├── handler/ # HTTP handlers +│ ├── middleware/ # HTTP middleware +│ ├── logic/ # Business logic +│ ├── model/ # Data models +│ ├── svc/ # Service layer +│ └── types/ # Type definitions +├── pkg/ # Utility code +├── queue/ # Queue services +├── scheduler/ # Scheduled tasks +├── script/ # Build scripts +├── go.mod # Go module definition +├── Makefile # Build automation +└── Dockerfile # Docker configuration ``` -##### Generate Swagger +## 💻 Development +### Format API Files ```bash -$ goctl api plugin -plugin goctl-swagger='swagger -filename ppanel.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ppanel.api -dir . +goctl api format --dir apis/user.api ``` -##### Format API File +### Add a New API + +1. Create a new API file in `apis/`. +2. Import it in `apis/ppanel.api`. +3. Regenerate code: + ```bash + ./script/generate.sh + ``` + +### Build for Multiple Platforms + +Use the `Makefile` to build for various platforms (e.g., Linux, Windows, macOS): ```bash -$ goctl api format --dir api/user.api +make all # Builds linux-amd64, darwin-amd64, windows-amd64 +make linux-arm64 # Build for specific platform ``` -##### Build +Supported platforms include: -```bash -$ go build -o ppanel ppanel.go -``` +- Linux: `386`, `amd64`, `arm64`, `armv5-v7`, `mips`, `riscv64`, `loong64`, etc. +- Windows: `386`, `amd64`, `arm64`, `armv7` +- macOS: `amd64`, `arm64` +- FreeBSD: `amd64`, `arm64` -##### Run +## 🤝 Contributing -```bash -$ ./ppanel run --config etc/ppanel.yaml -``` \ No newline at end of file +Contributions are welcome! Please follow the [Contribution Guidelines](CONTRIBUTING.md) for bug fixes, features, or +documentation improvements. + +## ✨ Special Thanks + +A huge thank you to the following outstanding open-source projects that have provided invaluable support for this +project's development! 🚀 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProjectDescriptionProjectDescription
+ + Gin
+ Gin
+ Gin Stars +
+
+ High-performance Go Web framework
+
+ + Gorm
+ Gorm
+ Gorm Stars +
+
+ Powerful Go ORM framework
+
+ + Asynq
+ Asynq
+ Asynq Stars +
+
+ Asynchronous task queue for Go
+
+ + Go-Swagger
+ Go-Swagger
+ Go-Swagger Stars +
+
+ Comprehensive Go Swagger toolkit
+
+ + Go-Zero
+ Go-Zero
+ Go-Zero Stars +
+
+ Go microservices framework (this project's API generator is built on Go-Zero)
+
+
+ +--- + +🎉 **Salute to Open Source**: Thank you to the open-source community for making development simpler and more efficient! +Please give these projects a ⭐ to support the open-source movement! +## 📄 License + +This project is licensed under the [GPL-3.0 License](LICENSE). \ No newline at end of file diff --git a/readme_zh.md b/readme_zh.md new file mode 100644 index 0000000..ee4a1c4 --- /dev/null +++ b/readme_zh.md @@ -0,0 +1,288 @@ +# PPanel 服务端 + +
+ +[![License](https://img.shields.io/github/license/perfect-panel/server)](LICENSE) +[![Go Version](https://img.shields.io/badge/Go-1.21%2B-blue)](https://go.dev/) +[![Go Report Card](https://goreportcard.com/badge/github.com/perfect-panel/server)](https://goreportcard.com/report/github.com/perfect-panel/server) +[![Docker](https://img.shields.io/badge/Docker-Available-blue)](Dockerfile) +[![CI/CD](https://img.shields.io/github/actions/workflow/status/perfect-panel/server/release.yml)](.github/workflows/release.yml) + +**PPanel 是一个纯净、专业、完美的开源代理面板工具,旨在成为您学习和实际使用的理想选择。** + +[English](README.md) | [中文](readme_zh.md) | [报告问题](https://github.com/perfect-panel/server/issues/new) | [功能请求](https://github.com/perfect-panel/server/issues/new) + +
+ +> **第一条** +> 人人生而自由,在尊严与权利上一律平等。 +> 他们赋有理性与良知,应当以兄弟般的精神彼此相待。 +> +> **第十二条** +> 任何人的隐私、家庭、住宅和通信不得任意干涉,其名誉与荣誉不得加以攻击。 +> 人人有权受到法律的保护,以免遭受这种干涉或攻击。 +> +> **第十九条** +> 人人有思想与表达的自由;此项自由包括持有主张而不受干预,以及通过任何媒介、无论国界,自由寻求、接受和传播信息与思想。 +> +> *来源: [United Nations – Universal Declaration of Human Rights (UN.org)](https://www.un.org/sites/un2.un.org/files/2021/03/udhr.pdf)* + +## 📋 概述 + +PPanel 服务端是 PPanel 项目的后端组件,为代理服务提供强大的 API 和核心功能。它基于 Go 语言开发,注重性能、安全性和可扩展性。 + +### 核心特性 + +- **多协议支持**:支持 Shadowsocks、V2Ray、Trojan 等多种加密协议。 +- **隐私保护**:不收集用户日志,确保隐私和安全。 +- **极简设计**:简单易用,保留完整的业务逻辑。 +- **用户管理**:完善的认证和授权系统。 +- **订阅管理**:处理用户订阅和服务开通。 +- **支付集成**:支持多种支付网关。 +- **订单管理**:跟踪和处理用户订单。 +- **工单系统**:内置客户支持和问题跟踪。 +- **节点管理**:监控和控制服务器节点。 +- **API 框架**:提供全面的 RESTful API,供前端集成。 + +## 🚀 快速开始 + +### 前提条件 + +- **Go**:1.21 或更高版本 +- **Docker**:可选,用于容器化部署 +- **Git**:用于克隆仓库 + +### 通过源代码运行 + +1. **克隆仓库**: + ```bash + git clone https://github.com/perfect-panel/ppanel-server.git + cd ppanel-server + ``` + +2. **安装依赖**: + ```bash + go mod download + ``` + +3. **生成代码**: + ```bash + chmod +x script/generate.sh + ./script/generate.sh + ``` + +4. **构建项目**: + ```bash + make linux-amd64 + ``` + +5. **启动服务器**: + ```bash + ./ppanel-server-linux-amd64 run --config etc/ppanel.yaml + ``` + +### 🐳 Docker 部署 + +1. **构建 Docker 镜像**: + ```bash + docker buildx build --platform linux/amd64 -t ppanel-server:latest . + ``` + +2. **运行容器**: + ```bash + docker run --rm -p 8080:8080 -v $(pwd)/etc:/app/etc ppanel-server:latest + ``` + +3. **使用 Docker Compose**(创建 `docker-compose.yml`): + ```yaml + version: '3.8' + services: + ppanel-server: + image: ppanel-server:latest + ports: + - "8080:8080" + volumes: + - ./etc:/app/etc + environment: + - TZ=Asia/Shanghai + ``` + 运行: + ```bash + docker-compose up -d + ``` + +4. **从 Docker Hub 拉取**(CI/CD 发布后): + ```bash + docker pull ppanel/ppanel-server:latest + docker run --rm -p 8080:8080 ppanel/ppanel-server:latest + ``` + +## 📖 API 文档 + +查看完整的 API 文档: + +- **Swagger 文档**:[https://ppanel.dev/zh-CN/swagger/ppanel](https://ppanel.dev/zh-CN/swagger/ppanel) + +文档涵盖所有 API 端点、请求/响应格式及认证要求。 + +## 🔗 相关项目 + +| 项目 | 描述 | 链接 | +|------------------|--------------|-------------------------------------------------------| +| PPanel Web | PPanel 前端应用 | [GitHub](https://github.com/perfect-panel/ppanel-web) | +| PPanel User Web | PPanel 用户界面 | [预览](https://user.ppanel.dev) | +| PPanel Admin Web | PPanel 管理员界面 | [预览](https://admin.ppanel.dev) | + +## 🌐 官方网站 + +访问 [ppanel.dev](https://ppanel.dev) 获取更多信息。 + +## 🏛 系统架构 + +![Architecture Diagram](./doc/image/architecture-zh.png) + +## 📁 目录结构 + +``` +. +├── apis/ # API 定义文件 +├── cmd/ # 应用程序入口 +├── doc/ # 文档 +├── etc/ # 配置文件(如 ppanel.yaml) +├── generate/ # 代码生成工具 +├── initialize/ # 系统初始化 +├── internal/ # 内部模块 +│ ├── config/ # 配置文件解析 +│ ├── handler/ # HTTP 处理程序 +│ ├── middleware/ # HTTP 中间件 +│ ├── logic/ # 业务逻辑 +│ ├── model/ # 数据模型 +│ ├── svc/ # 服务层 +│ └── types/ # 类型定义 +├── pkg/ # 公共工具代码 +├── queue/ # 队列服务 +├── scheduler/ # 定时任务 +├── script/ # 构建脚本 +├── go.mod # Go 模块定义 +├── Makefile # 构建自动化 +└── Dockerfile # Docker 配置 +``` + +## 💻 开发 + +### 格式化 API 文件 +```bash +goctl api format --dir apis/user.api +``` + +### 添加新 API + +1. 在 `apis/` 目录创建新的 API 文件。 +2. 在 `apis/ppanel.api` 中导入新 API。 +3. 重新生成代码: + ```bash + ./script/generate.sh + ``` + +### 多平台构建 + +使用 `Makefile` 构建多种平台(如 Linux、Windows、macOS): + +```bash +make all # 构建 linux-amd64、darwin-amd64、windows-amd64 +make linux-arm64 # 构建特定平台 +``` + +支持的平台包括: + +- Linux:`386`、`amd64`、`arm64`、`armv5-v7`、`mips`、`riscv64`、`loong64` 等 +- Windows:`386`、`amd64`、`arm64`、`armv7` +- macOS:`amd64`、`arm64` +- FreeBSD:`amd64`、`arm64` + +## 🤝 贡献 + +欢迎各种贡献,包括功能开发、错误修复和文档改进。请查看[贡献指南](CONTRIBUTING_ZH.md)了解详情。 + +## ✨ 特别感谢 + +感谢以下优秀的开源项目,它们为本项目的开发提供了强大的支持! 🚀 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
项目描述项目描述
+ + Gin
+ Gin
+ Gin Stars +
+
+ 高性能的 Go Web 框架
+
+ + Gorm
+ Gorm
+ Gorm Stars +
+
+ 功能强大的 Go ORM 框架
+
+ + Asynq
+ Asynq
+ Asynq Stars +
+
+ Go 语言的异步任务队列
+
+ + Go-Swagger
+ Go-Swagger
+ Go-Swagger Stars +
+
+ 完整的 Go Swagger 工具集
+
+ + Go-Zero
+ Go-Zero
+ Go-Zero Stars +
+
+ Go 微服务框架(本项目的 API 生成器,基于 Go-Zero 实现)
+
+
+ +--- + +🎉 **致敬开源**:感谢开源社区,让开发变得更简单、更高效!欢迎为这些项目点亮 ⭐,支持开源事业! + +## 📄 许可证 + +本项目采用 [GPL-3.0 许可证](LICENSE) 授权。 \ No newline at end of file diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index fa6b6f8..377dca3 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -3,11 +3,11 @@ package scheduler import ( "time" - "github.com/perfect-panel/ppanel-server/pkg/logger" + "github.com/perfect-panel/server/pkg/logger" "github.com/hibiken/asynq" - "github.com/perfect-panel/ppanel-server/internal/svc" - "github.com/perfect-panel/ppanel-server/queue/types" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/queue/types" ) type Service struct { @@ -29,16 +29,21 @@ func (m *Service) Start() { if _, err := m.server.Register("@every 60s", checkTask); err != nil { logger.Errorf("register check subscription task failed: %s", err.Error()) } - // schedule total server data task: every 5 minutes - totalServerDataTask := asynq.NewTask(types.SchedulerTotalServerData, nil) - if _, err := m.server.Register("@every 180s", totalServerDataTask); err != nil { - logger.Errorf("register total server data task failed: %s", err.Error()) + //// schedule total server data task: every 5 minutes + //totalServerDataTask := asynq.NewTask(types.SchedulerTotalServerData, nil) + //if _, err := m.server.Register("@every 180s", totalServerDataTask); err != nil { + // logger.Errorf("register total server data task failed: %s", err.Error()) + //} + // schedule reset traffic task: every day at 00:30 + resetTrafficTask := asynq.NewTask(types.SchedulerResetTraffic, nil) + if _, err := m.server.Register("30 0 * * *", resetTrafficTask); err != nil { + logger.Errorf("register reset traffic task failed: %s", err.Error()) } - // schedule total server data task: every 5 minutes - checkOrderTask := asynq.NewTask(types.SchedulerCheckOrder, nil) - if _, err := m.server.Register("@every 10s", checkOrderTask); err != nil { - logger.Errorf("register check order task failed: %s", err.Error()) + // schedule traffic stat task: every day at 00:00 + trafficStatTask := asynq.NewTask(types.SchedulerTrafficStat, nil) + if _, err := m.server.Register("0 0 * * *", trafficStatTask, asynq.MaxRetry(3)); err != nil { + logger.Errorf("register traffic stat task failed: %s", err.Error()) } if err := m.server.Run(); err != nil { diff --git a/script/generate.sh b/script/generate.sh old mode 100644 new mode 100755 index ea116d5..26fd79a --- a/script/generate.sh +++ b/script/generate.sh @@ -6,9 +6,13 @@ ARCH_TYPE=$(uname -m) if [[ "$OS_TYPE" == "Linux" ]]; then echo "The current operating system is Linux" if [[ "$ARCH_TYPE" == "x86_64" ]]; then + echo "Format api file" + ./generate/gopure-linux-amd64 api format --dir ./apis echo "Architecture: amd64" ./generate/gopure-linux-amd64 api go -api *.api -dir . -style goZero elif [[ "$ARCH_TYPE" == "aarch64" ]]; then + echo "Format api file" + ./generate/gopure-linux-arm64 api format --dir ./apis echo "Architecture: arm64" ./generate/gopure-linux-arm64 api go -api *.api -dir . -style goZero else @@ -17,9 +21,13 @@ if [[ "$OS_TYPE" == "Linux" ]]; then elif [[ "$OS_TYPE" == "Darwin" ]]; then echo "The current operating system is macOS" if [[ "$ARCH_TYPE" == "x86_64" ]]; then + echo "Format api file" + ./generate/gopure-darwin-amd64 api format --dir ./apis echo "Architecture: amd64" ./generate/gopure-darwin-amd64 api go -api *.api -dir . -style goZero elif [[ "$ARCH_TYPE" == "arm64" ]]; then + echo "Format api file" + ./generate/gopure-darwin-arm64 api format --dir ./apis echo "Architecture: arm64" ./generate/gopure-darwin-arm64 api go -api *.api -dir . -style goZero else @@ -28,9 +36,13 @@ elif [[ "$OS_TYPE" == "Darwin" ]]; then elif [[ "$OS_TYPE" == "CYGWIN"* || "$OS_TYPE" == "MINGW"* ]]; then echo "The current operating system is Windows" if [[ "$ARCH_TYPE" == "x86_64" ]]; then + echo "Format api file" + ./generate/gopure-amd64.exe api format --dir ./apis echo "Architecture: amd64" ./generate/gopure-amd64.exe api go -api *.api -dir . -style goZero elif [[ "$ARCH_TYPE" == "arm64" ]]; then + echo "Format api file" + ./generate/gopure-arm64.exe api format --dir ./apis echo "Architecture: arm64" ./generate/gopure-arm64.exe api go -api *.api -dir . -style goZero else

48QE$;G0X#vBrald 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 literal 0 HcmV?d00001 diff --git a/doc/image/architecture-zh.png b/doc/image/architecture-zh.png new file mode 100644 index 0000000000000000000000000000000000000000..7923f60f13306dd4d51a0a138d23b13356a0bf3a GIT binary patch literal 561032 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≤=HwYx9Kb$3zb=#;{wjc;3< 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