From 5f008b32289caaffa522672c01af1bf1b43d0dcf Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Sat, 16 Aug 2025 18:46:29 +0800 Subject: [PATCH] first commit --- .cursor/rules/api.mdc | 186 ++ .cursor/rules/logic.mdc | 270 +++ .cursorrules | 202 ++ .gitignore | 22 + app/main/api/Dockerfile | 33 + app/main/api/desc/admin/admin_agent.api | 385 ++++ app/main/api/desc/admin/admin_feature.api | 108 + app/main/api/desc/admin/admin_product.api | 175 ++ app/main/api/desc/admin/admin_query.api | 135 ++ app/main/api/desc/admin/admin_user.api | 131 ++ app/main/api/desc/admin/auth.api | 34 + app/main/api/desc/admin/menu.api | 149 ++ app/main/api/desc/admin/notification.api | 127 ++ app/main/api/desc/admin/order.api | 169 ++ app/main/api/desc/admin/platform_user.api | 124 ++ app/main/api/desc/admin/promotion.api | 183 ++ app/main/api/desc/admin/role.api | 124 ++ app/main/api/desc/front/agent.api | 401 ++++ app/main/api/desc/front/app.api | 39 + app/main/api/desc/front/pay.api | 73 + app/main/api/desc/front/product.api | 59 + app/main/api/desc/front/query.api | 238 +++ app/main/api/desc/front/user.api | 164 ++ app/main/api/desc/main.api | 30 + app/main/api/etc/main.dev.yaml | 78 + app/main/api/etc/main.yaml | 79 + .../api/etc/merchant/AuthKey_LAY65829DQ.p8 | 6 + .../etc/merchant/alipayCertPublicKey_RSA2.crt | 43 + app/main/api/etc/merchant/alipayRootCert.crt | 88 + app/main/api/etc/merchant/apiclient_key.pem | 28 + .../appCertPublicKey_2021005113664540.crt | 23 + app/main/api/etc/merchant/pub_key.pem | 9 + app/main/api/internal/config/config.go | 126 ++ ...ngetagentcommissiondeductionlisthandler.go | 29 + .../admingetagentcommissionlisthandler.go | 29 + .../admingetagentlinklisthandler.go | 29 + .../admin_agent/admingetagentlisthandler.go | 29 + ...dmingetagentmembershipconfiglisthandler.go | 29 + ...agentmembershiprechargeorderlisthandler.go | 29 + ...mingetagentplatformdeductionlisthandler.go | 29 + ...dmingetagentproductionconfiglisthandler.go | 29 + .../admingetagentrewardlisthandler.go | 29 + .../admingetagentwithdrawallisthandler.go | 29 + ...adminupdateagentmembershipconfighandler.go | 29 + ...adminupdateagentproductionconfighandler.go | 29 + .../handler/admin_auth/adminloginhandler.go | 29 + .../admincreatefeaturehandler.go | 29 + .../admindeletefeaturehandler.go | 29 + .../admingetfeaturedetailhandler.go | 29 + .../admingetfeaturelisthandler.go | 29 + .../adminupdatefeaturehandler.go | 29 + .../handler/admin_menu/createmenuhandler.go | 29 + .../handler/admin_menu/deletemenuhandler.go | 29 + .../handler/admin_menu/getmenuallhandler.go | 29 + .../admin_menu/getmenudetailhandler.go | 29 + .../handler/admin_menu/getmenulisthandler.go | 29 + .../handler/admin_menu/updatemenuhandler.go | 29 + .../admincreatenotificationhandler.go | 29 + .../admindeletenotificationhandler.go | 29 + .../admingetnotificationdetailhandler.go | 29 + .../admingetnotificationlisthandler.go | 29 + .../adminupdatenotificationhandler.go | 29 + .../admin_order/admincreateorderhandler.go | 29 + .../admin_order/admindeleteorderhandler.go | 29 + .../admin_order/admingetorderdetailhandler.go | 29 + .../admin_order/admingetorderlisthandler.go | 29 + .../admin_order/adminrefundorderhandler.go | 29 + .../admin_order/adminupdateorderhandler.go | 29 + .../admincreateplatformuserhandler.go | 29 + .../admindeleteplatformuserhandler.go | 29 + .../admingetplatformuserdetailhandler.go | 29 + .../admingetplatformuserlisthandler.go | 29 + .../adminupdateplatformuserhandler.go | 29 + .../admincreateproducthandler.go | 29 + .../admindeleteproducthandler.go | 29 + .../admingetproductdetailhandler.go | 29 + .../admingetproductfeaturelisthandler.go | 29 + .../admingetproductlisthandler.go | 29 + .../adminupdateproductfeatureshandler.go | 29 + .../adminupdateproducthandler.go | 29 + .../createpromotionlinkhandler.go | 29 + .../deletepromotionlinkhandler.go | 30 + .../getpromotionlinkdetailhandler.go | 29 + .../getpromotionlinklisthandler.go | 29 + .../getpromotionstatshistoryhandler.go | 29 + .../getpromotionstatstotalhandler.go | 29 + .../admin_promotion/recordlinkclickhandler.go | 29 + .../updatepromotionlinkhandler.go | 30 + .../admingetquerycleanupconfiglisthandler.go | 29 + .../admingetquerycleanupdetaillisthandler.go | 29 + .../admingetquerycleanuploglisthandler.go | 29 + .../admingetquerydetailbyorderidhandler.go | 29 + .../adminupdatequerycleanupconfighandler.go | 29 + .../handler/admin_role/createrolehandler.go | 29 + .../handler/admin_role/deleterolehandler.go | 29 + .../admin_role/getroledetailhandler.go | 29 + .../handler/admin_role/getrolelisthandler.go | 29 + .../handler/admin_role/updaterolehandler.go | 29 + .../admin_user/admincreateuserhandler.go | 29 + .../admin_user/admindeleteuserhandler.go | 29 + .../admin_user/admingetuserdetailhandler.go | 29 + .../admin_user/admingetuserlisthandler.go | 29 + .../admin_user/adminupdateuserhandler.go | 29 + .../admin_user/adminuserinfohandler.go | 29 + .../agent/activateagentmembershiphandler.go | 30 + .../handler/agent/agentrealnamehandler.go | 30 + .../handler/agent/agentwithdrawalhandler.go | 30 + .../handler/agent/applyforagenthandler.go | 30 + .../handler/agent/generatinglinkhandler.go | 30 + .../agent/getagentauditstatushandler.go | 17 + .../agent/getagentcommissionhandler.go | 30 + .../handler/agent/getagentinfohandler.go | 17 + .../getagentmembershipproductconfighandler.go | 30 + .../agent/getagentproductconfighandler.go | 17 + .../agent/getagentpromotionqrcodehandler.go | 36 + .../agent/getagentrevenueinfohandler.go | 30 + .../handler/agent/getagentrewardshandler.go | 30 + ...entsubordinatecontributiondetailhandler.go | 30 + .../agent/getagentsubordinatelisthandler.go | 30 + .../agent/getagentwithdrawalhandler.go | 30 + .../getagentwithdrawaltaxexemptionhandler.go | 29 + .../handler/agent/getlinkdatahandler.go | 30 + .../saveagentmembershipuserconfighandler.go | 30 + .../handler/app/getappversionhandler.go | 17 + .../handler/app/healthcheckhandler.go | 17 + .../internal/handler/auth/sendsmshandler.go | 30 + .../notification/getnotificationshandler.go | 17 + .../handler/pay/alipaycallbackhandler.go | 17 + .../handler/pay/iapcallbackhandler.go | 30 + .../handler/pay/paymentcheckhandler.go | 30 + .../internal/handler/pay/paymenthandler.go | 30 + .../handler/pay/wechatpaycallbackhandler.go | 17 + .../pay/wechatpayrefundcallbackhandler.go | 17 + .../product/getproductappbyenhandler.go | 30 + .../handler/product/getproductbyenhandler.go | 30 + .../handler/product/getproductbyidhandler.go | 30 + .../query/querydetailbyorderidhandler.go | 30 + .../query/querydetailbyordernohandler.go | 30 + .../handler/query/queryexamplehandler.go | 30 + .../query/querygeneratesharelinkhandler.go | 30 + .../handler/query/querylisthandler.go | 30 + .../query/queryprovisionalorderhandler.go | 30 + .../handler/query/queryretryhandler.go | 30 + .../handler/query/queryserviceagenthandler.go | 30 + .../handler/query/queryserviceapphandler.go | 30 + .../handler/query/queryservicehandler.go | 25 + .../handler/query/querysharedetailhandler.go | 30 + .../handler/query/querysingletesthandler.go | 30 + .../handler/query/updatequerydatahandler.go | 31 + app/main/api/internal/handler/routes.go | 955 +++++++++ .../handler/user/bindmobilehandler.go | 30 + .../internal/handler/user/cancelouthandler.go | 17 + .../internal/handler/user/detailhandler.go | 17 + .../internal/handler/user/gettokenhandler.go | 17 + .../handler/user/mobilecodeloginhandler.go | 30 + .../internal/handler/user/wxh5authhandler.go | 30 + .../handler/user/wxminiauthhandler.go | 30 + ...mingetagentcommissiondeductionlistlogic.go | 84 + .../admingetagentcommissionlistlogic.go | 84 + .../admin_agent/admingetagentlinklistlogic.go | 86 + .../admin_agent/admingetagentlistlogic.go | 115 ++ .../admingetagentmembershipconfiglistlogic.go | 51 + ...etagentmembershiprechargeorderlistlogic.go | 64 + ...admingetagentplatformdeductionlistlogic.go | 57 + .../admingetagentproductionconfiglistlogic.go | 74 + .../admingetagentrewardlistlogic.go | 58 + .../admingetagentwithdrawallistlogic.go | 59 + .../adminupdateagentmembershipconfiglogic.go | 96 + .../adminupdateagentproductionconfiglogic.go | 42 + .../logic/admin_auth/adminloginlogic.go | 93 + .../admin_feature/admincreatefeaturelogic.go | 46 + .../admin_feature/admindeletefeaturelogic.go | 45 + .../admingetfeaturedetaillogic.go | 46 + .../admin_feature/admingetfeaturelistlogic.go | 66 + .../admin_feature/adminupdatefeaturelogic.go | 30 + .../logic/admin_menu/createmenulogic.go | 97 + .../logic/admin_menu/deletemenulogic.go | 30 + .../logic/admin_menu/getmenualllogic.go | 250 +++ .../logic/admin_menu/getmenudetaillogic.go | 30 + .../logic/admin_menu/getmenulistlogic.go | 109 + .../logic/admin_menu/updatemenulogic.go | 96 + .../admincreatenotificationlogic.go | 50 + .../admindeletenotificationlogic.go | 38 + .../admingetnotificationdetaillogic.go | 53 + .../admingetnotificationlistlogic.go | 82 + .../adminupdatenotificationlogic.go | 66 + .../admin_order/admincreateorderlogic.go | 99 + .../admin_order/admindeleteorderlogic.go | 63 + .../admin_order/admingetorderdetaillogic.go | 104 + .../admin_order/admingetorderlistlogic.go | 229 +++ .../admin_order/adminrefundorderlogic.go | 92 + .../admin_order/adminupdateorderlogic.go | 113 ++ .../admincreateplatformuserlogic.go | 57 + .../admindeleteplatformuserlogic.go | 43 + .../admingetplatformuserdetaillogic.go | 53 + .../admingetplatformuserlistlogic.go | 88 + .../adminupdateplatformuserlogic.go | 64 + .../admin_product/admincreateproductlogic.go | 50 + .../admin_product/admindeleteproductlogic.go | 44 + .../admingetproductdetaillogic.go | 49 + .../admingetproductfeaturelistlogic.go | 119 ++ .../admin_product/admingetproductlistlogic.go | 69 + .../adminupdateproductfeatureslogic.go | 159 ++ .../admin_product/adminupdateproductlogic.go | 65 + .../createpromotionlinklogic.go | 136 ++ .../deletepromotionlinklogic.go | 91 + .../getpromotionlinkdetaillogic.go | 65 + .../getpromotionlinklistlogic.go | 104 + .../getpromotionstatshistorylogic.go | 83 + .../getpromotionstatstotallogic.go | 166 ++ .../admin_promotion/recordlinkclicklogic.go | 57 + .../updatepromotionlinklogic.go | 57 + .../admingetquerycleanupconfiglistlogic.go | 62 + .../admingetquerycleanupdetaillistlogic.go | 126 ++ .../admingetquerycleanuploglistlogic.go | 88 + .../admingetquerydetailbyorderidlogic.go | 189 ++ .../adminupdatequerycleanupconfiglogic.go | 63 + .../logic/admin_role/createrolelogic.go | 83 + .../logic/admin_role/deleterolelogic.go | 84 + .../logic/admin_role/getroledetaillogic.go | 91 + .../logic/admin_role/getrolelistlogic.go | 148 ++ .../logic/admin_role/updaterolelogic.go | 148 ++ .../logic/admin_user/admincreateuserlogic.go | 88 + .../logic/admin_user/admindeleteuserlogic.go | 68 + .../admin_user/admingetuserdetaillogic.go | 88 + .../logic/admin_user/admingetuserlistlogic.go | 149 ++ .../logic/admin_user/adminupdateuserlogic.go | 141 ++ .../logic/admin_user/adminuserinfologic.go | 67 + .../agent/activateagentmembershiplogic.go | 85 + .../logic/agent/agentrealnamelogic.go | 99 + .../logic/agent/agentwithdrawallogic.go | 450 +++++ .../logic/agent/applyforagentlogic.go | 187 ++ .../logic/agent/generatinglinklogic.go | 111 ++ .../logic/agent/getagentauditstatuslogic.go | 52 + .../logic/agent/getagentcommissionlogic.go | 71 + .../internal/logic/agent/getagentinfologic.go | 94 + .../getagentmembershipproductconfiglogic.go | 80 + .../logic/agent/getagentproductconfiglogic.go | 140 ++ .../agent/getagentpromotionqrcodelogic.go | 72 + .../logic/agent/getagentrevenueinfologic.go | 221 +++ .../logic/agent/getagentrewardslogic.go | 68 + ...agentsubordinatecontributiondetaillogic.go | 200 ++ .../agent/getagentsubordinatelistlogic.go | 173 ++ .../logic/agent/getagentwithdrawallogic.go | 66 + .../getagentwithdrawaltaxexemptionlogic.go | 78 + .../internal/logic/agent/getlinkdatalogic.go | 46 + .../saveagentmembershipuserconfiglogic.go | 82 + .../internal/logic/app/getappversionlogic.go | 31 + .../internal/logic/app/healthchecklogic.go | 31 + .../api/internal/logic/auth/sendsmslogic.go | 105 + .../notification/getnotificationslogic.go | 57 + .../internal/logic/pay/alipaycallbacklogic.go | 216 ++ .../internal/logic/pay/iapcallbacklogic.go | 82 + .../internal/logic/pay/paymentchecklogic.go | 49 + .../api/internal/logic/pay/paymentlogic.go | 227 +++ .../logic/pay/wechatpaycallbacklogic.go | 215 ++ .../logic/pay/wechatpayrefundcallbacklogic.go | 55 + .../logic/product/getproductappbyenlogic.go | 75 + .../logic/product/getproductbyenlogic.go | 75 + .../logic/product/getproductbyidlogic.go | 30 + .../logic/query/querydetailbyorderidlogic.go | 213 ++ .../logic/query/querydetailbyordernologic.go | 155 ++ .../logic/query/queryexamplelogic copy.go | 152 ++ .../internal/logic/query/queryexamplelogic.go | 110 ++ .../query/querygeneratesharelinklogic.go | 111 ++ .../internal/logic/query/querylistlogic.go | 70 + .../logic/query/queryprovisionalorderlogic.go | 63 + .../internal/logic/query/queryretrylogic.go | 44 + .../logic/query/queryserviceagentlogic.go | 27 + .../logic/query/queryserviceapplogic.go | 30 + .../internal/logic/query/queryservicelogic.go | 624 ++++++ .../logic/query/querysharedetaillogic.go | 164 ++ .../logic/query/querysingletestlogic.go | 52 + .../logic/query/updatequerydatalogic.go | 71 + .../internal/logic/user/bindmobilelogic.go | 106 + .../api/internal/logic/user/canceloutlogic.go | 252 +++ .../api/internal/logic/user/detaillogic.go | 74 + .../api/internal/logic/user/gettokenlogic.go | 47 + .../logic/user/mobilecodeloginlogic.go | 82 + .../api/internal/logic/user/wxh5authlogic.go | 130 ++ .../internal/logic/user/wxminiauthlogic.go | 151 ++ .../middleware/authinterceptormiddleware.go | 54 + .../global_sourceinterceptor_middleware.go | 29 + .../userauthinterceptormiddleware.go | 33 + app/main/api/internal/queue/cleanQueryData.go | 167 ++ .../api/internal/queue/paySuccessNotify.go | 374 ++++ app/main/api/internal/queue/routes.go | 40 + .../service/adminPromotionLinkStatsService.go | 211 ++ app/main/api/internal/service/agentService.go | 344 ++++ .../api/internal/service/alipayService.go | 258 +++ .../api/internal/service/apirequestService.go | 1298 ++++++++++++ .../api/internal/service/applepayService.go | 169 ++ app/main/api/internal/service/asynqService.go | 60 + app/main/api/internal/service/dictService.go | 47 + app/main/api/internal/service/imageService.go | 173 ++ .../service/tianyuanapi_sdk/client.go | 416 ++++ app/main/api/internal/service/userService.go | 296 +++ .../internal/service/verificationService.go | 217 ++ .../api/internal/service/wechatpayService.go | 377 ++++ app/main/api/internal/svc/servicecontext.go | 296 +++ app/main/api/internal/types/cache.go | 20 + app/main/api/internal/types/encrypPayload.go | 6 + app/main/api/internal/types/payload.go | 5 + app/main/api/internal/types/query.go | 113 ++ app/main/api/internal/types/queryMap.go | 47 + app/main/api/internal/types/queryParams.go | 73 + app/main/api/internal/types/taskname.go | 4 + app/main/api/internal/types/types.go | 1746 +++++++++++++++++ app/main/api/main.go | 66 + app/main/api/static/images/tg_qrcode_1.png | Bin 0 -> 866564 bytes app/main/api/static/images/yq_qrcode_1.png | Bin 0 -> 599044 bytes app/main/model/adminApiModel.go | 27 + app/main/model/adminApiModel_gen.go | 411 ++++ app/main/model/adminDictDataModel.go | 27 + app/main/model/adminDictDataModel_gen.go | 437 +++++ app/main/model/adminDictTypeModel.go | 27 + app/main/model/adminDictTypeModel_gen.go | 409 ++++ app/main/model/adminMenuModel.go | 27 + app/main/model/adminMenuModel_gen.go | 414 ++++ app/main/model/adminPromotionLinkModel.go | 27 + app/main/model/adminPromotionLinkModel_gen.go | 408 ++++ .../adminPromotionLinkStatsHistoryModel.go | 27 + ...adminPromotionLinkStatsHistoryModel_gen.go | 412 ++++ .../adminPromotionLinkStatsTotalModel.go | 27 + .../adminPromotionLinkStatsTotalModel_gen.go | 411 ++++ app/main/model/adminPromotionOrderModel.go | 27 + .../model/adminPromotionOrderModel_gen.go | 409 ++++ app/main/model/adminRoleApiModel.go | 27 + app/main/model/adminRoleApiModel_gen.go | 407 ++++ app/main/model/adminRoleMenuModel.go | 27 + app/main/model/adminRoleMenuModel_gen.go | 407 ++++ app/main/model/adminRoleModel.go | 27 + app/main/model/adminRoleModel_gen.go | 410 ++++ app/main/model/adminUserModel.go | 27 + app/main/model/adminUserModel_gen.go | 435 ++++ app/main/model/adminUserRoleModel.go | 27 + app/main/model/adminUserRoleModel_gen.go | 407 ++++ app/main/model/agentActiveStatModel.go | 27 + app/main/model/agentActiveStatModel_gen.go | 408 ++++ app/main/model/agentAuditModel.go | 27 + app/main/model/agentAuditModel_gen.go | 412 ++++ app/main/model/agentClosureModel.go | 99 + app/main/model/agentClosureModel_gen.go | 434 ++++ .../model/agentCommissionDeductionModel.go | 27 + .../agentCommissionDeductionModel_gen.go | 372 ++++ app/main/model/agentCommissionModel.go | 27 + app/main/model/agentCommissionModel_gen.go | 371 ++++ app/main/model/agentLinkModel.go | 27 + app/main/model/agentLinkModel_gen.go | 410 ++++ app/main/model/agentMembershipConfigModel.go | 27 + .../model/agentMembershipConfigModel_gen.go | 419 ++++ .../agentMembershipRechargeOrderModel.go | 27 + .../agentMembershipRechargeOrderModel_gen.go | 439 +++++ .../model/agentMembershipUserConfigModel.go | 27 + .../agentMembershipUserConfigModel_gen.go | 412 ++++ app/main/model/agentModel.go | 27 + app/main/model/agentModel_gen.go | 437 +++++ app/main/model/agentOrderModel.go | 27 + app/main/model/agentOrderModel_gen.go | 407 ++++ app/main/model/agentPlatformDeductionModel.go | 27 + .../model/agentPlatformDeductionModel_gen.go | 370 ++++ app/main/model/agentProductConfigModel.go | 27 + app/main/model/agentProductConfigModel_gen.go | 411 ++++ app/main/model/agentRealNameModel.go | 27 + app/main/model/agentRealNameModel_gen.go | 411 ++++ app/main/model/agentRewardsModel.go | 27 + app/main/model/agentRewardsModel_gen.go | 370 ++++ app/main/model/agentWalletModel.go | 27 + app/main/model/agentWalletModel_gen.go | 410 ++++ app/main/model/agentWithdrawalModel.go | 27 + app/main/model/agentWithdrawalModel_gen.go | 413 ++++ .../model/agentWithdrawalTaxExemptionModel.go | 27 + .../agentWithdrawalTaxExemptionModel_gen.go | 410 ++++ app/main/model/agentWithdrawalTaxModel.go | 27 + app/main/model/agentWithdrawalTaxModel_gen.go | 418 ++++ app/main/model/exampleModel.go | 27 + app/main/model/exampleModel_gen.go | 434 ++++ app/main/model/featureModel.go | 27 + app/main/model/featureModel_gen.go | 407 ++++ app/main/model/globalNotificationsModel.go | 27 + .../model/globalNotificationsModel_gen.go | 374 ++++ app/main/model/orderModel.go | 27 + app/main/model/orderModel_gen.go | 416 ++++ app/main/model/orderRefundModel.go | 27 + app/main/model/orderRefundModel_gen.go | 467 +++++ app/main/model/productFeatureModel.go | 27 + app/main/model/productFeatureModel_gen.go | 410 ++++ app/main/model/productModel.go | 27 + app/main/model/productModel_gen.go | 411 ++++ app/main/model/queryCleanupConfigModel.go | 27 + app/main/model/queryCleanupConfigModel_gen.go | 409 ++++ app/main/model/queryCleanupDetailModel.go | 27 + app/main/model/queryCleanupDetailModel_gen.go | 373 ++++ app/main/model/queryCleanupLogModel.go | 27 + app/main/model/queryCleanupLogModel_gen.go | 372 ++++ app/main/model/queryModel.go | 60 + app/main/model/queryModel_gen.go | 411 ++++ app/main/model/userAuthModel.go | 27 + app/main/model/userAuthModel_gen.go | 434 ++++ app/main/model/userModel.go | 27 + app/main/model/userModel_gen.go | 410 ++++ app/main/model/userTempModel.go | 27 + app/main/model/userTempModel_gen.go | 407 ++++ app/main/model/vars.go | 104 + common/ctxdata/ctxData.go | 104 + common/globalkey/constantKey.go | 14 + common/globalkey/redisCacheKey.go | 9 + .../rpcserver/loggerInterceptor.go | 39 + common/jwt/jwtx.go | 94 + common/jwt/jwtx_test.go | 393 ++++ common/kqueue/message.go | 8 + common/middleware/commonJwtAuthMiddleware.go | 31 + common/result/httpResult.go | 89 + common/result/jobResult.go | 44 + common/result/responseBean.go | 21 + common/tool/coinconvert.go | 19 + common/tool/encryption.go | 23 + common/tool/krand.go | 28 + common/tool/krand_test.go | 8 + common/tool/placeholders.go | 15 + common/uniqueid/sn.go | 20 + common/uniqueid/sn_test.go | 7 + common/uniqueid/uniqueid.go | 23 + common/wxminisub/tpl.go | 7 + common/xerr/errCode.go | 23 + common/xerr/errMsg.go | 30 + common/xerr/errors.go | 39 + deploy/script/gen_models.ps1 | 62 + deploy/script/model/vars.go | 9 + deploy/sql/order.sql | 41 + deploy/sql/product.sql | 116 ++ deploy/sql/query.sql | 18 + deploy/sql/template.sql | 20 + deploy/sql/user.sql | 43 + deploy/template/api/config.tpl | 9 + deploy/template/api/context.tpl | 17 + deploy/template/api/etc.tpl | 3 + deploy/template/api/handler.tpl | 27 + deploy/template/api/logic.tpl | 25 + deploy/template/api/main.tpl | 27 + deploy/template/api/middleware.tpl | 20 + deploy/template/api/route-addition.tpl | 4 + deploy/template/api/routes.tpl | 13 + deploy/template/api/template.tpl | 24 + deploy/template/api/types.tpl | 6 + deploy/template/docker/docker.tpl | 33 + deploy/template/gateway/etc.tpl | 18 + deploy/template/gateway/main.tpl | 20 + deploy/template/kube/deployment.tpl | 117 ++ deploy/template/kube/job.tpl | 37 + deploy/template/model/delete.tpl | 21 + deploy/template/model/err.tpl | 9 + deploy/template/model/field.tpl | 1 + .../model/find-one-by-field-extra-method.tpl | 7 + deploy/template/model/find-one-by-field.tpl | 32 + deploy/template/model/find-one.tpl | 26 + deploy/template/model/import-no-cache.tpl | 16 + deploy/template/model/import.tpl | 17 + deploy/template/model/insert.tpl | 17 + deploy/template/model/interface-delete.tpl | 1 + .../model/interface-find-one-by-field.tpl | 1 + deploy/template/model/interface-find-one.tpl | 1 + deploy/template/model/interface-insert.tpl | 1 + deploy/template/model/interface-update.tpl | 12 + deploy/template/model/model-gen.tpl | 13 + deploy/template/model/model-new.tpl | 7 + deploy/template/model/model.tpl | 37 + deploy/template/model/table-name.tpl | 4 + deploy/template/model/tag.tpl | 1 + deploy/template/model/types.tpl | 15 + deploy/template/model/update.tpl | 286 +++ deploy/template/model/var.tpl | 9 + deploy/template/mongo/err.tpl | 12 + deploy/template/mongo/model.tpl | 78 + deploy/template/mongo/model_custom.tpl | 38 + deploy/template/mongo/model_types.tpl | 14 + deploy/template/newapi/newtemplate.tpl | 12 + deploy/template/rpc/call.tpl | 33 + deploy/template/rpc/config.tpl | 7 + deploy/template/rpc/etc.tpl | 6 + deploy/template/rpc/logic-func.tpl | 6 + deploy/template/rpc/logic.tpl | 24 + deploy/template/rpc/main.tpl | 42 + deploy/template/rpc/server-func.tpl | 6 + deploy/template/rpc/server.tpl | 22 + deploy/template/rpc/svc.tpl | 13 + deploy/template/rpc/template.tpl | 16 + docker-compose.dev.yml | 83 + docker-compose.yml | 89 + gen_api.ps1 | 2 + go.mod | 109 + go.sum | 460 +++++ pkg/lzkit/crypto/README.md | 235 +++ pkg/lzkit/crypto/bcrypt.go | 28 + pkg/lzkit/crypto/crypto.go | 105 + pkg/lzkit/crypto/crypto_url.go | 67 + pkg/lzkit/crypto/ecb.go | 274 +++ pkg/lzkit/crypto/ecb_test.go | 186 ++ pkg/lzkit/crypto/generate.go | 63 + pkg/lzkit/crypto/west_crypto.go | 150 ++ pkg/lzkit/delay/ProgressiveDelay.go | 65 + pkg/lzkit/lzUtils/json.go | 35 + pkg/lzkit/lzUtils/sqlutls.go | 72 + pkg/lzkit/lzUtils/time.go | 26 + pkg/lzkit/lzUtils/utils.go | 15 + pkg/lzkit/md5/README.md | 106 + pkg/lzkit/md5/example_test.go | 79 + pkg/lzkit/md5/md5.go | 206 ++ pkg/lzkit/md5/md5_test.go | 192 ++ pkg/lzkit/validator/error_messages.go | 38 + pkg/lzkit/validator/validator.go | 174 ++ 511 files changed, 53667 insertions(+) create mode 100644 .cursor/rules/api.mdc create mode 100644 .cursor/rules/logic.mdc create mode 100644 .cursorrules create mode 100644 .gitignore create mode 100644 app/main/api/Dockerfile create mode 100644 app/main/api/desc/admin/admin_agent.api create mode 100644 app/main/api/desc/admin/admin_feature.api create mode 100644 app/main/api/desc/admin/admin_product.api create mode 100644 app/main/api/desc/admin/admin_query.api create mode 100644 app/main/api/desc/admin/admin_user.api create mode 100644 app/main/api/desc/admin/auth.api create mode 100644 app/main/api/desc/admin/menu.api create mode 100644 app/main/api/desc/admin/notification.api create mode 100644 app/main/api/desc/admin/order.api create mode 100644 app/main/api/desc/admin/platform_user.api create mode 100644 app/main/api/desc/admin/promotion.api create mode 100644 app/main/api/desc/admin/role.api create mode 100644 app/main/api/desc/front/agent.api create mode 100644 app/main/api/desc/front/app.api create mode 100644 app/main/api/desc/front/pay.api create mode 100644 app/main/api/desc/front/product.api create mode 100644 app/main/api/desc/front/query.api create mode 100644 app/main/api/desc/front/user.api create mode 100644 app/main/api/desc/main.api create mode 100644 app/main/api/etc/main.dev.yaml create mode 100644 app/main/api/etc/main.yaml create mode 100644 app/main/api/etc/merchant/AuthKey_LAY65829DQ.p8 create mode 100644 app/main/api/etc/merchant/alipayCertPublicKey_RSA2.crt create mode 100644 app/main/api/etc/merchant/alipayRootCert.crt create mode 100644 app/main/api/etc/merchant/apiclient_key.pem create mode 100644 app/main/api/etc/merchant/appCertPublicKey_2021005113664540.crt create mode 100644 app/main/api/etc/merchant/pub_key.pem create mode 100644 app/main/api/internal/config/config.go create mode 100644 app/main/api/internal/handler/admin_agent/admingetagentcommissiondeductionlisthandler.go create mode 100644 app/main/api/internal/handler/admin_agent/admingetagentcommissionlisthandler.go create mode 100644 app/main/api/internal/handler/admin_agent/admingetagentlinklisthandler.go create mode 100644 app/main/api/internal/handler/admin_agent/admingetagentlisthandler.go create mode 100644 app/main/api/internal/handler/admin_agent/admingetagentmembershipconfiglisthandler.go create mode 100644 app/main/api/internal/handler/admin_agent/admingetagentmembershiprechargeorderlisthandler.go create mode 100644 app/main/api/internal/handler/admin_agent/admingetagentplatformdeductionlisthandler.go create mode 100644 app/main/api/internal/handler/admin_agent/admingetagentproductionconfiglisthandler.go create mode 100644 app/main/api/internal/handler/admin_agent/admingetagentrewardlisthandler.go create mode 100644 app/main/api/internal/handler/admin_agent/admingetagentwithdrawallisthandler.go create mode 100644 app/main/api/internal/handler/admin_agent/adminupdateagentmembershipconfighandler.go create mode 100644 app/main/api/internal/handler/admin_agent/adminupdateagentproductionconfighandler.go create mode 100644 app/main/api/internal/handler/admin_auth/adminloginhandler.go create mode 100644 app/main/api/internal/handler/admin_feature/admincreatefeaturehandler.go create mode 100644 app/main/api/internal/handler/admin_feature/admindeletefeaturehandler.go create mode 100644 app/main/api/internal/handler/admin_feature/admingetfeaturedetailhandler.go create mode 100644 app/main/api/internal/handler/admin_feature/admingetfeaturelisthandler.go create mode 100644 app/main/api/internal/handler/admin_feature/adminupdatefeaturehandler.go create mode 100644 app/main/api/internal/handler/admin_menu/createmenuhandler.go create mode 100644 app/main/api/internal/handler/admin_menu/deletemenuhandler.go create mode 100644 app/main/api/internal/handler/admin_menu/getmenuallhandler.go create mode 100644 app/main/api/internal/handler/admin_menu/getmenudetailhandler.go create mode 100644 app/main/api/internal/handler/admin_menu/getmenulisthandler.go create mode 100644 app/main/api/internal/handler/admin_menu/updatemenuhandler.go create mode 100644 app/main/api/internal/handler/admin_notification/admincreatenotificationhandler.go create mode 100644 app/main/api/internal/handler/admin_notification/admindeletenotificationhandler.go create mode 100644 app/main/api/internal/handler/admin_notification/admingetnotificationdetailhandler.go create mode 100644 app/main/api/internal/handler/admin_notification/admingetnotificationlisthandler.go create mode 100644 app/main/api/internal/handler/admin_notification/adminupdatenotificationhandler.go create mode 100644 app/main/api/internal/handler/admin_order/admincreateorderhandler.go create mode 100644 app/main/api/internal/handler/admin_order/admindeleteorderhandler.go create mode 100644 app/main/api/internal/handler/admin_order/admingetorderdetailhandler.go create mode 100644 app/main/api/internal/handler/admin_order/admingetorderlisthandler.go create mode 100644 app/main/api/internal/handler/admin_order/adminrefundorderhandler.go create mode 100644 app/main/api/internal/handler/admin_order/adminupdateorderhandler.go create mode 100644 app/main/api/internal/handler/admin_platform_user/admincreateplatformuserhandler.go create mode 100644 app/main/api/internal/handler/admin_platform_user/admindeleteplatformuserhandler.go create mode 100644 app/main/api/internal/handler/admin_platform_user/admingetplatformuserdetailhandler.go create mode 100644 app/main/api/internal/handler/admin_platform_user/admingetplatformuserlisthandler.go create mode 100644 app/main/api/internal/handler/admin_platform_user/adminupdateplatformuserhandler.go create mode 100644 app/main/api/internal/handler/admin_product/admincreateproducthandler.go create mode 100644 app/main/api/internal/handler/admin_product/admindeleteproducthandler.go create mode 100644 app/main/api/internal/handler/admin_product/admingetproductdetailhandler.go create mode 100644 app/main/api/internal/handler/admin_product/admingetproductfeaturelisthandler.go create mode 100644 app/main/api/internal/handler/admin_product/admingetproductlisthandler.go create mode 100644 app/main/api/internal/handler/admin_product/adminupdateproductfeatureshandler.go create mode 100644 app/main/api/internal/handler/admin_product/adminupdateproducthandler.go create mode 100644 app/main/api/internal/handler/admin_promotion/createpromotionlinkhandler.go create mode 100644 app/main/api/internal/handler/admin_promotion/deletepromotionlinkhandler.go create mode 100644 app/main/api/internal/handler/admin_promotion/getpromotionlinkdetailhandler.go create mode 100644 app/main/api/internal/handler/admin_promotion/getpromotionlinklisthandler.go create mode 100644 app/main/api/internal/handler/admin_promotion/getpromotionstatshistoryhandler.go create mode 100644 app/main/api/internal/handler/admin_promotion/getpromotionstatstotalhandler.go create mode 100644 app/main/api/internal/handler/admin_promotion/recordlinkclickhandler.go create mode 100644 app/main/api/internal/handler/admin_promotion/updatepromotionlinkhandler.go create mode 100644 app/main/api/internal/handler/admin_query/admingetquerycleanupconfiglisthandler.go create mode 100644 app/main/api/internal/handler/admin_query/admingetquerycleanupdetaillisthandler.go create mode 100644 app/main/api/internal/handler/admin_query/admingetquerycleanuploglisthandler.go create mode 100644 app/main/api/internal/handler/admin_query/admingetquerydetailbyorderidhandler.go create mode 100644 app/main/api/internal/handler/admin_query/adminupdatequerycleanupconfighandler.go create mode 100644 app/main/api/internal/handler/admin_role/createrolehandler.go create mode 100644 app/main/api/internal/handler/admin_role/deleterolehandler.go create mode 100644 app/main/api/internal/handler/admin_role/getroledetailhandler.go create mode 100644 app/main/api/internal/handler/admin_role/getrolelisthandler.go create mode 100644 app/main/api/internal/handler/admin_role/updaterolehandler.go create mode 100644 app/main/api/internal/handler/admin_user/admincreateuserhandler.go create mode 100644 app/main/api/internal/handler/admin_user/admindeleteuserhandler.go create mode 100644 app/main/api/internal/handler/admin_user/admingetuserdetailhandler.go create mode 100644 app/main/api/internal/handler/admin_user/admingetuserlisthandler.go create mode 100644 app/main/api/internal/handler/admin_user/adminupdateuserhandler.go create mode 100644 app/main/api/internal/handler/admin_user/adminuserinfohandler.go create mode 100644 app/main/api/internal/handler/agent/activateagentmembershiphandler.go create mode 100644 app/main/api/internal/handler/agent/agentrealnamehandler.go create mode 100644 app/main/api/internal/handler/agent/agentwithdrawalhandler.go create mode 100644 app/main/api/internal/handler/agent/applyforagenthandler.go create mode 100644 app/main/api/internal/handler/agent/generatinglinkhandler.go create mode 100644 app/main/api/internal/handler/agent/getagentauditstatushandler.go create mode 100644 app/main/api/internal/handler/agent/getagentcommissionhandler.go create mode 100644 app/main/api/internal/handler/agent/getagentinfohandler.go create mode 100644 app/main/api/internal/handler/agent/getagentmembershipproductconfighandler.go create mode 100644 app/main/api/internal/handler/agent/getagentproductconfighandler.go create mode 100644 app/main/api/internal/handler/agent/getagentpromotionqrcodehandler.go create mode 100644 app/main/api/internal/handler/agent/getagentrevenueinfohandler.go create mode 100644 app/main/api/internal/handler/agent/getagentrewardshandler.go create mode 100644 app/main/api/internal/handler/agent/getagentsubordinatecontributiondetailhandler.go create mode 100644 app/main/api/internal/handler/agent/getagentsubordinatelisthandler.go create mode 100644 app/main/api/internal/handler/agent/getagentwithdrawalhandler.go create mode 100644 app/main/api/internal/handler/agent/getagentwithdrawaltaxexemptionhandler.go create mode 100644 app/main/api/internal/handler/agent/getlinkdatahandler.go create mode 100644 app/main/api/internal/handler/agent/saveagentmembershipuserconfighandler.go create mode 100644 app/main/api/internal/handler/app/getappversionhandler.go create mode 100644 app/main/api/internal/handler/app/healthcheckhandler.go create mode 100644 app/main/api/internal/handler/auth/sendsmshandler.go create mode 100644 app/main/api/internal/handler/notification/getnotificationshandler.go create mode 100644 app/main/api/internal/handler/pay/alipaycallbackhandler.go create mode 100644 app/main/api/internal/handler/pay/iapcallbackhandler.go create mode 100644 app/main/api/internal/handler/pay/paymentcheckhandler.go create mode 100644 app/main/api/internal/handler/pay/paymenthandler.go create mode 100644 app/main/api/internal/handler/pay/wechatpaycallbackhandler.go create mode 100644 app/main/api/internal/handler/pay/wechatpayrefundcallbackhandler.go create mode 100644 app/main/api/internal/handler/product/getproductappbyenhandler.go create mode 100644 app/main/api/internal/handler/product/getproductbyenhandler.go create mode 100644 app/main/api/internal/handler/product/getproductbyidhandler.go create mode 100644 app/main/api/internal/handler/query/querydetailbyorderidhandler.go create mode 100644 app/main/api/internal/handler/query/querydetailbyordernohandler.go create mode 100644 app/main/api/internal/handler/query/queryexamplehandler.go create mode 100644 app/main/api/internal/handler/query/querygeneratesharelinkhandler.go create mode 100644 app/main/api/internal/handler/query/querylisthandler.go create mode 100644 app/main/api/internal/handler/query/queryprovisionalorderhandler.go create mode 100644 app/main/api/internal/handler/query/queryretryhandler.go create mode 100644 app/main/api/internal/handler/query/queryserviceagenthandler.go create mode 100644 app/main/api/internal/handler/query/queryserviceapphandler.go create mode 100644 app/main/api/internal/handler/query/queryservicehandler.go create mode 100644 app/main/api/internal/handler/query/querysharedetailhandler.go create mode 100644 app/main/api/internal/handler/query/querysingletesthandler.go create mode 100644 app/main/api/internal/handler/query/updatequerydatahandler.go create mode 100644 app/main/api/internal/handler/routes.go create mode 100644 app/main/api/internal/handler/user/bindmobilehandler.go create mode 100644 app/main/api/internal/handler/user/cancelouthandler.go create mode 100644 app/main/api/internal/handler/user/detailhandler.go create mode 100644 app/main/api/internal/handler/user/gettokenhandler.go create mode 100644 app/main/api/internal/handler/user/mobilecodeloginhandler.go create mode 100644 app/main/api/internal/handler/user/wxh5authhandler.go create mode 100644 app/main/api/internal/handler/user/wxminiauthhandler.go create mode 100644 app/main/api/internal/logic/admin_agent/admingetagentcommissiondeductionlistlogic.go create mode 100644 app/main/api/internal/logic/admin_agent/admingetagentcommissionlistlogic.go create mode 100644 app/main/api/internal/logic/admin_agent/admingetagentlinklistlogic.go create mode 100644 app/main/api/internal/logic/admin_agent/admingetagentlistlogic.go create mode 100644 app/main/api/internal/logic/admin_agent/admingetagentmembershipconfiglistlogic.go create mode 100644 app/main/api/internal/logic/admin_agent/admingetagentmembershiprechargeorderlistlogic.go create mode 100644 app/main/api/internal/logic/admin_agent/admingetagentplatformdeductionlistlogic.go create mode 100644 app/main/api/internal/logic/admin_agent/admingetagentproductionconfiglistlogic.go create mode 100644 app/main/api/internal/logic/admin_agent/admingetagentrewardlistlogic.go create mode 100644 app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go create mode 100644 app/main/api/internal/logic/admin_agent/adminupdateagentmembershipconfiglogic.go create mode 100644 app/main/api/internal/logic/admin_agent/adminupdateagentproductionconfiglogic.go create mode 100644 app/main/api/internal/logic/admin_auth/adminloginlogic.go create mode 100644 app/main/api/internal/logic/admin_feature/admincreatefeaturelogic.go create mode 100644 app/main/api/internal/logic/admin_feature/admindeletefeaturelogic.go create mode 100644 app/main/api/internal/logic/admin_feature/admingetfeaturedetaillogic.go create mode 100644 app/main/api/internal/logic/admin_feature/admingetfeaturelistlogic.go create mode 100644 app/main/api/internal/logic/admin_feature/adminupdatefeaturelogic.go create mode 100644 app/main/api/internal/logic/admin_menu/createmenulogic.go create mode 100644 app/main/api/internal/logic/admin_menu/deletemenulogic.go create mode 100644 app/main/api/internal/logic/admin_menu/getmenualllogic.go create mode 100644 app/main/api/internal/logic/admin_menu/getmenudetaillogic.go create mode 100644 app/main/api/internal/logic/admin_menu/getmenulistlogic.go create mode 100644 app/main/api/internal/logic/admin_menu/updatemenulogic.go create mode 100644 app/main/api/internal/logic/admin_notification/admincreatenotificationlogic.go create mode 100644 app/main/api/internal/logic/admin_notification/admindeletenotificationlogic.go create mode 100644 app/main/api/internal/logic/admin_notification/admingetnotificationdetaillogic.go create mode 100644 app/main/api/internal/logic/admin_notification/admingetnotificationlistlogic.go create mode 100644 app/main/api/internal/logic/admin_notification/adminupdatenotificationlogic.go create mode 100644 app/main/api/internal/logic/admin_order/admincreateorderlogic.go create mode 100644 app/main/api/internal/logic/admin_order/admindeleteorderlogic.go create mode 100644 app/main/api/internal/logic/admin_order/admingetorderdetaillogic.go create mode 100644 app/main/api/internal/logic/admin_order/admingetorderlistlogic.go create mode 100644 app/main/api/internal/logic/admin_order/adminrefundorderlogic.go create mode 100644 app/main/api/internal/logic/admin_order/adminupdateorderlogic.go create mode 100644 app/main/api/internal/logic/admin_platform_user/admincreateplatformuserlogic.go create mode 100644 app/main/api/internal/logic/admin_platform_user/admindeleteplatformuserlogic.go create mode 100644 app/main/api/internal/logic/admin_platform_user/admingetplatformuserdetaillogic.go create mode 100644 app/main/api/internal/logic/admin_platform_user/admingetplatformuserlistlogic.go create mode 100644 app/main/api/internal/logic/admin_platform_user/adminupdateplatformuserlogic.go create mode 100644 app/main/api/internal/logic/admin_product/admincreateproductlogic.go create mode 100644 app/main/api/internal/logic/admin_product/admindeleteproductlogic.go create mode 100644 app/main/api/internal/logic/admin_product/admingetproductdetaillogic.go create mode 100644 app/main/api/internal/logic/admin_product/admingetproductfeaturelistlogic.go create mode 100644 app/main/api/internal/logic/admin_product/admingetproductlistlogic.go create mode 100644 app/main/api/internal/logic/admin_product/adminupdateproductfeatureslogic.go create mode 100644 app/main/api/internal/logic/admin_product/adminupdateproductlogic.go create mode 100644 app/main/api/internal/logic/admin_promotion/createpromotionlinklogic.go create mode 100644 app/main/api/internal/logic/admin_promotion/deletepromotionlinklogic.go create mode 100644 app/main/api/internal/logic/admin_promotion/getpromotionlinkdetaillogic.go create mode 100644 app/main/api/internal/logic/admin_promotion/getpromotionlinklistlogic.go create mode 100644 app/main/api/internal/logic/admin_promotion/getpromotionstatshistorylogic.go create mode 100644 app/main/api/internal/logic/admin_promotion/getpromotionstatstotallogic.go create mode 100644 app/main/api/internal/logic/admin_promotion/recordlinkclicklogic.go create mode 100644 app/main/api/internal/logic/admin_promotion/updatepromotionlinklogic.go create mode 100644 app/main/api/internal/logic/admin_query/admingetquerycleanupconfiglistlogic.go create mode 100644 app/main/api/internal/logic/admin_query/admingetquerycleanupdetaillistlogic.go create mode 100644 app/main/api/internal/logic/admin_query/admingetquerycleanuploglistlogic.go create mode 100644 app/main/api/internal/logic/admin_query/admingetquerydetailbyorderidlogic.go create mode 100644 app/main/api/internal/logic/admin_query/adminupdatequerycleanupconfiglogic.go create mode 100644 app/main/api/internal/logic/admin_role/createrolelogic.go create mode 100644 app/main/api/internal/logic/admin_role/deleterolelogic.go create mode 100644 app/main/api/internal/logic/admin_role/getroledetaillogic.go create mode 100644 app/main/api/internal/logic/admin_role/getrolelistlogic.go create mode 100644 app/main/api/internal/logic/admin_role/updaterolelogic.go create mode 100644 app/main/api/internal/logic/admin_user/admincreateuserlogic.go create mode 100644 app/main/api/internal/logic/admin_user/admindeleteuserlogic.go create mode 100644 app/main/api/internal/logic/admin_user/admingetuserdetaillogic.go create mode 100644 app/main/api/internal/logic/admin_user/admingetuserlistlogic.go create mode 100644 app/main/api/internal/logic/admin_user/adminupdateuserlogic.go create mode 100644 app/main/api/internal/logic/admin_user/adminuserinfologic.go create mode 100644 app/main/api/internal/logic/agent/activateagentmembershiplogic.go create mode 100644 app/main/api/internal/logic/agent/agentrealnamelogic.go create mode 100644 app/main/api/internal/logic/agent/agentwithdrawallogic.go create mode 100644 app/main/api/internal/logic/agent/applyforagentlogic.go create mode 100644 app/main/api/internal/logic/agent/generatinglinklogic.go create mode 100644 app/main/api/internal/logic/agent/getagentauditstatuslogic.go create mode 100644 app/main/api/internal/logic/agent/getagentcommissionlogic.go create mode 100644 app/main/api/internal/logic/agent/getagentinfologic.go create mode 100644 app/main/api/internal/logic/agent/getagentmembershipproductconfiglogic.go create mode 100644 app/main/api/internal/logic/agent/getagentproductconfiglogic.go create mode 100644 app/main/api/internal/logic/agent/getagentpromotionqrcodelogic.go create mode 100644 app/main/api/internal/logic/agent/getagentrevenueinfologic.go create mode 100644 app/main/api/internal/logic/agent/getagentrewardslogic.go create mode 100644 app/main/api/internal/logic/agent/getagentsubordinatecontributiondetaillogic.go create mode 100644 app/main/api/internal/logic/agent/getagentsubordinatelistlogic.go create mode 100644 app/main/api/internal/logic/agent/getagentwithdrawallogic.go create mode 100644 app/main/api/internal/logic/agent/getagentwithdrawaltaxexemptionlogic.go create mode 100644 app/main/api/internal/logic/agent/getlinkdatalogic.go create mode 100644 app/main/api/internal/logic/agent/saveagentmembershipuserconfiglogic.go create mode 100644 app/main/api/internal/logic/app/getappversionlogic.go create mode 100644 app/main/api/internal/logic/app/healthchecklogic.go create mode 100644 app/main/api/internal/logic/auth/sendsmslogic.go create mode 100644 app/main/api/internal/logic/notification/getnotificationslogic.go create mode 100644 app/main/api/internal/logic/pay/alipaycallbacklogic.go create mode 100644 app/main/api/internal/logic/pay/iapcallbacklogic.go create mode 100644 app/main/api/internal/logic/pay/paymentchecklogic.go create mode 100644 app/main/api/internal/logic/pay/paymentlogic.go create mode 100644 app/main/api/internal/logic/pay/wechatpaycallbacklogic.go create mode 100644 app/main/api/internal/logic/pay/wechatpayrefundcallbacklogic.go create mode 100644 app/main/api/internal/logic/product/getproductappbyenlogic.go create mode 100644 app/main/api/internal/logic/product/getproductbyenlogic.go create mode 100644 app/main/api/internal/logic/product/getproductbyidlogic.go create mode 100644 app/main/api/internal/logic/query/querydetailbyorderidlogic.go create mode 100644 app/main/api/internal/logic/query/querydetailbyordernologic.go create mode 100644 app/main/api/internal/logic/query/queryexamplelogic copy.go create mode 100644 app/main/api/internal/logic/query/queryexamplelogic.go create mode 100644 app/main/api/internal/logic/query/querygeneratesharelinklogic.go create mode 100644 app/main/api/internal/logic/query/querylistlogic.go create mode 100644 app/main/api/internal/logic/query/queryprovisionalorderlogic.go create mode 100644 app/main/api/internal/logic/query/queryretrylogic.go create mode 100644 app/main/api/internal/logic/query/queryserviceagentlogic.go create mode 100644 app/main/api/internal/logic/query/queryserviceapplogic.go create mode 100644 app/main/api/internal/logic/query/queryservicelogic.go create mode 100644 app/main/api/internal/logic/query/querysharedetaillogic.go create mode 100644 app/main/api/internal/logic/query/querysingletestlogic.go create mode 100644 app/main/api/internal/logic/query/updatequerydatalogic.go create mode 100644 app/main/api/internal/logic/user/bindmobilelogic.go create mode 100644 app/main/api/internal/logic/user/canceloutlogic.go create mode 100644 app/main/api/internal/logic/user/detaillogic.go create mode 100644 app/main/api/internal/logic/user/gettokenlogic.go create mode 100644 app/main/api/internal/logic/user/mobilecodeloginlogic.go create mode 100644 app/main/api/internal/logic/user/wxh5authlogic.go create mode 100644 app/main/api/internal/logic/user/wxminiauthlogic.go create mode 100644 app/main/api/internal/middleware/authinterceptormiddleware.go create mode 100644 app/main/api/internal/middleware/global_sourceinterceptor_middleware.go create mode 100644 app/main/api/internal/middleware/userauthinterceptormiddleware.go create mode 100644 app/main/api/internal/queue/cleanQueryData.go create mode 100644 app/main/api/internal/queue/paySuccessNotify.go create mode 100644 app/main/api/internal/queue/routes.go create mode 100644 app/main/api/internal/service/adminPromotionLinkStatsService.go create mode 100644 app/main/api/internal/service/agentService.go create mode 100644 app/main/api/internal/service/alipayService.go create mode 100644 app/main/api/internal/service/apirequestService.go create mode 100644 app/main/api/internal/service/applepayService.go create mode 100644 app/main/api/internal/service/asynqService.go create mode 100644 app/main/api/internal/service/dictService.go create mode 100644 app/main/api/internal/service/imageService.go create mode 100644 app/main/api/internal/service/tianyuanapi_sdk/client.go create mode 100644 app/main/api/internal/service/userService.go create mode 100644 app/main/api/internal/service/verificationService.go create mode 100644 app/main/api/internal/service/wechatpayService.go create mode 100644 app/main/api/internal/svc/servicecontext.go create mode 100644 app/main/api/internal/types/cache.go create mode 100644 app/main/api/internal/types/encrypPayload.go create mode 100644 app/main/api/internal/types/payload.go create mode 100644 app/main/api/internal/types/query.go create mode 100644 app/main/api/internal/types/queryMap.go create mode 100644 app/main/api/internal/types/queryParams.go create mode 100644 app/main/api/internal/types/taskname.go create mode 100644 app/main/api/internal/types/types.go create mode 100644 app/main/api/main.go create mode 100644 app/main/api/static/images/tg_qrcode_1.png create mode 100644 app/main/api/static/images/yq_qrcode_1.png create mode 100644 app/main/model/adminApiModel.go create mode 100644 app/main/model/adminApiModel_gen.go create mode 100644 app/main/model/adminDictDataModel.go create mode 100644 app/main/model/adminDictDataModel_gen.go create mode 100644 app/main/model/adminDictTypeModel.go create mode 100644 app/main/model/adminDictTypeModel_gen.go create mode 100644 app/main/model/adminMenuModel.go create mode 100644 app/main/model/adminMenuModel_gen.go create mode 100644 app/main/model/adminPromotionLinkModel.go create mode 100644 app/main/model/adminPromotionLinkModel_gen.go create mode 100644 app/main/model/adminPromotionLinkStatsHistoryModel.go create mode 100644 app/main/model/adminPromotionLinkStatsHistoryModel_gen.go create mode 100644 app/main/model/adminPromotionLinkStatsTotalModel.go create mode 100644 app/main/model/adminPromotionLinkStatsTotalModel_gen.go create mode 100644 app/main/model/adminPromotionOrderModel.go create mode 100644 app/main/model/adminPromotionOrderModel_gen.go create mode 100644 app/main/model/adminRoleApiModel.go create mode 100644 app/main/model/adminRoleApiModel_gen.go create mode 100644 app/main/model/adminRoleMenuModel.go create mode 100644 app/main/model/adminRoleMenuModel_gen.go create mode 100644 app/main/model/adminRoleModel.go create mode 100644 app/main/model/adminRoleModel_gen.go create mode 100644 app/main/model/adminUserModel.go create mode 100644 app/main/model/adminUserModel_gen.go create mode 100644 app/main/model/adminUserRoleModel.go create mode 100644 app/main/model/adminUserRoleModel_gen.go create mode 100644 app/main/model/agentActiveStatModel.go create mode 100644 app/main/model/agentActiveStatModel_gen.go create mode 100644 app/main/model/agentAuditModel.go create mode 100644 app/main/model/agentAuditModel_gen.go create mode 100644 app/main/model/agentClosureModel.go create mode 100644 app/main/model/agentClosureModel_gen.go create mode 100644 app/main/model/agentCommissionDeductionModel.go create mode 100644 app/main/model/agentCommissionDeductionModel_gen.go create mode 100644 app/main/model/agentCommissionModel.go create mode 100644 app/main/model/agentCommissionModel_gen.go create mode 100644 app/main/model/agentLinkModel.go create mode 100644 app/main/model/agentLinkModel_gen.go create mode 100644 app/main/model/agentMembershipConfigModel.go create mode 100644 app/main/model/agentMembershipConfigModel_gen.go create mode 100644 app/main/model/agentMembershipRechargeOrderModel.go create mode 100644 app/main/model/agentMembershipRechargeOrderModel_gen.go create mode 100644 app/main/model/agentMembershipUserConfigModel.go create mode 100644 app/main/model/agentMembershipUserConfigModel_gen.go create mode 100644 app/main/model/agentModel.go create mode 100644 app/main/model/agentModel_gen.go create mode 100644 app/main/model/agentOrderModel.go create mode 100644 app/main/model/agentOrderModel_gen.go create mode 100644 app/main/model/agentPlatformDeductionModel.go create mode 100644 app/main/model/agentPlatformDeductionModel_gen.go create mode 100644 app/main/model/agentProductConfigModel.go create mode 100644 app/main/model/agentProductConfigModel_gen.go create mode 100644 app/main/model/agentRealNameModel.go create mode 100644 app/main/model/agentRealNameModel_gen.go create mode 100644 app/main/model/agentRewardsModel.go create mode 100644 app/main/model/agentRewardsModel_gen.go create mode 100644 app/main/model/agentWalletModel.go create mode 100644 app/main/model/agentWalletModel_gen.go create mode 100644 app/main/model/agentWithdrawalModel.go create mode 100644 app/main/model/agentWithdrawalModel_gen.go create mode 100644 app/main/model/agentWithdrawalTaxExemptionModel.go create mode 100644 app/main/model/agentWithdrawalTaxExemptionModel_gen.go create mode 100644 app/main/model/agentWithdrawalTaxModel.go create mode 100644 app/main/model/agentWithdrawalTaxModel_gen.go create mode 100644 app/main/model/exampleModel.go create mode 100644 app/main/model/exampleModel_gen.go create mode 100644 app/main/model/featureModel.go create mode 100644 app/main/model/featureModel_gen.go create mode 100644 app/main/model/globalNotificationsModel.go create mode 100644 app/main/model/globalNotificationsModel_gen.go create mode 100644 app/main/model/orderModel.go create mode 100644 app/main/model/orderModel_gen.go create mode 100644 app/main/model/orderRefundModel.go create mode 100644 app/main/model/orderRefundModel_gen.go create mode 100644 app/main/model/productFeatureModel.go create mode 100644 app/main/model/productFeatureModel_gen.go create mode 100644 app/main/model/productModel.go create mode 100644 app/main/model/productModel_gen.go create mode 100644 app/main/model/queryCleanupConfigModel.go create mode 100644 app/main/model/queryCleanupConfigModel_gen.go create mode 100644 app/main/model/queryCleanupDetailModel.go create mode 100644 app/main/model/queryCleanupDetailModel_gen.go create mode 100644 app/main/model/queryCleanupLogModel.go create mode 100644 app/main/model/queryCleanupLogModel_gen.go create mode 100644 app/main/model/queryModel.go create mode 100644 app/main/model/queryModel_gen.go create mode 100644 app/main/model/userAuthModel.go create mode 100644 app/main/model/userAuthModel_gen.go create mode 100644 app/main/model/userModel.go create mode 100644 app/main/model/userModel_gen.go create mode 100644 app/main/model/userTempModel.go create mode 100644 app/main/model/userTempModel_gen.go create mode 100644 app/main/model/vars.go create mode 100644 common/ctxdata/ctxData.go create mode 100644 common/globalkey/constantKey.go create mode 100644 common/globalkey/redisCacheKey.go create mode 100644 common/interceptor/rpcserver/loggerInterceptor.go create mode 100644 common/jwt/jwtx.go create mode 100644 common/jwt/jwtx_test.go create mode 100644 common/kqueue/message.go create mode 100644 common/middleware/commonJwtAuthMiddleware.go create mode 100644 common/result/httpResult.go create mode 100644 common/result/jobResult.go create mode 100644 common/result/responseBean.go create mode 100644 common/tool/coinconvert.go create mode 100644 common/tool/encryption.go create mode 100644 common/tool/krand.go create mode 100644 common/tool/krand_test.go create mode 100644 common/tool/placeholders.go create mode 100644 common/uniqueid/sn.go create mode 100644 common/uniqueid/sn_test.go create mode 100644 common/uniqueid/uniqueid.go create mode 100644 common/wxminisub/tpl.go create mode 100644 common/xerr/errCode.go create mode 100644 common/xerr/errMsg.go create mode 100644 common/xerr/errors.go create mode 100644 deploy/script/gen_models.ps1 create mode 100644 deploy/script/model/vars.go create mode 100644 deploy/sql/order.sql create mode 100644 deploy/sql/product.sql create mode 100644 deploy/sql/query.sql create mode 100644 deploy/sql/template.sql create mode 100644 deploy/sql/user.sql create mode 100644 deploy/template/api/config.tpl create mode 100644 deploy/template/api/context.tpl create mode 100644 deploy/template/api/etc.tpl create mode 100644 deploy/template/api/handler.tpl create mode 100644 deploy/template/api/logic.tpl create mode 100644 deploy/template/api/main.tpl create mode 100644 deploy/template/api/middleware.tpl create mode 100644 deploy/template/api/route-addition.tpl create mode 100644 deploy/template/api/routes.tpl create mode 100644 deploy/template/api/template.tpl create mode 100644 deploy/template/api/types.tpl create mode 100644 deploy/template/docker/docker.tpl create mode 100644 deploy/template/gateway/etc.tpl create mode 100644 deploy/template/gateway/main.tpl create mode 100644 deploy/template/kube/deployment.tpl create mode 100644 deploy/template/kube/job.tpl create mode 100644 deploy/template/model/delete.tpl create mode 100644 deploy/template/model/err.tpl create mode 100644 deploy/template/model/field.tpl create mode 100644 deploy/template/model/find-one-by-field-extra-method.tpl create mode 100644 deploy/template/model/find-one-by-field.tpl create mode 100644 deploy/template/model/find-one.tpl create mode 100644 deploy/template/model/import-no-cache.tpl create mode 100644 deploy/template/model/import.tpl create mode 100644 deploy/template/model/insert.tpl create mode 100644 deploy/template/model/interface-delete.tpl create mode 100644 deploy/template/model/interface-find-one-by-field.tpl create mode 100644 deploy/template/model/interface-find-one.tpl create mode 100644 deploy/template/model/interface-insert.tpl create mode 100644 deploy/template/model/interface-update.tpl create mode 100644 deploy/template/model/model-gen.tpl create mode 100644 deploy/template/model/model-new.tpl create mode 100644 deploy/template/model/model.tpl create mode 100644 deploy/template/model/table-name.tpl create mode 100644 deploy/template/model/tag.tpl create mode 100644 deploy/template/model/types.tpl create mode 100644 deploy/template/model/update.tpl create mode 100644 deploy/template/model/var.tpl create mode 100644 deploy/template/mongo/err.tpl create mode 100644 deploy/template/mongo/model.tpl create mode 100644 deploy/template/mongo/model_custom.tpl create mode 100644 deploy/template/mongo/model_types.tpl create mode 100644 deploy/template/newapi/newtemplate.tpl create mode 100644 deploy/template/rpc/call.tpl create mode 100644 deploy/template/rpc/config.tpl create mode 100644 deploy/template/rpc/etc.tpl create mode 100644 deploy/template/rpc/logic-func.tpl create mode 100644 deploy/template/rpc/logic.tpl create mode 100644 deploy/template/rpc/main.tpl create mode 100644 deploy/template/rpc/server-func.tpl create mode 100644 deploy/template/rpc/server.tpl create mode 100644 deploy/template/rpc/svc.tpl create mode 100644 deploy/template/rpc/template.tpl create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 gen_api.ps1 create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/lzkit/crypto/README.md create mode 100644 pkg/lzkit/crypto/bcrypt.go create mode 100644 pkg/lzkit/crypto/crypto.go create mode 100644 pkg/lzkit/crypto/crypto_url.go create mode 100644 pkg/lzkit/crypto/ecb.go create mode 100644 pkg/lzkit/crypto/ecb_test.go create mode 100644 pkg/lzkit/crypto/generate.go create mode 100644 pkg/lzkit/crypto/west_crypto.go create mode 100644 pkg/lzkit/delay/ProgressiveDelay.go create mode 100644 pkg/lzkit/lzUtils/json.go create mode 100644 pkg/lzkit/lzUtils/sqlutls.go create mode 100644 pkg/lzkit/lzUtils/time.go create mode 100644 pkg/lzkit/lzUtils/utils.go create mode 100644 pkg/lzkit/md5/README.md create mode 100644 pkg/lzkit/md5/example_test.go create mode 100644 pkg/lzkit/md5/md5.go create mode 100644 pkg/lzkit/md5/md5_test.go create mode 100644 pkg/lzkit/validator/error_messages.go create mode 100644 pkg/lzkit/validator/validator.go diff --git a/.cursor/rules/api.mdc b/.cursor/rules/api.mdc new file mode 100644 index 0000000..c55f0bf --- /dev/null +++ b/.cursor/rules/api.mdc @@ -0,0 +1,186 @@ +# Cursor 工作流程和规范 + +## 目录结构说明 + +### API 定义目录 (`app/main/api/desc/`) +``` +desc/ +├── admin/ # 后台管理接口 +│ ├── admin_user.api # 管理员用户相关接口 +│ ├── platform_user.api # 平台用户相关接口 +│ ├── order.api # 订单管理接口 +│ ├── promotion.api # 促销活动接口 +│ ├── menu.api # 菜单管理接口 +│ ├── role.api # 角色权限接口 +│ └── auth.api # 后台认证接口 +│ +├── front/ # 前台业务接口 +│ ├── user.api # 用户相关接口 +│ ├── product.api # 产品相关接口 +│ ├── pay.api # 支付相关接口 +│ ├── query.api # 查询相关接口 +│ ├── auth/ # 前台认证相关子模块 +│ ├── product/ # 产品相关子模块 +│ ├── query/ # 查询相关子模块 +│ ├── user/ # 用户相关子模块 +│ └── pay/ # 支付相关子模块 +│ +└── main.api # API 主入口文件,用于引入所有子模块 +``` + +### 目录规范 +1. 后台管理接口统一放在 `admin/` 目录下 + - 所有后台管理相关的 API 定义文件都放在此目录 + - 文件名应清晰表明模块功能,如 `order.api`、`user.api` 等 + - 后台接口统一使用 `/api/v1/admin/` 前缀 + +2. 前台业务接口统一放在 `front/` 目录下 + - 所有面向用户的 API 定义文件都放在此目录 + - 可以根据业务模块创建子目录,如 `user/`、`product/` 等 + - 前台接口统一使用 `/api/v1/` 前缀 + +3. 主入口文件 `main.api` + - 用于引入所有子模块的 API 定义 + - 保持清晰的模块分类和注释 + +## API 开发流程 + +### 1. API 定义 +- 根据业务类型选择正确的目录: + - 后台管理接口:`app/main/api/desc/admin/` 目录 + - 前台业务接口:`app/main/api/desc/front/` 目录 +- 创建新的 `.api` 文件,遵循以下命名规范: + - 后台接口:`[模块名].api`,如 `order.api`、`user.api` + - 前台接口:`[模块名].api`,如 `product.api`、`pay.api` +- API 定义规范: + - 使用 RESTful 风格 + - 请求/响应结构体命名规范:`[模块名][操作名][Req/Resp]` + - 字段类型使用 string 而不是 int64 来存储枚举值 + - 添加必要的注释说明 + - 请求/响应参数定义规范: + ```go + type ( + // 创建类请求(必填字段) + AdminCreateXXXReq { + Field1 string `json:"field1"` // 字段1说明 + Field2 int64 `json:"field2"` // 字段2说明 + Field3 string `json:"field3"` // 字段3说明 + } + + // 更新类请求(可选字段使用指针类型) + AdminUpdateXXXReq { + Id int64 `path:"id"` // ID(路径参数) + Field1 *string `json:"field1,optional"` // 字段1说明(可选) + Field2 *int64 `json:"field2,optional"` // 字段2说明(可选) + Field3 *string `json:"field3,optional"` // 字段3说明(可选) + } + + // 查询列表请求(分页参数 + 可选查询条件) + AdminGetXXXListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + Field1 *string `form:"field1,optional"` // 查询条件1(可选) + Field2 *int64 `form:"field2,optional"` // 查询条件2(可选) + Field3 *string `form:"field3,optional"` // 查询条件3(可选) + } + + // 列表项结构体(用于列表响应) + XXXListItem { + Id int64 `json:"id"` // ID + Field1 string `json:"field1"` // 字段1 + Field2 string `json:"field2"` // 字段2 + Field3 string `json:"field3"` // 字段3 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + } + + // 列表响应 + AdminGetXXXListResp { + Total int64 `json:"total"` // 总数 + Items []XXXListItem `json:"items"` // 列表数据 + } + ) + ``` + + - 参数定义注意事项: + 1. 创建类请求(Create): + - 所有字段都是必填的,使用普通类型(非指针) + - 使用 `json` 标签,不需要 `optional` 标记 + - 必须添加字段说明注释 + + 2. 更新类请求(Update): + - 除 ID 外的所有字段都是可选的,使用指针类型(如 `*string`、`*int64`) + - 使用 `json` 标签,并添加 `optional` 标记 + - ID 字段使用 `path` 标签,因为是路径参数 + - 必须添加字段说明注释 + + 3. 查询列表请求(GetList): + - 分页参数(page、pageSize)使用普通类型 + - 查询条件字段都是可选的,使用指针类型 + - 使用 `form` 标签,并添加 `optional` 标记 + - 必须添加字段说明注释 + + 4. 响应结构体: + - 列表响应使用 `Total` 和 `Items` 字段 + - 列表项使用单独的结构体定义 + - 时间字段统一使用 string 类型 + - 必须添加字段说明注释 + + 5. 标签使用规范: + - 路径参数:`path:"id"` + - JSON 参数:`json:"field_name"` + - 表单参数:`form:"field_name"` + - 可选字段:添加 `optional` 标记 + - 所有字段必须添加说明注释 + + - 示例: + ```go + // 通知管理接口 + @server( + prefix: /api/v1/admin/notification + group: admin_notification + ) + service main { + // 创建通知 + @handler AdminCreateNotification + post /create (AdminCreateNotificationReq) returns (AdminCreateNotificationResp) + + // 更新通知 + @handler AdminUpdateNotification + put /update/:id (AdminUpdateNotificationReq) returns (AdminUpdateNotificationResp) + + // 获取通知列表 + @handler AdminGetNotificationList + get /list (AdminGetNotificationListReq) returns (AdminGetNotificationListResp) + } + ``` + +### 2. 引入 API +- 在 `app/main/api/main.api` 中引入新定义的 API 文件: + ```go + import "desc/order.api" + ``` + +### 3. 生成代码 +- 在项目根目录运行 `gen_api.ps1` 脚本生成相关代码: + ```powershell + ./gen_api.ps1 + ``` +- 生成的文件包括: + - `app/main/api/internal/handler/` - 处理器 + - `app/main/api/internal/logic/` - 业务逻辑 + - `app/main/api/internal/svc/` - 服务上下文 + - `app/main/api/internal/types/` - 类型定义 + +### 4. 实现业务逻辑 +- 在 `app/main/api/internal/logic/` 目录下实现各个接口的业务逻辑 +- 具体实现规范请参考 `logic.mdc` 文件 + +## 注意事项 +1. 所有枚举类型使用 string 而不是 int64 +2. 错误处理必须使用 errors.Wrapf 和 xerr 包 +3. 涉及多表操作时使用事务 +4. 并发操作时注意使用互斥锁保护共享资源 +5. 时间类型统一使用 "2006-01-02 15:04:05" 格式 +6. 代码生成后需要检查并完善业务逻辑实现 +7. 保持代码风格统一,添加必要的注释 \ No newline at end of file diff --git a/.cursor/rules/logic.mdc b/.cursor/rules/logic.mdc new file mode 100644 index 0000000..ac4eaf0 --- /dev/null +++ b/.cursor/rules/logic.mdc @@ -0,0 +1,270 @@ +# Your rule content + +- You can @ files here +- You can use markdown but dont have to + +# Logic 实现规范 + +## 目录结构 +``` +app/main/api/internal/logic/ +├── admin_notification/ # 通知管理模块 +│ ├── admincreatenotificationlogic.go # 创建通知 +│ ├── admindeletnotificationlogic.go # 删除通知 +│ ├── admingetnotificationdetaillogic.go # 获取通知详情 +│ ├── admingetnotificationlistlogic.go # 获取通知列表 +│ └── adminupdatenotificationlogic.go # 更新通知 +└── [其他模块]/ +``` + +## Logic 实现规范 + +### 1. 基础结构 +每个 Logic 文件都应包含以下基础结构: +```go +package [模块名] + +import ( + "context" + "tyc-server/app/main/api/internal/svc" + "tyc-server/app/main/api/internal/types" + "tyc-server/common/xerr" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type [操作名]Logic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func New[操作名]Logic(ctx context.Context, svcCtx *svc.ServiceContext) *[操作名]Logic { + return &[操作名]Logic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} +``` + +### 2. 增删改查实现规范 + +#### 2.1 创建操作(Create) +```go +func (l *[操作名]Logic) [操作名](req *types.[操作名]Req) (resp *types.[操作名]Resp, err error) { + // 1. 数据转换和验证 + data := &model.[表名]{ + Field1: req.Field1, + Field2: req.Field2, + // ... 其他字段映射 + } + + // 2. 数据库操作 + result, err := l.svcCtx.[表名]Model.Insert(l.ctx, nil, data) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "创建[操作对象]失败, err: %v, req: %+v", err, req) + } + + // 3. 返回结果 + id, _ := result.LastInsertId() + return &types.[操作名]Resp{Id: id}, nil +} +``` + +#### 2.2 删除操作(Delete) +```go +func (l *[操作名]Logic) [操作名](req *types.[操作名]Req) (resp *types.[操作名]Resp, err error) { + // 1. 查询记录是否存在 + record, err := l.svcCtx.[表名]Model.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "查找[操作对象]失败, err: %v, id: %d", err, req.Id) + } + + // 2. 执行删除操作(软删除) + err = l.svcCtx.[表名]Model.DeleteSoft(l.ctx, nil, record) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "删除[操作对象]失败, err: %v, id: %d", err, req.Id) + } + + // 3. 返回结果 + return &types.[操作名]Resp{Success: true}, nil +} +``` + +#### 2.3 更新操作(Update) +```go +func (l *[操作名]Logic) [操作名](req *types.[操作名]Req) (resp *types.[操作名]Resp, err error) { + // 1. 查询记录是否存在 + record, err := l.svcCtx.[表名]Model.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "查找[操作对象]失败, err: %v, id: %d", err, req.Id) + } + + // 2. 更新字段(使用指针判断是否更新) + if req.Field1 != nil { + record.Field1 = *req.Field1 + } + if req.Field2 != nil { + record.Field2 = *req.Field2 + } + // ... 其他字段更新 + + // 3. 执行更新操作 + _, err = l.svcCtx.[表名]Model.Update(l.ctx, nil, record) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "更新[操作对象]失败, err: %v, req: %+v", err, req) + } + + // 4. 返回结果 + return &types.[操作名]Resp{Success: true}, nil +} +``` + +#### 2.4 查询详情(GetDetail) +```go +func (l *[操作名]Logic) [操作名](req *types.[操作名]Req) (resp *types.[操作名]Resp, err error) { + // 1. 查询记录 + record, err := l.svcCtx.[表名]Model.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "查找[操作对象]失败, err: %v, id: %d", err, req.Id) + } + + // 2. 构建响应 + resp = &types.[操作名]Resp{ + Id: record.Id, + Field1: record.Field1, + Field2: record.Field2, + CreateTime: record.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: record.UpdateTime.Format("2006-01-02 15:04:05"), + } + + // 3. 处理可选字段(如时间字段) + if record.OptionalField.Valid { + resp.OptionalField = record.OptionalField.Time.Format("2006-01-02") + } + + return resp, nil +} +``` + +#### 2.5 查询列表(GetList) +```go +func (l *[操作名]Logic) [操作名](req *types.[操作名]Req) (resp *types.[操作名]Resp, err error) { + // 1. 构建查询条件 + builder := l.svcCtx.[表名]Model.SelectBuilder() + + // 2. 添加查询条件(使用指针判断是否添加条件) + if req.Field1 != nil { + builder = builder.Where("field1 LIKE ?", "%"+*req.Field1+"%") + } + if req.Field2 != nil { + builder = builder.Where("field2 = ?", *req.Field2) + } + // ... 其他查询条件 + + // 3. 执行分页查询 + list, total, err := l.svcCtx.[表名]Model.FindPageListByPageWithTotal( + l.ctx, builder, req.Page, req.PageSize, "id DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "查询[操作对象]列表失败, err: %v, req: %+v", err, req) + } + + // 4. 构建响应列表 + items := make([]types.[列表项类型], 0, len(list)) + for _, item := range list { + listItem := types.[列表项类型]{ + Id: item.Id, + Field1: item.Field1, + Field2: item.Field2, + CreateTime: item.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: item.UpdateTime.Format("2006-01-02 15:04:05"), + } + // 处理可选字段 + if item.OptionalField.Valid { + listItem.OptionalField = item.OptionalField.Time.Format("2006-01-02") + } + items = append(items, listItem) + } + + // 5. 返回结果 + return &types.[操作名]Resp{ + Total: total, + Items: items, + }, nil +} +``` + +### 3. 错误处理规范 +1. 使用 `errors.Wrapf` 包装错误 +2. 使用 `xerr.NewErrCode` 创建业务错误 +3. 错误信息应包含: + - 操作类型(创建/更新/删除/查询) + - 具体错误描述 + - 相关参数信息(ID、请求参数等) + +### 4. 时间处理规范 +1. 时间格式化: + - 日期时间:`"2006-01-02 15:04:05"` + - 仅日期:`"2006-01-02"` +2. 可选时间字段处理: + ```go + if field.Valid { + resp.Field = field.Time.Format("2006-01-02") + } + ``` + +### 5. 数据库操作规范 +1. 查询条件构建: + ```go + builder := l.svcCtx.[表名]Model.SelectBuilder() + builder = builder.Where("field = ?", value) + ``` +2. 分页查询: + ```go + list, total, err := l.svcCtx.[表名]Model.FindPageListByPageWithTotal( + ctx, builder, page, pageSize, "id DESC") + ``` +3. 事务处理: + ```go + err = l.svcCtx.[表名]Model.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 事务操作 + return nil + }) + ``` + +### 6. 并发处理规范 +```go +var mu sync.Mutex +err = mr.MapReduceVoid(func(source chan<- interface{}) { + // 并发处理 +}, func(item interface{}, writer mr.Writer[struct{}], cancel func(error)) { + // 处理单个项目 +}, func(pipe <-chan struct{}, cancel func(error)) { + // 完成处理 +}) +``` + +### 7. 注意事项 +1. 所有数据库操作必须进行错误处理 +2. 更新操作必须使用指针类型判断字段是否更新 +3. 查询列表必须支持分页 +4. 时间字段必须统一格式化 +5. 可选字段必须进行空值判断 +6. 保持代码风格统一,添加必要的注释 +7. 涉及多表操作时使用事务 +8. 并发操作时注意使用互斥锁保护共享资源 +9. 错误处理必须使用 errors.Wrapf 和 xerr 包 +10. 时间类型统一使用 "2006-01-02 15:04:05" 格式 + +# Your rule content + +- You can @ files here +- You can use markdown but dont have to diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..5f78f14 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,202 @@ +你是一位精通Go-Zero框架的AI编程助手,专门帮助开发基于Go-Zero的微服务API项目。 + +熟悉Go-Zero的项目结构和架构模式,包括: +- api服务开发 +- rpc服务开发 +- model层数据库操作 +- 中间件实现 +- 配置文件管理 +- JWT认证体系 +- 分布式事务处理 +- 使用goctl工具生成代码 + +项目目录结构说明: +``` +znc-server/ # 项目根目录 +├── app/ # 应用服务目录 +│ └── user/ # 用户服务 +│ ├── cmd/ # 服务启动入口 +│ │ ├── api/ # API服务 +│ │ │ ├── desc/ # API接口定义目录 +│ │ │ │ ├── user/ # 用户模块API定义 +│ │ │ │ │ └── user.api # 用户API类型定义 +│ │ │ │ └── main.api # 主API文件 +│ │ │ ├── etc/ # 配置文件目录 +│ │ │ │ └── user.yaml # 服务配置文件 +│ │ │ └── internal/ # 内部代码 +│ │ │ ├── config/ # 配置结构定义 +│ │ │ ├── handler/ # HTTP处理器 +│ │ │ ├── logic/ # 业务逻辑 +│ │ │ ├── middleware/ # 中间件 +│ │ │ ├── svc/ # 服务上下文 +│ │ │ └── types/ # 类型定义 +│ │ └── rpc/ # RPC服务(如果有) +│ └── model/ # 数据库模型 +├── common/ # 公共代码 +│ ├── ctxdata/ # 上下文数据处理 +│ ├── globalkey/ # 全局键值定义 +│ ├── interceptor/ # 拦截器 +│ ├── jwt/ # JWT认证 +│ ├── kqueue/ # 消息队列 +│ ├── middleware/ # 中间件 +│ ├── result/ # 统一返回结果 +│ ├── tool/ # 工具函数 +│ ├── uniqueid/ # 唯一ID生成 +│ ├── wxminisub/ # 微信小程序订阅 +│ └── xerr/ # 错误处理 +├── data/ # 数据文件目录 +├── deploy/ # 部署相关文件 +│ └── template/ # goctl模板 +├── pkg/ # 可复用的包 +│ └── lzkit/ # 工具包 +└── tmp/ # 临时文件目录 +``` + +目录作用说明: +1. app/user/cmd/api/:API服务目录 + - desc/:API接口定义,包含各模块的API文件 + - main.api:主API文件,导入所有模块API并定义路由 + - user/user.api:用户模块的请求响应参数定义 + - order/order.api:订单模块的请求响应参数定义 + - 其他模块API定义文件 + - etc/:配置文件目录,存放yaml配置 + - user.yaml:包含数据库、缓存、JWT等配置信息 + - internal/:服务内部代码 + - config/:配置结构定义,对应etc下的yaml文件 + - handler/:HTTP请求处理器,负责解析请求和返回响应 + - logic/:业务逻辑实现,处理具体业务 + - middleware/:HTTP中间件,如认证、日志等 + - svc/:服务上下文,管理服务依赖(如DB、Cache等) + - types/:请求响应的结构体定义,由goctl根据API文件生成(不允许自己修改) + +2. app/user/model/:数据库模型层 + - userModel.go:用户表模型定义及CRUD方法 + - userModel_gen.go:goctl工具生成的基础数据库操作代码(不允许自己修改) + - vars.go:定义数据库相关变量和常量 + - 其他数据表模型文件 + +3. common/:存放公共代码 + - ctxdata/:上下文数据处理 + - globalkey/:全局键值定义 + - interceptor/:拦截器 + - jwt/:JWT认证相关 + - kqueue/:消息队列处理 + - middleware/:全局中间件 + - result/:统一返回结果处理 + - tool/:通用工具函数 + - uniqueid/:唯一ID生成器 + - wxminisub/:微信小程序订阅功能 + - xerr/:统一错误处理 + +4. pkg/:可在多个服务间共享的包 + - lzkit/:通用工具包 + +5. deploy/:部署相关文件 + - template/:goctl代码生成模板 + +6. data/:数据文件目录 + - 用于存放项目相关的数据文件 + +7. tmp/:临时文件目录 + - 用于存放临时生成的文件 + +使用goctl生成API服务的步骤: +1. 首先确保API定义目录存在: + ```bash + mkdir -p app/user/cmd/api/desc/user + ``` + +2. API文件组织结构(单体服务模式): + ``` + app/user/cmd/api/desc/ + ├── user/ + │ └── user.api # 用户模块的请求响应参数定义 + ├── order/ + │ └── order.api # 订单模块的请求响应参数定义 + └── main.api # 主API文件,集中管理所有模块的API定义 + ``` + +3. 在app/user/cmd/api/desc/main.api中集中管理所有API: + ``` + syntax = "v1" + + info( + title: "单体服务API" + desc: "集中管理所有模块的API" + author: "team" + version: "v1" + ) + + // 导入各模块的类型定义 + import "user/user.api" + import "order/order.api" + + // 各模块下定义路由,例如user模块 desc/user.api + @server ( + prefix: api/v1 + group: user + ) + service main { + // 用户模块接口 + @handler Login + post /login (LoginReq) returns (LoginResp) + } + ``` + +4. 各模块在下一层定义类型,例如在app/user/cmd/api/desc/user/user.api中只定义用户模块的接口的类型: + ``` + type ( + LoginReq { + Username string `json:"username"` + Password string `json:"password"` + } + + LoginResp { + Token string `json:"token"` + ExpireAt int64 `json:"expireAt"` + } + ) + ``` + +5. 使用goctl生成API代码(始终使用main.api): + ```bash + goctl api go -api app/user/cmd/api/desc/main.api -dir app/user/cmd/api --home ./deploy/template + ``` + +注意:无论修改哪个模块的API文件,都需要执行main.api来生成代码,因为这是单体服务模式。 +6. goctl生成的文件和目录结构: + ``` + app/user/cmd/api/ + ├── desc/ # API接口定义目录(已存在) + ├── etc/ # 配置文件目录 + │ └── main.yaml # 服务配置文件 + ├── internal/ # 内部代码 + │ ├── config/ # 配置结构定义 + │ │ └── config.go # 配置结构体 + │ ├── handler/ # HTTP处理器 + │ │ ├── routes.go # 路由注册 + │ │ └── user/ # 用户模块处理器 + │ │ └── login_handler.go # 登录处理器 + │ ├── logic/ # 业务逻辑 + │ │ └── user/ # 用户模块逻辑 + │ │ └── login_logic.go # 登录逻辑 + │ ├── middleware/ # 中间件 + │ ├── svc/ # 服务上下文 + │ │ └── service_context.go # 服务上下文定义 + │ └── types/ # 类型定义 + │ └── types.go # 请求响应类型定义 + └── main.go # 服务入口文件 + ``` + +7. 生成代码后,才能够实现具体的业务逻辑,例如: + - user.api中的`mobileLogin`接口生成的逻辑文件在`app/user/cmd/api/internal/logic/user/mobile_login_logic.go` + - user.api中的`wxMiniAuth`接口生成的逻辑文件在`app/user/cmd/api/internal/logic/user/wx_mini_auth_logic.go` + - query.api中的`queryService`接口生成的逻辑文件在`app/user/cmd/api/internal/logic/query/query_service_logic.go` + + 生成的逻辑文件中需要实现`Logic`结构体的`XXX`方法(方法名与接口名对应),这是业务逻辑的核心部分。 + +代码说明尽量简洁,但关键逻辑和go-zero特有模式需要添加注释。 + +始终关注性能、安全性和可维护性。 + +在回答问题时,优先考虑Go-Zero的特性和设计理念,而不是通用的Go编程模式。 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e145787 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# idea +.idea +.idea/ +*.iml +.DS_Store +**/.DS_Store + +#deploy data + +data/* +!data/.gitkeep + +# gitlab ci +.cache + +#vscode +.vscode +.vscode/ + +/tmp/ + +/app/api diff --git a/app/main/api/Dockerfile b/app/main/api/Dockerfile new file mode 100644 index 0000000..12f447e --- /dev/null +++ b/app/main/api/Dockerfile @@ -0,0 +1,33 @@ +FROM golang:1.23.4-alpine AS builder + +LABEL stage=gobuilder + +ENV CGO_ENABLED 0 +ENV GOPROXY https://goproxy.cn,direct +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories + +RUN apk update --no-cache && apk add --no-cache tzdata + +WORKDIR /build + +ADD go.mod . +ADD go.sum . +RUN go mod download +COPY . . +COPY app/main/api/etc /app/etc +COPY app/main/api/static /app/static +RUN go build -ldflags="-s -w" -o /app/main app/main/api/main.go + + +FROM scratch + +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai +ENV TZ Asia/Shanghai + +WORKDIR /app +COPY --from=builder /app/main /app/main +COPY --from=builder /app/etc /app/etc +COPY --from=builder /app/static /app/static + +CMD ["./main", "-f", "etc/main.yaml"] diff --git a/app/main/api/desc/admin/admin_agent.api b/app/main/api/desc/admin/admin_agent.api new file mode 100644 index 0000000..516c03f --- /dev/null +++ b/app/main/api/desc/admin/admin_agent.api @@ -0,0 +1,385 @@ +syntax = "v1" + +info ( + title: "后台代理管理服务" + desc: "后台代理相关接口" + author: "team" + version: "v1" +) + +// 代理管理接口 +@server( + prefix: /api/v1/admin/agent + group: admin_agent +) +service main { + // 代理分页查询 + @handler AdminGetAgentList + get /list (AdminGetAgentListReq) returns (AdminGetAgentListResp) + + // 代理推广链接分页查询 + @handler AdminGetAgentLinkList + get /agent-link/list (AdminGetAgentLinkListReq) returns (AdminGetAgentLinkListResp) + + // 代理佣金分页查询 + @handler AdminGetAgentCommissionList + get /agent-commission/list (AdminGetAgentCommissionListReq) returns (AdminGetAgentCommissionListResp) + + // 代理奖励分页查询 + @handler AdminGetAgentRewardList + get /agent-reward/list (AdminGetAgentRewardListReq) returns (AdminGetAgentRewardListResp) + + // 代理提现分页查询 + @handler AdminGetAgentWithdrawalList + get /agent-withdrawal/list (AdminGetAgentWithdrawalListReq) returns (AdminGetAgentWithdrawalListResp) + + // 代理上级抽佣分页查询 + @handler AdminGetAgentCommissionDeductionList + get /agent-commission-deduction/list (AdminGetAgentCommissionDeductionListReq) returns (AdminGetAgentCommissionDeductionListResp) + + // 平台抽佣分页查询 + @handler AdminGetAgentPlatformDeductionList + get /agent-platform-deduction/list (AdminGetAgentPlatformDeductionListReq) returns (AdminGetAgentPlatformDeductionListResp) + + // 代理产品配置分页查询 + @handler AdminGetAgentProductionConfigList + get /agent-production-config/list (AdminGetAgentProductionConfigListReq) returns (AdminGetAgentProductionConfigListResp) + + // 代理产品配置编辑 + @handler AdminUpdateAgentProductionConfig + post /agent-production-config/update (AdminUpdateAgentProductionConfigReq) returns (AdminUpdateAgentProductionConfigResp) + + // 代理会员充值订单分页查询 + @handler AdminGetAgentMembershipRechargeOrderList + get /agent-membership-recharge-order/list (AdminGetAgentMembershipRechargeOrderListReq) returns (AdminGetAgentMembershipRechargeOrderListResp) + + // 代理会员配置分页查询 + @handler AdminGetAgentMembershipConfigList + get /agent-membership-config/list (AdminGetAgentMembershipConfigListReq) returns (AdminGetAgentMembershipConfigListResp) + + // 代理会员配置编辑 + @handler AdminUpdateAgentMembershipConfig + post /agent-membership-config/update (AdminUpdateAgentMembershipConfigReq) returns (AdminUpdateAgentMembershipConfigResp) +} + +type ( + // 代理分页查询请求 + AdminGetAgentListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + Mobile *string `form:"mobile,optional"` // 手机号(可选) + Region *string `form:"region,optional"` // 区域(可选) + ParentAgentId *int64 `form:"parent_agent_id,optional"` // 上级代理ID(可选) + } + + // 代理列表项 + AgentListItem { + Id int64 `json:"id"` // 主键 + UserId int64 `json:"user_id"` // 用户ID + ParentAgentId int64 `json:"parent_agent_id"` // 上级代理ID + LevelName string `json:"level_name"` // 等级名称 + Region string `json:"region"` // 区域 + Mobile string `json:"mobile"` // 手机号 + MembershipExpiryTime string `json:"membership_expiry_time"` // 会员到期时间 + Balance float64 `json:"balance"` // 钱包余额 + TotalEarnings float64 `json:"total_earnings"` // 累计收益 + FrozenBalance float64 `json:"frozen_balance"` // 冻结余额 + WithdrawnAmount float64 `json:"withdrawn_amount"` // 提现总额 + CreateTime string `json:"create_time"` // 创建时间 + IsRealNameVerified bool `json:"is_real_name_verified"` // 是否已实名认证 + RealName string `json:"real_name"` // 实名姓名 + IdCard string `json:"id_card"` // 身份证号 + RealNameStatus string `json:"real_name_status"` // 实名状态(pending/approved/rejected) + } + + // 代理分页查询响应 + AdminGetAgentListResp { + Total int64 `json:"total"` // 总数 + Items []AgentListItem `json:"items"` // 列表数据 + } + + // 代理推广链接分页查询请求 + AdminGetAgentLinkListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + ProductName *string `form:"product_name,optional"` // 产品名(可选) + LinkIdentifier *string `form:"link_identifier,optional"` // 推广码(可选) + } + + // 代理推广链接列表项 + AgentLinkListItem { + AgentId int64 `json:"agent_id"` // 代理ID + ProductName string `json:"product_name"` // 产品名 + Price float64 `json:"price"` // 价格 + LinkIdentifier string `json:"link_identifier"` // 推广码 + CreateTime string `json:"create_time"` // 创建时间 + } + + // 代理推广链接分页查询响应 + AdminGetAgentLinkListResp { + Total int64 `json:"total"` // 总数 + Items []AgentLinkListItem `json:"items"` // 列表数据 + } + + // 代理佣金分页查询请求 + AdminGetAgentCommissionListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + ProductName *string `form:"product_name,optional"` // 产品名(可选) + Status *int64 `form:"status,optional"` // 状态(可选) + } + + // 代理佣金列表项 + AgentCommissionListItem { + Id int64 `json:"id"` // 主键 + AgentId int64 `json:"agent_id"` // 代理ID + OrderId int64 `json:"order_id"` // 订单ID + Amount float64 `json:"amount"` // 金额 + ProductName string `json:"product_name"` // 产品名 + Status int64 `json:"status"` // 状态 + CreateTime string `json:"create_time"` // 创建时间 + } + + // 代理佣金分页查询响应 + AdminGetAgentCommissionListResp { + Total int64 `json:"total"` // 总数 + Items []AgentCommissionListItem `json:"items"` // 列表数据 + } + + // 代理奖励分页查询请求 + AdminGetAgentRewardListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + RelationAgentId *int64 `form:"relation_agent_id,optional"` // 关联代理ID(可选) + Type *string `form:"type,optional"` // 奖励类型(可选) + } + + // 代理奖励列表项 + AgentRewardListItem { + Id int64 `json:"id"` // 主键 + AgentId int64 `json:"agent_id"` // 代理ID + RelationAgentId int64 `json:"relation_agent_id"` // 关联代理ID + Amount float64 `json:"amount"` // 金额 + Type string `json:"type"` // 奖励类型 + CreateTime string `json:"create_time"` // 创建时间 + } + + // 代理奖励分页查询响应 + AdminGetAgentRewardListResp { + Total int64 `json:"total"` // 总数 + Items []AgentRewardListItem `json:"items"` // 列表数据 + } + + // 代理提现分页查询请求 + AdminGetAgentWithdrawalListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + Status *int64 `form:"status,optional"` // 状态(可选) + WithdrawNo *string `form:"withdraw_no,optional"` // 提现单号(可选) + } + + // 代理提现列表项 + AgentWithdrawalListItem { + Id int64 `json:"id"` // 主键 + AgentId int64 `json:"agent_id"` // 代理ID + WithdrawNo string `json:"withdraw_no"` // 提现单号 + Amount float64 `json:"amount"` // 金额 + Status int64 `json:"status"` // 状态 + PayeeAccount string `json:"payee_account"` // 收款账户 + Remark string `json:"remark"` // 备注 + CreateTime string `json:"create_time"` // 创建时间 + } + + // 代理提现分页查询响应 + AdminGetAgentWithdrawalListResp { + Total int64 `json:"total"` // 总数 + Items []AgentWithdrawalListItem `json:"items"` // 列表数据 + } + + // 代理抽佣分页查询请求 + AdminGetAgentCommissionDeductionListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + ProductName *string `form:"product_name,optional"` // 产品名(可选) + Type *string `form:"type,optional"` // 类型(cost/pricing,可选) + Status *int64 `form:"status,optional"` // 状态(可选) + } + + // 代理抽佣列表项 + AgentCommissionDeductionListItem { + Id int64 `json:"id"` // 主键 + AgentId int64 `json:"agent_id"` // 代理ID + DeductedAgentId int64 `json:"deducted_agent_id"` // 被扣代理ID + Amount float64 `json:"amount"` // 金额 + ProductName string `json:"product_name"` // 产品名 + Type string `json:"type"` // 类型(cost/pricing) + Status int64 `json:"status"` // 状态 + CreateTime string `json:"create_time"` // 创建时间 + } + + // 代理抽佣分页查询响应 + AdminGetAgentCommissionDeductionListResp { + Total int64 `json:"total"` // 总数 + Items []AgentCommissionDeductionListItem `json:"items"` // 列表数据 + } + + // 平台抽佣分页查询请求 + AdminGetAgentPlatformDeductionListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + Type *string `form:"type,optional"` // 类型(cost/pricing,可选) + Status *int64 `form:"status,optional"` // 状态(可选) + } + + // 平台抽佣列表项 + AgentPlatformDeductionListItem { + Id int64 `json:"id"` // 主键 + AgentId int64 `json:"agent_id"` // 代理ID + Amount float64 `json:"amount"` // 金额 + Type string `json:"type"` // 类型(cost/pricing) + Status int64 `json:"status"` // 状态 + CreateTime string `json:"create_time"` // 创建时间 + } + + // 平台抽佣分页查询响应 + AdminGetAgentPlatformDeductionListResp { + Total int64 `json:"total"` // 总数 + Items []AgentPlatformDeductionListItem `json:"items"` // 列表数据 + } + + // 代理产品配置分页查询请求 + AdminGetAgentProductionConfigListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + ProductName *string `form:"product_name,optional"` // 产品名(可选) + Id *int64 `form:"id,optional"` // 配置ID(可选) + } + + // 代理产品配置分页查询响应 + AdminGetAgentProductionConfigListResp { + Total int64 `json:"total"` // 总数 + Items []AgentProductionConfigItem `json:"items"` // 列表数据 + } + + // 代理产品配置列表项 + AgentProductionConfigItem { + Id int64 `json:"id"` // 主键 + ProductName string `json:"product_name"` // 产品名 + CostPrice float64 `json:"cost_price"` // 成本 + PriceRangeMin float64 `json:"price_range_min"` // 最低定价 + PriceRangeMax float64 `json:"price_range_max"` // 最高定价 + PricingStandard float64 `json:"pricing_standard"` // 定价标准 + OverpricingRatio float64 `json:"overpricing_ratio"` // 超价比例 + CreateTime string `json:"create_time"` // 创建时间 + } + + // 代理产品配置编辑请求 + AdminUpdateAgentProductionConfigReq { + Id int64 `json:"id"` // 主键 + CostPrice float64 `json:"cost_price"` // 成本 + PriceRangeMin float64 `json:"price_range_min"` // 最低定价 + PriceRangeMax float64 `json:"price_range_max"` // 最高定价 + PricingStandard float64 `json:"pricing_standard"` // 定价标准 + OverpricingRatio float64 `json:"overpricing_ratio"` // 超价比例 + } + + // 代理产品配置编辑响应 + AdminUpdateAgentProductionConfigResp { + Success bool `json:"success"` // 是否成功 + } + + // 代理会员充值订单分页查询请求 + AdminGetAgentMembershipRechargeOrderListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + UserId *int64 `form:"user_id,optional"` // 用户ID(可选) + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + OrderNo *string `form:"order_no,optional"` // 订单号(可选) + PlatformOrderId *string `form:"platform_order_id,optional"` // 平台订单号(可选) + Status *string `form:"status,optional"` // 状态(可选) + PaymentMethod *string `form:"payment_method,optional"` // 支付方式(可选) + } + + // 代理会员充值订单列表项 + AgentMembershipRechargeOrderListItem { + Id int64 `json:"id"` // 主键 + UserId int64 `json:"user_id"` // 用户ID + AgentId int64 `json:"agent_id"` // 代理ID + LevelName string `json:"level_name"` // 等级名称 + Amount float64 `json:"amount"` // 金额 + PaymentMethod string `json:"payment_method"` // 支付方式 + OrderNo string `json:"order_no"` // 订单号 + PlatformOrderId string `json:"platform_order_id"` // 平台订单号 + Status string `json:"status"` // 状态 + CreateTime string `json:"create_time"` // 创建时间 + } + + // 代理会员充值订单分页查询响应 + AdminGetAgentMembershipRechargeOrderListResp { + Total int64 `json:"total"` // 总数 + Items []AgentMembershipRechargeOrderListItem `json:"items"` // 列表数据 + } + + // 代理会员配置分页查询请求 + AdminGetAgentMembershipConfigListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + LevelName *string `form:"level_name,optional"` // 会员级别名称(可选) + } + + // 代理会员配置分页查询响应 + AdminGetAgentMembershipConfigListResp { + Total int64 `json:"total"` // 总数 + Items []AgentMembershipConfigListItem `json:"items"` // 列表数据 + } + + // 代理会员配置列表项 + AgentMembershipConfigListItem { + Id int64 `json:"id"` // 主键 + LevelName string `json:"level_name"` // 会员级别名称 + Price *float64 `json:"price"` // 会员年费 + ReportCommission *float64 `json:"report_commission"` // 直推报告收益 + LowerActivityReward *float64 `json:"lower_activity_reward"` // 下级活跃奖励金额 + NewActivityReward *float64 `json:"new_activity_reward"` // 新增活跃奖励金额 + LowerStandardCount *int64 `json:"lower_standard_count"` // 活跃下级达标个数 + NewLowerStandardCount *int64 `json:"new_lower_standard_count"` // 新增活跃下级达标个数 + LowerWithdrawRewardRatio *float64 `json:"lower_withdraw_reward_ratio"` // 下级提现奖励比例 + LowerConvertVipReward *float64 `json:"lower_convert_vip_reward"` // 下级转化VIP奖励 + LowerConvertSvipReward *float64 `json:"lower_convert_svip_reward"` // 下级转化SVIP奖励 + ExemptionAmount *float64 `json:"exemption_amount"` // 免责金额 + PriceIncreaseMax *float64 `json:"price_increase_max"` // 提价最高金额 + PriceRatio *float64 `json:"price_ratio"` // 提价区间收取比例 + PriceIncreaseAmount *float64 `json:"price_increase_amount"` // 在原本成本上加价的金额 + CreateTime string `json:"create_time"` // 创建时间 + } + + // 代理会员配置编辑请求 + AdminUpdateAgentMembershipConfigReq { + Id int64 `json:"id"` // 主键 + LevelName string `json:"level_name"` // 会员级别名称 + Price float64 `json:"price"` // 会员年费 + ReportCommission float64 `json:"report_commission"` // 直推报告收益 + LowerActivityReward *float64 `json:"lower_activity_reward,optional,omitempty"` // 下级活跃奖励金额 + NewActivityReward *float64 `json:"new_activity_reward,optional,omitempty"` // 新增活跃奖励金额 + LowerStandardCount *int64 `json:"lower_standard_count,optional,omitempty"` // 活跃下级达标个数 + NewLowerStandardCount *int64 `json:"new_lower_standard_count,optional,omitempty"` // 新增活跃下级达标个数 + LowerWithdrawRewardRatio *float64 `json:"lower_withdraw_reward_ratio,optional,omitempty"` // 下级提现奖励比例 + LowerConvertVipReward *float64 `json:"lower_convert_vip_reward,optional,omitempty"` // 下级转化VIP奖励 + LowerConvertSvipReward *float64 `json:"lower_convert_svip_reward,optional,omitempty"` // 下级转化SVIP奖励 + ExemptionAmount *float64 `json:"exemption_amount,optional,omitempty"` // 免责金额 + PriceIncreaseMax *float64 `json:"price_increase_max,optional,omitempty"` // 提价最高金额 + PriceRatio *float64 `json:"price_ratio,optional,omitempty"` // 提价区间收取比例 + PriceIncreaseAmount *float64 `json:"price_increase_amount,optional,omitempty"` // 在原本成本上加价的金额 + } + + // 代理会员配置编辑响应 + AdminUpdateAgentMembershipConfigResp { + Success bool `json:"success"` // 是否成功 + } +) \ No newline at end of file diff --git a/app/main/api/desc/admin/admin_feature.api b/app/main/api/desc/admin/admin_feature.api new file mode 100644 index 0000000..3249c38 --- /dev/null +++ b/app/main/api/desc/admin/admin_feature.api @@ -0,0 +1,108 @@ +syntax = "v1" + +info ( + title: "后台功能管理服务" + desc: "后台功能管理相关接口" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +// 功能管理接口 +@server( + prefix: /api/v1/admin/feature + group: admin_feature +) +service main { + // 创建功能 + @handler AdminCreateFeature + post /create (AdminCreateFeatureReq) returns (AdminCreateFeatureResp) + + // 更新功能 + @handler AdminUpdateFeature + put /update/:id (AdminUpdateFeatureReq) returns (AdminUpdateFeatureResp) + + // 删除功能 + @handler AdminDeleteFeature + delete /delete/:id (AdminDeleteFeatureReq) returns (AdminDeleteFeatureResp) + + // 获取功能列表 + @handler AdminGetFeatureList + get /list (AdminGetFeatureListReq) returns (AdminGetFeatureListResp) + + // 获取功能详情 + @handler AdminGetFeatureDetail + get /detail/:id (AdminGetFeatureDetailReq) returns (AdminGetFeatureDetailResp) +} + +type ( + // 创建功能请求 + AdminCreateFeatureReq { + ApiId string `json:"api_id"` // API标识 + Name string `json:"name"` // 描述 + } + + // 创建功能响应 + AdminCreateFeatureResp { + Id int64 `json:"id"` // 功能ID + } + + // 更新功能请求 + AdminUpdateFeatureReq { + Id int64 `path:"id"` // 功能ID + ApiId *string `json:"api_id,optional"` // API标识 + Name *string `json:"name,optional"` // 描述 + } + + // 更新功能响应 + AdminUpdateFeatureResp { + Success bool `json:"success"` // 是否成功 + } + + // 删除功能请求 + AdminDeleteFeatureReq { + Id int64 `path:"id"` // 功能ID + } + + // 删除功能响应 + AdminDeleteFeatureResp { + Success bool `json:"success"` // 是否成功 + } + + // 获取功能列表请求 + AdminGetFeatureListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + ApiId *string `form:"api_id,optional"` // API标识 + Name *string `form:"name,optional"` // 描述 + } + + // 功能列表项 + FeatureListItem { + Id int64 `json:"id"` // 功能ID + ApiId string `json:"api_id"` // API标识 + Name string `json:"name"` // 描述 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + } + + // 获取功能列表响应 + AdminGetFeatureListResp { + Total int64 `json:"total"` // 总数 + Items []FeatureListItem `json:"items"` // 列表数据 + } + + // 获取功能详情请求 + AdminGetFeatureDetailReq { + Id int64 `path:"id"` // 功能ID + } + + // 获取功能详情响应 + AdminGetFeatureDetailResp { + Id int64 `json:"id"` // 功能ID + ApiId string `json:"api_id"` // API标识 + Name string `json:"name"` // 描述 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + } +) \ No newline at end of file diff --git a/app/main/api/desc/admin/admin_product.api b/app/main/api/desc/admin/admin_product.api new file mode 100644 index 0000000..d748ad2 --- /dev/null +++ b/app/main/api/desc/admin/admin_product.api @@ -0,0 +1,175 @@ +syntax = "v1" + +info ( + title: "后台产品管理服务" + desc: "后台产品管理相关接口" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +// 产品管理接口 +@server( + prefix: /api/v1/admin/product + group: admin_product +) +service main { + // 创建产品 + @handler AdminCreateProduct + post /create (AdminCreateProductReq) returns (AdminCreateProductResp) + + // 更新产品 + @handler AdminUpdateProduct + put /update/:id (AdminUpdateProductReq) returns (AdminUpdateProductResp) + + // 删除产品 + @handler AdminDeleteProduct + delete /delete/:id (AdminDeleteProductReq) returns (AdminDeleteProductResp) + + // 获取产品列表 + @handler AdminGetProductList + get /list (AdminGetProductListReq) returns (AdminGetProductListResp) + + // 获取产品详情 + @handler AdminGetProductDetail + get /detail/:id (AdminGetProductDetailReq) returns (AdminGetProductDetailResp) + + // 获取产品功能列表 + @handler AdminGetProductFeatureList + get /feature/list/:product_id (AdminGetProductFeatureListReq) returns ([]AdminGetProductFeatureListResp) + + // 更新产品功能关联(批量) + @handler AdminUpdateProductFeatures + put /feature/update/:product_id (AdminUpdateProductFeaturesReq) returns (AdminUpdateProductFeaturesResp) +} + +type ( + // 创建产品请求 + AdminCreateProductReq { + ProductName string `json:"product_name"` // 服务名 + ProductEn string `json:"product_en"` // 英文名 + Description string `json:"description"` // 描述 + Notes string `json:"notes,optional"` // 备注 + CostPrice float64 `json:"cost_price"` // 成本 + SellPrice float64 `json:"sell_price"` // 售价 + } + + // 创建产品响应 + AdminCreateProductResp { + Id int64 `json:"id"` // 产品ID + } + + // 更新产品请求 + AdminUpdateProductReq { + Id int64 `path:"id"` // 产品ID + ProductName *string `json:"product_name,optional"` // 服务名 + ProductEn *string `json:"product_en,optional"` // 英文名 + Description *string `json:"description,optional"` // 描述 + Notes *string `json:"notes,optional"` // 备注 + CostPrice *float64 `json:"cost_price,optional"` // 成本 + SellPrice *float64 `json:"sell_price,optional"` // 售价 + } + + // 更新产品响应 + AdminUpdateProductResp { + Success bool `json:"success"` // 是否成功 + } + + // 删除产品请求 + AdminDeleteProductReq { + Id int64 `path:"id"` // 产品ID + } + + // 删除产品响应 + AdminDeleteProductResp { + Success bool `json:"success"` // 是否成功 + } + + // 获取产品列表请求 + AdminGetProductListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + ProductName *string `form:"product_name,optional"` // 服务名 + ProductEn *string `form:"product_en,optional"` // 英文名 + } + + // 产品列表项 + ProductListItem { + Id int64 `json:"id"` // 产品ID + ProductName string `json:"product_name"` // 服务名 + ProductEn string `json:"product_en"` // 英文名 + Description string `json:"description"` // 描述 + Notes string `json:"notes"` // 备注 + CostPrice float64 `json:"cost_price"` // 成本 + SellPrice float64 `json:"sell_price"` // 售价 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + } + + // 获取产品列表响应 + AdminGetProductListResp { + Total int64 `json:"total"` // 总数 + Items []ProductListItem `json:"items"` // 列表数据 + } + + // 获取产品详情请求 + AdminGetProductDetailReq { + Id int64 `path:"id"` // 产品ID + } + + // 获取产品详情响应 + AdminGetProductDetailResp { + Id int64 `json:"id"` // 产品ID + ProductName string `json:"product_name"` // 服务名 + ProductEn string `json:"product_en"` // 英文名 + Description string `json:"description"` // 描述 + Notes string `json:"notes"` // 备注 + CostPrice float64 `json:"cost_price"` // 成本 + SellPrice float64 `json:"sell_price"` // 售价 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + } + + // 获取产品功能列表请求 + AdminGetProductFeatureListReq { + ProductId int64 `path:"product_id"` // 产品ID + } + + // 获取产品功能列表响应Item + AdminGetProductFeatureListResp { + Id int64 `json:"id"` // 关联ID + ProductId int64 `json:"product_id"` // 产品ID + FeatureId int64 `json:"feature_id"` // 功能ID + ApiId string `json:"api_id"` // API标识 + Name string `json:"name"` // 功能描述 + Sort int64 `json:"sort"` // 排序 + Enable int64 `json:"enable"` // 是否启用 + IsImportant int64 `json:"is_important"` // 是否重要 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + } + + // // 获取产品功能列表响应 + // AdminGetProductFeatureListResp { + // Items []ProductFeatureListItem `json:"items"` // 列表数据 + // } + + // 产品功能关联项 + ProductFeatureItem { + FeatureId int64 `json:"feature_id"` // 功能ID + Sort int64 `json:"sort"` // 排序 + Enable int64 `json:"enable"` // 是否启用 + IsImportant int64 `json:"is_important"` // 是否重要 + } + + // 更新产品功能关联请求(批量) + AdminUpdateProductFeaturesReq { + ProductId int64 `path:"product_id"` // 产品ID + Features []ProductFeatureItem `json:"features"` // 功能列表 + } + + // 更新产品功能关联响应 + AdminUpdateProductFeaturesResp { + Success bool `json:"success"` // 是否成功 + } +) \ No newline at end of file diff --git a/app/main/api/desc/admin/admin_query.api b/app/main/api/desc/admin/admin_query.api new file mode 100644 index 0000000..a2e06d0 --- /dev/null +++ b/app/main/api/desc/admin/admin_query.api @@ -0,0 +1,135 @@ +syntax = "v1" + +info ( + title: "查询服务" + desc: "查询服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +@server ( + prefix: api/v1/admin/query + group: admin_query + jwt: JwtAuth +) +service main { + @doc "获取查询详情" + @handler AdminGetQueryDetailByOrderId + get /detail/:order_id (AdminGetQueryDetailByOrderIdReq) returns (AdminGetQueryDetailByOrderIdResp) + + @doc "获取清理日志列表" + @handler AdminGetQueryCleanupLogList + get /cleanup/logs (AdminGetQueryCleanupLogListReq) returns (AdminGetQueryCleanupLogListResp) + + @doc "获取清理详情列表" + @handler AdminGetQueryCleanupDetailList + get /cleanup/details/:log_id (AdminGetQueryCleanupDetailListReq) returns (AdminGetQueryCleanupDetailListResp) + + @doc "获取清理配置列表" + @handler AdminGetQueryCleanupConfigList + get /cleanup/configs (AdminGetQueryCleanupConfigListReq) returns (AdminGetQueryCleanupConfigListResp) + + @doc "更新清理配置" + @handler AdminUpdateQueryCleanupConfig + put /cleanup/config (AdminUpdateQueryCleanupConfigReq) returns (AdminUpdateQueryCleanupConfigResp) +} + +type AdminGetQueryDetailByOrderIdReq { + OrderId int64 `path:"order_id"` +} + +type AdminGetQueryDetailByOrderIdResp { + Id int64 `json:"id"` // 主键ID + OrderId int64 `json:"order_id"` // 订单ID + UserId int64 `json:"user_id"` // 用户ID + ProductName string `json:"product_name"` // 产品ID + QueryParams map[string]interface{} `json:"query_params"` + QueryData []AdminQueryItem `json:"query_data"` + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + QueryState string `json:"query_state"` // 查询状态 +} + +type AdminQueryItem { + Feature interface{} `json:"feature"` + Data interface{} `json:"data"` // 这里可以是 map 或 具体的 struct +} + +// 清理日志相关请求响应定义 +type AdminGetQueryCleanupLogListReq { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"page_size,default=20"` // 每页数量 + Status int64 `form:"status,optional"` // 状态:1-成功,2-失败 + StartTime string `form:"start_time,optional"` // 开始时间 + EndTime string `form:"end_time,optional"` // 结束时间 +} + +type AdminGetQueryCleanupLogListResp { + Total int64 `json:"total"` // 总数 + Items []QueryCleanupLogItem `json:"items"` // 列表 +} + +type QueryCleanupLogItem { + Id int64 `json:"id"` // 主键ID + CleanupTime string `json:"cleanup_time"` // 清理时间 + CleanupBefore string `json:"cleanup_before"` // 清理截止时间 + Status int64 `json:"status"` // 状态:1-成功,2-失败 + AffectedRows int64 `json:"affected_rows"` // 影响行数 + ErrorMsg string `json:"error_msg"` // 错误信息 + Remark string `json:"remark"` // 备注 + CreateTime string `json:"create_time"` // 创建时间 +} + +// 清理详情相关请求响应定义 +type AdminGetQueryCleanupDetailListReq { + LogId int64 `path:"log_id"` // 清理日志ID + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"page_size,default=20"` // 每页数量 +} + +type AdminGetQueryCleanupDetailListResp { + Total int64 `json:"total"` // 总数 + Items []QueryCleanupDetailItem `json:"items"` // 列表 +} + +type QueryCleanupDetailItem { + Id int64 `json:"id"` // 主键ID + CleanupLogId int64 `json:"cleanup_log_id"` // 清理日志ID + QueryId int64 `json:"query_id"` // 查询ID + OrderId int64 `json:"order_id"` // 订单ID + UserId int64 `json:"user_id"` // 用户ID + ProductName string `json:"product_name"` // 产品名称 + QueryState string `json:"query_state"` // 查询状态 + CreateTimeOld string `json:"create_time_old"` // 原创建时间 + CreateTime string `json:"create_time"` // 创建时间 +} + +// 清理配置相关请求响应定义 +type AdminGetQueryCleanupConfigListReq { + Status int64 `form:"status,optional"` // 状态:1-启用,0-禁用 +} + +type AdminGetQueryCleanupConfigListResp { + Items []QueryCleanupConfigItem `json:"items"` // 配置列表 +} + +type QueryCleanupConfigItem { + Id int64 `json:"id"` // 主键ID + ConfigKey string `json:"config_key"` // 配置键 + ConfigValue string `json:"config_value"` // 配置值 + ConfigDesc string `json:"config_desc"` // 配置描述 + Status int64 `json:"status"` // 状态:1-启用,0-禁用 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type AdminUpdateQueryCleanupConfigReq { + Id int64 `json:"id"` // 主键ID + ConfigValue string `json:"config_value"` // 配置值 + Status int64 `json:"status"` // 状态:1-启用,0-禁用 +} + +type AdminUpdateQueryCleanupConfigResp { + Success bool `json:"success"` // 是否成功 +} \ No newline at end of file diff --git a/app/main/api/desc/admin/admin_user.api b/app/main/api/desc/admin/admin_user.api new file mode 100644 index 0000000..a83014d --- /dev/null +++ b/app/main/api/desc/admin/admin_user.api @@ -0,0 +1,131 @@ +syntax = "v1" + +info ( + title: "后台用户中心服务" + desc: "后台用户中心服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +@server ( + prefix: api/v1/admin/user + group: admin_user + jwt: JwtAuth +) +service main { + @doc "获取用户列表" + @handler AdminGetUserList + get /list (AdminGetUserListReq) returns (AdminGetUserListResp) + + @doc "获取用户详情" + @handler AdminGetUserDetail + get /detail/:id (AdminGetUserDetailReq) returns (AdminGetUserDetailResp) + + @doc "创建用户" + @handler AdminCreateUser + post /create (AdminCreateUserReq) returns (AdminCreateUserResp) + + @doc "更新用户" + @handler AdminUpdateUser + put /update/:id (AdminUpdateUserReq) returns (AdminUpdateUserResp) + + @doc "删除用户" + @handler AdminDeleteUser + delete /delete/:id (AdminDeleteUserReq) returns (AdminDeleteUserResp) + + @doc "用户信息" + @handler AdminUserInfo + get /info (AdminUserInfoReq) returns (AdminUserInfoResp) +} + +type ( + // 列表请求 + AdminGetUserListReq { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + Username string `form:"username,optional"` // 用户名 + RealName string `form:"real_name,optional"` // 真实姓名 + Status int64 `form:"status,optional,default=-1"` // 状态:0-禁用,1-启用 + } + + // 列表响应 + AdminGetUserListResp { + Total int64 `json:"total"` // 总数 + Items []AdminUserListItem `json:"items"` // 列表 + } + + // 列表项 + AdminUserListItem { + Id int64 `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + RealName string `json:"real_name"` // 真实姓名 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + CreateTime string `json:"create_time"` // 创建时间 + RoleIds []int64 `json:"role_ids"` // 关联的角色ID列表 + } + + // 详情请求 + AdminGetUserDetailReq { + Id int64 `path:"id"` // 用户ID + } + + // 详情响应 + AdminGetUserDetailResp { + Id int64 `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + RealName string `json:"real_name"` // 真实姓名 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + RoleIds []int64 `json:"role_ids"` // 关联的角色ID列表 + } + + // 创建请求 + AdminCreateUserReq { + Username string `json:"username"` // 用户名 + RealName string `json:"real_name"` // 真实姓名 + Status int64 `json:"status,default=1"` // 状态:0-禁用,1-启用 + RoleIds []int64 `json:"role_ids"` // 关联的角色ID列表 + } + + // 创建响应 + AdminCreateUserResp { + Id int64 `json:"id"` // 用户ID + } + + // 更新请求 + AdminUpdateUserReq { + Id int64 `path:"id"` // 用户ID + Username *string `json:"username,optional"` // 用户名 + RealName *string `json:"real_name,optional"` // 真实姓名 + Status *int64 `json:"status,optional"` // 状态:0-禁用,1-启用 + RoleIds []int64 `json:"role_ids,optional"` // 关联的角色ID列表 + } + + // 更新响应 + AdminUpdateUserResp { + Success bool `json:"success"` // 是否成功 + } + + // 删除请求 + AdminDeleteUserReq { + Id int64 `path:"id"` // 用户ID + } + + // 删除响应 + AdminDeleteUserResp { + Success bool `json:"success"` // 是否成功 + } + + // 用户信息请求 + AdminUserInfoReq { + } + + // 用户信息响应 + AdminUserInfoResp { + Username string `json:"username"` // 用户名 + RealName string `json:"real_name"` // 真实姓名 + Roles []string `json:"roles"` // 角色编码列表 + } +) \ No newline at end of file diff --git a/app/main/api/desc/admin/auth.api b/app/main/api/desc/admin/auth.api new file mode 100644 index 0000000..e8be573 --- /dev/null +++ b/app/main/api/desc/admin/auth.api @@ -0,0 +1,34 @@ +syntax = "v1" + +info ( + title: "认证中心服务" + desc: "认证中心服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +@server ( + prefix: api/v1/admin/auth + group: admin_auth +) +service main { + @doc "登录" + @handler AdminLogin + post /login (AdminLoginReq) returns (AdminLoginResp) + +} + +type ( + AdminLoginReq { + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + Captcha bool `json:"captcha" validate:"required"` + } + AdminLoginResp { + AccessToken string `json:"access_token"` + AccessExpire int64 `json:"access_expire"` + RefreshAfter int64 `json:"refresh_after"` + Roles []string `json:"roles"` + } +) \ No newline at end of file diff --git a/app/main/api/desc/admin/menu.api b/app/main/api/desc/admin/menu.api new file mode 100644 index 0000000..52d87be --- /dev/null +++ b/app/main/api/desc/admin/menu.api @@ -0,0 +1,149 @@ +syntax = "v1" + +info ( + title: "菜单中心服务" + desc: "菜单中心服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +@server ( + prefix: api/v1/admin/menu + group: admin_menu + jwt: JwtAuth +) +service main { + @doc "获取菜单列表" + @handler GetMenuList + get /list (GetMenuListReq) returns ([]MenuListItem) + + @doc "获取菜单详情" + @handler GetMenuDetail + get /detail/:id (GetMenuDetailReq) returns (GetMenuDetailResp) + + @doc "创建菜单" + @handler CreateMenu + post /create (CreateMenuReq) returns (CreateMenuResp) + + @doc "更新菜单" + @handler UpdateMenu + put /update/:id (UpdateMenuReq) returns (UpdateMenuResp) + + @doc "删除菜单" + @handler DeleteMenu + delete /delete/:id (DeleteMenuReq) returns (DeleteMenuResp) + + @doc "获取所有菜单(树形结构)" + @handler GetMenuAll + get /all (GetMenuAllReq) returns ([]GetMenuAllResp) +} + +type ( + // 列表请求 + GetMenuListReq { + Name string `form:"name,optional"` // 菜单名称 + Path string `form:"path,optional"` // 路由路径 + Status int64 `form:"status,optional,default=-1"` // 状态:0-禁用,1-启用 + Type string `form:"type,optional"` // 类型 + } + + // 列表项 + MenuListItem { + Id int64 `json:"id"` // 菜单ID + Pid int64 `json:"pid"` // 父菜单ID + Name string `json:"name"` // 路由名称 + Path string `json:"path"` // 路由路径 + Component string `json:"component"` // 组件路径 + Redirect string `json:"redirect"` // 重定向路径 + Meta map[string]interface{} `json:"meta"` // 路由元数据 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + Type string `json:"type"` // 类型 + Sort int64 `json:"sort"` // 排序 + CreateTime string `json:"createTime"` // 创建时间 + Children []MenuListItem `json:"children"` // 子菜单 + } + + // 详情请求 + GetMenuDetailReq { + Id int64 `path:"id"` // 菜单ID + } + + // 详情响应 + GetMenuDetailResp { + Id int64 `json:"id"` // 菜单ID + Pid int64 `json:"pid"` // 父菜单ID + Name string `json:"name"` // 路由名称 + Path string `json:"path"` // 路由路径 + Component string `json:"component"` // 组件路径 + Redirect string `json:"redirect"` // 重定向路径 + Meta map[string]interface{} `json:"meta"` // 路由元数据 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + Type string `json:"type"` // 类型 + Sort int64 `json:"sort"` // 排序 + CreateTime string `json:"createTime"` // 创建时间 + UpdateTime string `json:"updateTime"` // 更新时间 + } + + // 创建请求 + CreateMenuReq { + Pid int64 `json:"pid,optional"` // 父菜单ID + Name string `json:"name"` // 路由名称 + Path string `json:"path,optional"` // 路由路径 + Component string `json:"component,optional"` // 组件路径 + Redirect string `json:"redirect,optional"` // 重定向路径 + Meta map[string]interface{} `json:"meta"` // 路由元数据 + Status int64 `json:"status,optional,default=1"` // 状态:0-禁用,1-启用 + Type string `json:"type"` // 类型 + Sort int64 `json:"sort,optional"` // 排序 + } + + // 创建响应 + CreateMenuResp { + Id int64 `json:"id"` // 菜单ID + } + + // 更新请求 + UpdateMenuReq { + Id int64 `path:"id"` // 菜单ID + Pid int64 `json:"pid,optional"` // 父菜单ID + Name string `json:"name"` // 路由名称 + Path string `json:"path,optional"` // 路由路径 + Component string `json:"component,optional"` // 组件路径 + Redirect string `json:"redirect,optional"` // 重定向路径 + Meta map[string]interface{} `json:"meta"` // 路由元数据 + Status int64 `json:"status,optional"` // 状态:0-禁用,1-启用 + Type string `json:"type"` // 类型 + Sort int64 `json:"sort,optional"` // 排序 + } + + // 更新响应 + UpdateMenuResp { + Success bool `json:"success"` // 是否成功 + } + + // 删除请求 + DeleteMenuReq { + Id int64 `path:"id"` // 菜单ID + } + + // 删除响应 + DeleteMenuResp { + Success bool `json:"success"` // 是否成功 + } + + // 获取所有菜单请求 + GetMenuAllReq { + } + + // 获取所有菜单响应 + GetMenuAllResp { + Name string `json:"name"` + Path string `json:"path"` + Redirect string `json:"redirect,omitempty"` + Component string `json:"component,omitempty"` + Sort int64 `json:"sort"` + Meta map[string]interface{} `json:"meta"` + Children []GetMenuAllResp `json:"children"` + } +) \ No newline at end of file diff --git a/app/main/api/desc/admin/notification.api b/app/main/api/desc/admin/notification.api new file mode 100644 index 0000000..169f60d --- /dev/null +++ b/app/main/api/desc/admin/notification.api @@ -0,0 +1,127 @@ +syntax = "v1" + +type ( + // 创建通知请求 + AdminCreateNotificationReq { + Title string `json:"title"` // 通知标题 + NotificationPage string `json:"notification_page"` // 通知页面 + Content string `json:"content"` // 通知内容 + StartDate string `json:"start_date"` // 生效开始日期(yyyy-MM-dd) + StartTime string `json:"start_time"` // 生效开始时间(HH:mm:ss) + EndDate string `json:"end_date"` // 生效结束日期(yyyy-MM-dd) + EndTime string `json:"end_time"` // 生效结束时间(HH:mm:ss) + Status int64 `json:"status"` // 状态:1-启用,0-禁用 + } + + // 创建通知响应 + AdminCreateNotificationResp { + Id int64 `json:"id"` // 通知ID + } + + // 更新通知请求 + AdminUpdateNotificationReq { + Id int64 `path:"id"` // 通知ID + Title *string `json:"title,optional"` // 通知标题 + Content *string `json:"content,optional"` // 通知内容 + NotificationPage *string `json:"notification_page,optional"` // 通知页面 + StartDate *string `json:"start_date,optional"` // 生效开始日期 + StartTime *string `json:"start_time,optional"` // 生效开始时间 + EndDate *string `json:"end_date,optional"` // 生效结束日期 + EndTime *string `json:"end_time,optional"` // 生效结束时间 + Status *int64 `json:"status,optional"` // 状态 + } + + // 更新通知响应 + AdminUpdateNotificationResp { + Success bool `json:"success"` // 是否成功 + } + + // 删除通知请求 + AdminDeleteNotificationReq { + Id int64 `path:"id"` // 通知ID + } + + // 删除通知响应 + AdminDeleteNotificationResp { + Success bool `json:"success"` // 是否成功 + } + + // 获取通知详情请求 + AdminGetNotificationDetailReq { + Id int64 `path:"id"` // 通知ID + } + + // 获取通知详情响应 + AdminGetNotificationDetailResp { + Id int64 `json:"id"` // 通知ID + Title string `json:"title"` // 通知标题 + Content string `json:"content"` // 通知内容 + NotificationPage string `json:"notification_page"` // 通知页面 + StartDate string `json:"start_date"` // 生效开始日期 + StartTime string `json:"start_time"` // 生效开始时间 + EndDate string `json:"end_date"` // 生效结束日期 + EndTime string `json:"end_time"` // 生效结束时间 + Status int64 `json:"status"` // 状态 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + } + + // 获取通知列表请求 + AdminGetNotificationListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + Title *string `form:"title,optional"` // 通知标题(可选) + NotificationPage *string `form:"notification_page,optional"` // 通知页面(可选) + Status *int64 `form:"status,optional"` // 状态(可选) + StartDate *string `form:"start_date,optional"` // 开始日期范围(可选) + EndDate *string `form:"end_date,optional"` // 结束日期范围(可选) + } + + // 通知列表项 + NotificationListItem { + Id int64 `json:"id"` // 通知ID + Title string `json:"title"` // 通知标题 + NotificationPage string `json:"notification_page"` // 通知页面 + Content string `json:"content"` // 通知内容 + StartDate string `json:"start_date"` // 生效开始日期 + StartTime string `json:"start_time"` // 生效开始时间 + EndDate string `json:"end_date"` // 生效结束日期 + EndTime string `json:"end_time"` // 生效结束时间 + Status int64 `json:"status"` // 状态 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + } + + // 获取通知列表响应 + AdminGetNotificationListResp { + Total int64 `json:"total"` // 总数 + Items []NotificationListItem `json:"items"` // 列表数据 + } +) + +// 通知管理接口 +@server( + prefix: /api/v1/admin/notification + group: admin_notification +) +service main { + // 创建通知 + @handler AdminCreateNotification + post /create (AdminCreateNotificationReq) returns (AdminCreateNotificationResp) + + // 更新通知 + @handler AdminUpdateNotification + put /update/:id (AdminUpdateNotificationReq) returns (AdminUpdateNotificationResp) + + // 删除通知 + @handler AdminDeleteNotification + delete /delete/:id (AdminDeleteNotificationReq) returns (AdminDeleteNotificationResp) + + // 获取通知详情 + @handler AdminGetNotificationDetail + get /detail/:id (AdminGetNotificationDetailReq) returns (AdminGetNotificationDetailResp) + + // 获取通知列表 + @handler AdminGetNotificationList + get /list (AdminGetNotificationListReq) returns (AdminGetNotificationListResp) +} \ No newline at end of file diff --git a/app/main/api/desc/admin/order.api b/app/main/api/desc/admin/order.api new file mode 100644 index 0000000..73a9f65 --- /dev/null +++ b/app/main/api/desc/admin/order.api @@ -0,0 +1,169 @@ +syntax = "v1" + +info ( + title: "订单服务" + desc: "订单服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +@server ( + prefix: api/v1/admin/order + group: admin_order + jwt: JwtAuth +) +service main { + @doc "获取订单列表" + @handler AdminGetOrderList + get /list (AdminGetOrderListReq) returns (AdminGetOrderListResp) + + @doc "获取订单详情" + @handler AdminGetOrderDetail + get /detail/:id (AdminGetOrderDetailReq) returns (AdminGetOrderDetailResp) + + @doc "创建订单" + @handler AdminCreateOrder + post /create (AdminCreateOrderReq) returns (AdminCreateOrderResp) + + @doc "更新订单" + @handler AdminUpdateOrder + put /update/:id (AdminUpdateOrderReq) returns (AdminUpdateOrderResp) + + @doc "删除订单" + @handler AdminDeleteOrder + delete /delete/:id (AdminDeleteOrderReq) returns (AdminDeleteOrderResp) + + @doc "订单退款" + @handler AdminRefundOrder + post /refund/:id (AdminRefundOrderReq) returns (AdminRefundOrderResp) +} + +type ( + // 列表请求 + AdminGetOrderListReq { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + OrderNo string `form:"order_no,optional"` // 商户订单号 + PlatformOrderId string `form:"platform_order_id,optional"` // 支付订单号 + ProductName string `form:"product_name,optional"` // 产品名称 + PaymentPlatform string `form:"payment_platform,optional"` // 支付方式 + PaymentScene string `form:"payment_scene,optional"` // 支付平台 + Amount float64 `form:"amount,optional"` // 金额 + Status string `form:"status,optional"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 + IsPromotion int64 `form:"is_promotion,optional,default=-1"` // 是否推广订单:0-否,1-是 + CreateTimeStart string `form:"create_time_start,optional"` // 创建时间开始 + CreateTimeEnd string `form:"create_time_end,optional"` // 创建时间结束 + PayTimeStart string `form:"pay_time_start,optional"` // 支付时间开始 + PayTimeEnd string `form:"pay_time_end,optional"` // 支付时间结束 + RefundTimeStart string `form:"refund_time_start,optional"` // 退款时间开始 + RefundTimeEnd string `form:"refund_time_end,optional"` // 退款时间结束 + } + + // 列表响应 + AdminGetOrderListResp { + Total int64 `json:"total"` // 总数 + Items []OrderListItem `json:"items"` // 列表 + } + + // 列表项 + OrderListItem { + Id int64 `json:"id"` // 订单ID + OrderNo string `json:"order_no"` // 商户订单号 + PlatformOrderId string `json:"platform_order_id"` // 支付订单号 + ProductName string `json:"product_name"` // 产品名称 + PaymentPlatform string `json:"payment_platform"` // 支付方式 + PaymentScene string `json:"payment_scene"` // 支付平台 + Amount float64 `json:"amount"` // 金额 + Status string `json:"status"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 + QueryState string `json:"query_state"` // 查询状态:pending-待查询,success-查询成功,failed-查询失败 processing-查询中 + CreateTime string `json:"create_time"` // 创建时间 + PayTime string `json:"pay_time"` // 支付时间 + RefundTime string `json:"refund_time"` // 退款时间 + IsPromotion int64 `json:"is_promotion"` // 是否推广订单:0-否,1-是 + } + + // 详情请求 + AdminGetOrderDetailReq { + Id int64 `path:"id"` // 订单ID + } + + // 详情响应 + AdminGetOrderDetailResp { + Id int64 `json:"id"` // 订单ID + OrderNo string `json:"order_no"` // 商户订单号 + PlatformOrderId string `json:"platform_order_id"` // 支付订单号 + ProductName string `json:"product_name"` // 产品名称 + PaymentPlatform string `json:"payment_platform"` // 支付方式 + PaymentScene string `json:"payment_scene"` // 支付平台 + Amount float64 `json:"amount"` // 金额 + Status string `json:"status"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 + QueryState string `json:"query_state"` // 查询状态:pending-待查询,success-查询成功,failed-查询失败 processing-查询中 + CreateTime string `json:"create_time"` // 创建时间 + PayTime string `json:"pay_time"` // 支付时间 + RefundTime string `json:"refund_time"` // 退款时间 + IsPromotion int64 `json:"is_promotion"` // 是否推广订单:0-否,1-是 + UpdateTime string `json:"update_time"` // 更新时间 + } + + // 创建请求 + AdminCreateOrderReq { + OrderNo string `json:"order_no"` // 商户订单号 + PlatformOrderId string `json:"platform_order_id"` // 支付订单号 + ProductName string `json:"product_name"` // 产品名称 + PaymentPlatform string `json:"payment_platform"` // 支付方式 + PaymentScene string `json:"payment_scene"` // 支付平台 + Amount float64 `json:"amount"` // 金额 + Status string `json:"status,default=pending"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 + IsPromotion int64 `json:"is_promotion,default=0"` // 是否推广订单:0-否,1-是 + } + + // 创建响应 + AdminCreateOrderResp { + Id int64 `json:"id"` // 订单ID + } + + // 更新请求 + AdminUpdateOrderReq { + Id int64 `path:"id"` // 订单ID + OrderNo *string `json:"order_no,optional"` // 商户订单号 + PlatformOrderId *string `json:"platform_order_id,optional"` // 支付订单号 + ProductName *string `json:"product_name,optional"` // 产品名称 + PaymentPlatform *string `json:"payment_platform,optional"` // 支付方式 + PaymentScene *string `json:"payment_scene,optional"` // 支付平台 + Amount *float64 `json:"amount,optional"` // 金额 + Status *string `json:"status,optional"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 + PayTime *string `json:"pay_time,optional"` // 支付时间 + RefundTime *string `json:"refund_time,optional"` // 退款时间 + IsPromotion *int64 `json:"is_promotion,optional"` // 是否推广订单:0-否,1-是 + } + + // 更新响应 + AdminUpdateOrderResp { + Success bool `json:"success"` // 是否成功 + } + + // 删除请求 + AdminDeleteOrderReq { + Id int64 `path:"id"` // 订单ID + } + + // 删除响应 + AdminDeleteOrderResp { + Success bool `json:"success"` // 是否成功 + } + + // 退款请求 + AdminRefundOrderReq { + Id int64 `path:"id"` // 订单ID + RefundAmount float64 `json:"refund_amount"` // 退款金额 + RefundReason string `json:"refund_reason"` // 退款原因 + } + + // 退款响应 + AdminRefundOrderResp { + Status string `json:"status"` // 退款状态 + RefundNo string `json:"refund_no"` // 退款单号 + Amount float64 `json:"amount"` // 退款金额 + } +) \ No newline at end of file diff --git a/app/main/api/desc/admin/platform_user.api b/app/main/api/desc/admin/platform_user.api new file mode 100644 index 0000000..d63abfc --- /dev/null +++ b/app/main/api/desc/admin/platform_user.api @@ -0,0 +1,124 @@ +syntax = "v1" + +info ( + title: "平台用户管理" + desc: "平台用户管理" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +// 平台用户管理接口 +@server( + prefix: /api/v1/admin/platform_user + group: admin_platform_user + jwt: JwtAuth +) +service main { + // 创建平台用户 + @handler AdminCreatePlatformUser + post /create (AdminCreatePlatformUserReq) returns (AdminCreatePlatformUserResp) + + // 更新平台用户 + @handler AdminUpdatePlatformUser + put /update/:id (AdminUpdatePlatformUserReq) returns (AdminUpdatePlatformUserResp) + + // 删除平台用户 + @handler AdminDeletePlatformUser + delete /delete/:id (AdminDeletePlatformUserReq) returns (AdminDeletePlatformUserResp) + + // 获取平台用户分页列表 + @handler AdminGetPlatformUserList + get /list (AdminGetPlatformUserListReq) returns (AdminGetPlatformUserListResp) + + // 获取平台用户详情 + @handler AdminGetPlatformUserDetail + get /detail/:id (AdminGetPlatformUserDetailReq) returns (AdminGetPlatformUserDetailResp) +} + +type ( + // 分页列表请求 + AdminGetPlatformUserListReq { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + Mobile string `form:"mobile,optional"` // 手机号 + Nickname string `form:"nickname,optional"` // 昵称 + Inside int64 `form:"inside,optional"` // 是否内部用户 1-是 0-否 + CreateTimeStart string `form:"create_time_start,optional"` // 创建时间开始 + CreateTimeEnd string `form:"create_time_end,optional"` // 创建时间结束 + OrderBy string `form:"order_by,optional"` // 排序字段 + OrderType string `form:"order_type,optional"` // 排序类型 + } + + // 分页列表响应 + AdminGetPlatformUserListResp { + Total int64 `json:"total"` // 总数 + Items []PlatformUserListItem `json:"items"` // 列表 + } + + // 列表项 + PlatformUserListItem { + Id int64 `json:"id"` // 用户ID + Mobile string `json:"mobile"` // 手机号 + Nickname string `json:"nickname"` // 昵称 + Info string `json:"info"` // 备注信息 + Inside int64 `json:"inside"` // 是否内部用户 1-是 0-否 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + } + + // 详情请求 + AdminGetPlatformUserDetailReq { + Id int64 `path:"id"` // 用户ID + } + + // 详情响应 + AdminGetPlatformUserDetailResp { + Id int64 `json:"id"` // 用户ID + Mobile string `json:"mobile"` // 手机号 + Nickname string `json:"nickname"` // 昵称 + Info string `json:"info"` // 备注信息 + Inside int64 `json:"inside"` // 是否内部用户 1-是 0-否 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + } + + // 创建请求 + AdminCreatePlatformUserReq { + Mobile string `json:"mobile"` // 手机号 + Password string `json:"password"` // 密码 + Nickname string `json:"nickname"` // 昵称 + Info string `json:"info"` // 备注信息 + Inside int64 `json:"inside"` // 是否内部用户 1-是 0-否 + } + + // 创建响应 + AdminCreatePlatformUserResp { + Id int64 `json:"id"` // 用户ID + } + + // 更新请求 + AdminUpdatePlatformUserReq { + Id int64 `path:"id"` // 用户ID + Mobile *string `json:"mobile,optional"` // 手机号 + Password *string `json:"password,optional"` // 密码 + Nickname *string `json:"nickname,optional"` // 昵称 + Info *string `json:"info,optional"` // 备注信息 + Inside *int64 `json:"inside,optional"` // 是否内部用户 1-是 0-否 + } + + // 更新响应 + AdminUpdatePlatformUserResp { + Success bool `json:"success"` // 是否成功 + } + + // 删除请求 + AdminDeletePlatformUserReq { + Id int64 `path:"id"` // 用户ID + } + + // 删除响应 + AdminDeletePlatformUserResp { + Success bool `json:"success"` // 是否成功 + } +) \ No newline at end of file diff --git a/app/main/api/desc/admin/promotion.api b/app/main/api/desc/admin/promotion.api new file mode 100644 index 0000000..b821b9b --- /dev/null +++ b/app/main/api/desc/admin/promotion.api @@ -0,0 +1,183 @@ +syntax = "v1" + +info ( + title: "推广服务" + desc: "推广服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +@server ( + prefix: api/v1/admin/promotion/link + group: admin_promotion + jwt: JwtAuth +) +service main { + @doc "获取推广链接列表" + @handler GetPromotionLinkList + get /list (GetPromotionLinkListReq) returns (GetPromotionLinkListResp) + + @doc "获取推广链接详情" + @handler GetPromotionLinkDetail + get /detail/:id (GetPromotionLinkDetailReq) returns (GetPromotionLinkDetailResp) + + @doc "创建推广链接" + @handler CreatePromotionLink + post /create (CreatePromotionLinkReq) returns (CreatePromotionLinkResp) + + @doc "更新推广链接" + @handler UpdatePromotionLink + put /update/:id (UpdatePromotionLinkReq) returns (UpdatePromotionLinkResp) + + @doc "删除推广链接" + @handler DeletePromotionLink + delete /delete/:id (DeletePromotionLinkReq) returns (DeletePromotionLinkResp) +} + +type ( + // 列表请求 + GetPromotionLinkListReq { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + Name string `form:"name,optional"` // 链接名称 + Url string `form:"url,optional"` // 推广链接URL + } + + // 列表响应 + GetPromotionLinkListResp { + Total int64 `json:"total"` // 总数 + Items []PromotionLinkItem `json:"items"` // 列表 + } + + // 列表项 + PromotionLinkItem { + Id int64 `json:"id"` // 链接ID + Name string `json:"name"` // 链接名称 + Url string `json:"url"` // 推广链接URL + ClickCount int64 `json:"click_count"` // 点击数 + PayCount int64 `json:"pay_count"` // 付费次数 + PayAmount string `json:"pay_amount"` // 付费金额 + CreateTime string `json:"create_time"` // 创建时间 + LastClickTime string `json:"last_click_time,optional"` // 最后点击时间 + LastPayTime string `json:"last_pay_time,optional"` // 最后付费时间 + } + + // 详情请求 + GetPromotionLinkDetailReq { + Id int64 `path:"id"` // 链接ID + } + + // 详情响应 + GetPromotionLinkDetailResp { + Name string `json:"name"` // 链接名称 + Url string `json:"url"` // 推广链接URL + ClickCount int64 `json:"click_count"` // 点击数 + PayCount int64 `json:"pay_count"` // 付费次数 + PayAmount string `json:"pay_amount"` // 付费金额 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + LastClickTime string `json:"last_click_time,optional"` // 最后点击时间 + LastPayTime string `json:"last_pay_time,optional"` // 最后付费时间 + } + + // 创建请求 + CreatePromotionLinkReq { + Name string `json:"name"` // 链接名称 + } + + // 创建响应 + CreatePromotionLinkResp { + Id int64 `json:"id"` // 链接ID + Url string `json:"url"` // 生成的推广链接URL + } + + // 更新请求 + UpdatePromotionLinkReq { + Id int64 `path:"id"` // 链接ID + Name *string `json:"name,optional"` // 链接名称 + } + + // 更新响应 + UpdatePromotionLinkResp { + Success bool `json:"success"` // 是否成功 + } + + // 删除请求 + DeletePromotionLinkReq { + Id int64 `path:"id"` // 链接ID + } + + // 删除响应 + DeletePromotionLinkResp { + Success bool `json:"success"` // 是否成功 + } +) + +@server ( + prefix: api/v1/admin/promotion/link + group: admin_promotion +) +service main { + @doc "记录链接点击" + @handler RecordLinkClick + get /record/:path (RecordLinkClickReq) returns (RecordLinkClickResp) +} + +type ( + // 记录链接点击请求 + RecordLinkClickReq { + Path string `path:"path"` // 链接路径 + } + + // 记录链接点击响应 + RecordLinkClickResp { + Success bool `json:"success"` // 是否成功 + } +) +@server ( + prefix: api/v1/admin/promotion/stats + group: admin_promotion + jwt: JwtAuth +) +service main { + @doc "获取推广历史记录" + @handler GetPromotionStatsHistory + get /history (GetPromotionStatsHistoryReq) returns ([]PromotionStatsHistoryItem) + + @doc "获取推广总统计" + @handler GetPromotionStatsTotal + get /total (GetPromotionStatsTotalReq) returns (GetPromotionStatsTotalResp) +} + +type ( + // 获取推广历史记录请求 + GetPromotionStatsHistoryReq { + StartDate string `form:"start_date"` // 开始日期,格式:YYYY-MM-DD + EndDate string `form:"end_date"` // 结束日期,格式:YYYY-MM-DD + } + + // 推广历史记录项 + PromotionStatsHistoryItem { + Id int64 `json:"id"` // 记录ID + LinkId int64 `json:"link_id"` // 链接ID + PayAmount float64 `json:"pay_amount"` // 金额 + ClickCount int64 `json:"click_count"` // 点击数 + PayCount int64 `json:"pay_count"` // 付费次数 + StatsDate string `json:"stats_date"` // 统计日期 + } + + // 获取推广总统计请求 + GetPromotionStatsTotalReq { + } + + // 获取推广总统计响应 + GetPromotionStatsTotalResp { + TodayPayAmount float64 `json:"today_pay_amount"` // 今日金额 + TodayClickCount int64 `json:"today_click_count"` // 今日点击数 + TodayPayCount int64 `json:"today_pay_count"` // 今日付费次数 + TotalPayAmount float64 `json:"total_pay_amount"` // 总金额 + TotalClickCount int64 `json:"total_click_count"` // 总点击数 + TotalPayCount int64 `json:"total_pay_count"` // 总付费次数 + } +) \ No newline at end of file diff --git a/app/main/api/desc/admin/role.api b/app/main/api/desc/admin/role.api new file mode 100644 index 0000000..51a3de9 --- /dev/null +++ b/app/main/api/desc/admin/role.api @@ -0,0 +1,124 @@ +syntax = "v1" + +info ( + title: "角色服务" + desc: "角色服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +@server ( + prefix: api/v1/admin/role + group: admin_role + jwt: JwtAuth +) +service main { + @doc "获取角色列表" + @handler GetRoleList + get /list (GetRoleListReq) returns (GetRoleListResp) + + @doc "获取角色详情" + @handler GetRoleDetail + get /detail/:id (GetRoleDetailReq) returns (GetRoleDetailResp) + + @doc "创建角色" + @handler CreateRole + post /create (CreateRoleReq) returns (CreateRoleResp) + + @doc "更新角色" + @handler UpdateRole + put /update/:id (UpdateRoleReq) returns (UpdateRoleResp) + + @doc "删除角色" + @handler DeleteRole + delete /delete/:id (DeleteRoleReq) returns (DeleteRoleResp) +} + +type ( + // 列表请求 + GetRoleListReq { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + Name string `form:"name,optional"` // 角色名称 + Code string `form:"code,optional"` // 角色编码 + Status int64 `form:"status,optional,default=-1"` // 状态:0-禁用,1-启用 + } + + // 列表响应 + GetRoleListResp { + Total int64 `json:"total"` // 总数 + Items []RoleListItem `json:"items"` // 列表 + } + + // 列表项 + RoleListItem { + Id int64 `json:"id"` // 角色ID + RoleName string `json:"role_name"` // 角色名称 + RoleCode string `json:"role_code"` // 角色编码 + Description string `json:"description"` // 角色描述 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + Sort int64 `json:"sort"` // 排序 + CreateTime string `json:"create_time"` // 创建时间 + MenuIds []int64 `json:"menu_ids"` // 关联的菜单ID列表 + } + + // 详情请求 + GetRoleDetailReq { + Id int64 `path:"id"` // 角色ID + } + + // 详情响应 + GetRoleDetailResp { + Id int64 `json:"id"` // 角色ID + RoleName string `json:"role_name"` // 角色名称 + RoleCode string `json:"role_code"` // 角色编码 + Description string `json:"description"` // 角色描述 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + Sort int64 `json:"sort"` // 排序 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + MenuIds []int64 `json:"menu_ids"` // 关联的菜单ID列表 + } + + // 创建请求 + CreateRoleReq { + RoleName string `json:"role_name"` // 角色名称 + RoleCode string `json:"role_code"` // 角色编码 + Description string `json:"description"` // 角色描述 + Status int64 `json:"status,default=1"` // 状态:0-禁用,1-启用 + Sort int64 `json:"sort,default=0"` // 排序 + MenuIds []int64 `json:"menu_ids"` // 关联的菜单ID列表 + } + + // 创建响应 + CreateRoleResp { + Id int64 `json:"id"` // 角色ID + } + + // 更新请求 + UpdateRoleReq { + Id int64 `path:"id"` // 角色ID + RoleName *string `json:"role_name,optional"` // 角色名称 + RoleCode *string `json:"role_code,optional"` // 角色编码 + Description *string `json:"description,optional"` // 角色描述 + Status *int64 `json:"status,optional"` // 状态:0-禁用,1-启用 + Sort *int64 `json:"sort,optional"` // 排序 + MenuIds []int64 `json:"menu_ids,optional"` // 关联的菜单ID列表 + } + + // 更新响应 + UpdateRoleResp { + Success bool `json:"success"` // 是否成功 + } + + // 删除请求 + DeleteRoleReq { + Id int64 `path:"id"` // 角色ID + } + + // 删除响应 + DeleteRoleResp { + Success bool `json:"success"` // 是否成功 + } +) \ No newline at end of file diff --git a/app/main/api/desc/front/agent.api b/app/main/api/desc/front/agent.api new file mode 100644 index 0000000..39390c6 --- /dev/null +++ b/app/main/api/desc/front/agent.api @@ -0,0 +1,401 @@ +syntax = "v1" + +info ( + title: "代理服务" + desc: "代理服务接口" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) +@server ( + prefix: api/v1/agent + group: agent +) +service main { + // 获取推广二维码海报 + @handler GetAgentPromotionQrcode + get /promotion/qrcode (GetAgentPromotionQrcodeReq) + +} + +type ( + GetAgentPromotionQrcodeReq{ + QrcodeType string `form:"qrcode_type"` + QrcodeUrl string `form:"qrcode_url"` + } +) +// 代理服务基本类型定义 +type AgentProductConfig { + ProductID int64 `json:"product_id"` + CostPrice float64 `json:"cost_price"` + PriceRangeMin float64 `json:"price_range_min"` + PriceRangeMax float64 `json:"price_range_max"` + PPricingStandard float64 `json:"p_pricing_standard"` + POverpricingRatio float64 `json:"p_overpricing_ratio"` + APricingStandard float64 `json:"a_pricing_standard"` + APricingEnd float64 `json:"a_pricing_end"` + AOverpricingRatio float64 `json:"a_overpricing_ratio"` +} + +type AgentMembershipUserConfig { + ProductID int64 `json:"product_id"` + PriceIncreaseAmount float64 `json:"price_increase_amount"` + PriceRangeFrom float64 `json:"price_range_from"` + PriceRangeTo float64 `json:"price_range_to"` + PriceRatio float64 `json:"price_ratio"` +} + +type ProductConfig { + ProductID int64 `json:"product_id"` + CostPrice float64 `json:"cost_price"` + PriceRangeMin float64 `json:"price_range_min"` + PriceRangeMax float64 `json:"price_range_max"` +} +@server ( + prefix: api/v1/agent + group: agent + jwt: JwtAuth +) +service main { + // 查看代理信息 + @handler GetAgentInfo + get /info returns (AgentInfoResp) + + @handler GetAgentRevenueInfo + get /revenue (GetAgentRevenueInfoReq) returns (GetAgentRevenueInfoResp) +} +@server ( + prefix: api/v1/agent + group: agent + jwt: JwtAuth + middleware: UserAuthInterceptor +) +service main { + // 查询代理申请状态 + @handler GetAgentAuditStatus + get /audit/status returns (AgentAuditStatusResp) + + // 生成推广标识 + @handler GeneratingLink + post /generating_link (AgentGeneratingLinkReq) returns (AgentGeneratingLinkResp) + + // 获取推广定价配置 + @handler GetAgentProductConfig + get /product_config returns (AgentProductConfigResp) + + // 获取下级分页列表 + @handler GetAgentSubordinateList + get /subordinate/list (GetAgentSubordinateListReq) returns (GetAgentSubordinateListResp) + + // 下级贡献详情 + @handler GetAgentSubordinateContributionDetail + get /subordinate/contribution/detail (GetAgentSubordinateContributionDetailReq) returns (GetAgentSubordinateContributionDetailResp) + + @handler AgentRealName + post /real_name (AgentRealNameReq) returns (AgentRealNameResp) +} + +type ( + AgentInfoResp { + status int64 `json:"status"` // 0=待审核,1=审核通过,2=审核未通过,3=未申请 + isAgent bool `json:"is_agent"` + agentID int64 `json:"agent_id"` + level string `json:"level"` + region string `json:"region"` + mobile string `json:"mobile"` + expiryTime string `json:"expiry_time"` + isRealName bool `json:"is_real_name"` + } + // 查询代理申请状态响应 + AgentAuditStatusResp { + Status int64 `json:"status"` // 0=待审核,1=审核通过,2=审核未通过 + AuditReason string `json:"audit_reason"` + } + AgentGeneratingLinkReq { + Product string `json:"product"` + Price string `json:"price"` + } + AgentGeneratingLinkResp { + LinkIdentifier string `json:"link_identifier"` + } + AgentProductConfigResp { + AgentProductConfig []AgentProductConfig + } + GetAgentSubordinateListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 + } + GetAgentSubordinateListResp { + Total int64 `json:"total"` // 总记录数 + List []AgentSubordinateList `json:"list"` // 查询列表 + } + AgentSubordinateList { + ID int64 `json:"id"` + Mobile string `json:"mobile"` + CreateTime string `json:"create_time"` + LevelName string `json:"level_name"` + TotalOrders int64 `json:"total_orders"` // 总单量 + TotalEarnings float64 `json:"total_earnings"` // 总金额 + TotalContribution float64 `json:"total_contribution"` // 总贡献 + } + GetAgentSubordinateContributionDetailReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 + SubordinateID int64 `form:"subordinate_id"` // 下级ID + } + GetAgentSubordinateContributionDetailResp { + Mobile string `json:"mobile"` + Total int64 `json:"total"` // 总记录数 + CreateTime string `json:"create_time"` + TotalEarnings float64 `json:"total_earnings"` // 总金额 + TotalContribution float64 `json:"total_contribution"` // 总贡献 + TotalOrders int64 `json:"total_orders"` // 总单量 + LevelName string `json:"level_name"` // 等级名称 + List []AgentSubordinateContributionDetail `json:"list"` // 查询列表 + Stats AgentSubordinateContributionStats `json:"stats"` // 统计数据 + } + AgentSubordinateContributionDetail { + ID int64 `json:"id"` + CreateTime string `json:"create_time"` + Amount float64 `json:"amount"` + Type string `json:"type"` + } + AgentSubordinateContributionStats { + CostCount int64 `json:"cost_count"` // 成本扣除次数 + CostAmount float64 `json:"cost_amount"` // 成本扣除总额 + PricingCount int64 `json:"pricing_count"` // 定价扣除次数 + PricingAmount float64 `json:"pricing_amount"` // 定价扣除总额 + DescendantPromotionCount int64 `json:"descendant_promotion_count"` // 下级推广次数 + DescendantPromotionAmount float64 `json:"descendant_promotion_amount"` // 下级推广总额 + DescendantUpgradeVipCount int64 `json:"descendant_upgrade_vip_count"` // 下级升级VIP次数 + DescendantUpgradeVipAmount float64 `json:"descendant_upgrade_vip_amount"` // 下级升级VIP总额 + DescendantUpgradeSvipCount int64 `json:"descendant_upgrade_svip_count"` // 下级升级SVIP次数 + DescendantUpgradeSvipAmount float64 `json:"descendant_upgrade_svip_amount"` // 下级升级SVIP总额 + DescendantStayActiveCount int64 `json:"descendant_stay_active_count"` // 下级保持活跃次数 + DescendantStayActiveAmount float64 `json:"descendant_stay_active_amount"` // 下级保持活跃总额 + DescendantNewActiveCount int64 `json:"descendant_new_active_count"` // 下级新增活跃次数 + DescendantNewActiveAmount float64 `json:"descendant_new_active_amount"` // 下级新增活跃总额 + DescendantWithdrawCount int64 `json:"descendant_withdraw_count"` // 下级提现次数 + DescendantWithdrawAmount float64 `json:"descendant_withdraw_amount"` // 下级提现总额 + } + + AgentRealNameReq { + Name string `json:"name"` + IDCard string `json:"id_card"` + Mobile string `json:"mobile"` + Code string `json:"code"` + } + AgentRealNameResp { + Status string `json:"status"` + } +) + +@server ( + prefix: api/v1/agent + group: agent + jwt: JwtAuth + middleware: UserAuthInterceptor + +) +service main { + @handler GetAgentMembershipProductConfig + get /membership/user_config (AgentMembershipProductConfigReq) returns (AgentMembershipProductConfigResp) + + @handler SaveAgentMembershipUserConfig + post /membership/save_user_config (SaveAgentMembershipUserConfigReq) +} + +type ( + // 获取会员当前配置 + AgentMembershipProductConfigReq { + ProductID int64 `form:"product_id"` + } + // 获取会员当前配置 + AgentMembershipProductConfigResp { + AgentMembershipUserConfig AgentMembershipUserConfig `json:"agent_membership_user_config"` + ProductConfig ProductConfig `json:"product_config"` + PriceIncreaseMax float64 `json:"price_increase_max"` + PriceIncreaseAmount float64 `json:"price_increase_amount"` + PriceRatio float64 `json:"price_ratio"` + } + SaveAgentMembershipUserConfigReq { + ProductID int64 `json:"product_id"` + PriceIncreaseAmount float64 `json:"price_increase_amount"` + PriceRangeFrom float64 `json:"price_range_from"` + PriceRangeTo float64 `json:"price_range_to"` + PriceRatio float64 `json:"price_ratio"` + } +) + +@server ( + prefix: api/v1/agent + group: agent + jwt: JwtAuth + middleware: UserAuthInterceptor + +) +service main { + @handler GetAgentCommission + get /commission (GetCommissionReq) returns (GetCommissionResp) + + @handler GetAgentRewards + get /rewards (GetRewardsReq) returns (GetRewardsResp) + + @handler GetAgentWithdrawal + get /withdrawal (GetWithdrawalReq) returns (GetWithdrawalResp) + + @handler AgentWithdrawal + post /withdrawal (WithdrawalReq) returns (WithdrawalResp) + + @handler ActivateAgentMembership + post /membership/activate (AgentActivateMembershipReq) returns (AgentActivateMembershipResp) + + @handler GetAgentWithdrawalTaxExemption + get /withdrawal/tax/exemption (GetWithdrawalTaxExemptionReq) returns (GetWithdrawalTaxExemptionResp) +} + +type ( + // 收益信息 + GetAgentRevenueInfoReq {} + GetAgentRevenueInfoResp { + Balance float64 `json:"balance"` + FrozenBalance float64 `json:"frozen_balance"` + TotalEarnings float64 `json:"total_earnings"` + DirectPush DirectPushReport `json:"direct_push"` // 直推报告数据 + ActiveReward ActiveReward `json:"active_reward"` // 活跃下级奖励数据 + } + // 直推报告数据结构 + DirectPushReport { + TotalCommission float64 `json:"total_commission"` + TotalReport int `json:"total_report"` + Today TimeRangeReport `json:"today"` // 近24小时数据 + Last7D TimeRangeReport `json:"last7d"` // 近7天数据 + Last30D TimeRangeReport `json:"last30d"` // 近30天数据 + } + // 活跃下级奖励数据结构 + ActiveReward { + TotalReward float64 `json:"total_reward"` + Today ActiveRewardData `json:"today"` // 今日数据 + Last7D ActiveRewardData `json:"last7d"` // 近7天数据 + Last30D ActiveRewardData `json:"last30d"` // 近30天数据 + } + // 通用时间范围报告结构 + TimeRangeReport { + Commission float64 `json:"commission"` // 佣金 + Report int `json:"report"` // 报告量 + } + // 活跃奖励专用结构 + ActiveRewardData { + NewActiveReward float64 `json:"active_reward"` + SubPromoteReward float64 `json:"sub_promote_reward"` + SubUpgradeReward float64 `json:"sub_upgrade_reward"` + SubWithdrawReward float64 `json:"sub_withdraw_reward"` + } + Commission { + ProductName string `json:"product_name"` + Amount float64 `json:"amount"` + CreateTime string `json:"create_time"` + } + GetCommissionReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 + } + GetCommissionResp { + Total int64 `json:"total"` // 总记录数 + List []Commission `json:"list"` // 查询列表 + } + Rewards { + Type string `json:"type"` + Amount float64 `json:"amount"` + CreateTime string `json:"create_time"` + } + GetRewardsReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 + } + GetRewardsResp { + Total int64 `json:"total"` // 总记录数 + List []Rewards `json:"list"` // 查询列表 + } + Withdrawal { + Status int64 `json:"status"` + Amount float64 `json:"amount"` + WithdrawalNo string `json:"withdrawal_no"` + Remark string `json:"remark"` + payeeAccount string `json:"payee_account"` + CreateTime string `json:"create_time"` + } + GetWithdrawalReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 + } + GetWithdrawalResp { + Total int64 `json:"total"` // 总记录数 + List []Withdrawal `json:"list"` // 查询列表 + } + WithdrawalReq { + Amount float64 `json:"amount"` // 提现金额 + payeeAccount string `json:"payee_account"` + payeeName string `json:"payee_name"` + } + WithdrawalResp { + Status int64 `json:"status"` // 1申请中 2成功 3失败 + failMsg string `json:"fail_msg"` + } + + // 开通代理会员请求参数 + AgentActivateMembershipReq { + Type string `json:"type,oneof=VIP SVIP"` // 会员类型:vip/svip + } + // 开通代理会员响应 + AgentActivateMembershipResp { + Id string `json:"id"` + } + GetWithdrawalTaxExemptionReq { + } + GetWithdrawalTaxExemptionResp { + TotalExemptionAmount float64 `json:"total_exemption_amount"` + UsedExemptionAmount float64 `json:"used_exemption_amount"` + RemainingExemptionAmount float64 `json:"remaining_exemption_amount"` + TaxRate float64 `json:"tax_rate"` + } +) + +@server ( + prefix: api/v1/agent + group: agent + middleware: AuthInterceptor + +) +service main { + // 提交代理申请 + @handler ApplyForAgent + post /apply (AgentApplyReq) returns (AgentApplyResp) + + // 获取推广标识数据 + @handler GetLinkData + get /link (GetLinkDataReq) returns (GetLinkDataResp) + +} + +type ( + // 代理申请请求参数 + AgentApplyReq { + Region string `json:"region"` + Mobile string `json:"mobile"` + Code string `json:"code"` + Ancestor string `json:"ancestor,optional"` + } + AgentApplyResp{ + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` + } + GetLinkDataReq { + LinkIdentifier string `form:"link_identifier"` + } + GetLinkDataResp { + Product + } +) + diff --git a/app/main/api/desc/front/app.api b/app/main/api/desc/front/app.api new file mode 100644 index 0000000..1c3338a --- /dev/null +++ b/app/main/api/desc/front/app.api @@ -0,0 +1,39 @@ +syntax = "v1" + +info ( + title: "APP服务" + desc: "APP服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +@server ( + prefix: api/v1 + group: app +) +service main { + @doc( + summary: "心跳检测接口" + ) + @handler healthCheck + get /health/check returns (HealthCheckResp) + + @handler getAppVersion + get /app/version returns (getAppVersionResp) +} + +type ( + // 心跳检测响应 + HealthCheckResp { + Status string `json:"status"` // 服务状态 + Message string `json:"message"` // 状态信息 + } +) + +type ( + getAppVersionResp { + Version string `json:"version"` + WgtUrl string `json:"wgtUrl"` + } +) \ No newline at end of file diff --git a/app/main/api/desc/front/pay.api b/app/main/api/desc/front/pay.api new file mode 100644 index 0000000..dc4b246 --- /dev/null +++ b/app/main/api/desc/front/pay.api @@ -0,0 +1,73 @@ +syntax = "v1" + +info ( + title: "支付服务" + desc: "支付服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +@server ( + prefix: api/v1 + group: pay +) +service main { + // 微信支付回调 + @handler WechatPayCallback + post /pay/wechat/callback + + // 支付宝支付回调 + @handler AlipayCallback + post /pay/alipay/callback + + // 微信退款回调 + @handler WechatPayRefundCallback + post /pay/wechat/refund_callback +} + +@server ( + prefix: api/v1 + group: pay + jwt: JwtAuth + middleware: UserAuthInterceptor + +) +service main { + // 支付 + @handler Payment + post /pay/payment (PaymentReq) returns (PaymentResp) + + @handler IapCallback + post /pay/iap_callback (IapCallbackReq) + + @handler PaymentCheck + post /pay/check (PaymentCheckReq) returns (PaymentCheckResp) +} + +type ( + PaymentReq { + Id string `json:"id"` + PayMethod string `json:"pay_method"` + PayType string `json:"pay_type" validate:"required,oneof=query agent_vip"` + } + PaymentResp { + PrepayData interface{} `json:"prepay_data"` + PrepayId string `json:"prepay_id"` + OrderNo string `json:"order_no"` + } + PaymentCheckReq { + OrderNo string `json:"order_no" validate:"required"` + } + PaymentCheckResp { + Type string `json:"type"` + Status string `json:"status"` + } +) + +type ( + IapCallbackReq { + OrderID int64 `json:"order_id" validate:"required"` + TransactionReceipt string `json:"transaction_receipt" validate:"required"` + } +) \ No newline at end of file diff --git a/app/main/api/desc/front/product.api b/app/main/api/desc/front/product.api new file mode 100644 index 0000000..e006b07 --- /dev/null +++ b/app/main/api/desc/front/product.api @@ -0,0 +1,59 @@ +syntax = "v1" + +info ( + title: "产品服务" + desc: "产品服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) +type Feature { + ID int64 `json:"id"` // 功能ID + ApiID string `json:"api_id"` // API标识 + Name string `json:"name"` // 功能描述 +} +// 产品基本类型定义 +type Product { + ProductName string `json:"product_name"` + ProductEn string `json:"product_en"` + Description string `json:"description"` + Notes string `json:"notes,optional"` + SellPrice float64 `json:"sell_price"` + Features []Feature `json:"features"` // 关联功能列表 +} + +@server ( + prefix: api/v1/product + group: product + jwt: JwtAuth + middleware: UserAuthInterceptor + +) +service main { + @handler GetProductByID + get /:id (GetProductByIDRequest) returns (ProductResponse) + + @handler GetProductByEn + get /en/:product_en (GetProductByEnRequest) returns (ProductResponse) +} + +type GetProductByIDRequest { + Id int64 `path:"id"` +} + +type GetProductByEnRequest { + ProductEn string `path:"product_en"` +} + +type ProductResponse { + Product +} + +@server ( + prefix: api/v1/product + group: product +) +service main { + @handler GetProductAppByEn + get /app_en/:product_en (GetProductByEnRequest) returns (ProductResponse) +} \ No newline at end of file diff --git a/app/main/api/desc/front/query.api b/app/main/api/desc/front/query.api new file mode 100644 index 0000000..353b681 --- /dev/null +++ b/app/main/api/desc/front/query.api @@ -0,0 +1,238 @@ +syntax = "v1" + +info ( + title: "产品查询服务" + desc: "产品查询服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +//============================> query v1 <============================ +// 查询基本类型定义 +type Query { + Id int64 `json:"id"` // 主键ID + OrderId int64 `json:"order_id"` // 订单ID + UserId int64 `json:"user_id"` // 用户ID + ProductName string `json:"product_name"` // 产品ID + QueryParams map[string]interface{} `json:"query_params"` + QueryData []QueryItem `json:"query_data"` + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + QueryState string `json:"query_state"` // 查询状态 +} + + +type QueryItem { + Feature interface{} `json:"feature"` + Data interface{} `json:"data"` // 这里可以是 map 或 具体的 struct +} + +@server ( + prefix: api/v1 + group: query + middleware: AuthInterceptor +) +service main { + @doc "query service agent" + @handler queryServiceAgent + post /query/service_agent/:product (QueryServiceReq) returns (QueryServiceResp) + + @handler queryServiceApp + post /query/service_app/:product (QueryServiceReq) returns (QueryServiceResp) +} + +type ( + QueryReq { + Data string `json:"data" validate:"required"` + } + QueryResp { + Id string `json:"id"` + } +) + +type ( + QueryServiceReq { + Product string `path:"product"` + Data string `json:"data" validate:"required"` + AgentIdentifier string `json:"agent_identifier,optional"` + App bool `json:"app,optional"` + } + QueryServiceResp { + Id string `json:"id"` + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` + } +) + +@server ( + prefix: api/v1 + group: query + jwt: JwtAuth + middleware: UserAuthInterceptor + +) +service main { + @doc "query service" + @handler queryService + post /query/service/:product (QueryServiceReq) returns (QueryServiceResp) +} + +@server ( + prefix: api/v1 + group: query + jwt: JwtAuth + middleware: UserAuthInterceptor + +) +service main { + @doc "获取查询临时订单" + @handler queryProvisionalOrder + get /query/provisional_order/:id (QueryProvisionalOrderReq) returns (QueryProvisionalOrderResp) + + @doc "查询列表" + @handler queryList + get /query/list (QueryListReq) returns (QueryListResp) + + @doc "查询详情 按订单号 付款查询时" + @handler queryDetailByOrderId + get /query/orderId/:order_id (QueryDetailByOrderIdReq) returns (QueryDetailByOrderIdResp) + + @doc "查询详情 按订单号" + @handler queryDetailByOrderNo + get /query/orderNo/:order_no (QueryDetailByOrderNoReq) returns (QueryDetailByOrderNoResp) + + @doc "重试查询" + @handler queryRetry + post /query/retry/:id (QueryRetryReq) returns (QueryRetryResp) + + @doc "更新查询数据" + @handler updateQueryData + post /query/update_data (UpdateQueryDataReq) returns (UpdateQueryDataResp) + + @doc "生成分享链接" + @handler QueryGenerateShareLink + post /query/generate_share_link (QueryGenerateShareLinkReq) returns (QueryGenerateShareLinkResp) +} + +type ( + QueryGenerateShareLinkReq { + OrderId *int64 `json:"order_id,optional"` + OrderNo *string `json:"order_no,optional"` + } + QueryGenerateShareLinkResp { + ShareLink string `json:"share_link"` + } +) + +// 获取查询临时订单 +type ( + QueryProvisionalOrderReq { + Id string `path:"id"` + } + QueryProvisionalOrderResp { + Name string `json:"name"` + IdCard string `json:"id_card"` + Mobile string `json:"mobile"` + Product Product `json:"product"` + } +) + +type ( + QueryListReq { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 + } + QueryListResp { + Total int64 `json:"total"` // 总记录数 + List []Query `json:"list"` // 查询列表 + } +) + +type ( + QueryExampleReq { + Feature string `form:"feature"` + } + QueryExampleResp { + Query + } +) + + +type ( + QueryDetailByOrderIdReq { + OrderId int64 `path:"order_id"` + } + QueryDetailByOrderIdResp { + Query + } +) + +type ( + QueryDetailByOrderNoReq { + OrderNo string `path:"order_no"` + } + QueryDetailByOrderNoResp { + Query + } +) + +type ( + QueryRetryReq { + Id int64 `path:"id"` + } + QueryRetryResp { + Query + } +) + + +type ( + UpdateQueryDataReq { + Id int64 `json:"id"` // 查询ID + QueryData string `json:"query_data"` // 查询数据(未加密的JSON) + } + UpdateQueryDataResp { + Id int64 `json:"id"` + UpdatedAt string `json:"updated_at"` // 更新时间 + } +) + +@server ( + prefix: api/v1 + group: query +) +service main { + @handler querySingleTest + post /query/single/test (QuerySingleTestReq) returns (QuerySingleTestResp) + + @doc "查询详情" + @handler queryShareDetail + get /query/share/:id (QueryShareDetailReq) returns (QueryShareDetailResp) + + @doc "查询示例" + @handler queryExample + get /query/example (QueryExampleReq) returns (QueryExampleResp) + +} +type ( + QueryShareDetailReq { + Id string `path:"id"` + } + QueryShareDetailResp { + Status string `json:"status"` + Query + } +) + +type QuerySingleTestReq { + Params map[string]interface{} `json:"params"` + Api string `json:"api"` +} + +type QuerySingleTestResp { + Data interface{} `json:"data"` + Api string `json:"api"` +} + diff --git a/app/main/api/desc/front/user.api b/app/main/api/desc/front/user.api new file mode 100644 index 0000000..31476a0 --- /dev/null +++ b/app/main/api/desc/front/user.api @@ -0,0 +1,164 @@ +syntax = "v1" + +info ( + title: "用户中心服务" + desc: "用户中心服务" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +//============================> user v1 <============================ +// 用户基本类型定义 +type User { + Id int64 `json:"id"` + Mobile string `json:"mobile"` + NickName string `json:"nickName"` + UserType int64 `json:"userType"` +} + +//no need login +@server ( + prefix: api/v1 + group: user +) +service main { + @doc "mobile code login" + @handler mobileCodeLogin + post /user/mobileCodeLogin (MobileCodeLoginReq) returns (MobileCodeLoginResp) + + @doc "wechat mini auth" + @handler wxMiniAuth + post /user/wxMiniAuth (WXMiniAuthReq) returns (WXMiniAuthResp) + + @doc "wechat h5 auth" + @handler wxH5Auth + post /user/wxh5Auth (WXH5AuthReq) returns (WXH5AuthResp) +} + +type ( + MobileCodeLoginReq { + Mobile string `json:"mobile"` + Code string `json:"code" validate:"required"` + } + MobileCodeLoginResp { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` + } +) + +type ( + WXMiniAuthReq { + Code string `json:"code"` + Platform string `json:"platform,optional,default=tyc"` + } + WXMiniAuthResp { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` + } +) + +type ( + WXH5AuthReq { + Code string `json:"code"` + } + WXH5AuthResp { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` + } +) +@server ( + prefix: api/v1 + group: user + middleware: AuthInterceptor +) +service main { + @doc "绑定手机号" + @handler bindMobile + post /user/bindMobile (BindMobileReq) returns (BindMobileResp) +} + +//need login +@server ( + prefix: api/v1 + group: user + jwt: JwtAuth +) +service main { + @doc "get user info" + @handler detail + get /user/detail returns (UserInfoResp) + + @doc "get new token" + @handler getToken + post /user/getToken returns (MobileCodeLoginResp) + + @handler cancelOut + post /user/cancelOut +} + +type ( + UserInfoResp { + UserInfo User `json:"userInfo"` + } + + BindMobileReq { + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` + } + BindMobileResp { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` + } +) + +//============================> auth v1 <============================ +@server ( + prefix: api/v1 + group: auth +) +service main { + @doc "get mobile verify code" + @handler sendSms + post /auth/sendSms (sendSmsReq) +} + +type ( + sendSmsReq { + Mobile string `json:"mobile" validate:"required,mobile"` + ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"` + } +) + +//============================> notification v1 <============================ +@server ( + prefix: api/v1 + group: notification +) +service main { + @doc "get notifications" + @handler getNotifications + get /notification/list returns (GetNotificationsResp) +} + +type Notification { + Title string `json:"title"` // 通知标题 + Content string `json:"content"` // 通知内容 (富文本) + NotificationPage string `json:"notificationPage"` // 通知页面 + StartDate string `json:"startDate"` // 通知开始日期,格式 "YYYY-MM-DD" + EndDate string `json:"endDate"` // 通知结束日期,格式 "YYYY-MM-DD" + StartTime string `json:"startTime"` // 每天通知开始时间,格式 "HH:MM:SS" + EndTime string `json:"endTime"` // 每天通知结束时间,格式 "HH:MM:SS" +} + +type ( + // 获取通知响应体(分页) + GetNotificationsResp { + Notifications []Notification `json:"notifications"` // 通知列表 + Total int64 `json:"total"` // 总记录数 + } +) \ No newline at end of file diff --git a/app/main/api/desc/main.api b/app/main/api/desc/main.api new file mode 100644 index 0000000..3928235 --- /dev/null +++ b/app/main/api/desc/main.api @@ -0,0 +1,30 @@ +syntax = "v1" + +info ( + title: "单体服务中心" + desc: "单体服务中心" + author: "Liangzai" + email: "2440983361@qq.com" + version: "v1" +) + +// 前台 +import "./front/user.api" +import "./front/query.api" +import "./front/pay.api" +import "./front/product.api" +import "./front/agent.api" +import "./front/app.api" +// 后台 +import "./admin/auth.api" +import "./admin/menu.api" +import "./admin/role.api" +import "./admin/promotion.api" +import "./admin/order.api" +import "./admin/admin_user.api" +import "./admin/platform_user.api" +import "./admin/notification.api" +import "./admin/admin_product.api" +import "./admin/admin_feature.api" +import "./admin/admin_query.api" +import "./admin/admin_agent.api" diff --git a/app/main/api/etc/main.dev.yaml b/app/main/api/etc/main.dev.yaml new file mode 100644 index 0000000..b433ea7 --- /dev/null +++ b/app/main/api/etc/main.dev.yaml @@ -0,0 +1,78 @@ +Name: main +Host: 0.0.0.0 +Port: 8888 +DataSource: "znc:Hn7kL4mO9pQ2rS5tU8vW1xY4zA7bC0dE@tcp(127.0.0.1:21001)/znc?charset=utf8mb4&parseTime=True&loc=Local" +CacheRedis: + - Host: "127.0.0.1:21002" + Pass: "Xq5wE2rT7yU1iO8pA3sD6fG9hJ2kL5mN" # Redis 密码,如果未设置则留空 + Type: "node" # 单节点模式 +JwtAuth: + AccessSecret: "Vb8nM5kP2qR7tU1vW4xY0zA3bC6dE9fG" + AccessExpire: 2592000 + RefreshAfter: 1296000 +VerifyCode: + AccessKeyID: "LTAI5tKGB3TVJbMHSoZN3yr9" + AccessKeySecret: "OCQ30GWp4yENMjmfOAaagksE18bp65" + EndpointURL: "dysmsapi.aliyuncs.com" + SignName: "天远数据" + TemplateCode: "SMS_302641455" + ValidTime: 300 +Encrypt: + SecretKey: "Lk4jH7gF2dS5aQ8wE1rT4yU7iO0pA3sD" +Alipay: + AppID: "2021005180646821" + PrivateKey: "MIIEowIBAAKCAQEAn2ieQfPxC/38hmXQvkoqLMVti8iXd6HkFu2nVpQg0DddfNtxV6Ii8pIRxKLKCBdrl43sT+inTAtUz5j9SdLts77qtF9UXQXiCryXVr593nN03MKmVHow9NzbN3tPWsW6XnxpLaPAHAjCloYFMiAvb9f/GvXhK9iwYcUXL6d1IsFSnDelbUruTUc9OLT/EhepMZO/RXQFExEfW0ou9eSJ29XGO3vBnBBzEYwhPi8VstnqpBOJIGNVMkGUqrbkMeGyRzp0eI2UxY46fiwbpYyuxyJKz2uAHowEsE7seu1YoJK3Zr0oG7S01xyX0zufq9JSM3EpmkIasDQVZ6SQmI8/fQIDAQABAoIBAHqmq0XBpRD+DmN2SWNwevzRtxTbdTd2F6JQnvVtqcWrI8JisdWkidEr9IHgYyRQqNcGOvHM55QKD+pfI1u+8GfhmILJ6oZcdWyfaK40iXI0UZFeL05Gag6tM/p/ZTJJerkibmbQXIr1bosUeUD1JKqgfcdHskXjRusjE1D2PplK/uS382M8eAoiJhcQGtZ6VJUU8EFNfLNkzuPQLGkafggvhfNWNqNmcvH6jWhfGi7Rlyrqm1TLwRHrFMy8JAxWMHxq19VXVH7E1DV8MKI0odJy7KQmPtrICmowtxcR4yaDv0Z+XbJJeGKoE9c9YG3MyGQyTxTA6UIsVTz7WgjgM90CgYEA47jl+X6rT268TYFdrcIdXfBwzO/5mqJluerzaFMLOowLA34rM67/S6PTzWpgXHF5VOD/FtgJWBm3vpO97AB0DDyig94LE9bncL1OY/WM+0+9uNDi7GUkvG2O7PWRWASPItOkiluY9WaOIWLWo0xnv7g8+ySphGuKSRtI9pT3748CgYEAszQWXlD7quDJJZ2xHg25WHZZcJVtbd9nMpDPZpIUnem+nb7H8lz+7SeFGu0shpa+4JoyeYZ2Ew9ZO2OVG+lOMDMZmeuoQs+6D1cSnh+h3EdbZV5QWdfMjy2UMZRpj7/EAlhO3xs/QcbkGKxTVyQJUwAfTdgXjHgvaI1HDOrKGjMCgYEA3IB2Vy8bbG/ab+YbMpwq2YJvh1G7TMuBWxQxG0yGK+vc4kXySTpjQ/ffqCEgK4NJLDItbw5DhgZpEGV42qPZutuftbZ03YITWuxDkrD7EYG2QNYrVSHe/4HKipKCaUsI2n067yogo0bpy1QsZ7UdJNyeV8S5TFrhbUa5UOMQbOsCgYBg6YBXTVBs3tepAhiw+hcMIiTIX+coDjMPA4VGISYJKEmvoWccSPKMalzvbOgxeQCNEpbNZcwhDqHhHj+bMpbYNipYNTtvtksW1K362Xx9VhG1RkYJ8Ext+eY00eAsnzZvVjaBLYkOF3NvbHI9o/1u55gGTyCdFLn+vrh34dmeRQKBgAVOB4qVJEu60/QGt7XlsSGks4xaW/aifQpjnCnKNfI5xT9L5XKLceG0T7rcADYGSbQwehqYOLOtAcVxZnJFPpxPVIczLHSk3KPeeKrHjQlUa7eUXOhDiVZG6lS0KIot9bJB2vFPhAynQjq8xM1X6issU6nICYrbUt9EMlBVCky/" + AlipayPublicKey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzTCyAakFo4JngoXBXjlRxmfXbJnaMUt8DDcL7ypBtlkxPozQ/UIYV6lyDmJXzEitXXEejOcrPytPigMlFFKonEhR+kOk8y1ELHjG1Xq45Z6Uxq40Eu8CRcD9PN49/dvzOORHuOJbwJSPst/JPjg72xLzf6BAe8Q2EK5EhRGaSXS981AQBMOfVfdha48rYv3E5NrwGRW97NEkOrkL/1IuZtqfk4l6yiBj7Rsaz+JMy6NPJZdh30txhDX8A4NhrGlI+pBttJWCvNzOEdsX06z5B+ppzWtwblCfuTWJi+2pXn9NWfL+fBCoqAIAlQtPCYIzlY+r43wgUgz/5xlvCFVaeQIDAQAB" + AppCertPath: "etc/merchant/appCertPublicKey_2021005113664540.crt" + AlipayCertPath: "etc/merchant/alipayCertPublicKey_RSA2.crt" + AlipayRootCertPath: "etc/merchant/alipayRootCert.crt" + IsProduction: true + NotifyUrl: "https://6m4685017o.goho.co/api/v1/pay/alipay/callback" + ReturnURL: "http://localhost:5678/inquire" + +Wxpay: + AppID: "wxa581992dc74d860e" + MchID: "1704330055" + MchCertificateSerialNumber: "749065854D0CECCE8F98EAFEA55AD4FB17F868C4" + MchApiv3Key: "A9f3G7kL2mP5sQ8tV1xY4zB6nC0dE3hJ" + MchPrivateKeyPath: "etc/merchant/apiclient_key.pem" + MchPublicKeyID: "PUB_KEY_ID_0117043300552025010900447500000187" + MchPublicKeyPath: "etc/merchant/pub_key.pem" + NotifyUrl: "https://6m4685017o.goho.co/api/v1/pay/wechat/callback" + RefundNotifyUrl: "https://6m4685017o.goho.co/api/v1/wechat/refund_callback" +Applepay: + ProductionVerifyURL: "https://api.storekit.itunes.apple.com/inApps/v1/transactions/receipt" + SandboxVerifyURL: "https://api.storekit-sandbox.itunes.apple.com/inApps/v1/transactions/receipt" + Sandbox: false + BundleID: "com.allinone.check" + IssuerID: "bf828d85-5269-4914-9660-c066e09cd6ef" + KeyID: "LAY65829DQ" + LoadPrivateKeyPath: "etc/merchant/AuthKey_LAY65829DQ.p8" +Ali: + Code: "d55b58829efb41c8aa8e86769cba4844" +SystemConfig: + ThreeVerify: false +WechatH5: + AppID: "wxa581992dc74d860e" + AppSecret: "4de1fbf521712247542d49907fcd5dbf" +WechatMini: + AppID: "wx781abb66b3368963" # 小程序的AppID + AppSecret: "c7d02cdb0fc23c35c93187af9243b00d" # 小程序的AppSecret + TycAppID: "wxe74617f3dd56c196" + TycAppSecret: "c8207e54aef5689b2a7c1f91ed7ae8a0" +Query: + ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒 +AdminConfig: + AccessSecret: "Qw9eR6tY2uI5oP8aS1dF4gH7jK0lM3nO" + AccessExpire: 604800 + RefreshAfter: 302400 +AdminPromotion: + URLDomain: "https://zhinengcha.cn/p" +TaxConfig: + TaxRate: 0.2 + TaxExemptionAmount: 800.00 +Tianyuanapi: + AccessID: "3c042bb99b240ccc" + Key: "2732f526167c2de9b8dc6aa0f24ba8b7" + BaseURL: "https://api.tianyuanapi.com" + Timeout: 60 \ No newline at end of file diff --git a/app/main/api/etc/main.yaml b/app/main/api/etc/main.yaml new file mode 100644 index 0000000..b31389a --- /dev/null +++ b/app/main/api/etc/main.yaml @@ -0,0 +1,79 @@ +Name: main +Host: 0.0.0.0 +Port: 8888 +DataSource: "znc:Hn7kL4mO9pQ2rS5tU8vW1xY4zA7bC0dE@tcp(znc_mysql:3306)/znc?charset=utf8mb4&parseTime=True&loc=Local" +CacheRedis: + - Host: "znc_redis:6379" + Pass: "Xq5wE2rT7yU1iO8pA3sD6fG9hJ2kL5mN" # Redis 密码,如果未设置则留空 + Type: "node" # 单节点模式 + +JwtAuth: + AccessSecret: "Vb8nM5kP2qR7tU1vW4xY0zA3bC6dE9fG" + AccessExpire: 2592000 + RefreshAfter: 1296000 + +VerifyCode: + AccessKeyID: "LTAI5tKGB3TVJbMHSoZN3yr9" + AccessKeySecret: "OCQ30GWp4yENMjmfOAaagksE18bp65" + EndpointURL: "dysmsapi.aliyuncs.com" + SignName: "天远数据" + TemplateCode: "SMS_302641455" + ValidTime: 300 +Encrypt: + SecretKey: "Lk4jH7gF2dS5aQ8wE1rT4yU7iO0pA3sD" +Alipay: + AppID: "2021005180646821" + PrivateKey: "MIIEowIBAAKCAQEAn2ieQfPxC/38hmXQvkoqLMVti8iXd6HkFu2nVpQg0DddfNtxV6Ii8pIRxKLKCBdrl43sT+inTAtUz5j9SdLts77qtF9UXQXiCryXVr593nN03MKmVHow9NzbN3tPWsW6XnxpLaPAHAjCloYFMiAvb9f/GvXhK9iwYcUXL6d1IsFSnDelbUruTUc9OLT/EhepMZO/RXQFExEfW0ou9eSJ29XGO3vBnBBzEYwhPi8VstnqpBOJIGNVMkGUqrbkMeGyRzp0eI2UxY46fiwbpYyuxyJKz2uAHowEsE7seu1YoJK3Zr0oG7S01xyX0zufq9JSM3EpmkIasDQVZ6SQmI8/fQIDAQABAoIBAHqmq0XBpRD+DmN2SWNwevzRtxTbdTd2F6JQnvVtqcWrI8JisdWkidEr9IHgYyRQqNcGOvHM55QKD+pfI1u+8GfhmILJ6oZcdWyfaK40iXI0UZFeL05Gag6tM/p/ZTJJerkibmbQXIr1bosUeUD1JKqgfcdHskXjRusjE1D2PplK/uS382M8eAoiJhcQGtZ6VJUU8EFNfLNkzuPQLGkafggvhfNWNqNmcvH6jWhfGi7Rlyrqm1TLwRHrFMy8JAxWMHxq19VXVH7E1DV8MKI0odJy7KQmPtrICmowtxcR4yaDv0Z+XbJJeGKoE9c9YG3MyGQyTxTA6UIsVTz7WgjgM90CgYEA47jl+X6rT268TYFdrcIdXfBwzO/5mqJluerzaFMLOowLA34rM67/S6PTzWpgXHF5VOD/FtgJWBm3vpO97AB0DDyig94LE9bncL1OY/WM+0+9uNDi7GUkvG2O7PWRWASPItOkiluY9WaOIWLWo0xnv7g8+ySphGuKSRtI9pT3748CgYEAszQWXlD7quDJJZ2xHg25WHZZcJVtbd9nMpDPZpIUnem+nb7H8lz+7SeFGu0shpa+4JoyeYZ2Ew9ZO2OVG+lOMDMZmeuoQs+6D1cSnh+h3EdbZV5QWdfMjy2UMZRpj7/EAlhO3xs/QcbkGKxTVyQJUwAfTdgXjHgvaI1HDOrKGjMCgYEA3IB2Vy8bbG/ab+YbMpwq2YJvh1G7TMuBWxQxG0yGK+vc4kXySTpjQ/ffqCEgK4NJLDItbw5DhgZpEGV42qPZutuftbZ03YITWuxDkrD7EYG2QNYrVSHe/4HKipKCaUsI2n067yogo0bpy1QsZ7UdJNyeV8S5TFrhbUa5UOMQbOsCgYBg6YBXTVBs3tepAhiw+hcMIiTIX+coDjMPA4VGISYJKEmvoWccSPKMalzvbOgxeQCNEpbNZcwhDqHhHj+bMpbYNipYNTtvtksW1K362Xx9VhG1RkYJ8Ext+eY00eAsnzZvVjaBLYkOF3NvbHI9o/1u55gGTyCdFLn+vrh34dmeRQKBgAVOB4qVJEu60/QGt7XlsSGks4xaW/aifQpjnCnKNfI5xT9L5XKLceG0T7rcADYGSbQwehqYOLOtAcVxZnJFPpxPVIczLHSk3KPeeKrHjQlUa7eUXOhDiVZG6lS0KIot9bJB2vFPhAynQjq8xM1X6issU6nICYrbUt9EMlBVCky/" + AlipayPublicKey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzTCyAakFo4JngoXBXjlRxmfXbJnaMUt8DDcL7ypBtlkxPozQ/UIYV6lyDmJXzEitXXEejOcrPytPigMlFFKonEhR+kOk8y1ELHjG1Xq45Z6Uxq40Eu8CRcD9PN49/dvzOORHuOJbwJSPst/JPjg72xLzf6BAe8Q2EK5EhRGaSXS981AQBMOfVfdha48rYv3E5NrwGRW97NEkOrkL/1IuZtqfk4l6yiBj7Rsaz+JMy6NPJZdh30txhDX8A4NhrGlI+pBttJWCvNzOEdsX06z5B+ppzWtwblCfuTWJi+2pXn9NWfL+fBCoqAIAlQtPCYIzlY+r43wgUgz/5xlvCFVaeQIDAQAB" + AppCertPath: "etc/merchant/appCertPublicKey_2021005113664540.crt" + AlipayCertPath: "etc/merchant/alipayCertPublicKey_RSA2.crt" + AlipayRootCertPath: "etc/merchant/alipayRootCert.crt" + IsProduction: true + NotifyUrl: "https://www.zhinengcha.cn/api/v1/pay/alipay/callback" + ReturnURL: "https://www.zhinengcha.cn/payment/result" +Wxpay: + AppID: "wxa581992dc74d860e" + MchID: "1704330055" + MchCertificateSerialNumber: "749065854D0CECCE8F98EAFEA55AD4FB17F868C4" + MchApiv3Key: "A9f3G7kL2mP5sQ8tV1xY4zB6nC0dE3hJ" + MchPrivateKeyPath: "etc/merchant/apiclient_key.pem" + MchPublicKeyID: "PUB_KEY_ID_0117043300552025010900447500000187" + MchPublicKeyPath: "etc/merchant/pub_key.pem" + NotifyUrl: "https://www.zhinengcha.cn/api/v1/pay/wechat/callback" + RefundNotifyUrl: "https://www.zhinengcha.cn/api/v1/wechat/refund_callback" +Applepay: + ProductionVerifyURL: "https://api.storekit.itunes.apple.com/inApps/v1/transactions/receipt" + SandboxVerifyURL: "https://api.storekit-sandbox.itunes.apple.com/inApps/v1/transactions/receipt" + Sandbox: true + BundleID: "com.allinone.check" + IssuerID: "bf828d85-5269-4914-9660-c066e09cd6ef" + KeyID: "LAY65829DQ" + LoadPrivateKeyPath: "etc/merchant/AuthKey_LAY65829DQ.p8" +Ali: + Code: "d55b58829efb41c8aa8e86769cba4844" +SystemConfig: + ThreeVerify: true +WechatH5: + AppID: "wxa581992dc74d860e" + AppSecret: "4de1fbf521712247542d49907fcd5dbf" +WechatMini: + AppID: "wx781abb66b3368963" # 小程序的AppID + AppSecret: "c7d02cdb0fc23c35c93187af9243b00d" # 小程序的AppSecret + TycAppID: "wxe74617f3dd56c196" + TycAppSecret: "c8207e54aef5689b2a7c1f91ed7ae8a0" +Query: + ShareLinkExpire: 604800 # 7天 = 7 * 24 * 60 * 60 = 604800秒 +AdminConfig: + AccessSecret: "Qw9eR6tY2uI5oP8aS1dF4gH7jK0lM3nO" + AccessExpire: 604800 + RefreshAfter: 302400 +AdminPromotion: + URLDomain: "https://zhinengcha.cn/p" +TaxConfig: + TaxRate: 0.2 + TaxExemptionAmount: 800.00 +Tianyuanapi: + AccessID: "3c042bb99b240ccc" + Key: "2732f526167c2de9b8dc6aa0f24ba8b7" + BaseURL: "https://api.tianyuanapi.com" + Timeout: 60 \ No newline at end of file diff --git a/app/main/api/etc/merchant/AuthKey_LAY65829DQ.p8 b/app/main/api/etc/merchant/AuthKey_LAY65829DQ.p8 new file mode 100644 index 0000000..b448586 --- /dev/null +++ b/app/main/api/etc/merchant/AuthKey_LAY65829DQ.p8 @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgkidSHV1OeJN84sDD +xWLGIVjTyhn6sAQDyHfqKW6lxnGgCgYIKoZIzj0DAQehRANCAAQSAlAcuuuRNFqk +aMPVpXxsiR/pwhyM62tFhdFsbULq1C7MItQxKVMKCiwz3r5rZZy7HcbkqL47LPZ1 +q6V8Wyop +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/app/main/api/etc/merchant/alipayCertPublicKey_RSA2.crt b/app/main/api/etc/merchant/alipayCertPublicKey_RSA2.crt new file mode 100644 index 0000000..5846438 --- /dev/null +++ b/app/main/api/etc/merchant/alipayCertPublicKey_RSA2.crt @@ -0,0 +1,43 @@ +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQICUDBjgAYvSDOiVZlr/A9zANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UE +BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNVBAsMF0NlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MTkwNwYDVQQDDDBBbnQgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IENs +YXNzIDIgUjEwHhcNMjUwMzA2MTE1ODQ2WhcNMzAwMzA1MTE1ODQ2WjCBlTELMAkGA1UEBhMCQ04x +MDAuBgNVBAoMJ+a1t+WNl+Wkqei/nOWkp+aVsOaNruenkeaKgOaciemZkOWFrOWPuDEPMA0GA1UE +CwwGQWxpcGF5MUMwQQYDVQQDDDrmlK/ku5jlrp0o5Lit5Zu9Kee9kee7nOaKgOacr+aciemZkOWF +rOWPuC0yMDg4MDUxMDMyOTk4NDkyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0kkR +L7lgKYs7f8Xi4DNKzp2ggjwy4By7RunwT4Ur4A71HVOqRQed9r45a6/W4JPuVv51tiHMojZifEKX +7ixSlDG6be677RiNslMJ5G3mjw/+Ku01tV9Qzw5YyhvxbqmS8Qp9vgL8VPYhxqTxKO6WW+xiyVvx +ko+mrU+dbSFIVbBjp88NVVcquu+vZT/uwtjriKSwsesAm8DkKT6mTqY5P/JroMzTU7xa3/ErAMte +6t2dOsxPS7kqWjJyoLBHRk+AH87X5lNBEjLgYPk1ADU7zFsLdC+nv4fm7nihYre7fCrdCTVKguXm +PCEFBjqwSkag7BSIxRQjS3qHxi+DUMst7wIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCA/gwDQYJKoZI +hvcNAQELBQADggEBAIrXa4HfvKtjIb+F5YRi1GuhdPn20tkyQaw8GB/xZ30kpe1NyQdr2D3JPSIi +wd+MBGEhAF2HrD+UT9AnqsHQOwFrWJUNFArw1joMkMJQtnpD9nH1po1l0ECR5KF0gzsDroXOFXsW +QVicHhbZ4J54LswgedEKURETP74o/NdTD24IzXt+rjQe1Nsu7mgkj+VqmXVtqjOIS5IllRo3TD30 +h031HCg+OLGpGmJylYiD5C5Y+7YkPzJC0pzsvqLvT1yGNForSlujPB/s7rCBq4ZEX08/u2fbfMpd +PuZJgZdRmF7Xr5LV7JeeYrTzOKmzSEByXIFM0NsEdggpSD8eyqclQd4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE4jCCAsqgAwIBAgIIYsSr5bKAMl8wDQYJKoZIhvcNAQELBQAwejELMAkGA1UEBhMCQ04xFjAU +BgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNVBAsMF0NlcnRpZmljYXRpb24gQXV0aG9yaXR5MTEw +LwYDVQQDDChBbnQgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFIxMB4XDTE4MDMy +MjE0MzQxNVoXDTM3MTEyNjE0MzQxNVowgYIxCzAJBgNVBAYTAkNOMRYwFAYDVQQKDA1BbnQgRmlu +YW5jaWFsMSAwHgYDVQQLDBdDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTE5MDcGA1UEAwwwQW50IEZp +bmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBDbGFzcyAyIFIxMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAsLMfYaoRoPRbmDcAfXPCmKf43pWRN5yTXa/KJWO0l+mrgQvs89bA +NEvbDUxlkGwycwtwi5DgBuBgVhLliXu+R9CYgr2dXs8D8Hx/gsggDcyGPLmVrDOnL+dyeauheARZ +fA3du60fwEwwbGcVIpIxPa/4n3IS/ElxQa6DNgqxh8J9Xwh7qMGl0JK9+bALuxf7B541Gr4p0WEN +G8fhgjBV4w4ut9eQLOoa1eddOUSZcy46Z7allwowwgt7b5VFfx/P1iKJ3LzBMgkCK7GZ2kiLrL7R +iqV+h482J7hkJD+ardoc6LnrHO/hIZymDxok+VH9fVeUdQa29IZKrIDVj65THQIDAQABo2MwYTAf +BgNVHSMEGDAWgBRfdLQEwE8HWurlsdsio4dBspzhATAdBgNVHQ4EFgQUSqHkYINtUSAtDPnS8Xoy +oP9p7qEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIB +AIQ8TzFy4bVIVb8+WhHKCkKNPcJe2EZuIcqvRoi727lZTJOfYy/JzLtckyZYfEI8J0lasZ29wkTt +a1IjSo+a6XdhudU4ONVBrL70U8Kzntplw/6TBNbLFpp7taRALjUgbCOk4EoBMbeCL0GiYYsTS0mw +7xdySzmGQku4GTyqutIGPQwKxSj9iSFw1FCZqr4VP4tyXzMUgc52SzagA6i7AyLedd3tbS6lnR5B +L+W9Kx9hwT8L7WANAxQzv/jGldeuSLN8bsTxlOYlsdjmIGu/C9OWblPYGpjQQIRyvs4Cc/mNhrh+ +14EQgwuemIIFDLOgcD+iISoN8CqegelNcJndFw1PDN6LkVoiHz9p7jzsge8RKay/QW6C03KNDpWZ +EUCgCUdfHfo8xKeR+LL1cfn24HKJmZt8L/aeRZwZ1jwePXFRVtiXELvgJuM/tJDIFj2KD337iV64 +fWcKQ/ydDVGqfDZAdcU4hQdsrPWENwPTQPfVPq2NNLMyIH9+WKx9Ed6/WzeZmIy5ZWpX1TtTolo6 +OJXQFeItMAjHxW/ZSZTok5IS3FuRhExturaInnzjYpx50a6kS34c5+c8hYq7sAtZ/CNLZmBnBCFD +aMQqT8xFZJ5uolUaSeXxg7JFY1QsYp5RKvj4SjFwCGKJ2+hPPe9UyyltxOidNtxjaknOCeBHytOr +-----END CERTIFICATE----- diff --git a/app/main/api/etc/merchant/alipayRootCert.crt b/app/main/api/etc/merchant/alipayRootCert.crt new file mode 100644 index 0000000..76417c5 --- /dev/null +++ b/app/main/api/etc/merchant/alipayRootCert.crt @@ -0,0 +1,88 @@ +-----BEGIN CERTIFICATE----- +MIIBszCCAVegAwIBAgIIaeL+wBcKxnswDAYIKoEcz1UBg3UFADAuMQswCQYDVQQG +EwJDTjEOMAwGA1UECgwFTlJDQUMxDzANBgNVBAMMBlJPT1RDQTAeFw0xMjA3MTQw +MzExNTlaFw00MjA3MDcwMzExNTlaMC4xCzAJBgNVBAYTAkNOMQ4wDAYDVQQKDAVO +UkNBQzEPMA0GA1UEAwwGUk9PVENBMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE +MPCca6pmgcchsTf2UnBeL9rtp4nw+itk1Kzrmbnqo05lUwkwlWK+4OIrtFdAqnRT +V7Q9v1htkv42TsIutzd126NdMFswHwYDVR0jBBgwFoAUTDKxl9kzG8SmBcHG5Yti +W/CXdlgwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFEwysZfZ +MxvEpgXBxuWLYlvwl3ZYMAwGCCqBHM9VAYN1BQADSAAwRQIgG1bSLeOXp3oB8H7b +53W+CKOPl2PknmWEq/lMhtn25HkCIQDaHDgWxWFtnCrBjH16/W3Ezn7/U/Vjo5xI +pDoiVhsLwg== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIF0zCCA7ugAwIBAgIIH8+hjWpIDREwDQYJKoZIhvcNAQELBQAwejELMAkGA1UE +BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNVBAsMF0NlcnRpZmlj +YXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5jaWFsIENlcnRpZmlj +YXRpb24gQXV0aG9yaXR5IFIxMB4XDTE4MDMyMTEzNDg0MFoXDTM4MDIyODEzNDg0 +MFowejELMAkGA1UEBhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNV +BAsMF0NlcnRpZmljYXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFIxMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEAtytTRcBNuur5h8xuxnlKJetT65cHGemGi8oD+beHFPTk +rUTlFt9Xn7fAVGo6QSsPb9uGLpUFGEdGmbsQ2q9cV4P89qkH04VzIPwT7AywJdt2 +xAvMs+MgHFJzOYfL1QkdOOVO7NwKxH8IvlQgFabWomWk2Ei9WfUyxFjVO1LVh0Bp +dRBeWLMkdudx0tl3+21t1apnReFNQ5nfX29xeSxIhesaMHDZFViO/DXDNW2BcTs6 +vSWKyJ4YIIIzStumD8K1xMsoaZBMDxg4itjWFaKRgNuPiIn4kjDY3kC66Sl/6yTl +YUz8AybbEsICZzssdZh7jcNb1VRfk79lgAprm/Ktl+mgrU1gaMGP1OE25JCbqli1 +Pbw/BpPynyP9+XulE+2mxFwTYhKAwpDIDKuYsFUXuo8t261pCovI1CXFzAQM2w7H +DtA2nOXSW6q0jGDJ5+WauH+K8ZSvA6x4sFo4u0KNCx0ROTBpLif6GTngqo3sj+98 +SZiMNLFMQoQkjkdN5Q5g9N6CFZPVZ6QpO0JcIc7S1le/g9z5iBKnifrKxy0TQjtG +PsDwc8ubPnRm/F82RReCoyNyx63indpgFfhN7+KxUIQ9cOwwTvemmor0A+ZQamRe +9LMuiEfEaWUDK+6O0Gl8lO571uI5onYdN1VIgOmwFbe+D8TcuzVjIZ/zvHrAGUcC +AwEAAaNdMFswCwYDVR0PBAQDAgEGMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFF90 +tATATwda6uWx2yKjh0GynOEBMB8GA1UdIwQYMBaAFF90tATATwda6uWx2yKjh0Gy +nOEBMA0GCSqGSIb3DQEBCwUAA4ICAQCVYaOtqOLIpsrEikE5lb+UARNSFJg6tpkf +tJ2U8QF/DejemEHx5IClQu6ajxjtu0Aie4/3UnIXop8nH/Q57l+Wyt9T7N2WPiNq +JSlYKYbJpPF8LXbuKYG3BTFTdOVFIeRe2NUyYh/xs6bXGr4WKTXb3qBmzR02FSy3 +IODQw5Q6zpXj8prYqFHYsOvGCEc1CwJaSaYwRhTkFedJUxiyhyB5GQwoFfExCVHW +05ZFCAVYFldCJvUzfzrWubN6wX0DD2dwultgmldOn/W/n8at52mpPNvIdbZb2F41 +T0YZeoWnCJrYXjq/32oc1cmifIHqySnyMnavi75DxPCdZsCOpSAT4j4lAQRGsfgI +kkLPGQieMfNNkMCKh7qjwdXAVtdqhf0RVtFILH3OyEodlk1HYXqX5iE5wlaKzDop +PKwf2Q3BErq1xChYGGVS+dEvyXc/2nIBlt7uLWKp4XFjqekKbaGaLJdjYP5b2s7N +1dM0MXQ/f8XoXKBkJNzEiM3hfsU6DOREgMc1DIsFKxfuMwX3EkVQM1If8ghb6x5Y +jXayv+NLbidOSzk4vl5QwngO/JYFMkoc6i9LNwEaEtR9PhnrdubxmrtM+RjfBm02 +77q3dSWFESFQ4QxYWew4pHE0DpWbWy/iMIKQ6UZ5RLvB8GEcgt8ON7BBJeMc+Dyi +kT9qhqn+lw== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIICiDCCAgygAwIBAgIIQX76UsB/30owDAYIKoZIzj0EAwMFADB6MQswCQYDVQQG +EwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UECwwXQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNpYWwgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkgRTEwHhcNMTkwNDI4MTYyMDQ0WhcNNDkwNDIwMTYyMDQ0 +WjB6MQswCQYDVQQGEwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UE +CwwXQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNp +YWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRTEwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAASCCRa94QI0vR5Up9Yr9HEupz6hSoyjySYqo7v837KnmjveUIUNiuC9pWAU +WP3jwLX3HkzeiNdeg22a0IZPoSUCpasufiLAnfXh6NInLiWBrjLJXDSGaY7vaokt +rpZvAdmjXTBbMAsGA1UdDwQEAwIBBjAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBRZ +4ZTgDpksHL2qcpkFkxD2zVd16TAfBgNVHSMEGDAWgBRZ4ZTgDpksHL2qcpkFkxD2 +zVd16TAMBggqhkjOPQQDAwUAA2gAMGUCMQD4IoqT2hTUn0jt7oXLdMJ8q4vLp6sg +wHfPiOr9gxreb+e6Oidwd2LDnC4OUqCWiF8CMAzwKs4SnDJYcMLf2vpkbuVE4dTH +Rglz+HGcTLWsFs4KxLsq7MuU+vJTBUeDJeDjdA== +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIUEMdk6dVgOEIS2cCP0Q43P90Ps5YwDQYJKoZIhvcNAQEF +BQAwajELMAkGA1UEBhMCQ04xEzARBgNVBAoMCmlUcnVzQ2hpbmExHDAaBgNVBAsM +E0NoaW5hIFRydXN0IE5ldHdvcmsxKDAmBgNVBAMMH2lUcnVzQ2hpbmEgQ2xhc3Mg +MiBSb290IENBIC0gRzMwHhcNMTMwNDE4MDkzNjU2WhcNMzMwNDE4MDkzNjU2WjBq +MQswCQYDVQQGEwJDTjETMBEGA1UECgwKaVRydXNDaGluYTEcMBoGA1UECwwTQ2hp +bmEgVHJ1c3QgTmV0d29yazEoMCYGA1UEAwwfaVRydXNDaGluYSBDbGFzcyAyIFJv +b3QgQ0EgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOPPShpV +nJbMqqCw6Bz1kehnoPst9pkr0V9idOwU2oyS47/HjJXk9Rd5a9xfwkPO88trUpz5 +4GmmwspDXjVFu9L0eFaRuH3KMha1Ak01citbF7cQLJlS7XI+tpkTGHEY5pt3EsQg +wykfZl/A1jrnSkspMS997r2Gim54cwz+mTMgDRhZsKK/lbOeBPpWtcFizjXYCqhw +WktvQfZBYi6o4sHCshnOswi4yV1p+LuFcQ2ciYdWvULh1eZhLxHbGXyznYHi0dGN +z+I9H8aXxqAQfHVhbdHNzi77hCxFjOy+hHrGsyzjrd2swVQ2iUWP8BfEQqGLqM1g +KgWKYfcTGdbPB1MCAwEAAaNjMGEwHQYDVR0OBBYEFG/oAMxTVe7y0+408CTAK8hA +uTyRMB8GA1UdIwQYMBaAFG/oAMxTVe7y0+408CTAK8hAuTyRMA8GA1UdEwEB/wQF +MAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBLnUTfW7hp +emMbuUGCk7RBswzOT83bDM6824EkUnf+X0iKS95SUNGeeSWK2o/3ALJo5hi7GZr3 +U8eLaWAcYizfO99UXMRBPw5PRR+gXGEronGUugLpxsjuynoLQu8GQAeysSXKbN1I +UugDo9u8igJORYA+5ms0s5sCUySqbQ2R5z/GoceyI9LdxIVa1RjVX8pYOj8JFwtn +DJN3ftSFvNMYwRuILKuqUYSHc2GPYiHVflDh5nDymCMOQFcFG3WsEuB+EYQPFgIU +1DHmdZcz7Llx8UOZXX2JupWCYzK1XhJb+r4hK5ncf/w8qGtYlmyJpxk3hr1TfUJX +Yf4Zr0fJsGuv +-----END CERTIFICATE----- \ No newline at end of file diff --git a/app/main/api/etc/merchant/apiclient_key.pem b/app/main/api/etc/merchant/apiclient_key.pem new file mode 100644 index 0000000..871f8da --- /dev/null +++ b/app/main/api/etc/merchant/apiclient_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDd7p4NV2SkjF3d +zLDltShURK21VIMalnjimqpXiFSuwwIlMb3Ad6rQyClpRAHA4V/POnRlPEDc7ddm +W97uw01Fyn1Hapta3aeHTA4EOdzUVk9i74ZiLKP1SJgNNelonAD6YFmqlVvvBV9w +ufCUu+9GAM8h9tdHlVR6o+BG3hP5A7FH/yuPuXplf1SuQh/SBahxX37zP0Av14g+ +FS9slawJiAn9wEpDb2moj3Y6i+afDA8nsgZNBhnMcv49VV0pwx/oKTQMATcavR0d +DKgohqLiUG+KtgoJU3KutiarQZRiVf4iSTHw+G8IGl8f/3LqgUK18QoX81hGp68F +hnWp/5KdAgMBAAECggEBAMY+lrS4MlDCij7Mz+ABmQrdZoYp/grMCyPwoOUcBPkv +fUUYT7YTr2RcyJEdjKttJxaH0t1zm0U+uEDZJCUIFIiZPpuC4U+j3DiBeavQvDB5 +AOURrWsZEUTUGe8DD4LAiCcf1jkIvlye4ghiMEPMNQrFQkHGq7tn61S5+meTjSfL +uJz1Ta587R/2ptAyo/QE0iKTFRRvymekOb0OLu7nyIC9vTCD7V9xjARwp2OwBDBu +Ztdv7WTFLeoO3Xt+Liopvwk1DNiqhamnLeBr/UttqYbtkotGO7wUlwZW9yM5fCnp +GHhuaejVk4eEw34lurEo8X7HMc+WwQ0/s9+5iM62RgECgYEA9Mki8eIaD2Wk0FdA +9PL7diW1h4KHMi/lb2AUXGkH4zNxp6k0bmT4n5S588OkXsT8YgUdAhwfTz+M3Olz +e39rwcN7u4NCoOmNcwfpW1o7w6rn04aC2Iz1SpZhtM+DBVhuS87VmC3ViGt242FW +bfSrSw0vDrvjMIj/5ApiVpwf2N0CgYEA6Blz0IBedtQkjBf+OC33wnBNwq/5VICs +V8eAHsq/EexRf2Z/JtCkPDYCiLddLjRt+jIPFCPyR8AsKQf8vdUFfhZ029GBCCrZ +usn1hoN3rDv4GuOMXJWCvpS45KoXZt8h31NTRAVqKRWXsIWkARu0++J6NfZR8FO7 +Jrx/QnKWJMECgYEA5HDlHMk+OspH8mrLYw1z4UG11H3a/9o1CyimN8uJId57ndVJ +6hBu+jaJB1W4ivzY7/0HolVuXr3XDr8LF+DFRnHRgiAwSQ1NBWIHxEpEZgmUChKI +/+EkdXQ8QMo74vwxCqw/J6L2mTZ5ICBR0ZG5XfQyy1RK5Jul+0I5ncxb6D0CgYAJ +6aRfoEvoiVDyRsgNwDDXthIsIXXlnQU/Tn7zUbdtXYlxhoAhuUF6bNgY3LP3GDgm +OmMYehyL4fJA4l1yAhoU84KULNN09NeNubhpwU2oJnuHMna5MY1+9D0dTwJm21rH +/fgNbKnHDWwIFv0VKwjExTxw948yU3Eny18oCFrPQQKBgQCCHWUVIzzci/YkX3lv +IzSgs4VTs4979hGeUYB4u+ihVU3cpXGLbuhm37Cgf0aX+I5vyxplKURgxg8jGnPB +KDuI2+1TlwXYt+5zCrpmBtZXpQbknde5Pfser+PMuGjybeWWpHzek7kZKPNCHwTL +BJG/ccbM6dULpIVcziB/hQMvLA== +-----END PRIVATE KEY----- diff --git a/app/main/api/etc/merchant/appCertPublicKey_2021005113664540.crt b/app/main/api/etc/merchant/appCertPublicKey_2021005113664540.crt new file mode 100644 index 0000000..405a9bc --- /dev/null +++ b/app/main/api/etc/merchant/appCertPublicKey_2021005113664540.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIEozCCA4ugAwIBAgIQICUDBmfevHFjK0cMiK6zxDANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UE +BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNVBAsMF0NlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MTkwNwYDVQQDDDBBbnQgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IENs +YXNzIDEgUjEwHhcNMjUwMzA2MTE1ODQ1WhcNMzAwMzA1MTE1ODQ1WjBrMQswCQYDVQQGEwJDTjEw +MC4GA1UECgwn5rW35Y2X5aSp6L+c5aSn5pWw5o2u56eR5oqA5pyJ6ZmQ5YWs5Y+4MQ8wDQYDVQQL +DAZBbGlwYXkxGTAXBgNVBAMMEDIwODgwNTEwMzI5OTg0OTIwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCA1mtTVZmB/7/wWV37Z8hUXEXFs0Gn1/Ie7c6rPQQRUlPHyJGcPAZvDii+ySC1 +/bplneMENRAjCuoJEM1z4X1FMt8rLggCqnF1xzUN2p9fdXUwcRPmSV4yi9ggMiFXldm0/eyaobV2 +fj0/VSLED2Qc8xBStM9pqkfszwf2rsAAKL15WQXOUiQw0s25s+Du18H4+YgkQ0HBr0+VPfhL4QoO +vsE34ZYP0TuTwxVheYNkvSOPXFXmtE3z/b+75y2n2msa9S4HItNVYpOkB7z3GDB+0/rvX+Q+GvYI +9BSBbgJwEuqiMN2SwQyAjH608JBoAUGnk0ygfG8juF77shBxzr/vAgMBAAGjggEpMIIBJTAfBgNV +HSMEGDAWgBRxB+IEYRbk5fJl6zEPyeD0PJrVkTAdBgNVHQ4EFgQU8Izqtjr8qvIpRYglmzELt22E +b7MwQAYDVR0gBDkwNzA1BgdggRwBbgEBMCowKAYIKwYBBQUHAgEWHGh0dHA6Ly9jYS5hbGlwYXku +Y29tL2Nwcy5wZGYwDgYDVR0PAQH/BAQDAgbAMC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9jYS5h +bGlwYXkuY29tL2NybDk5LmNybDBgBggrBgEFBQcBAQRUMFIwKAYIKwYBBQUHMAKGHGh0dHA6Ly9j +YS5hbGlwYXkuY29tL2NhNi5jZXIwJgYIKwYBBQUHMAGGGmh0dHA6Ly9jYS5hbGlwYXkuY29tOjgz +NDAvMA0GCSqGSIb3DQEBCwUAA4IBAQC5j9d26HpXXf/eC40ejQ8E/r5PwN87129jfdDpCQGVJopL +ZyrJzxAHoPW+pG5lbDGmlDC9g8CRgeVcpNNkKDshyDAjlAoKbedTSaI8Bacly6fKPlCwAipepiy3 +fBTovcQYgPNl4aw3spi/0ZsnoTJ3Ye8n7KD4j3j3iBwptXdWFDVSQCJFSdC5tSMQSnm6WUgW3Duw +UalPi2I5CjxCZK35Pbk0GFP5dJwtz4h3YGkLX20TP18HZi0hVrxOErH5U2mP+dlq72DclJEZZZj0 +PPMyOLUqmcQRWMmaVmGhDKpkcx81jSowFKuPlSQfU0dCXZGdqpnXVnAYlLMuMAILQqE+ +-----END CERTIFICATE----- \ No newline at end of file diff --git a/app/main/api/etc/merchant/pub_key.pem b/app/main/api/etc/merchant/pub_key.pem new file mode 100644 index 0000000..9d8523b --- /dev/null +++ b/app/main/api/etc/merchant/pub_key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsNH2kztg9gybkuulreL+ +BMyakxmKTFqrujYLm+S40v64KbNH3+sWdf1XR59vWjSvGWo+BAbuSIHNmIIFMFKE +sUxqHAYbta4oD9Ogr0+88drnXv+AA6vxQML0KaaTuHessvUhGC5GEUxa+TFefO9/ +EjbwL1E/XQ8oBkxHJO6RjKevuts39RjEyocnNhV7m8RP6WIBQeJDXhbfO1etcwdJ +B2yQ1eoPK9kGAqQ7wL4pDXrLXMfS1DXlNHsLf4if7rwu3fibk/qfkKdtmqvUw39f +tCKZRiexIq6ad9kTTjouXUU5EMRAn3ocRvNzCD4RaW1qVYMxFQ8AraQ8W3MXlPeL +EQIDAQAB +-----END PUBLIC KEY----- diff --git a/app/main/api/internal/config/config.go b/app/main/api/internal/config/config.go new file mode 100644 index 0000000..0b9551f --- /dev/null +++ b/app/main/api/internal/config/config.go @@ -0,0 +1,126 @@ +package config + +import ( + "github.com/zeromicro/go-zero/core/stores/cache" + "github.com/zeromicro/go-zero/rest" +) + +type Config struct { + rest.RestConf + DataSource string + CacheRedis cache.CacheConf + JwtAuth JwtAuth // JWT 鉴权相关配置 + VerifyCode VerifyCode + Encrypt Encrypt + Alipay AlipayConfig + Wxpay WxpayConfig + Applepay ApplepayConfig + Ali AliConfig + Tianyuanapi TianyuanapiConfig + SystemConfig SystemConfig + WechatH5 WechatH5Config + WechatMini WechatMiniConfig + Query QueryConfig + AdminConfig AdminConfig + AdminPromotion AdminPromotion + TaxConfig TaxConfig +} + +// JwtAuth 用于 JWT 鉴权配置 +type JwtAuth struct { + AccessSecret string // JWT 密钥,用于签发 Token + AccessExpire int64 // Token 过期时间,单位为秒 + RefreshAfter int64 +} +type VerifyCode struct { + AccessKeyID string + AccessKeySecret string + EndpointURL string + SignName string + TemplateCode string + ValidTime int +} +type Encrypt struct { + SecretKey string +} + +type AlipayConfig struct { + AppID string + PrivateKey string + AlipayPublicKey string + AppCertPath string // 应用公钥证书路径 + AlipayCertPath string // 支付宝公钥证书路径 + AlipayRootCertPath string // 根证书路径 + IsProduction bool + NotifyUrl string + ReturnURL string +} +type WxpayConfig struct { + AppID string + MchID string + MchCertificateSerialNumber string + MchApiv3Key string + MchPrivateKeyPath string + MchPublicKeyID string + MchPublicKeyPath string + NotifyUrl string + RefundNotifyUrl string +} +type AliConfig struct { + Code string +} +type ApplepayConfig struct { + ProductionVerifyURL string + SandboxVerifyURL string // 沙盒环境的验证 URL + Sandbox bool + BundleID string + IssuerID string + KeyID string + LoadPrivateKeyPath string +} +type WestConfig struct { + Url string + Key string + SecretId string + SecretSecondId string +} +type YushanConfig struct { + ApiKey string + AcctID string + Url string +} +type SystemConfig struct { + ThreeVerify bool +} +type WechatH5Config struct { + AppID string + AppSecret string +} +type WechatMiniConfig struct { + AppID string + AppSecret string + TycAppID string + TycAppSecret string +} +type QueryConfig struct { + ShareLinkExpire int64 +} +type AdminConfig struct { + AccessSecret string + AccessExpire int64 + RefreshAfter int64 +} + +type AdminPromotion struct { + URLDomain string +} +type TaxConfig struct { + TaxRate float64 + TaxExemptionAmount float64 +} +type TianyuanapiConfig struct { + AccessID string + Key string + BaseURL string + Timeout int64 +} diff --git a/app/main/api/internal/handler/admin_agent/admingetagentcommissiondeductionlisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentcommissiondeductionlisthandler.go new file mode 100644 index 0000000..168f5ae --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/admingetagentcommissiondeductionlisthandler.go @@ -0,0 +1,29 @@ +package admin_agent + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetAgentCommissionDeductionListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetAgentCommissionDeductionListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_agent.NewAdminGetAgentCommissionDeductionListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetAgentCommissionDeductionList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_agent/admingetagentcommissionlisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentcommissionlisthandler.go new file mode 100644 index 0000000..581003e --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/admingetagentcommissionlisthandler.go @@ -0,0 +1,29 @@ +package admin_agent + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetAgentCommissionListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetAgentCommissionListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_agent.NewAdminGetAgentCommissionListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetAgentCommissionList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_agent/admingetagentlinklisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentlinklisthandler.go new file mode 100644 index 0000000..fab7d55 --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/admingetagentlinklisthandler.go @@ -0,0 +1,29 @@ +package admin_agent + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetAgentLinkListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetAgentLinkListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_agent.NewAdminGetAgentLinkListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetAgentLinkList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_agent/admingetagentlisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentlisthandler.go new file mode 100644 index 0000000..d9c587d --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/admingetagentlisthandler.go @@ -0,0 +1,29 @@ +package admin_agent + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetAgentListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetAgentListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_agent.NewAdminGetAgentListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetAgentList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_agent/admingetagentmembershipconfiglisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentmembershipconfiglisthandler.go new file mode 100644 index 0000000..80d2a53 --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/admingetagentmembershipconfiglisthandler.go @@ -0,0 +1,29 @@ +package admin_agent + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetAgentMembershipConfigListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetAgentMembershipConfigListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_agent.NewAdminGetAgentMembershipConfigListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetAgentMembershipConfigList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_agent/admingetagentmembershiprechargeorderlisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentmembershiprechargeorderlisthandler.go new file mode 100644 index 0000000..69c4daa --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/admingetagentmembershiprechargeorderlisthandler.go @@ -0,0 +1,29 @@ +package admin_agent + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetAgentMembershipRechargeOrderListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetAgentMembershipRechargeOrderListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_agent.NewAdminGetAgentMembershipRechargeOrderListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetAgentMembershipRechargeOrderList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_agent/admingetagentplatformdeductionlisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentplatformdeductionlisthandler.go new file mode 100644 index 0000000..7188ef5 --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/admingetagentplatformdeductionlisthandler.go @@ -0,0 +1,29 @@ +package admin_agent + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetAgentPlatformDeductionListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetAgentPlatformDeductionListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_agent.NewAdminGetAgentPlatformDeductionListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetAgentPlatformDeductionList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_agent/admingetagentproductionconfiglisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentproductionconfiglisthandler.go new file mode 100644 index 0000000..802a070 --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/admingetagentproductionconfiglisthandler.go @@ -0,0 +1,29 @@ +package admin_agent + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetAgentProductionConfigListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetAgentProductionConfigListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_agent.NewAdminGetAgentProductionConfigListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetAgentProductionConfigList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_agent/admingetagentrewardlisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentrewardlisthandler.go new file mode 100644 index 0000000..58cc6b7 --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/admingetagentrewardlisthandler.go @@ -0,0 +1,29 @@ +package admin_agent + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetAgentRewardListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetAgentRewardListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_agent.NewAdminGetAgentRewardListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetAgentRewardList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_agent/admingetagentwithdrawallisthandler.go b/app/main/api/internal/handler/admin_agent/admingetagentwithdrawallisthandler.go new file mode 100644 index 0000000..92b3257 --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/admingetagentwithdrawallisthandler.go @@ -0,0 +1,29 @@ +package admin_agent + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetAgentWithdrawalListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetAgentWithdrawalListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_agent.NewAdminGetAgentWithdrawalListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetAgentWithdrawalList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_agent/adminupdateagentmembershipconfighandler.go b/app/main/api/internal/handler/admin_agent/adminupdateagentmembershipconfighandler.go new file mode 100644 index 0000000..4446ad3 --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/adminupdateagentmembershipconfighandler.go @@ -0,0 +1,29 @@ +package admin_agent + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminUpdateAgentMembershipConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminUpdateAgentMembershipConfigReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_agent.NewAdminUpdateAgentMembershipConfigLogic(r.Context(), svcCtx) + resp, err := l.AdminUpdateAgentMembershipConfig(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_agent/adminupdateagentproductionconfighandler.go b/app/main/api/internal/handler/admin_agent/adminupdateagentproductionconfighandler.go new file mode 100644 index 0000000..5cd2b05 --- /dev/null +++ b/app/main/api/internal/handler/admin_agent/adminupdateagentproductionconfighandler.go @@ -0,0 +1,29 @@ +package admin_agent + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminUpdateAgentProductionConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminUpdateAgentProductionConfigReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_agent.NewAdminUpdateAgentProductionConfigLogic(r.Context(), svcCtx) + resp, err := l.AdminUpdateAgentProductionConfig(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_auth/adminloginhandler.go b/app/main/api/internal/handler/admin_auth/adminloginhandler.go new file mode 100644 index 0000000..8fc27ac --- /dev/null +++ b/app/main/api/internal/handler/admin_auth/adminloginhandler.go @@ -0,0 +1,29 @@ +package admin_auth + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_auth" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminLoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminLoginReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_auth.NewAdminLoginLogic(r.Context(), svcCtx) + resp, err := l.AdminLogin(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_feature/admincreatefeaturehandler.go b/app/main/api/internal/handler/admin_feature/admincreatefeaturehandler.go new file mode 100644 index 0000000..992ab0c --- /dev/null +++ b/app/main/api/internal/handler/admin_feature/admincreatefeaturehandler.go @@ -0,0 +1,29 @@ +package admin_feature + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_feature" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminCreateFeatureHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminCreateFeatureReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_feature.NewAdminCreateFeatureLogic(r.Context(), svcCtx) + resp, err := l.AdminCreateFeature(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_feature/admindeletefeaturehandler.go b/app/main/api/internal/handler/admin_feature/admindeletefeaturehandler.go new file mode 100644 index 0000000..e8f67e3 --- /dev/null +++ b/app/main/api/internal/handler/admin_feature/admindeletefeaturehandler.go @@ -0,0 +1,29 @@ +package admin_feature + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_feature" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminDeleteFeatureHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminDeleteFeatureReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_feature.NewAdminDeleteFeatureLogic(r.Context(), svcCtx) + resp, err := l.AdminDeleteFeature(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_feature/admingetfeaturedetailhandler.go b/app/main/api/internal/handler/admin_feature/admingetfeaturedetailhandler.go new file mode 100644 index 0000000..0495f9c --- /dev/null +++ b/app/main/api/internal/handler/admin_feature/admingetfeaturedetailhandler.go @@ -0,0 +1,29 @@ +package admin_feature + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_feature" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetFeatureDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetFeatureDetailReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_feature.NewAdminGetFeatureDetailLogic(r.Context(), svcCtx) + resp, err := l.AdminGetFeatureDetail(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_feature/admingetfeaturelisthandler.go b/app/main/api/internal/handler/admin_feature/admingetfeaturelisthandler.go new file mode 100644 index 0000000..c6b73f4 --- /dev/null +++ b/app/main/api/internal/handler/admin_feature/admingetfeaturelisthandler.go @@ -0,0 +1,29 @@ +package admin_feature + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_feature" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetFeatureListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetFeatureListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_feature.NewAdminGetFeatureListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetFeatureList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_feature/adminupdatefeaturehandler.go b/app/main/api/internal/handler/admin_feature/adminupdatefeaturehandler.go new file mode 100644 index 0000000..647b8f7 --- /dev/null +++ b/app/main/api/internal/handler/admin_feature/adminupdatefeaturehandler.go @@ -0,0 +1,29 @@ +package admin_feature + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_feature" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminUpdateFeatureHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminUpdateFeatureReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_feature.NewAdminUpdateFeatureLogic(r.Context(), svcCtx) + resp, err := l.AdminUpdateFeature(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_menu/createmenuhandler.go b/app/main/api/internal/handler/admin_menu/createmenuhandler.go new file mode 100644 index 0000000..f4518da --- /dev/null +++ b/app/main/api/internal/handler/admin_menu/createmenuhandler.go @@ -0,0 +1,29 @@ +package admin_menu + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_menu" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func CreateMenuHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateMenuReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_menu.NewCreateMenuLogic(r.Context(), svcCtx) + resp, err := l.CreateMenu(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_menu/deletemenuhandler.go b/app/main/api/internal/handler/admin_menu/deletemenuhandler.go new file mode 100644 index 0000000..ce7fd1b --- /dev/null +++ b/app/main/api/internal/handler/admin_menu/deletemenuhandler.go @@ -0,0 +1,29 @@ +package admin_menu + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_menu" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func DeleteMenuHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.DeleteMenuReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_menu.NewDeleteMenuLogic(r.Context(), svcCtx) + resp, err := l.DeleteMenu(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_menu/getmenuallhandler.go b/app/main/api/internal/handler/admin_menu/getmenuallhandler.go new file mode 100644 index 0000000..a1d4ef4 --- /dev/null +++ b/app/main/api/internal/handler/admin_menu/getmenuallhandler.go @@ -0,0 +1,29 @@ +package admin_menu + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_menu" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func GetMenuAllHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetMenuAllReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_menu.NewGetMenuAllLogic(r.Context(), svcCtx) + resp, err := l.GetMenuAll(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_menu/getmenudetailhandler.go b/app/main/api/internal/handler/admin_menu/getmenudetailhandler.go new file mode 100644 index 0000000..7f5949a --- /dev/null +++ b/app/main/api/internal/handler/admin_menu/getmenudetailhandler.go @@ -0,0 +1,29 @@ +package admin_menu + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_menu" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func GetMenuDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetMenuDetailReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_menu.NewGetMenuDetailLogic(r.Context(), svcCtx) + resp, err := l.GetMenuDetail(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_menu/getmenulisthandler.go b/app/main/api/internal/handler/admin_menu/getmenulisthandler.go new file mode 100644 index 0000000..0aad0e5 --- /dev/null +++ b/app/main/api/internal/handler/admin_menu/getmenulisthandler.go @@ -0,0 +1,29 @@ +package admin_menu + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_menu" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func GetMenuListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetMenuListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_menu.NewGetMenuListLogic(r.Context(), svcCtx) + resp, err := l.GetMenuList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_menu/updatemenuhandler.go b/app/main/api/internal/handler/admin_menu/updatemenuhandler.go new file mode 100644 index 0000000..966bd8e --- /dev/null +++ b/app/main/api/internal/handler/admin_menu/updatemenuhandler.go @@ -0,0 +1,29 @@ +package admin_menu + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_menu" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func UpdateMenuHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateMenuReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_menu.NewUpdateMenuLogic(r.Context(), svcCtx) + resp, err := l.UpdateMenu(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_notification/admincreatenotificationhandler.go b/app/main/api/internal/handler/admin_notification/admincreatenotificationhandler.go new file mode 100644 index 0000000..7839210 --- /dev/null +++ b/app/main/api/internal/handler/admin_notification/admincreatenotificationhandler.go @@ -0,0 +1,29 @@ +package admin_notification + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_notification" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminCreateNotificationHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminCreateNotificationReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_notification.NewAdminCreateNotificationLogic(r.Context(), svcCtx) + resp, err := l.AdminCreateNotification(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_notification/admindeletenotificationhandler.go b/app/main/api/internal/handler/admin_notification/admindeletenotificationhandler.go new file mode 100644 index 0000000..ed4a954 --- /dev/null +++ b/app/main/api/internal/handler/admin_notification/admindeletenotificationhandler.go @@ -0,0 +1,29 @@ +package admin_notification + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_notification" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminDeleteNotificationHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminDeleteNotificationReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_notification.NewAdminDeleteNotificationLogic(r.Context(), svcCtx) + resp, err := l.AdminDeleteNotification(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_notification/admingetnotificationdetailhandler.go b/app/main/api/internal/handler/admin_notification/admingetnotificationdetailhandler.go new file mode 100644 index 0000000..1245514 --- /dev/null +++ b/app/main/api/internal/handler/admin_notification/admingetnotificationdetailhandler.go @@ -0,0 +1,29 @@ +package admin_notification + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_notification" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetNotificationDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetNotificationDetailReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_notification.NewAdminGetNotificationDetailLogic(r.Context(), svcCtx) + resp, err := l.AdminGetNotificationDetail(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_notification/admingetnotificationlisthandler.go b/app/main/api/internal/handler/admin_notification/admingetnotificationlisthandler.go new file mode 100644 index 0000000..be4c9d4 --- /dev/null +++ b/app/main/api/internal/handler/admin_notification/admingetnotificationlisthandler.go @@ -0,0 +1,29 @@ +package admin_notification + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_notification" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetNotificationListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetNotificationListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_notification.NewAdminGetNotificationListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetNotificationList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_notification/adminupdatenotificationhandler.go b/app/main/api/internal/handler/admin_notification/adminupdatenotificationhandler.go new file mode 100644 index 0000000..175dd15 --- /dev/null +++ b/app/main/api/internal/handler/admin_notification/adminupdatenotificationhandler.go @@ -0,0 +1,29 @@ +package admin_notification + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_notification" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminUpdateNotificationHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminUpdateNotificationReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_notification.NewAdminUpdateNotificationLogic(r.Context(), svcCtx) + resp, err := l.AdminUpdateNotification(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_order/admincreateorderhandler.go b/app/main/api/internal/handler/admin_order/admincreateorderhandler.go new file mode 100644 index 0000000..51d72be --- /dev/null +++ b/app/main/api/internal/handler/admin_order/admincreateorderhandler.go @@ -0,0 +1,29 @@ +package admin_order + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_order" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminCreateOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminCreateOrderReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_order.NewAdminCreateOrderLogic(r.Context(), svcCtx) + resp, err := l.AdminCreateOrder(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_order/admindeleteorderhandler.go b/app/main/api/internal/handler/admin_order/admindeleteorderhandler.go new file mode 100644 index 0000000..ebc8a15 --- /dev/null +++ b/app/main/api/internal/handler/admin_order/admindeleteorderhandler.go @@ -0,0 +1,29 @@ +package admin_order + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_order" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminDeleteOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminDeleteOrderReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_order.NewAdminDeleteOrderLogic(r.Context(), svcCtx) + resp, err := l.AdminDeleteOrder(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_order/admingetorderdetailhandler.go b/app/main/api/internal/handler/admin_order/admingetorderdetailhandler.go new file mode 100644 index 0000000..4b86d9f --- /dev/null +++ b/app/main/api/internal/handler/admin_order/admingetorderdetailhandler.go @@ -0,0 +1,29 @@ +package admin_order + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_order" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetOrderDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetOrderDetailReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_order.NewAdminGetOrderDetailLogic(r.Context(), svcCtx) + resp, err := l.AdminGetOrderDetail(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_order/admingetorderlisthandler.go b/app/main/api/internal/handler/admin_order/admingetorderlisthandler.go new file mode 100644 index 0000000..d2e8e23 --- /dev/null +++ b/app/main/api/internal/handler/admin_order/admingetorderlisthandler.go @@ -0,0 +1,29 @@ +package admin_order + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_order" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetOrderListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetOrderListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_order.NewAdminGetOrderListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetOrderList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_order/adminrefundorderhandler.go b/app/main/api/internal/handler/admin_order/adminrefundorderhandler.go new file mode 100644 index 0000000..be359c2 --- /dev/null +++ b/app/main/api/internal/handler/admin_order/adminrefundorderhandler.go @@ -0,0 +1,29 @@ +package admin_order + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_order" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminRefundOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminRefundOrderReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_order.NewAdminRefundOrderLogic(r.Context(), svcCtx) + resp, err := l.AdminRefundOrder(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_order/adminupdateorderhandler.go b/app/main/api/internal/handler/admin_order/adminupdateorderhandler.go new file mode 100644 index 0000000..aaf990b --- /dev/null +++ b/app/main/api/internal/handler/admin_order/adminupdateorderhandler.go @@ -0,0 +1,29 @@ +package admin_order + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_order" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminUpdateOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminUpdateOrderReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_order.NewAdminUpdateOrderLogic(r.Context(), svcCtx) + resp, err := l.AdminUpdateOrder(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_platform_user/admincreateplatformuserhandler.go b/app/main/api/internal/handler/admin_platform_user/admincreateplatformuserhandler.go new file mode 100644 index 0000000..8e74999 --- /dev/null +++ b/app/main/api/internal/handler/admin_platform_user/admincreateplatformuserhandler.go @@ -0,0 +1,29 @@ +package admin_platform_user + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_platform_user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminCreatePlatformUserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminCreatePlatformUserReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_platform_user.NewAdminCreatePlatformUserLogic(r.Context(), svcCtx) + resp, err := l.AdminCreatePlatformUser(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_platform_user/admindeleteplatformuserhandler.go b/app/main/api/internal/handler/admin_platform_user/admindeleteplatformuserhandler.go new file mode 100644 index 0000000..97d6fa1 --- /dev/null +++ b/app/main/api/internal/handler/admin_platform_user/admindeleteplatformuserhandler.go @@ -0,0 +1,29 @@ +package admin_platform_user + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_platform_user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminDeletePlatformUserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminDeletePlatformUserReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_platform_user.NewAdminDeletePlatformUserLogic(r.Context(), svcCtx) + resp, err := l.AdminDeletePlatformUser(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_platform_user/admingetplatformuserdetailhandler.go b/app/main/api/internal/handler/admin_platform_user/admingetplatformuserdetailhandler.go new file mode 100644 index 0000000..4a7ff53 --- /dev/null +++ b/app/main/api/internal/handler/admin_platform_user/admingetplatformuserdetailhandler.go @@ -0,0 +1,29 @@ +package admin_platform_user + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_platform_user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetPlatformUserDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetPlatformUserDetailReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_platform_user.NewAdminGetPlatformUserDetailLogic(r.Context(), svcCtx) + resp, err := l.AdminGetPlatformUserDetail(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_platform_user/admingetplatformuserlisthandler.go b/app/main/api/internal/handler/admin_platform_user/admingetplatformuserlisthandler.go new file mode 100644 index 0000000..76d5d55 --- /dev/null +++ b/app/main/api/internal/handler/admin_platform_user/admingetplatformuserlisthandler.go @@ -0,0 +1,29 @@ +package admin_platform_user + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_platform_user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetPlatformUserListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetPlatformUserListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_platform_user.NewAdminGetPlatformUserListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetPlatformUserList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_platform_user/adminupdateplatformuserhandler.go b/app/main/api/internal/handler/admin_platform_user/adminupdateplatformuserhandler.go new file mode 100644 index 0000000..2dd555d --- /dev/null +++ b/app/main/api/internal/handler/admin_platform_user/adminupdateplatformuserhandler.go @@ -0,0 +1,29 @@ +package admin_platform_user + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_platform_user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminUpdatePlatformUserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminUpdatePlatformUserReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_platform_user.NewAdminUpdatePlatformUserLogic(r.Context(), svcCtx) + resp, err := l.AdminUpdatePlatformUser(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_product/admincreateproducthandler.go b/app/main/api/internal/handler/admin_product/admincreateproducthandler.go new file mode 100644 index 0000000..b4d653e --- /dev/null +++ b/app/main/api/internal/handler/admin_product/admincreateproducthandler.go @@ -0,0 +1,29 @@ +package admin_product + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_product" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminCreateProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminCreateProductReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_product.NewAdminCreateProductLogic(r.Context(), svcCtx) + resp, err := l.AdminCreateProduct(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_product/admindeleteproducthandler.go b/app/main/api/internal/handler/admin_product/admindeleteproducthandler.go new file mode 100644 index 0000000..9b847f9 --- /dev/null +++ b/app/main/api/internal/handler/admin_product/admindeleteproducthandler.go @@ -0,0 +1,29 @@ +package admin_product + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_product" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminDeleteProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminDeleteProductReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_product.NewAdminDeleteProductLogic(r.Context(), svcCtx) + resp, err := l.AdminDeleteProduct(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_product/admingetproductdetailhandler.go b/app/main/api/internal/handler/admin_product/admingetproductdetailhandler.go new file mode 100644 index 0000000..43c847e --- /dev/null +++ b/app/main/api/internal/handler/admin_product/admingetproductdetailhandler.go @@ -0,0 +1,29 @@ +package admin_product + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_product" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetProductDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetProductDetailReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_product.NewAdminGetProductDetailLogic(r.Context(), svcCtx) + resp, err := l.AdminGetProductDetail(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_product/admingetproductfeaturelisthandler.go b/app/main/api/internal/handler/admin_product/admingetproductfeaturelisthandler.go new file mode 100644 index 0000000..c97e583 --- /dev/null +++ b/app/main/api/internal/handler/admin_product/admingetproductfeaturelisthandler.go @@ -0,0 +1,29 @@ +package admin_product + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_product" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetProductFeatureListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetProductFeatureListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_product.NewAdminGetProductFeatureListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetProductFeatureList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_product/admingetproductlisthandler.go b/app/main/api/internal/handler/admin_product/admingetproductlisthandler.go new file mode 100644 index 0000000..24fd1d8 --- /dev/null +++ b/app/main/api/internal/handler/admin_product/admingetproductlisthandler.go @@ -0,0 +1,29 @@ +package admin_product + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_product" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetProductListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetProductListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_product.NewAdminGetProductListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetProductList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_product/adminupdateproductfeatureshandler.go b/app/main/api/internal/handler/admin_product/adminupdateproductfeatureshandler.go new file mode 100644 index 0000000..14c1e0d --- /dev/null +++ b/app/main/api/internal/handler/admin_product/adminupdateproductfeatureshandler.go @@ -0,0 +1,29 @@ +package admin_product + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_product" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminUpdateProductFeaturesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminUpdateProductFeaturesReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_product.NewAdminUpdateProductFeaturesLogic(r.Context(), svcCtx) + resp, err := l.AdminUpdateProductFeatures(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_product/adminupdateproducthandler.go b/app/main/api/internal/handler/admin_product/adminupdateproducthandler.go new file mode 100644 index 0000000..578b6ad --- /dev/null +++ b/app/main/api/internal/handler/admin_product/adminupdateproducthandler.go @@ -0,0 +1,29 @@ +package admin_product + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_product" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminUpdateProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminUpdateProductReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_product.NewAdminUpdateProductLogic(r.Context(), svcCtx) + resp, err := l.AdminUpdateProduct(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_promotion/createpromotionlinkhandler.go b/app/main/api/internal/handler/admin_promotion/createpromotionlinkhandler.go new file mode 100644 index 0000000..a69212d --- /dev/null +++ b/app/main/api/internal/handler/admin_promotion/createpromotionlinkhandler.go @@ -0,0 +1,29 @@ +package admin_promotion + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_promotion" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func CreatePromotionLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreatePromotionLinkReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_promotion.NewCreatePromotionLinkLogic(r.Context(), svcCtx) + resp, err := l.CreatePromotionLink(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_promotion/deletepromotionlinkhandler.go b/app/main/api/internal/handler/admin_promotion/deletepromotionlinkhandler.go new file mode 100644 index 0000000..8156c72 --- /dev/null +++ b/app/main/api/internal/handler/admin_promotion/deletepromotionlinkhandler.go @@ -0,0 +1,30 @@ +package admin_promotion + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/admin_promotion" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func DeletePromotionLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.DeletePromotionLinkReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_promotion.NewDeletePromotionLinkLogic(r.Context(), svcCtx) + err := l.DeletePromotionLink(&req) + result.HttpResult(r, w, nil, err) + } +} diff --git a/app/main/api/internal/handler/admin_promotion/getpromotionlinkdetailhandler.go b/app/main/api/internal/handler/admin_promotion/getpromotionlinkdetailhandler.go new file mode 100644 index 0000000..dab0fa4 --- /dev/null +++ b/app/main/api/internal/handler/admin_promotion/getpromotionlinkdetailhandler.go @@ -0,0 +1,29 @@ +package admin_promotion + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_promotion" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func GetPromotionLinkDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetPromotionLinkDetailReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_promotion.NewGetPromotionLinkDetailLogic(r.Context(), svcCtx) + resp, err := l.GetPromotionLinkDetail(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_promotion/getpromotionlinklisthandler.go b/app/main/api/internal/handler/admin_promotion/getpromotionlinklisthandler.go new file mode 100644 index 0000000..1b3e2fd --- /dev/null +++ b/app/main/api/internal/handler/admin_promotion/getpromotionlinklisthandler.go @@ -0,0 +1,29 @@ +package admin_promotion + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_promotion" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func GetPromotionLinkListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetPromotionLinkListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_promotion.NewGetPromotionLinkListLogic(r.Context(), svcCtx) + resp, err := l.GetPromotionLinkList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_promotion/getpromotionstatshistoryhandler.go b/app/main/api/internal/handler/admin_promotion/getpromotionstatshistoryhandler.go new file mode 100644 index 0000000..5137ea0 --- /dev/null +++ b/app/main/api/internal/handler/admin_promotion/getpromotionstatshistoryhandler.go @@ -0,0 +1,29 @@ +package admin_promotion + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_promotion" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func GetPromotionStatsHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetPromotionStatsHistoryReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_promotion.NewGetPromotionStatsHistoryLogic(r.Context(), svcCtx) + resp, err := l.GetPromotionStatsHistory(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_promotion/getpromotionstatstotalhandler.go b/app/main/api/internal/handler/admin_promotion/getpromotionstatstotalhandler.go new file mode 100644 index 0000000..baba434 --- /dev/null +++ b/app/main/api/internal/handler/admin_promotion/getpromotionstatstotalhandler.go @@ -0,0 +1,29 @@ +package admin_promotion + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_promotion" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func GetPromotionStatsTotalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetPromotionStatsTotalReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_promotion.NewGetPromotionStatsTotalLogic(r.Context(), svcCtx) + resp, err := l.GetPromotionStatsTotal(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_promotion/recordlinkclickhandler.go b/app/main/api/internal/handler/admin_promotion/recordlinkclickhandler.go new file mode 100644 index 0000000..e4b92f9 --- /dev/null +++ b/app/main/api/internal/handler/admin_promotion/recordlinkclickhandler.go @@ -0,0 +1,29 @@ +package admin_promotion + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_promotion" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func RecordLinkClickHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.RecordLinkClickReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_promotion.NewRecordLinkClickLogic(r.Context(), svcCtx) + resp, err := l.RecordLinkClick(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_promotion/updatepromotionlinkhandler.go b/app/main/api/internal/handler/admin_promotion/updatepromotionlinkhandler.go new file mode 100644 index 0000000..f73d8f9 --- /dev/null +++ b/app/main/api/internal/handler/admin_promotion/updatepromotionlinkhandler.go @@ -0,0 +1,30 @@ +package admin_promotion + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/admin_promotion" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func UpdatePromotionLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdatePromotionLinkReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_promotion.NewUpdatePromotionLinkLogic(r.Context(), svcCtx) + err := l.UpdatePromotionLink(&req) + result.HttpResult(r, w, nil, err) + } +} diff --git a/app/main/api/internal/handler/admin_query/admingetquerycleanupconfiglisthandler.go b/app/main/api/internal/handler/admin_query/admingetquerycleanupconfiglisthandler.go new file mode 100644 index 0000000..0f534b1 --- /dev/null +++ b/app/main/api/internal/handler/admin_query/admingetquerycleanupconfiglisthandler.go @@ -0,0 +1,29 @@ +package admin_query + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetQueryCleanupConfigListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetQueryCleanupConfigListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_query.NewAdminGetQueryCleanupConfigListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetQueryCleanupConfigList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_query/admingetquerycleanupdetaillisthandler.go b/app/main/api/internal/handler/admin_query/admingetquerycleanupdetaillisthandler.go new file mode 100644 index 0000000..ed9e433 --- /dev/null +++ b/app/main/api/internal/handler/admin_query/admingetquerycleanupdetaillisthandler.go @@ -0,0 +1,29 @@ +package admin_query + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetQueryCleanupDetailListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetQueryCleanupDetailListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_query.NewAdminGetQueryCleanupDetailListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetQueryCleanupDetailList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_query/admingetquerycleanuploglisthandler.go b/app/main/api/internal/handler/admin_query/admingetquerycleanuploglisthandler.go new file mode 100644 index 0000000..0ec15ff --- /dev/null +++ b/app/main/api/internal/handler/admin_query/admingetquerycleanuploglisthandler.go @@ -0,0 +1,29 @@ +package admin_query + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetQueryCleanupLogListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetQueryCleanupLogListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_query.NewAdminGetQueryCleanupLogListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetQueryCleanupLogList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_query/admingetquerydetailbyorderidhandler.go b/app/main/api/internal/handler/admin_query/admingetquerydetailbyorderidhandler.go new file mode 100644 index 0000000..3f1c401 --- /dev/null +++ b/app/main/api/internal/handler/admin_query/admingetquerydetailbyorderidhandler.go @@ -0,0 +1,29 @@ +package admin_query + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetQueryDetailByOrderIdHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetQueryDetailByOrderIdReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_query.NewAdminGetQueryDetailByOrderIdLogic(r.Context(), svcCtx) + resp, err := l.AdminGetQueryDetailByOrderId(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_query/adminupdatequerycleanupconfighandler.go b/app/main/api/internal/handler/admin_query/adminupdatequerycleanupconfighandler.go new file mode 100644 index 0000000..3ebea85 --- /dev/null +++ b/app/main/api/internal/handler/admin_query/adminupdatequerycleanupconfighandler.go @@ -0,0 +1,29 @@ +package admin_query + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminUpdateQueryCleanupConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminUpdateQueryCleanupConfigReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_query.NewAdminUpdateQueryCleanupConfigLogic(r.Context(), svcCtx) + resp, err := l.AdminUpdateQueryCleanupConfig(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_role/createrolehandler.go b/app/main/api/internal/handler/admin_role/createrolehandler.go new file mode 100644 index 0000000..274c252 --- /dev/null +++ b/app/main/api/internal/handler/admin_role/createrolehandler.go @@ -0,0 +1,29 @@ +package admin_role + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_role" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func CreateRoleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.CreateRoleReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_role.NewCreateRoleLogic(r.Context(), svcCtx) + resp, err := l.CreateRole(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_role/deleterolehandler.go b/app/main/api/internal/handler/admin_role/deleterolehandler.go new file mode 100644 index 0000000..25a2cb6 --- /dev/null +++ b/app/main/api/internal/handler/admin_role/deleterolehandler.go @@ -0,0 +1,29 @@ +package admin_role + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_role" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func DeleteRoleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.DeleteRoleReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_role.NewDeleteRoleLogic(r.Context(), svcCtx) + resp, err := l.DeleteRole(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_role/getroledetailhandler.go b/app/main/api/internal/handler/admin_role/getroledetailhandler.go new file mode 100644 index 0000000..7de8abd --- /dev/null +++ b/app/main/api/internal/handler/admin_role/getroledetailhandler.go @@ -0,0 +1,29 @@ +package admin_role + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_role" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func GetRoleDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetRoleDetailReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_role.NewGetRoleDetailLogic(r.Context(), svcCtx) + resp, err := l.GetRoleDetail(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_role/getrolelisthandler.go b/app/main/api/internal/handler/admin_role/getrolelisthandler.go new file mode 100644 index 0000000..40487db --- /dev/null +++ b/app/main/api/internal/handler/admin_role/getrolelisthandler.go @@ -0,0 +1,29 @@ +package admin_role + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_role" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func GetRoleListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetRoleListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_role.NewGetRoleListLogic(r.Context(), svcCtx) + resp, err := l.GetRoleList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_role/updaterolehandler.go b/app/main/api/internal/handler/admin_role/updaterolehandler.go new file mode 100644 index 0000000..b022a9a --- /dev/null +++ b/app/main/api/internal/handler/admin_role/updaterolehandler.go @@ -0,0 +1,29 @@ +package admin_role + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_role" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func UpdateRoleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateRoleReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_role.NewUpdateRoleLogic(r.Context(), svcCtx) + resp, err := l.UpdateRole(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_user/admincreateuserhandler.go b/app/main/api/internal/handler/admin_user/admincreateuserhandler.go new file mode 100644 index 0000000..76a023e --- /dev/null +++ b/app/main/api/internal/handler/admin_user/admincreateuserhandler.go @@ -0,0 +1,29 @@ +package admin_user + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminCreateUserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminCreateUserReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_user.NewAdminCreateUserLogic(r.Context(), svcCtx) + resp, err := l.AdminCreateUser(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_user/admindeleteuserhandler.go b/app/main/api/internal/handler/admin_user/admindeleteuserhandler.go new file mode 100644 index 0000000..030dffe --- /dev/null +++ b/app/main/api/internal/handler/admin_user/admindeleteuserhandler.go @@ -0,0 +1,29 @@ +package admin_user + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminDeleteUserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminDeleteUserReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_user.NewAdminDeleteUserLogic(r.Context(), svcCtx) + resp, err := l.AdminDeleteUser(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_user/admingetuserdetailhandler.go b/app/main/api/internal/handler/admin_user/admingetuserdetailhandler.go new file mode 100644 index 0000000..3efa1cf --- /dev/null +++ b/app/main/api/internal/handler/admin_user/admingetuserdetailhandler.go @@ -0,0 +1,29 @@ +package admin_user + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetUserDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetUserDetailReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_user.NewAdminGetUserDetailLogic(r.Context(), svcCtx) + resp, err := l.AdminGetUserDetail(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_user/admingetuserlisthandler.go b/app/main/api/internal/handler/admin_user/admingetuserlisthandler.go new file mode 100644 index 0000000..7105a84 --- /dev/null +++ b/app/main/api/internal/handler/admin_user/admingetuserlisthandler.go @@ -0,0 +1,29 @@ +package admin_user + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminGetUserListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminGetUserListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_user.NewAdminGetUserListLogic(r.Context(), svcCtx) + resp, err := l.AdminGetUserList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_user/adminupdateuserhandler.go b/app/main/api/internal/handler/admin_user/adminupdateuserhandler.go new file mode 100644 index 0000000..edf038c --- /dev/null +++ b/app/main/api/internal/handler/admin_user/adminupdateuserhandler.go @@ -0,0 +1,29 @@ +package admin_user + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminUpdateUserHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminUpdateUserReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_user.NewAdminUpdateUserLogic(r.Context(), svcCtx) + resp, err := l.AdminUpdateUser(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/admin_user/adminuserinfohandler.go b/app/main/api/internal/handler/admin_user/adminuserinfohandler.go new file mode 100644 index 0000000..2f9dde0 --- /dev/null +++ b/app/main/api/internal/handler/admin_user/adminuserinfohandler.go @@ -0,0 +1,29 @@ +package admin_user + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/admin_user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func AdminUserInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AdminUserInfoReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := admin_user.NewAdminUserInfoLogic(r.Context(), svcCtx) + resp, err := l.AdminUserInfo(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/activateagentmembershiphandler.go b/app/main/api/internal/handler/agent/activateagentmembershiphandler.go new file mode 100644 index 0000000..562dff9 --- /dev/null +++ b/app/main/api/internal/handler/agent/activateagentmembershiphandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func ActivateAgentMembershipHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AgentActivateMembershipReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewActivateAgentMembershipLogic(r.Context(), svcCtx) + resp, err := l.ActivateAgentMembership(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/agentrealnamehandler.go b/app/main/api/internal/handler/agent/agentrealnamehandler.go new file mode 100644 index 0000000..761efa9 --- /dev/null +++ b/app/main/api/internal/handler/agent/agentrealnamehandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func AgentRealNameHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AgentRealNameReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewAgentRealNameLogic(r.Context(), svcCtx) + resp, err := l.AgentRealName(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/agentwithdrawalhandler.go b/app/main/api/internal/handler/agent/agentwithdrawalhandler.go new file mode 100644 index 0000000..61842a1 --- /dev/null +++ b/app/main/api/internal/handler/agent/agentwithdrawalhandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func AgentWithdrawalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.WithdrawalReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewAgentWithdrawalLogic(r.Context(), svcCtx) + resp, err := l.AgentWithdrawal(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/applyforagenthandler.go b/app/main/api/internal/handler/agent/applyforagenthandler.go new file mode 100644 index 0000000..6f5d954 --- /dev/null +++ b/app/main/api/internal/handler/agent/applyforagenthandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func ApplyForAgentHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AgentApplyReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewApplyForAgentLogic(r.Context(), svcCtx) + resp, err := l.ApplyForAgent(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/generatinglinkhandler.go b/app/main/api/internal/handler/agent/generatinglinkhandler.go new file mode 100644 index 0000000..73a41ff --- /dev/null +++ b/app/main/api/internal/handler/agent/generatinglinkhandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GeneratingLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AgentGeneratingLinkReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewGeneratingLinkLogic(r.Context(), svcCtx) + resp, err := l.GeneratingLink(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/getagentauditstatushandler.go b/app/main/api/internal/handler/agent/getagentauditstatushandler.go new file mode 100644 index 0000000..42abbca --- /dev/null +++ b/app/main/api/internal/handler/agent/getagentauditstatushandler.go @@ -0,0 +1,17 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/common/result" +) + +func GetAgentAuditStatusHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := agent.NewGetAgentAuditStatusLogic(r.Context(), svcCtx) + resp, err := l.GetAgentAuditStatus() + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/getagentcommissionhandler.go b/app/main/api/internal/handler/agent/getagentcommissionhandler.go new file mode 100644 index 0000000..ff2f1b4 --- /dev/null +++ b/app/main/api/internal/handler/agent/getagentcommissionhandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetAgentCommissionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetCommissionReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewGetAgentCommissionLogic(r.Context(), svcCtx) + resp, err := l.GetAgentCommission(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/getagentinfohandler.go b/app/main/api/internal/handler/agent/getagentinfohandler.go new file mode 100644 index 0000000..25847b8 --- /dev/null +++ b/app/main/api/internal/handler/agent/getagentinfohandler.go @@ -0,0 +1,17 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/common/result" +) + +func GetAgentInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := agent.NewGetAgentInfoLogic(r.Context(), svcCtx) + resp, err := l.GetAgentInfo() + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/getagentmembershipproductconfighandler.go b/app/main/api/internal/handler/agent/getagentmembershipproductconfighandler.go new file mode 100644 index 0000000..6b63a91 --- /dev/null +++ b/app/main/api/internal/handler/agent/getagentmembershipproductconfighandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetAgentMembershipProductConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AgentMembershipProductConfigReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewGetAgentMembershipProductConfigLogic(r.Context(), svcCtx) + resp, err := l.GetAgentMembershipProductConfig(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/getagentproductconfighandler.go b/app/main/api/internal/handler/agent/getagentproductconfighandler.go new file mode 100644 index 0000000..41fee1d --- /dev/null +++ b/app/main/api/internal/handler/agent/getagentproductconfighandler.go @@ -0,0 +1,17 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/common/result" +) + +func GetAgentProductConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := agent.NewGetAgentProductConfigLogic(r.Context(), svcCtx) + resp, err := l.GetAgentProductConfig() + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/getagentpromotionqrcodehandler.go b/app/main/api/internal/handler/agent/getagentpromotionqrcodehandler.go new file mode 100644 index 0000000..e4bac88 --- /dev/null +++ b/app/main/api/internal/handler/agent/getagentpromotionqrcodehandler.go @@ -0,0 +1,36 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetAgentPromotionQrcodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetAgentPromotionQrcodeReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + + // 注意:这里传入了 ResponseWriter,用于直接写入图片数据 + l := agent.NewGetAgentPromotionQrcodeLogic(r.Context(), svcCtx, w) + err := l.GetAgentPromotionQrcode(&req) + if err != nil { + // 如果处理过程中出错,返回JSON错误响应 + result.HttpResult(r, w, nil, err) + } + // 成功时,图片数据已经通过logic直接写入ResponseWriter,不需要额外处理 + } +} diff --git a/app/main/api/internal/handler/agent/getagentrevenueinfohandler.go b/app/main/api/internal/handler/agent/getagentrevenueinfohandler.go new file mode 100644 index 0000000..4defb5f --- /dev/null +++ b/app/main/api/internal/handler/agent/getagentrevenueinfohandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetAgentRevenueInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetAgentRevenueInfoReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewGetAgentRevenueInfoLogic(r.Context(), svcCtx) + resp, err := l.GetAgentRevenueInfo(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/getagentrewardshandler.go b/app/main/api/internal/handler/agent/getagentrewardshandler.go new file mode 100644 index 0000000..919be92 --- /dev/null +++ b/app/main/api/internal/handler/agent/getagentrewardshandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetAgentRewardsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetRewardsReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewGetAgentRewardsLogic(r.Context(), svcCtx) + resp, err := l.GetAgentRewards(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/getagentsubordinatecontributiondetailhandler.go b/app/main/api/internal/handler/agent/getagentsubordinatecontributiondetailhandler.go new file mode 100644 index 0000000..f4e72f8 --- /dev/null +++ b/app/main/api/internal/handler/agent/getagentsubordinatecontributiondetailhandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetAgentSubordinateContributionDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetAgentSubordinateContributionDetailReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewGetAgentSubordinateContributionDetailLogic(r.Context(), svcCtx) + resp, err := l.GetAgentSubordinateContributionDetail(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/getagentsubordinatelisthandler.go b/app/main/api/internal/handler/agent/getagentsubordinatelisthandler.go new file mode 100644 index 0000000..6524758 --- /dev/null +++ b/app/main/api/internal/handler/agent/getagentsubordinatelisthandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetAgentSubordinateListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetAgentSubordinateListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewGetAgentSubordinateListLogic(r.Context(), svcCtx) + resp, err := l.GetAgentSubordinateList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/getagentwithdrawalhandler.go b/app/main/api/internal/handler/agent/getagentwithdrawalhandler.go new file mode 100644 index 0000000..772693d --- /dev/null +++ b/app/main/api/internal/handler/agent/getagentwithdrawalhandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetAgentWithdrawalHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetWithdrawalReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewGetAgentWithdrawalLogic(r.Context(), svcCtx) + resp, err := l.GetAgentWithdrawal(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/getagentwithdrawaltaxexemptionhandler.go b/app/main/api/internal/handler/agent/getagentwithdrawaltaxexemptionhandler.go new file mode 100644 index 0000000..b44b4ad --- /dev/null +++ b/app/main/api/internal/handler/agent/getagentwithdrawaltaxexemptionhandler.go @@ -0,0 +1,29 @@ +package agent + +import ( + "net/http" + + "github.com/zeromicro/go-zero/rest/httpx" + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" +) + +func GetAgentWithdrawalTaxExemptionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetWithdrawalTaxExemptionReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewGetAgentWithdrawalTaxExemptionLogic(r.Context(), svcCtx) + resp, err := l.GetAgentWithdrawalTaxExemption(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/getlinkdatahandler.go b/app/main/api/internal/handler/agent/getlinkdatahandler.go new file mode 100644 index 0000000..0541205 --- /dev/null +++ b/app/main/api/internal/handler/agent/getlinkdatahandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetLinkDataHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetLinkDataReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewGetLinkDataLogic(r.Context(), svcCtx) + resp, err := l.GetLinkData(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/agent/saveagentmembershipuserconfighandler.go b/app/main/api/internal/handler/agent/saveagentmembershipuserconfighandler.go new file mode 100644 index 0000000..3064a2c --- /dev/null +++ b/app/main/api/internal/handler/agent/saveagentmembershipuserconfighandler.go @@ -0,0 +1,30 @@ +package agent + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/agent" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func SaveAgentMembershipUserConfigHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.SaveAgentMembershipUserConfigReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := agent.NewSaveAgentMembershipUserConfigLogic(r.Context(), svcCtx) + err := l.SaveAgentMembershipUserConfig(&req) + result.HttpResult(r, w, nil, err) + } +} diff --git a/app/main/api/internal/handler/app/getappversionhandler.go b/app/main/api/internal/handler/app/getappversionhandler.go new file mode 100644 index 0000000..0a9ee1e --- /dev/null +++ b/app/main/api/internal/handler/app/getappversionhandler.go @@ -0,0 +1,17 @@ +package app + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/app" + "znc-server/app/main/api/internal/svc" + "znc-server/common/result" +) + +func GetAppVersionHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := app.NewGetAppVersionLogic(r.Context(), svcCtx) + resp, err := l.GetAppVersion() + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/app/healthcheckhandler.go b/app/main/api/internal/handler/app/healthcheckhandler.go new file mode 100644 index 0000000..ac4929c --- /dev/null +++ b/app/main/api/internal/handler/app/healthcheckhandler.go @@ -0,0 +1,17 @@ +package app + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/app" + "znc-server/app/main/api/internal/svc" + "znc-server/common/result" +) + +func HealthCheckHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := app.NewHealthCheckLogic(r.Context(), svcCtx) + resp, err := l.HealthCheck() + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/auth/sendsmshandler.go b/app/main/api/internal/handler/auth/sendsmshandler.go new file mode 100644 index 0000000..534f64d --- /dev/null +++ b/app/main/api/internal/handler/auth/sendsmshandler.go @@ -0,0 +1,30 @@ +package auth + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/auth" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func SendSmsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.SendSmsReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := auth.NewSendSmsLogic(r.Context(), svcCtx) + err := l.SendSms(&req) + result.HttpResult(r, w, nil, err) + } +} diff --git a/app/main/api/internal/handler/notification/getnotificationshandler.go b/app/main/api/internal/handler/notification/getnotificationshandler.go new file mode 100644 index 0000000..345c227 --- /dev/null +++ b/app/main/api/internal/handler/notification/getnotificationshandler.go @@ -0,0 +1,17 @@ +package notification + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/notification" + "znc-server/app/main/api/internal/svc" + "znc-server/common/result" +) + +func GetNotificationsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := notification.NewGetNotificationsLogic(r.Context(), svcCtx) + resp, err := l.GetNotifications() + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/pay/alipaycallbackhandler.go b/app/main/api/internal/handler/pay/alipaycallbackhandler.go new file mode 100644 index 0000000..800db76 --- /dev/null +++ b/app/main/api/internal/handler/pay/alipaycallbackhandler.go @@ -0,0 +1,17 @@ +package pay + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/pay" + "znc-server/app/main/api/internal/svc" + "znc-server/common/result" +) + +func AlipayCallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := pay.NewAlipayCallbackLogic(r.Context(), svcCtx) + err := l.AlipayCallback(w, r) + result.HttpResult(r, w, nil, err) + } +} diff --git a/app/main/api/internal/handler/pay/iapcallbackhandler.go b/app/main/api/internal/handler/pay/iapcallbackhandler.go new file mode 100644 index 0000000..3f9c9b4 --- /dev/null +++ b/app/main/api/internal/handler/pay/iapcallbackhandler.go @@ -0,0 +1,30 @@ +package pay + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/pay" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func IapCallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.IapCallbackReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := pay.NewIapCallbackLogic(r.Context(), svcCtx) + err := l.IapCallback(&req) + result.HttpResult(r, w, nil, err) + } +} diff --git a/app/main/api/internal/handler/pay/paymentcheckhandler.go b/app/main/api/internal/handler/pay/paymentcheckhandler.go new file mode 100644 index 0000000..640f952 --- /dev/null +++ b/app/main/api/internal/handler/pay/paymentcheckhandler.go @@ -0,0 +1,30 @@ +package pay + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/pay" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func PaymentCheckHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.PaymentCheckReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := pay.NewPaymentCheckLogic(r.Context(), svcCtx) + resp, err := l.PaymentCheck(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/pay/paymenthandler.go b/app/main/api/internal/handler/pay/paymenthandler.go new file mode 100644 index 0000000..a93d2aa --- /dev/null +++ b/app/main/api/internal/handler/pay/paymenthandler.go @@ -0,0 +1,30 @@ +package pay + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/pay" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func PaymentHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.PaymentReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := pay.NewPaymentLogic(r.Context(), svcCtx) + resp, err := l.Payment(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/pay/wechatpaycallbackhandler.go b/app/main/api/internal/handler/pay/wechatpaycallbackhandler.go new file mode 100644 index 0000000..1f201c6 --- /dev/null +++ b/app/main/api/internal/handler/pay/wechatpaycallbackhandler.go @@ -0,0 +1,17 @@ +package pay + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/pay" + "znc-server/app/main/api/internal/svc" + "znc-server/common/result" +) + +func WechatPayCallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := pay.NewWechatPayCallbackLogic(r.Context(), svcCtx) + err := l.WechatPayCallback(w, r) + result.HttpResult(r, w, nil, err) + } +} diff --git a/app/main/api/internal/handler/pay/wechatpayrefundcallbackhandler.go b/app/main/api/internal/handler/pay/wechatpayrefundcallbackhandler.go new file mode 100644 index 0000000..aa1cdfd --- /dev/null +++ b/app/main/api/internal/handler/pay/wechatpayrefundcallbackhandler.go @@ -0,0 +1,17 @@ +package pay + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/pay" + "znc-server/app/main/api/internal/svc" + "znc-server/common/result" +) + +func WechatPayRefundCallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := pay.NewWechatPayRefundCallbackLogic(r.Context(), svcCtx) + err := l.WechatPayRefundCallback(w, r) + result.HttpResult(r, w, nil, err) + } +} diff --git a/app/main/api/internal/handler/product/getproductappbyenhandler.go b/app/main/api/internal/handler/product/getproductappbyenhandler.go new file mode 100644 index 0000000..f809bd4 --- /dev/null +++ b/app/main/api/internal/handler/product/getproductappbyenhandler.go @@ -0,0 +1,30 @@ +package product + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/product" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetProductAppByEnHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetProductByEnRequest + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := product.NewGetProductAppByEnLogic(r.Context(), svcCtx) + resp, err := l.GetProductAppByEn(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/product/getproductbyenhandler.go b/app/main/api/internal/handler/product/getproductbyenhandler.go new file mode 100644 index 0000000..6a34942 --- /dev/null +++ b/app/main/api/internal/handler/product/getproductbyenhandler.go @@ -0,0 +1,30 @@ +package product + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/product" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetProductByEnHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetProductByEnRequest + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := product.NewGetProductByEnLogic(r.Context(), svcCtx) + resp, err := l.GetProductByEn(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/product/getproductbyidhandler.go b/app/main/api/internal/handler/product/getproductbyidhandler.go new file mode 100644 index 0000000..5ceb421 --- /dev/null +++ b/app/main/api/internal/handler/product/getproductbyidhandler.go @@ -0,0 +1,30 @@ +package product + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/product" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func GetProductByIDHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.GetProductByIDRequest + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := product.NewGetProductByIDLogic(r.Context(), svcCtx) + resp, err := l.GetProductByID(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/query/querydetailbyorderidhandler.go b/app/main/api/internal/handler/query/querydetailbyorderidhandler.go new file mode 100644 index 0000000..edac053 --- /dev/null +++ b/app/main/api/internal/handler/query/querydetailbyorderidhandler.go @@ -0,0 +1,30 @@ +package query + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func QueryDetailByOrderIdHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QueryDetailByOrderIdReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := query.NewQueryDetailByOrderIdLogic(r.Context(), svcCtx) + resp, err := l.QueryDetailByOrderId(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/query/querydetailbyordernohandler.go b/app/main/api/internal/handler/query/querydetailbyordernohandler.go new file mode 100644 index 0000000..880a3a1 --- /dev/null +++ b/app/main/api/internal/handler/query/querydetailbyordernohandler.go @@ -0,0 +1,30 @@ +package query + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func QueryDetailByOrderNoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QueryDetailByOrderNoReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := query.NewQueryDetailByOrderNoLogic(r.Context(), svcCtx) + resp, err := l.QueryDetailByOrderNo(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/query/queryexamplehandler.go b/app/main/api/internal/handler/query/queryexamplehandler.go new file mode 100644 index 0000000..b9b44a3 --- /dev/null +++ b/app/main/api/internal/handler/query/queryexamplehandler.go @@ -0,0 +1,30 @@ +package query + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func QueryExampleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QueryExampleReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := query.NewQueryExampleLogic(r.Context(), svcCtx) + resp, err := l.QueryExample(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/query/querygeneratesharelinkhandler.go b/app/main/api/internal/handler/query/querygeneratesharelinkhandler.go new file mode 100644 index 0000000..cfe2020 --- /dev/null +++ b/app/main/api/internal/handler/query/querygeneratesharelinkhandler.go @@ -0,0 +1,30 @@ +package query + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func QueryGenerateShareLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QueryGenerateShareLinkReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := query.NewQueryGenerateShareLinkLogic(r.Context(), svcCtx) + resp, err := l.QueryGenerateShareLink(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/query/querylisthandler.go b/app/main/api/internal/handler/query/querylisthandler.go new file mode 100644 index 0000000..cdb975a --- /dev/null +++ b/app/main/api/internal/handler/query/querylisthandler.go @@ -0,0 +1,30 @@ +package query + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func QueryListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QueryListReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := query.NewQueryListLogic(r.Context(), svcCtx) + resp, err := l.QueryList(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/query/queryprovisionalorderhandler.go b/app/main/api/internal/handler/query/queryprovisionalorderhandler.go new file mode 100644 index 0000000..0bc996a --- /dev/null +++ b/app/main/api/internal/handler/query/queryprovisionalorderhandler.go @@ -0,0 +1,30 @@ +package query + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func QueryProvisionalOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QueryProvisionalOrderReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := query.NewQueryProvisionalOrderLogic(r.Context(), svcCtx) + resp, err := l.QueryProvisionalOrder(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/query/queryretryhandler.go b/app/main/api/internal/handler/query/queryretryhandler.go new file mode 100644 index 0000000..4b0cbf6 --- /dev/null +++ b/app/main/api/internal/handler/query/queryretryhandler.go @@ -0,0 +1,30 @@ +package query + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func QueryRetryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QueryRetryReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := query.NewQueryRetryLogic(r.Context(), svcCtx) + resp, err := l.QueryRetry(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/query/queryserviceagenthandler.go b/app/main/api/internal/handler/query/queryserviceagenthandler.go new file mode 100644 index 0000000..9e784c0 --- /dev/null +++ b/app/main/api/internal/handler/query/queryserviceagenthandler.go @@ -0,0 +1,30 @@ +package query + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func QueryServiceAgentHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QueryServiceReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := query.NewQueryServiceLogic(r.Context(), svcCtx) + resp, err := l.QueryService(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/query/queryserviceapphandler.go b/app/main/api/internal/handler/query/queryserviceapphandler.go new file mode 100644 index 0000000..6ecf653 --- /dev/null +++ b/app/main/api/internal/handler/query/queryserviceapphandler.go @@ -0,0 +1,30 @@ +package query + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func QueryServiceAppHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QueryServiceReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := query.NewQueryServiceLogic(r.Context(), svcCtx) + resp, err := l.QueryService(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/query/queryservicehandler.go b/app/main/api/internal/handler/query/queryservicehandler.go new file mode 100644 index 0000000..842a2f8 --- /dev/null +++ b/app/main/api/internal/handler/query/queryservicehandler.go @@ -0,0 +1,25 @@ +package query + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func QueryServiceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QueryServiceReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + l := query.NewQueryServiceLogic(r.Context(), svcCtx) + resp, err := l.QueryService(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/query/querysharedetailhandler.go b/app/main/api/internal/handler/query/querysharedetailhandler.go new file mode 100644 index 0000000..89ba71c --- /dev/null +++ b/app/main/api/internal/handler/query/querysharedetailhandler.go @@ -0,0 +1,30 @@ +package query + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func QueryShareDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QueryShareDetailReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := query.NewQueryShareDetailLogic(r.Context(), svcCtx) + resp, err := l.QueryShareDetail(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/query/querysingletesthandler.go b/app/main/api/internal/handler/query/querysingletesthandler.go new file mode 100644 index 0000000..b8ebecd --- /dev/null +++ b/app/main/api/internal/handler/query/querysingletesthandler.go @@ -0,0 +1,30 @@ +package query + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func QuerySingleTestHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.QuerySingleTestReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := query.NewQuerySingleTestLogic(r.Context(), svcCtx) + resp, err := l.QuerySingleTest(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/query/updatequerydatahandler.go b/app/main/api/internal/handler/query/updatequerydatahandler.go new file mode 100644 index 0000000..fddcedd --- /dev/null +++ b/app/main/api/internal/handler/query/updatequerydatahandler.go @@ -0,0 +1,31 @@ +package query + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/query" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// 更新查询数据 +func UpdateQueryDataHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.UpdateQueryDataReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := query.NewUpdateQueryDataLogic(r.Context(), svcCtx) + resp, err := l.UpdateQueryData(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/routes.go b/app/main/api/internal/handler/routes.go new file mode 100644 index 0000000..1ace614 --- /dev/null +++ b/app/main/api/internal/handler/routes.go @@ -0,0 +1,955 @@ +// Code generated by goctl. DO NOT EDIT. +package handler + +import ( + "net/http" + + admin_agent "znc-server/app/main/api/internal/handler/admin_agent" + admin_auth "znc-server/app/main/api/internal/handler/admin_auth" + admin_feature "znc-server/app/main/api/internal/handler/admin_feature" + admin_menu "znc-server/app/main/api/internal/handler/admin_menu" + admin_notification "znc-server/app/main/api/internal/handler/admin_notification" + admin_order "znc-server/app/main/api/internal/handler/admin_order" + admin_platform_user "znc-server/app/main/api/internal/handler/admin_platform_user" + admin_product "znc-server/app/main/api/internal/handler/admin_product" + admin_promotion "znc-server/app/main/api/internal/handler/admin_promotion" + admin_query "znc-server/app/main/api/internal/handler/admin_query" + admin_role "znc-server/app/main/api/internal/handler/admin_role" + admin_user "znc-server/app/main/api/internal/handler/admin_user" + agent "znc-server/app/main/api/internal/handler/agent" + app "znc-server/app/main/api/internal/handler/app" + auth "znc-server/app/main/api/internal/handler/auth" + notification "znc-server/app/main/api/internal/handler/notification" + pay "znc-server/app/main/api/internal/handler/pay" + product "znc-server/app/main/api/internal/handler/product" + query "znc-server/app/main/api/internal/handler/query" + user "znc-server/app/main/api/internal/handler/user" + "znc-server/app/main/api/internal/svc" + + "github.com/zeromicro/go-zero/rest" +) + +func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodGet, + Path: "/agent-commission-deduction/list", + Handler: admin_agent.AdminGetAgentCommissionDeductionListHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/agent-commission/list", + Handler: admin_agent.AdminGetAgentCommissionListHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/agent-link/list", + Handler: admin_agent.AdminGetAgentLinkListHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/agent-membership-config/list", + Handler: admin_agent.AdminGetAgentMembershipConfigListHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/agent-membership-config/update", + Handler: admin_agent.AdminUpdateAgentMembershipConfigHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/agent-membership-recharge-order/list", + Handler: admin_agent.AdminGetAgentMembershipRechargeOrderListHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/agent-platform-deduction/list", + Handler: admin_agent.AdminGetAgentPlatformDeductionListHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/agent-production-config/list", + Handler: admin_agent.AdminGetAgentProductionConfigListHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/agent-production-config/update", + Handler: admin_agent.AdminUpdateAgentProductionConfigHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/agent-reward/list", + Handler: admin_agent.AdminGetAgentRewardListHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/agent-withdrawal/list", + Handler: admin_agent.AdminGetAgentWithdrawalListHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/list", + Handler: admin_agent.AdminGetAgentListHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1/admin/agent"), + ) + + server.AddRoutes( + []rest.Route{ + { + // 登录 + Method: http.MethodPost, + Path: "/login", + Handler: admin_auth.AdminLoginHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1/admin/auth"), + ) + + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodPost, + Path: "/create", + Handler: admin_feature.AdminCreateFeatureHandler(serverCtx), + }, + { + Method: http.MethodDelete, + Path: "/delete/:id", + Handler: admin_feature.AdminDeleteFeatureHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/detail/:id", + Handler: admin_feature.AdminGetFeatureDetailHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/list", + Handler: admin_feature.AdminGetFeatureListHandler(serverCtx), + }, + { + Method: http.MethodPut, + Path: "/update/:id", + Handler: admin_feature.AdminUpdateFeatureHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1/admin/feature"), + ) + + server.AddRoutes( + []rest.Route{ + { + // 获取所有菜单(树形结构) + Method: http.MethodGet, + Path: "/all", + Handler: admin_menu.GetMenuAllHandler(serverCtx), + }, + { + // 创建菜单 + Method: http.MethodPost, + Path: "/create", + Handler: admin_menu.CreateMenuHandler(serverCtx), + }, + { + // 删除菜单 + Method: http.MethodDelete, + Path: "/delete/:id", + Handler: admin_menu.DeleteMenuHandler(serverCtx), + }, + { + // 获取菜单详情 + Method: http.MethodGet, + Path: "/detail/:id", + Handler: admin_menu.GetMenuDetailHandler(serverCtx), + }, + { + // 获取菜单列表 + Method: http.MethodGet, + Path: "/list", + Handler: admin_menu.GetMenuListHandler(serverCtx), + }, + { + // 更新菜单 + Method: http.MethodPut, + Path: "/update/:id", + Handler: admin_menu.UpdateMenuHandler(serverCtx), + }, + }, + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/admin/menu"), + ) + + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodPost, + Path: "/create", + Handler: admin_notification.AdminCreateNotificationHandler(serverCtx), + }, + { + Method: http.MethodDelete, + Path: "/delete/:id", + Handler: admin_notification.AdminDeleteNotificationHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/detail/:id", + Handler: admin_notification.AdminGetNotificationDetailHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/list", + Handler: admin_notification.AdminGetNotificationListHandler(serverCtx), + }, + { + Method: http.MethodPut, + Path: "/update/:id", + Handler: admin_notification.AdminUpdateNotificationHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1/admin/notification"), + ) + + server.AddRoutes( + []rest.Route{ + { + // 创建订单 + Method: http.MethodPost, + Path: "/create", + Handler: admin_order.AdminCreateOrderHandler(serverCtx), + }, + { + // 删除订单 + Method: http.MethodDelete, + Path: "/delete/:id", + Handler: admin_order.AdminDeleteOrderHandler(serverCtx), + }, + { + // 获取订单详情 + Method: http.MethodGet, + Path: "/detail/:id", + Handler: admin_order.AdminGetOrderDetailHandler(serverCtx), + }, + { + // 获取订单列表 + Method: http.MethodGet, + Path: "/list", + Handler: admin_order.AdminGetOrderListHandler(serverCtx), + }, + { + // 订单退款 + Method: http.MethodPost, + Path: "/refund/:id", + Handler: admin_order.AdminRefundOrderHandler(serverCtx), + }, + { + // 更新订单 + Method: http.MethodPut, + Path: "/update/:id", + Handler: admin_order.AdminUpdateOrderHandler(serverCtx), + }, + }, + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/admin/order"), + ) + + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodPost, + Path: "/create", + Handler: admin_platform_user.AdminCreatePlatformUserHandler(serverCtx), + }, + { + Method: http.MethodDelete, + Path: "/delete/:id", + Handler: admin_platform_user.AdminDeletePlatformUserHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/detail/:id", + Handler: admin_platform_user.AdminGetPlatformUserDetailHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/list", + Handler: admin_platform_user.AdminGetPlatformUserListHandler(serverCtx), + }, + { + Method: http.MethodPut, + Path: "/update/:id", + Handler: admin_platform_user.AdminUpdatePlatformUserHandler(serverCtx), + }, + }, + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/admin/platform_user"), + ) + + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodPost, + Path: "/create", + Handler: admin_product.AdminCreateProductHandler(serverCtx), + }, + { + Method: http.MethodDelete, + Path: "/delete/:id", + Handler: admin_product.AdminDeleteProductHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/detail/:id", + Handler: admin_product.AdminGetProductDetailHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/feature/list/:product_id", + Handler: admin_product.AdminGetProductFeatureListHandler(serverCtx), + }, + { + Method: http.MethodPut, + Path: "/feature/update/:product_id", + Handler: admin_product.AdminUpdateProductFeaturesHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/list", + Handler: admin_product.AdminGetProductListHandler(serverCtx), + }, + { + Method: http.MethodPut, + Path: "/update/:id", + Handler: admin_product.AdminUpdateProductHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1/admin/product"), + ) + + server.AddRoutes( + []rest.Route{ + { + // 创建推广链接 + Method: http.MethodPost, + Path: "/create", + Handler: admin_promotion.CreatePromotionLinkHandler(serverCtx), + }, + { + // 删除推广链接 + Method: http.MethodDelete, + Path: "/delete/:id", + Handler: admin_promotion.DeletePromotionLinkHandler(serverCtx), + }, + { + // 获取推广链接详情 + Method: http.MethodGet, + Path: "/detail/:id", + Handler: admin_promotion.GetPromotionLinkDetailHandler(serverCtx), + }, + { + // 获取推广链接列表 + Method: http.MethodGet, + Path: "/list", + Handler: admin_promotion.GetPromotionLinkListHandler(serverCtx), + }, + { + // 更新推广链接 + Method: http.MethodPut, + Path: "/update/:id", + Handler: admin_promotion.UpdatePromotionLinkHandler(serverCtx), + }, + }, + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/admin/promotion/link"), + ) + + server.AddRoutes( + []rest.Route{ + { + // 记录链接点击 + Method: http.MethodGet, + Path: "/record/:path", + Handler: admin_promotion.RecordLinkClickHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1/admin/promotion/link"), + ) + + server.AddRoutes( + []rest.Route{ + { + // 获取推广历史记录 + Method: http.MethodGet, + Path: "/history", + Handler: admin_promotion.GetPromotionStatsHistoryHandler(serverCtx), + }, + { + // 获取推广总统计 + Method: http.MethodGet, + Path: "/total", + Handler: admin_promotion.GetPromotionStatsTotalHandler(serverCtx), + }, + }, + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/admin/promotion/stats"), + ) + + server.AddRoutes( + []rest.Route{ + { + // 更新清理配置 + Method: http.MethodPut, + Path: "/cleanup/config", + Handler: admin_query.AdminUpdateQueryCleanupConfigHandler(serverCtx), + }, + { + // 获取清理配置列表 + Method: http.MethodGet, + Path: "/cleanup/configs", + Handler: admin_query.AdminGetQueryCleanupConfigListHandler(serverCtx), + }, + { + // 获取清理详情列表 + Method: http.MethodGet, + Path: "/cleanup/details/:log_id", + Handler: admin_query.AdminGetQueryCleanupDetailListHandler(serverCtx), + }, + { + // 获取清理日志列表 + Method: http.MethodGet, + Path: "/cleanup/logs", + Handler: admin_query.AdminGetQueryCleanupLogListHandler(serverCtx), + }, + { + // 获取查询详情 + Method: http.MethodGet, + Path: "/detail/:order_id", + Handler: admin_query.AdminGetQueryDetailByOrderIdHandler(serverCtx), + }, + }, + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/admin/query"), + ) + + server.AddRoutes( + []rest.Route{ + { + // 创建角色 + Method: http.MethodPost, + Path: "/create", + Handler: admin_role.CreateRoleHandler(serverCtx), + }, + { + // 删除角色 + Method: http.MethodDelete, + Path: "/delete/:id", + Handler: admin_role.DeleteRoleHandler(serverCtx), + }, + { + // 获取角色详情 + Method: http.MethodGet, + Path: "/detail/:id", + Handler: admin_role.GetRoleDetailHandler(serverCtx), + }, + { + // 获取角色列表 + Method: http.MethodGet, + Path: "/list", + Handler: admin_role.GetRoleListHandler(serverCtx), + }, + { + // 更新角色 + Method: http.MethodPut, + Path: "/update/:id", + Handler: admin_role.UpdateRoleHandler(serverCtx), + }, + }, + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/admin/role"), + ) + + server.AddRoutes( + []rest.Route{ + { + // 创建用户 + Method: http.MethodPost, + Path: "/create", + Handler: admin_user.AdminCreateUserHandler(serverCtx), + }, + { + // 删除用户 + Method: http.MethodDelete, + Path: "/delete/:id", + Handler: admin_user.AdminDeleteUserHandler(serverCtx), + }, + { + // 获取用户详情 + Method: http.MethodGet, + Path: "/detail/:id", + Handler: admin_user.AdminGetUserDetailHandler(serverCtx), + }, + { + // 用户信息 + Method: http.MethodGet, + Path: "/info", + Handler: admin_user.AdminUserInfoHandler(serverCtx), + }, + { + // 获取用户列表 + Method: http.MethodGet, + Path: "/list", + Handler: admin_user.AdminGetUserListHandler(serverCtx), + }, + { + // 更新用户 + Method: http.MethodPut, + Path: "/update/:id", + Handler: admin_user.AdminUpdateUserHandler(serverCtx), + }, + }, + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/admin/user"), + ) + + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodGet, + Path: "/promotion/qrcode", + Handler: agent.GetAgentPromotionQrcodeHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1/agent"), + ) + + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodGet, + Path: "/info", + Handler: agent.GetAgentInfoHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/revenue", + Handler: agent.GetAgentRevenueInfoHandler(serverCtx), + }, + }, + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/agent"), + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.UserAuthInterceptor}, + []rest.Route{ + { + Method: http.MethodGet, + Path: "/audit/status", + Handler: agent.GetAgentAuditStatusHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/generating_link", + Handler: agent.GeneratingLinkHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/product_config", + Handler: agent.GetAgentProductConfigHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/real_name", + Handler: agent.AgentRealNameHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/subordinate/contribution/detail", + Handler: agent.GetAgentSubordinateContributionDetailHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/subordinate/list", + Handler: agent.GetAgentSubordinateListHandler(serverCtx), + }, + }..., + ), + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/agent"), + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.UserAuthInterceptor}, + []rest.Route{ + { + Method: http.MethodPost, + Path: "/membership/save_user_config", + Handler: agent.SaveAgentMembershipUserConfigHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/membership/user_config", + Handler: agent.GetAgentMembershipProductConfigHandler(serverCtx), + }, + }..., + ), + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/agent"), + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.UserAuthInterceptor}, + []rest.Route{ + { + Method: http.MethodGet, + Path: "/commission", + Handler: agent.GetAgentCommissionHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/membership/activate", + Handler: agent.ActivateAgentMembershipHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/rewards", + Handler: agent.GetAgentRewardsHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/withdrawal", + Handler: agent.GetAgentWithdrawalHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/withdrawal", + Handler: agent.AgentWithdrawalHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/withdrawal/tax/exemption", + Handler: agent.GetAgentWithdrawalTaxExemptionHandler(serverCtx), + }, + }..., + ), + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/agent"), + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.AuthInterceptor}, + []rest.Route{ + { + Method: http.MethodPost, + Path: "/apply", + Handler: agent.ApplyForAgentHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/link", + Handler: agent.GetLinkDataHandler(serverCtx), + }, + }..., + ), + rest.WithPrefix("/api/v1/agent"), + ) + + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodGet, + Path: "/app/version", + Handler: app.GetAppVersionHandler(serverCtx), + }, + { + // 心跳检测接口 + Method: http.MethodGet, + Path: "/health/check", + Handler: app.HealthCheckHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + []rest.Route{ + { + // get mobile verify code + Method: http.MethodPost, + Path: "/auth/sendSms", + Handler: auth.SendSmsHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + []rest.Route{ + { + // get notifications + Method: http.MethodGet, + Path: "/notification/list", + Handler: notification.GetNotificationsHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodPost, + Path: "/pay/alipay/callback", + Handler: pay.AlipayCallbackHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/pay/wechat/callback", + Handler: pay.WechatPayCallbackHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/pay/wechat/refund_callback", + Handler: pay.WechatPayRefundCallbackHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.UserAuthInterceptor}, + []rest.Route{ + { + Method: http.MethodPost, + Path: "/pay/check", + Handler: pay.PaymentCheckHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/pay/iap_callback", + Handler: pay.IapCallbackHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/pay/payment", + Handler: pay.PaymentHandler(serverCtx), + }, + }..., + ), + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.UserAuthInterceptor}, + []rest.Route{ + { + Method: http.MethodGet, + Path: "/:id", + Handler: product.GetProductByIDHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/en/:product_en", + Handler: product.GetProductByEnHandler(serverCtx), + }, + }..., + ), + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1/product"), + ) + + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodGet, + Path: "/app_en/:product_en", + Handler: product.GetProductAppByEnHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1/product"), + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.AuthInterceptor}, + []rest.Route{ + { + // query service agent + Method: http.MethodPost, + Path: "/query/service_agent/:product", + Handler: query.QueryServiceAgentHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/query/service_app/:product", + Handler: query.QueryServiceAppHandler(serverCtx), + }, + }..., + ), + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.UserAuthInterceptor}, + []rest.Route{ + { + // query service + Method: http.MethodPost, + Path: "/query/service/:product", + Handler: query.QueryServiceHandler(serverCtx), + }, + }..., + ), + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.UserAuthInterceptor}, + []rest.Route{ + { + // 生成分享链接 + Method: http.MethodPost, + Path: "/query/generate_share_link", + Handler: query.QueryGenerateShareLinkHandler(serverCtx), + }, + { + // 查询列表 + Method: http.MethodGet, + Path: "/query/list", + Handler: query.QueryListHandler(serverCtx), + }, + { + // 查询详情 按订单号 付款查询时 + Method: http.MethodGet, + Path: "/query/orderId/:order_id", + Handler: query.QueryDetailByOrderIdHandler(serverCtx), + }, + { + // 查询详情 按订单号 + Method: http.MethodGet, + Path: "/query/orderNo/:order_no", + Handler: query.QueryDetailByOrderNoHandler(serverCtx), + }, + { + // 获取查询临时订单 + Method: http.MethodGet, + Path: "/query/provisional_order/:id", + Handler: query.QueryProvisionalOrderHandler(serverCtx), + }, + { + // 重试查询 + Method: http.MethodPost, + Path: "/query/retry/:id", + Handler: query.QueryRetryHandler(serverCtx), + }, + { + // 更新查询数据 + Method: http.MethodPost, + Path: "/query/update_data", + Handler: query.UpdateQueryDataHandler(serverCtx), + }, + }..., + ), + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + []rest.Route{ + { + // 查询示例 + Method: http.MethodGet, + Path: "/query/example", + Handler: query.QueryExampleHandler(serverCtx), + }, + { + // 查询详情 + Method: http.MethodGet, + Path: "/query/share/:id", + Handler: query.QueryShareDetailHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/query/single/test", + Handler: query.QuerySingleTestHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + []rest.Route{ + { + // mobile code login + Method: http.MethodPost, + Path: "/user/mobileCodeLogin", + Handler: user.MobileCodeLoginHandler(serverCtx), + }, + { + // wechat mini auth + Method: http.MethodPost, + Path: "/user/wxMiniAuth", + Handler: user.WxMiniAuthHandler(serverCtx), + }, + { + // wechat h5 auth + Method: http.MethodPost, + Path: "/user/wxh5Auth", + Handler: user.WxH5AuthHandler(serverCtx), + }, + }, + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + rest.WithMiddlewares( + []rest.Middleware{serverCtx.AuthInterceptor}, + []rest.Route{ + { + // 绑定手机号 + Method: http.MethodPost, + Path: "/user/bindMobile", + Handler: user.BindMobileHandler(serverCtx), + }, + }..., + ), + rest.WithPrefix("/api/v1"), + ) + + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodPost, + Path: "/user/cancelOut", + Handler: user.CancelOutHandler(serverCtx), + }, + { + // get user info + Method: http.MethodGet, + Path: "/user/detail", + Handler: user.DetailHandler(serverCtx), + }, + { + // get new token + Method: http.MethodPost, + Path: "/user/getToken", + Handler: user.GetTokenHandler(serverCtx), + }, + }, + rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), + rest.WithPrefix("/api/v1"), + ) +} diff --git a/app/main/api/internal/handler/user/bindmobilehandler.go b/app/main/api/internal/handler/user/bindmobilehandler.go new file mode 100644 index 0000000..2313d45 --- /dev/null +++ b/app/main/api/internal/handler/user/bindmobilehandler.go @@ -0,0 +1,30 @@ +package user + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func BindMobileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.BindMobileReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := user.NewBindMobileLogic(r.Context(), svcCtx) + resp, err := l.BindMobile(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/user/cancelouthandler.go b/app/main/api/internal/handler/user/cancelouthandler.go new file mode 100644 index 0000000..0a5b97c --- /dev/null +++ b/app/main/api/internal/handler/user/cancelouthandler.go @@ -0,0 +1,17 @@ +package user + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/user" + "znc-server/app/main/api/internal/svc" + "znc-server/common/result" +) + +func CancelOutHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := user.NewCancelOutLogic(r.Context(), svcCtx) + err := l.CancelOut() + result.HttpResult(r, w, nil, err) + } +} diff --git a/app/main/api/internal/handler/user/detailhandler.go b/app/main/api/internal/handler/user/detailhandler.go new file mode 100644 index 0000000..df0275f --- /dev/null +++ b/app/main/api/internal/handler/user/detailhandler.go @@ -0,0 +1,17 @@ +package user + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/user" + "znc-server/app/main/api/internal/svc" + "znc-server/common/result" +) + +func DetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := user.NewDetailLogic(r.Context(), svcCtx) + resp, err := l.Detail() + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/user/gettokenhandler.go b/app/main/api/internal/handler/user/gettokenhandler.go new file mode 100644 index 0000000..89f4c0b --- /dev/null +++ b/app/main/api/internal/handler/user/gettokenhandler.go @@ -0,0 +1,17 @@ +package user + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/user" + "znc-server/app/main/api/internal/svc" + "znc-server/common/result" +) + +func GetTokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := user.NewGetTokenLogic(r.Context(), svcCtx) + resp, err := l.GetToken() + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/user/mobilecodeloginhandler.go b/app/main/api/internal/handler/user/mobilecodeloginhandler.go new file mode 100644 index 0000000..a081d78 --- /dev/null +++ b/app/main/api/internal/handler/user/mobilecodeloginhandler.go @@ -0,0 +1,30 @@ +package user + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func MobileCodeLoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.MobileCodeLoginReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := user.NewMobileCodeLoginLogic(r.Context(), svcCtx) + resp, err := l.MobileCodeLogin(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/user/wxh5authhandler.go b/app/main/api/internal/handler/user/wxh5authhandler.go new file mode 100644 index 0000000..e0b67b6 --- /dev/null +++ b/app/main/api/internal/handler/user/wxh5authhandler.go @@ -0,0 +1,30 @@ +package user + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func WxH5AuthHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.WXH5AuthReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := user.NewWxH5AuthLogic(r.Context(), svcCtx) + resp, err := l.WxH5Auth(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/user/wxminiauthhandler.go b/app/main/api/internal/handler/user/wxminiauthhandler.go new file mode 100644 index 0000000..76a513e --- /dev/null +++ b/app/main/api/internal/handler/user/wxminiauthhandler.go @@ -0,0 +1,30 @@ +package user + +import ( + "net/http" + + "znc-server/app/main/api/internal/logic/user" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/result" + "znc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func WxMiniAuthHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.WXMiniAuthReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := user.NewWxMiniAuthLogic(r.Context(), svcCtx) + resp, err := l.WxMiniAuth(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/logic/admin_agent/admingetagentcommissiondeductionlistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentcommissiondeductionlistlogic.go new file mode 100644 index 0000000..65e2b51 --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/admingetagentcommissiondeductionlistlogic.go @@ -0,0 +1,84 @@ +package admin_agent + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetAgentCommissionDeductionListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetAgentCommissionDeductionListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetAgentCommissionDeductionListLogic { + return &AdminGetAgentCommissionDeductionListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetAgentCommissionDeductionListLogic) AdminGetAgentCommissionDeductionList(req *types.AdminGetAgentCommissionDeductionListReq) (resp *types.AdminGetAgentCommissionDeductionListResp, err error) { + builder := l.svcCtx.AgentCommissionDeductionModel.SelectBuilder() + if req.AgentId != nil { + builder = builder.Where(squirrel.Eq{"agent_id": *req.AgentId}) + } + if req.Type != nil && *req.Type != "" { + builder = builder.Where(squirrel.Eq{"type": *req.Type}) + } + if req.Status != nil { + builder = builder.Where(squirrel.Eq{"status": *req.Status}) + } + // 产品名筛选需先查product_id + if req.ProductName != nil && *req.ProductName != "" { + products, err := l.svcCtx.ProductModel.FindAll(l.ctx, l.svcCtx.ProductModel.SelectBuilder().Where(squirrel.Eq{"product_name": *req.ProductName}), "") + if err != nil || len(products) == 0 { + return &types.AdminGetAgentCommissionDeductionListResp{Total: 0, Items: []types.AgentCommissionDeductionListItem{}}, nil + } + builder = builder.Where("product_id = ?", products[0].Id) + } + + list, total, err := l.svcCtx.AgentCommissionDeductionModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, err + } + + // 批量查product_id->name + productIds := make(map[int64]struct{}) + for _, v := range list { + productIds[v.ProductId] = struct{}{} + } + productIdArr := make([]int64, 0, len(productIds)) + for id := range productIds { + productIdArr = append(productIdArr, id) + } + productNameMap := make(map[int64]string) + if len(productIdArr) > 0 { + build := l.svcCtx.ProductModel.SelectBuilder().Where(squirrel.Eq{"id": productIdArr}) + products, _ := l.svcCtx.ProductModel.FindAll(l.ctx, build, "") + for _, p := range products { + productNameMap[p.Id] = p.ProductName + } + } + + items := make([]types.AgentCommissionDeductionListItem, 0, len(list)) + for _, v := range list { + item := types.AgentCommissionDeductionListItem{} + _ = copier.Copy(&item, v) + item.ProductName = productNameMap[v.ProductId] + item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05") + items = append(items, item) + } + resp = &types.AdminGetAgentCommissionDeductionListResp{ + Total: total, + Items: items, + } + return +} diff --git a/app/main/api/internal/logic/admin_agent/admingetagentcommissionlistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentcommissionlistlogic.go new file mode 100644 index 0000000..7b222ac --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/admingetagentcommissionlistlogic.go @@ -0,0 +1,84 @@ +package admin_agent + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetAgentCommissionListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetAgentCommissionListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetAgentCommissionListLogic { + return &AdminGetAgentCommissionListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetAgentCommissionListLogic) AdminGetAgentCommissionList(req *types.AdminGetAgentCommissionListReq) (resp *types.AdminGetAgentCommissionListResp, err error) { + builder := l.svcCtx.AgentCommissionModel.SelectBuilder() + if req.AgentId != nil { + builder = builder.Where(squirrel.Eq{"agent_id": *req.AgentId}) + } + if req.Status != nil { + builder = builder.Where(squirrel.Eq{"status": *req.Status}) + } + // 先查出所有product_id对应的product_name(如有product_name筛选,需反查id) + var productIdFilter int64 + if req.ProductName != nil && *req.ProductName != "" { + // 只支持精确匹配,如需模糊可扩展 + products, err := l.svcCtx.ProductModel.FindAll(l.ctx, l.svcCtx.ProductModel.SelectBuilder().Where(squirrel.Eq{"product_name": *req.ProductName}), "") + if err != nil || len(products) == 0 { + return &types.AdminGetAgentCommissionListResp{Total: 0, Items: []types.AgentCommissionListItem{}}, nil + } + productIdFilter = products[0].Id + builder = builder.Where("product_id = ?", productIdFilter) + } + + list, total, err := l.svcCtx.AgentCommissionModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, err + } + + // 批量查product_name + productIds := make(map[int64]struct{}) + for _, v := range list { + productIds[v.ProductId] = struct{}{} + } + productNameMap := make(map[int64]string) + if len(productIds) > 0 { + ids := make([]int64, 0, len(productIds)) + for id := range productIds { + ids = append(ids, id) + } + builder := l.svcCtx.ProductModel.SelectBuilder().Where(squirrel.Eq{"id": ids}) + products, _ := l.svcCtx.ProductModel.FindAll(l.ctx, builder, "") + for _, p := range products { + productNameMap[p.Id] = p.ProductName + } + } + + items := make([]types.AgentCommissionListItem, 0, len(list)) + for _, v := range list { + item := types.AgentCommissionListItem{} + _ = copier.Copy(&item, v) + item.ProductName = productNameMap[v.ProductId] + item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05") + items = append(items, item) + } + resp = &types.AdminGetAgentCommissionListResp{ + Total: total, + Items: items, + } + return +} diff --git a/app/main/api/internal/logic/admin_agent/admingetagentlinklistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentlinklistlogic.go new file mode 100644 index 0000000..6dad318 --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/admingetagentlinklistlogic.go @@ -0,0 +1,86 @@ +package admin_agent + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/Masterminds/squirrel" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetAgentLinkListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetAgentLinkListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetAgentLinkListLogic { + return &AdminGetAgentLinkListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetAgentLinkListLogic) AdminGetAgentLinkList(req *types.AdminGetAgentLinkListReq) (resp *types.AdminGetAgentLinkListResp, err error) { + builder := l.svcCtx.AgentLinkModel.SelectBuilder() + if req.AgentId != nil { + builder = builder.Where("agent_id = ?", *req.AgentId) + } + if req.LinkIdentifier != nil && *req.LinkIdentifier != "" { + builder = builder.Where("link_identifier = ?", *req.LinkIdentifier) + } + + // 先查出所有product_id对应的product_name(如有product_name筛选,需反查id) + var productIdFilter int64 + if req.ProductName != nil && *req.ProductName != "" { + // 只支持精确匹配,如需模糊可扩展 + products, err := l.svcCtx.ProductModel.FindAll(l.ctx, l.svcCtx.ProductModel.SelectBuilder().Where(squirrel.Eq{"product_name": *req.ProductName}), "") + if err != nil || len(products) == 0 { + return &types.AdminGetAgentLinkListResp{Total: 0, Items: []types.AgentLinkListItem{}}, nil + } + productIdFilter = products[0].Id + builder = builder.Where("product_id = ?", productIdFilter) + } + + links, total, err := l.svcCtx.AgentLinkModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "id DESC") + if err != nil { + return nil, err + } + + // 批量查product_id->name,避免N+1 + productIdSet := make(map[int64]struct{}) + for _, link := range links { + productIdSet[link.ProductId] = struct{}{} + } + productIdList := make([]int64, 0, len(productIdSet)) + for id := range productIdSet { + productIdList = append(productIdList, id) + } + productNameMap := make(map[int64]string) + if len(productIdList) > 0 { + products, _ := l.svcCtx.ProductModel.FindAll(l.ctx, l.svcCtx.ProductModel.SelectBuilder().Where(squirrel.Eq{"id": productIdList}), "") + for _, p := range products { + productNameMap[p.Id] = p.ProductName + } + } + + items := make([]types.AgentLinkListItem, 0, len(links)) + for _, link := range links { + items = append(items, types.AgentLinkListItem{ + AgentId: link.AgentId, + ProductName: productNameMap[link.ProductId], + Price: link.Price, + LinkIdentifier: link.LinkIdentifier, + CreateTime: link.CreateTime.Format("2006-01-02 15:04:05"), + }) + } + + resp = &types.AdminGetAgentLinkListResp{ + Total: total, + Items: items, + } + return +} diff --git a/app/main/api/internal/logic/admin_agent/admingetagentlistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentlistlogic.go new file mode 100644 index 0000000..43610c8 --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/admingetagentlistlogic.go @@ -0,0 +1,115 @@ +package admin_agent + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetAgentListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetAgentListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetAgentListLogic { + return &AdminGetAgentListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetAgentListLogic) AdminGetAgentList(req *types.AdminGetAgentListReq) (resp *types.AdminGetAgentListResp, err error) { + builder := l.svcCtx.AgentModel.SelectBuilder() + if req.Mobile != nil && *req.Mobile != "" { + builder = builder.Where("mobile = ?", *req.Mobile) + } + if req.Region != nil && *req.Region != "" { + builder = builder.Where("region = ?", *req.Region) + } + + // 新增:如果传入ParentAgentId,则查找其所有1级下级代理 + if req.ParentAgentId != nil { + closureBuilder := l.svcCtx.AgentClosureModel.SelectBuilder().Where("ancestor_id = ? AND depth = 1", *req.ParentAgentId) + closures, cerr := l.svcCtx.AgentClosureModel.FindAll(l.ctx, closureBuilder, "") + if cerr != nil { + return nil, cerr + } + if len(closures) == 0 { + resp = &types.AdminGetAgentListResp{Total: 0, Items: []types.AgentListItem{}} + return resp, nil + } + ids := make([]int64, 0, len(closures)) + for _, c := range closures { + ids = append(ids, c.DescendantId) + } + builder = builder.Where("id IN (?)", ids) + } + + agents, total, err := l.svcCtx.AgentModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "id DESC") + if err != nil { + return nil, err + } + + items := make([]types.AgentListItem, 0, len(agents)) + + for _, agent := range agents { + item := types.AgentListItem{ + Id: agent.Id, + UserId: agent.UserId, + LevelName: agent.LevelName, + Region: agent.Region, + CreateTime: agent.CreateTime.Format("2006-01-02 15:04:05"), + } + if req.ParentAgentId != nil { + item.ParentAgentId = *req.ParentAgentId + } + agent.Mobile, err = crypto.DecryptMobile(agent.Mobile, l.svcCtx.Config.Encrypt.SecretKey) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理信息, 解密手机号失败: %v", err) + } + item.Mobile = agent.Mobile + if agent.MembershipExpiryTime.Valid { + item.MembershipExpiryTime = agent.MembershipExpiryTime.Time.Format("2006-01-02 15:04:05") + } + + // 查询钱包信息 + wallet, _ := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agent.Id) + if wallet != nil { + item.Balance = wallet.Balance + item.TotalEarnings = wallet.TotalEarnings + item.FrozenBalance = wallet.FrozenBalance + item.WithdrawnAmount = wallet.WithdrawnAmount + } + + // 查询实名认证信息 + realNameInfo, _ := l.svcCtx.AgentRealNameModel.FindOneByAgentId(l.ctx, agent.Id) + if realNameInfo != nil { + item.IsRealNameVerified = realNameInfo.Status == model.AgentRealNameStatusApproved + item.RealName = realNameInfo.Name + item.IdCard = realNameInfo.IdCard + item.RealNameStatus = realNameInfo.Status + } else { + item.IsRealNameVerified = false + item.RealName = "" + item.IdCard = "" + item.RealNameStatus = "" + } + + items = append(items, item) + } + + resp = &types.AdminGetAgentListResp{ + Total: total, + Items: items, + } + return +} diff --git a/app/main/api/internal/logic/admin_agent/admingetagentmembershipconfiglistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentmembershipconfiglistlogic.go new file mode 100644 index 0000000..c2c0a0a --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/admingetagentmembershipconfiglistlogic.go @@ -0,0 +1,51 @@ +package admin_agent + +import ( + "context" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetAgentMembershipConfigListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetAgentMembershipConfigListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetAgentMembershipConfigListLogic { + return &AdminGetAgentMembershipConfigListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetAgentMembershipConfigListLogic) AdminGetAgentMembershipConfigList(req *types.AdminGetAgentMembershipConfigListReq) (resp *types.AdminGetAgentMembershipConfigListResp, err error) { + builder := l.svcCtx.AgentMembershipConfigModel.SelectBuilder() + if req.LevelName != nil && *req.LevelName != "" { + builder = builder.Where(squirrel.Eq{"level_name": *req.LevelName}) + } + list, total, err := l.svcCtx.AgentMembershipConfigModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "id DESC") + if err != nil { + return nil, err + } + items := make([]types.AgentMembershipConfigListItem, 0, len(list)) + for _, v := range list { + var item types.AgentMembershipConfigListItem + if err := copier.Copy(&item, v); err != nil { + l.Logger.Errorf("copy error: %v", err) + continue + } + item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05") + items = append(items, item) + } + resp = &types.AdminGetAgentMembershipConfigListResp{ + Total: total, + Items: items, + } + return +} diff --git a/app/main/api/internal/logic/admin_agent/admingetagentmembershiprechargeorderlistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentmembershiprechargeorderlistlogic.go new file mode 100644 index 0000000..9c0d593 --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/admingetagentmembershiprechargeorderlistlogic.go @@ -0,0 +1,64 @@ +package admin_agent + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetAgentMembershipRechargeOrderListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetAgentMembershipRechargeOrderListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetAgentMembershipRechargeOrderListLogic { + return &AdminGetAgentMembershipRechargeOrderListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetAgentMembershipRechargeOrderListLogic) AdminGetAgentMembershipRechargeOrderList(req *types.AdminGetAgentMembershipRechargeOrderListReq) (resp *types.AdminGetAgentMembershipRechargeOrderListResp, err error) { + builder := l.svcCtx.AgentMembershipRechargeOrderModel.SelectBuilder() + if req.UserId != nil { + builder = builder.Where(squirrel.Eq{"user_id": *req.UserId}) + } + if req.AgentId != nil { + builder = builder.Where(squirrel.Eq{"agent_id": *req.AgentId}) + } + if req.OrderNo != nil && *req.OrderNo != "" { + builder = builder.Where(squirrel.Eq{"order_no": *req.OrderNo}) + } + if req.PlatformOrderId != nil && *req.PlatformOrderId != "" { + builder = builder.Where(squirrel.Eq{"platform_order_id": *req.PlatformOrderId}) + } + if req.Status != nil && *req.Status != "" { + builder = builder.Where(squirrel.Eq{"status": *req.Status}) + } + if req.PaymentMethod != nil && *req.PaymentMethod != "" { + builder = builder.Where(squirrel.Eq{"payment_method": *req.PaymentMethod}) + } + list, total, err := l.svcCtx.AgentMembershipRechargeOrderModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, err + } + items := make([]types.AgentMembershipRechargeOrderListItem, 0, len(list)) + for _, v := range list { + item := types.AgentMembershipRechargeOrderListItem{} + _ = copier.Copy(&item, v) + item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05") + items = append(items, item) + } + resp = &types.AdminGetAgentMembershipRechargeOrderListResp{ + Total: total, + Items: items, + } + return +} diff --git a/app/main/api/internal/logic/admin_agent/admingetagentplatformdeductionlistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentplatformdeductionlistlogic.go new file mode 100644 index 0000000..ffe1529 --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/admingetagentplatformdeductionlistlogic.go @@ -0,0 +1,57 @@ +package admin_agent + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetAgentPlatformDeductionListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetAgentPlatformDeductionListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetAgentPlatformDeductionListLogic { + return &AdminGetAgentPlatformDeductionListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetAgentPlatformDeductionListLogic) AdminGetAgentPlatformDeductionList(req *types.AdminGetAgentPlatformDeductionListReq) (resp *types.AdminGetAgentPlatformDeductionListResp, err error) { + builder := l.svcCtx.AgentPlatformDeductionModel.SelectBuilder() + if req.AgentId != nil { + builder = builder.Where(squirrel.Eq{"agent_id": *req.AgentId}) + } + if req.Type != nil && *req.Type != "" { + builder = builder.Where(squirrel.Eq{"type": *req.Type}) + } + if req.Status != nil { + builder = builder.Where(squirrel.Eq{"status": *req.Status}) + } + + list, total, err := l.svcCtx.AgentPlatformDeductionModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, err + } + + items := make([]types.AgentPlatformDeductionListItem, 0, len(list)) + for _, v := range list { + item := types.AgentPlatformDeductionListItem{} + _ = copier.Copy(&item, v) + item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05") + items = append(items, item) + } + resp = &types.AdminGetAgentPlatformDeductionListResp{ + Total: total, + Items: items, + } + return +} diff --git a/app/main/api/internal/logic/admin_agent/admingetagentproductionconfiglistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentproductionconfiglistlogic.go new file mode 100644 index 0000000..aa9a704 --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/admingetagentproductionconfiglistlogic.go @@ -0,0 +1,74 @@ +package admin_agent + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetAgentProductionConfigListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetAgentProductionConfigListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetAgentProductionConfigListLogic { + return &AdminGetAgentProductionConfigListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetAgentProductionConfigListLogic) AdminGetAgentProductionConfigList(req *types.AdminGetAgentProductionConfigListReq) (resp *types.AdminGetAgentProductionConfigListResp, err error) { + builder := l.svcCtx.AgentProductConfigModel.SelectBuilder() + if req.ProductName != nil && *req.ProductName != "" { + products, err := l.svcCtx.ProductModel.FindAll(l.ctx, l.svcCtx.ProductModel.SelectBuilder().Where(squirrel.Eq{"product_name": *req.ProductName}), "") + if err != nil || len(products) == 0 { + return &types.AdminGetAgentProductionConfigListResp{Total: 0, Items: []types.AgentProductionConfigItem{}}, nil + } + builder = builder.Where(squirrel.Eq{"product_id": products[0].Id}) + } + if req.Id != nil { + builder = builder.Where(squirrel.Eq{"id": *req.Id}) + } + list, total, err := l.svcCtx.AgentProductConfigModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, err + } + // 查询所有涉及到的product_id对应的product_name + productIdSet := make(map[int64]struct{}) + for _, v := range list { + productIdSet[v.ProductId] = struct{}{} + } + productIdArr := make([]int64, 0, len(productIdSet)) + for id := range productIdSet { + productIdArr = append(productIdArr, id) + } + productNameMap := make(map[int64]string) + if len(productIdArr) > 0 { + build := l.svcCtx.ProductModel.SelectBuilder().Where(squirrel.Eq{"id": productIdArr}) + products, _ := l.svcCtx.ProductModel.FindAll(l.ctx, build, "") + for _, p := range products { + productNameMap[p.Id] = p.ProductName + } + } + items := make([]types.AgentProductionConfigItem, 0, len(list)) + for _, v := range list { + item := types.AgentProductionConfigItem{} + _ = copier.Copy(&item, v) + item.ProductName = productNameMap[v.ProductId] + item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05") + items = append(items, item) + } + resp = &types.AdminGetAgentProductionConfigListResp{ + Total: total, + Items: items, + } + return +} diff --git a/app/main/api/internal/logic/admin_agent/admingetagentrewardlistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentrewardlistlogic.go new file mode 100644 index 0000000..4f09b89 --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/admingetagentrewardlistlogic.go @@ -0,0 +1,58 @@ +package admin_agent + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetAgentRewardListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetAgentRewardListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetAgentRewardListLogic { + return &AdminGetAgentRewardListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetAgentRewardListLogic) AdminGetAgentRewardList(req *types.AdminGetAgentRewardListReq) (resp *types.AdminGetAgentRewardListResp, err error) { + builder := l.svcCtx.AgentRewardsModel.SelectBuilder() + if req.AgentId != nil { + builder = builder.Where(squirrel.Eq{"agent_id": *req.AgentId}) + } + if req.RelationAgentId != nil { + builder = builder.Where(squirrel.Eq{"relation_agent_id": *req.RelationAgentId}) + } + if req.Type != nil && *req.Type != "" { + builder = builder.Where(squirrel.Eq{"type": *req.Type}) + } + list, total, err := l.svcCtx.AgentRewardsModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, err + } + items := make([]types.AgentRewardListItem, 0, len(list)) + for _, v := range list { + item := types.AgentRewardListItem{} + _ = copier.Copy(&item, v) + item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05") + if v.RelationAgentId.Valid { + item.RelationAgentId = v.RelationAgentId.Int64 + } + items = append(items, item) + } + resp = &types.AdminGetAgentRewardListResp{ + Total: total, + Items: items, + } + return +} diff --git a/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go b/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go new file mode 100644 index 0000000..2437009 --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/admingetagentwithdrawallistlogic.go @@ -0,0 +1,59 @@ +package admin_agent + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetAgentWithdrawalListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetAgentWithdrawalListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetAgentWithdrawalListLogic { + return &AdminGetAgentWithdrawalListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetAgentWithdrawalListLogic) AdminGetAgentWithdrawalList(req *types.AdminGetAgentWithdrawalListReq) (resp *types.AdminGetAgentWithdrawalListResp, err error) { + builder := l.svcCtx.AgentWithdrawalModel.SelectBuilder() + if req.AgentId != nil { + builder = builder.Where(squirrel.Eq{"agent_id": *req.AgentId}) + } + if req.Status != nil { + builder = builder.Where(squirrel.Eq{"status": *req.Status}) + } + if req.WithdrawNo != nil && *req.WithdrawNo != "" { + builder = builder.Where(squirrel.Eq{"withdraw_no": *req.WithdrawNo}) + } + list, total, err := l.svcCtx.AgentWithdrawalModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, err + } + items := make([]types.AgentWithdrawalListItem, 0, len(list)) + for _, v := range list { + item := types.AgentWithdrawalListItem{} + _ = copier.Copy(&item, v) + item.Remark = "" + if v.Remark.Valid { + item.Remark = v.Remark.String + } + item.CreateTime = v.CreateTime.Format("2006-01-02 15:04:05") + items = append(items, item) + } + resp = &types.AdminGetAgentWithdrawalListResp{ + Total: total, + Items: items, + } + return +} diff --git a/app/main/api/internal/logic/admin_agent/adminupdateagentmembershipconfiglogic.go b/app/main/api/internal/logic/admin_agent/adminupdateagentmembershipconfiglogic.go new file mode 100644 index 0000000..e643815 --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/adminupdateagentmembershipconfiglogic.go @@ -0,0 +1,96 @@ +package admin_agent + +import ( + "context" + "database/sql" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminUpdateAgentMembershipConfigLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminUpdateAgentMembershipConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminUpdateAgentMembershipConfigLogic { + return &AdminUpdateAgentMembershipConfigLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminUpdateAgentMembershipConfigLogic) AdminUpdateAgentMembershipConfig(req *types.AdminUpdateAgentMembershipConfigReq) (resp *types.AdminUpdateAgentMembershipConfigResp, err error) { + cfg, err := l.svcCtx.AgentMembershipConfigModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, err + } + cfg.LevelName = req.LevelName + cfg.Price = sql.NullFloat64{Float64: req.Price, Valid: true} + cfg.ReportCommission = sql.NullFloat64{Float64: req.ReportCommission, Valid: true} + if req.LowerActivityReward == nil { + cfg.LowerActivityReward = sql.NullFloat64{Valid: false} + } else { + cfg.LowerActivityReward = sql.NullFloat64{Float64: *req.LowerActivityReward, Valid: true} + } + if req.NewActivityReward == nil { + cfg.NewActivityReward = sql.NullFloat64{Valid: false} + } else { + cfg.NewActivityReward = sql.NullFloat64{Float64: *req.NewActivityReward, Valid: true} + } + if req.LowerStandardCount == nil { + cfg.LowerStandardCount = sql.NullInt64{Valid: false} + } else { + cfg.LowerStandardCount = sql.NullInt64{Int64: *req.LowerStandardCount, Valid: true} + } + if req.NewLowerStandardCount == nil { + cfg.NewLowerStandardCount = sql.NullInt64{Valid: false} + } else { + cfg.NewLowerStandardCount = sql.NullInt64{Int64: *req.NewLowerStandardCount, Valid: true} + } + if req.LowerWithdrawRewardRatio == nil { + cfg.LowerWithdrawRewardRatio = sql.NullFloat64{Valid: false} + } else { + cfg.LowerWithdrawRewardRatio = sql.NullFloat64{Float64: *req.LowerWithdrawRewardRatio, Valid: true} + } + if req.LowerConvertVipReward == nil { + cfg.LowerConvertVipReward = sql.NullFloat64{Valid: false} + } else { + cfg.LowerConvertVipReward = sql.NullFloat64{Float64: *req.LowerConvertVipReward, Valid: true} + } + if req.LowerConvertSvipReward == nil { + cfg.LowerConvertSvipReward = sql.NullFloat64{Valid: false} + } else { + cfg.LowerConvertSvipReward = sql.NullFloat64{Float64: *req.LowerConvertSvipReward, Valid: true} + } + if req.ExemptionAmount == nil { + cfg.ExemptionAmount = sql.NullFloat64{Valid: false} + } else { + cfg.ExemptionAmount = sql.NullFloat64{Float64: *req.ExemptionAmount, Valid: true} + } + if req.PriceIncreaseMax == nil { + cfg.PriceIncreaseMax = sql.NullFloat64{Valid: false} + } else { + cfg.PriceIncreaseMax = sql.NullFloat64{Float64: *req.PriceIncreaseMax, Valid: true} + } + if req.PriceRatio == nil { + cfg.PriceRatio = sql.NullFloat64{Valid: false} + } else { + cfg.PriceRatio = sql.NullFloat64{Float64: *req.PriceRatio, Valid: true} + } + if req.PriceIncreaseAmount == nil { + cfg.PriceIncreaseAmount = sql.NullFloat64{Valid: false} + } else { + cfg.PriceIncreaseAmount = sql.NullFloat64{Float64: *req.PriceIncreaseAmount, Valid: true} + } + _, err = l.svcCtx.AgentMembershipConfigModel.Update(l.ctx, nil, cfg) + if err != nil { + return nil, err + } + resp = &types.AdminUpdateAgentMembershipConfigResp{Success: true} + return +} diff --git a/app/main/api/internal/logic/admin_agent/adminupdateagentproductionconfiglogic.go b/app/main/api/internal/logic/admin_agent/adminupdateagentproductionconfiglogic.go new file mode 100644 index 0000000..7e64053 --- /dev/null +++ b/app/main/api/internal/logic/admin_agent/adminupdateagentproductionconfiglogic.go @@ -0,0 +1,42 @@ +package admin_agent + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminUpdateAgentProductionConfigLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminUpdateAgentProductionConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminUpdateAgentProductionConfigLogic { + return &AdminUpdateAgentProductionConfigLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminUpdateAgentProductionConfigLogic) AdminUpdateAgentProductionConfig(req *types.AdminUpdateAgentProductionConfigReq) (resp *types.AdminUpdateAgentProductionConfigResp, err error) { + cfg, err := l.svcCtx.AgentProductConfigModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, err + } + cfg.CostPrice = req.CostPrice + cfg.PriceRangeMin = req.PriceRangeMin + cfg.PriceRangeMax = req.PriceRangeMax + cfg.PricingStandard = req.PricingStandard + cfg.OverpricingRatio = req.OverpricingRatio + _, err = l.svcCtx.AgentProductConfigModel.Update(l.ctx, nil, cfg) + if err != nil { + return nil, err + } + resp = &types.AdminUpdateAgentProductionConfigResp{Success: true} + return +} diff --git a/app/main/api/internal/logic/admin_auth/adminloginlogic.go b/app/main/api/internal/logic/admin_auth/adminloginlogic.go new file mode 100644 index 0000000..6b04bc1 --- /dev/null +++ b/app/main/api/internal/logic/admin_auth/adminloginlogic.go @@ -0,0 +1,93 @@ +package admin_auth + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + jwtx "znc-server/common/jwt" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminLoginLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminLoginLogic { + return &AdminLoginLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminLoginLogic) AdminLogin(req *types.AdminLoginReq) (resp *types.AdminLoginResp, err error) { + // 1. 验证验证码 + if !req.Captcha { + return nil, errors.Wrapf(xerr.NewErrMsg("验证码错误"), "用户登录, 验证码错误, 验证码: %v", req.Captcha) + } + + // 2. 验证用户名和密码 + user, err := l.svcCtx.AdminUserModel.FindOneByUsername(l.ctx, req.Username) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrMsg("用户名或密码错误"), "用户登录, 用户名或密码错误, 用户名: %s", req.Username) + } + + // 3. 验证密码 + if !crypto.PasswordVerify(req.Password, user.Password) { + return nil, errors.Wrapf(xerr.NewErrMsg("用户名或密码错误"), "用户登录, 用户名或密码错误, 用户名: %s", req.Username) + } + + // 4. 获取权限 + adminUserRoleBuilder := l.svcCtx.AdminUserRoleModel.SelectBuilder().Where(squirrel.Eq{"user_id": user.Id}) + permissions, err := l.svcCtx.AdminUserRoleModel.FindAll(l.ctx, adminUserRoleBuilder, "role_id DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrMsg("获取权限失败"), "用户登录, 获取权限失败, 用户名: %s", req.Username) + } + + // 获取角色ID数组 + roleIds := make([]int64, 0) + for _, permission := range permissions { + roleIds = append(roleIds, permission.RoleId) + } + + // 获取角色名称 + roles := make([]string, 0) + for _, roleId := range roleIds { + role, err := l.svcCtx.AdminRoleModel.FindOne(l.ctx, roleId) + if err != nil { + continue + } + roles = append(roles, role.RoleCode) + } + + // 5. 生成token + refreshToken := l.svcCtx.Config.JwtAuth.RefreshAfter + expiresAt := l.svcCtx.Config.JwtAuth.AccessExpire + claims := jwtx.JwtClaims{ + UserId: user.Id, + AgentId: 0, + Platform: model.PlatformAdmin, + UserType: model.UserTypeAdmin, + IsAgent: model.AgentStatusNo, + } + token, err := jwtx.GenerateJwtToken(claims, l.svcCtx.Config.JwtAuth.AccessSecret, expiresAt) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrMsg("生成token失败"), "用户登录, 生成token失败, 用户名: %s", req.Username) + } + + return &types.AdminLoginResp{ + AccessToken: token, + AccessExpire: expiresAt, + RefreshAfter: refreshToken, + Roles: roles, + }, nil +} diff --git a/app/main/api/internal/logic/admin_feature/admincreatefeaturelogic.go b/app/main/api/internal/logic/admin_feature/admincreatefeaturelogic.go new file mode 100644 index 0000000..e8f0743 --- /dev/null +++ b/app/main/api/internal/logic/admin_feature/admincreatefeaturelogic.go @@ -0,0 +1,46 @@ +package admin_feature + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminCreateFeatureLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminCreateFeatureLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminCreateFeatureLogic { + return &AdminCreateFeatureLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminCreateFeatureLogic) AdminCreateFeature(req *types.AdminCreateFeatureReq) (resp *types.AdminCreateFeatureResp, err error) { + // 1. 数据转换 + data := &model.Feature{ + ApiId: req.ApiId, + Name: req.Name, + } + + // 2. 数据库操作 + result, err := l.svcCtx.FeatureModel.Insert(l.ctx, nil, data) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "创建功能失败, err: %v, req: %+v", err, req) + } + + // 3. 返回结果 + id, _ := result.LastInsertId() + return &types.AdminCreateFeatureResp{Id: id}, nil +} diff --git a/app/main/api/internal/logic/admin_feature/admindeletefeaturelogic.go b/app/main/api/internal/logic/admin_feature/admindeletefeaturelogic.go new file mode 100644 index 0000000..f1ed11e --- /dev/null +++ b/app/main/api/internal/logic/admin_feature/admindeletefeaturelogic.go @@ -0,0 +1,45 @@ +package admin_feature + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminDeleteFeatureLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminDeleteFeatureLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminDeleteFeatureLogic { + return &AdminDeleteFeatureLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminDeleteFeatureLogic) AdminDeleteFeature(req *types.AdminDeleteFeatureReq) (resp *types.AdminDeleteFeatureResp, err error) { + // 1. 查询记录是否存在 + record, err := l.svcCtx.FeatureModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "查找功能失败, err: %v, id: %d", err, req.Id) + } + + // 2. 执行软删除 + err = l.svcCtx.FeatureModel.DeleteSoft(l.ctx, nil, record) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "删除功能失败, err: %v, id: %d", err, req.Id) + } + + // 3. 返回结果 + return &types.AdminDeleteFeatureResp{Success: true}, nil +} diff --git a/app/main/api/internal/logic/admin_feature/admingetfeaturedetaillogic.go b/app/main/api/internal/logic/admin_feature/admingetfeaturedetaillogic.go new file mode 100644 index 0000000..a0d2a6f --- /dev/null +++ b/app/main/api/internal/logic/admin_feature/admingetfeaturedetaillogic.go @@ -0,0 +1,46 @@ +package admin_feature + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetFeatureDetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetFeatureDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetFeatureDetailLogic { + return &AdminGetFeatureDetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetFeatureDetailLogic) AdminGetFeatureDetail(req *types.AdminGetFeatureDetailReq) (resp *types.AdminGetFeatureDetailResp, err error) { + // 1. 查询记录 + record, err := l.svcCtx.FeatureModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "查找功能失败, err: %v, id: %d", err, req.Id) + } + + // 2. 构建响应 + resp = &types.AdminGetFeatureDetailResp{ + Id: record.Id, + ApiId: record.ApiId, + Name: record.Name, + CreateTime: record.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: record.UpdateTime.Format("2006-01-02 15:04:05"), + } + + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_feature/admingetfeaturelistlogic.go b/app/main/api/internal/logic/admin_feature/admingetfeaturelistlogic.go new file mode 100644 index 0000000..e10f6be --- /dev/null +++ b/app/main/api/internal/logic/admin_feature/admingetfeaturelistlogic.go @@ -0,0 +1,66 @@ +package admin_feature + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetFeatureListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetFeatureListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetFeatureListLogic { + return &AdminGetFeatureListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetFeatureListLogic) AdminGetFeatureList(req *types.AdminGetFeatureListReq) (resp *types.AdminGetFeatureListResp, err error) { + // 1. 构建查询条件 + builder := l.svcCtx.FeatureModel.SelectBuilder() + + // 2. 添加查询条件 + if req.ApiId != nil && *req.ApiId != "" { + builder = builder.Where("api_id LIKE ?", "%"+*req.ApiId+"%") + } + if req.Name != nil && *req.Name != "" { + builder = builder.Where("name LIKE ?", "%"+*req.Name+"%") + } + + // 3. 执行分页查询 + list, total, err := l.svcCtx.FeatureModel.FindPageListByPageWithTotal( + l.ctx, builder, req.Page, req.PageSize, "id DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "查询功能列表失败, err: %v, req: %+v", err, req) + } + + // 4. 构建响应列表 + items := make([]types.FeatureListItem, 0, len(list)) + for _, item := range list { + listItem := types.FeatureListItem{ + Id: item.Id, + ApiId: item.ApiId, + Name: item.Name, + CreateTime: item.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: item.UpdateTime.Format("2006-01-02 15:04:05"), + } + items = append(items, listItem) + } + + // 5. 返回结果 + return &types.AdminGetFeatureListResp{ + Total: total, + Items: items, + }, nil +} diff --git a/app/main/api/internal/logic/admin_feature/adminupdatefeaturelogic.go b/app/main/api/internal/logic/admin_feature/adminupdatefeaturelogic.go new file mode 100644 index 0000000..c1657f9 --- /dev/null +++ b/app/main/api/internal/logic/admin_feature/adminupdatefeaturelogic.go @@ -0,0 +1,30 @@ +package admin_feature + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminUpdateFeatureLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminUpdateFeatureLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminUpdateFeatureLogic { + return &AdminUpdateFeatureLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminUpdateFeatureLogic) AdminUpdateFeature(req *types.AdminUpdateFeatureReq) (resp *types.AdminUpdateFeatureResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/app/main/api/internal/logic/admin_menu/createmenulogic.go b/app/main/api/internal/logic/admin_menu/createmenulogic.go new file mode 100644 index 0000000..facd961 --- /dev/null +++ b/app/main/api/internal/logic/admin_menu/createmenulogic.go @@ -0,0 +1,97 @@ +package admin_menu + +import ( + "context" + "database/sql" + "encoding/json" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type CreateMenuLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewCreateMenuLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateMenuLogic { + return &CreateMenuLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateMenuLogic) CreateMenu(req *types.CreateMenuReq) (resp *types.CreateMenuResp, err error) { + // 1. 参数验证 + if req.Name == "" { + return nil, errors.Wrapf(xerr.NewErrMsg("菜单名称不能为空"), "菜单名称不能为空") + } + if req.Type == "menu" && req.Component == "" { + return nil, errors.Wrapf(xerr.NewErrMsg("组件路径不能为空"), "组件路径不能为空") + } + + // 2. 检查名称和路径是否重复 + exists, err := l.svcCtx.AdminMenuModel.FindOneByNamePath(l.ctx, req.Name, req.Path) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询菜单失败, err: %v", err) + } + if exists != nil { + return nil, errors.Wrapf(xerr.NewErrMsg("菜单名称或路径已存在"), "菜单名称或路径已存在") + } + + // 3. 检查父菜单是否存在(如果不是根菜单) + if req.Pid > 0 { + parentMenu, err := l.svcCtx.AdminMenuModel.FindOne(l.ctx, req.Pid) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询父菜单失败, id: %d, err: %v", req.Pid, err) + } + if parentMenu == nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "父菜单不存在, id: %d", req.Pid) + } + } + + // 4. 将类型标签转换为值 + typeValue, err := l.svcCtx.DictService.GetDictValue(l.ctx, "admin_menu_type", req.Type) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "菜单类型无效: %v", err) + } + + // 5. 创建菜单记录 + menu := &model.AdminMenu{ + Pid: req.Pid, + Name: req.Name, + Path: req.Path, + Component: req.Component, + Redirect: sql.NullString{String: req.Redirect, Valid: req.Redirect != ""}, + Status: req.Status, + Type: typeValue, + Sort: req.Sort, + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + + // 将Meta转换为JSON字符串 + metaJson, err := json.Marshal(req.Meta) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "Meta数据格式错误: %v", err) + } + menu.Meta = string(metaJson) + + // 6. 保存到数据库 + _, err = l.svcCtx.AdminMenuModel.Insert(l.ctx, nil, menu) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建菜单失败, err: %v", err) + } + + return &types.CreateMenuResp{ + Id: menu.Id, + }, nil +} diff --git a/app/main/api/internal/logic/admin_menu/deletemenulogic.go b/app/main/api/internal/logic/admin_menu/deletemenulogic.go new file mode 100644 index 0000000..045bf5b --- /dev/null +++ b/app/main/api/internal/logic/admin_menu/deletemenulogic.go @@ -0,0 +1,30 @@ +package admin_menu + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteMenuLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDeleteMenuLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteMenuLogic { + return &DeleteMenuLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteMenuLogic) DeleteMenu(req *types.DeleteMenuReq) (resp *types.DeleteMenuResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/app/main/api/internal/logic/admin_menu/getmenualllogic.go b/app/main/api/internal/logic/admin_menu/getmenualllogic.go new file mode 100644 index 0000000..a137c27 --- /dev/null +++ b/app/main/api/internal/logic/admin_menu/getmenualllogic.go @@ -0,0 +1,250 @@ +package admin_menu + +import ( + "context" + "sort" + "strconv" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/Masterminds/squirrel" + "github.com/bytedance/sonic" + "github.com/pkg/errors" + "github.com/samber/lo" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" +) + +type GetMenuAllLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetMenuAllLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetMenuAllLogic { + return &GetMenuAllLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetMenuAllLogic) GetMenuAll(req *types.GetMenuAllReq) (resp *[]types.GetMenuAllResp, err error) { + userId, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户ID失败, %+v", err) + } + + // 使用MapReduceVoid并发获取用户角色 + var roleIds []int64 + var permissions []*struct { + RoleId int64 + } + + type UserRoleResult struct { + RoleId int64 + } + + err = mr.MapReduceVoid( + func(source chan<- interface{}) { + adminUserRoleBuilder := l.svcCtx.AdminUserRoleModel.SelectBuilder().Where(squirrel.Eq{"user_id": userId}) + source <- adminUserRoleBuilder + }, + func(item interface{}, writer mr.Writer[*UserRoleResult], cancel func(error)) { + builder := item.(squirrel.SelectBuilder) + result, err := l.svcCtx.AdminUserRoleModel.FindAll(l.ctx, builder, "role_id DESC") + if err != nil { + cancel(errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户角色信息失败, %+v", err)) + return + } + + for _, r := range result { + writer.Write(&UserRoleResult{RoleId: r.RoleId}) + } + }, + func(pipe <-chan *UserRoleResult, cancel func(error)) { + for item := range pipe { + permissions = append(permissions, &struct{ RoleId int64 }{RoleId: item.RoleId}) + } + }, + ) + if err != nil { + return nil, err + } + + for _, permission := range permissions { + roleIds = append(roleIds, permission.RoleId) + } + + // 使用MapReduceVoid并发获取角色菜单 + var menuIds []int64 + var roleMenus []*struct { + MenuId int64 + } + + type RoleMenuResult struct { + MenuId int64 + } + + err = mr.MapReduceVoid( + func(source chan<- interface{}) { + getRoleMenuBuilder := l.svcCtx.AdminRoleMenuModel.SelectBuilder().Where(squirrel.Eq{"role_id": roleIds}) + source <- getRoleMenuBuilder + }, + func(item interface{}, writer mr.Writer[*RoleMenuResult], cancel func(error)) { + builder := item.(squirrel.SelectBuilder) + result, err := l.svcCtx.AdminRoleMenuModel.FindAll(l.ctx, builder, "id DESC") + if err != nil { + cancel(errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取角色菜单信息失败, %+v", err)) + return + } + + for _, r := range result { + writer.Write(&RoleMenuResult{MenuId: r.MenuId}) + } + }, + func(pipe <-chan *RoleMenuResult, cancel func(error)) { + for item := range pipe { + roleMenus = append(roleMenus, &struct{ MenuId int64 }{MenuId: item.MenuId}) + } + }, + ) + if err != nil { + return nil, err + } + + for _, roleMenu := range roleMenus { + menuIds = append(menuIds, roleMenu.MenuId) + } + + // 使用MapReduceVoid并发获取菜单 + type AdminMenuStruct struct { + Id int64 + Pid int64 + Name string + Path string + Component string + Redirect struct { + String string + Valid bool + } + Meta string + Sort int64 + Type int64 + Status int64 + } + + var menus []*AdminMenuStruct + + err = mr.MapReduceVoid( + func(source chan<- interface{}) { + adminMenuBuilder := l.svcCtx.AdminMenuModel.SelectBuilder().Where(squirrel.Eq{"id": menuIds}) + source <- adminMenuBuilder + }, + func(item interface{}, writer mr.Writer[*AdminMenuStruct], cancel func(error)) { + builder := item.(squirrel.SelectBuilder) + result, err := l.svcCtx.AdminMenuModel.FindAll(l.ctx, builder, "sort ASC") + if err != nil { + cancel(errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取菜单信息失败, %+v", err)) + return + } + + for _, r := range result { + menu := &AdminMenuStruct{ + Id: r.Id, + Pid: r.Pid, + Name: r.Name, + Path: r.Path, + Component: r.Component, + Redirect: r.Redirect, + Meta: r.Meta, + Sort: r.Sort, + Type: r.Type, + Status: r.Status, + } + writer.Write(menu) + } + }, + func(pipe <-chan *AdminMenuStruct, cancel func(error)) { + for item := range pipe { + menus = append(menus, item) + } + }, + ) + if err != nil { + return nil, err + } + + // 转换为types.Menu结构并存储到映射表 + menuMap := make(map[string]types.GetMenuAllResp) + for _, menu := range menus { + // 只处理状态正常的菜单 + if menu.Status != 1 { + continue + } + + meta := make(map[string]interface{}) + err = sonic.Unmarshal([]byte(menu.Meta), &meta) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解析菜单Meta信息失败, %+v", err) + } + + redirect := func() string { + if menu.Redirect.Valid { + return menu.Redirect.String + } + return "" + }() + + menuId := strconv.FormatInt(menu.Id, 10) + menuMap[menuId] = types.GetMenuAllResp{ + Name: menu.Name, + Path: menu.Path, + Redirect: redirect, + Component: menu.Component, + Sort: menu.Sort, + Meta: meta, + Children: make([]types.GetMenuAllResp, 0), + } + } + + // 按ParentId将菜单分组 + menuGroups := lo.GroupBy(menus, func(item *AdminMenuStruct) int64 { + return item.Pid + }) + + // 递归构建菜单树 + var buildMenuTree func(parentId int64) []types.GetMenuAllResp + buildMenuTree = func(parentId int64) []types.GetMenuAllResp { + children := make([]types.GetMenuAllResp, 0) + + childMenus, ok := menuGroups[parentId] + if !ok { + return children + } + + // 按Sort排序 + sort.Slice(childMenus, func(i, j int) bool { + return childMenus[i].Sort < childMenus[j].Sort + }) + + for _, childMenu := range childMenus { + menuId := strconv.FormatInt(childMenu.Id, 10) + if menu, exists := menuMap[menuId]; exists && childMenu.Status == 1 { + // 递归构建子菜单 + menu.Children = buildMenuTree(childMenu.Id) + children = append(children, menu) + } + } + + return children + } + + // 从根菜单开始构建(ParentId为0的是根菜单) + menuTree := buildMenuTree(0) + + return &menuTree, nil +} diff --git a/app/main/api/internal/logic/admin_menu/getmenudetaillogic.go b/app/main/api/internal/logic/admin_menu/getmenudetaillogic.go new file mode 100644 index 0000000..432b4f0 --- /dev/null +++ b/app/main/api/internal/logic/admin_menu/getmenudetaillogic.go @@ -0,0 +1,30 @@ +package admin_menu + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetMenuDetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetMenuDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetMenuDetailLogic { + return &GetMenuDetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetMenuDetailLogic) GetMenuDetail(req *types.GetMenuDetailReq) (resp *types.GetMenuDetailResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/app/main/api/internal/logic/admin_menu/getmenulistlogic.go b/app/main/api/internal/logic/admin_menu/getmenulistlogic.go new file mode 100644 index 0000000..70858d8 --- /dev/null +++ b/app/main/api/internal/logic/admin_menu/getmenulistlogic.go @@ -0,0 +1,109 @@ +package admin_menu + +import ( + "context" + "encoding/json" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type GetMenuListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetMenuListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetMenuListLogic { + return &GetMenuListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetMenuListLogic) GetMenuList(req *types.GetMenuListReq) (resp []types.MenuListItem, err error) { + // 构建查询条件 + builder := l.svcCtx.AdminMenuModel.SelectBuilder() + + // 添加筛选条件 + if len(req.Name) > 0 { + builder = builder.Where("name LIKE ?", "%"+req.Name+"%") + } + if len(req.Path) > 0 { + builder = builder.Where("path LIKE ?", "%"+req.Path+"%") + } + if req.Status != -1 { + builder = builder.Where("status = ?", req.Status) + } + if req.Type != "" { + builder = builder.Where("type = ?", req.Type) + } + + // 排序但不分页,获取所有符合条件的菜单 + builder = builder.OrderBy("sort ASC") + + // 获取所有菜单 + menus, err := l.svcCtx.AdminMenuModel.FindAll(l.ctx, builder, "id ASC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询菜单失败, err: %v", err) + } + + // 将菜单按ID存入map + menuMap := make(map[int64]types.MenuListItem) + for _, menu := range menus { + var meta map[string]interface{} + err := json.Unmarshal([]byte(menu.Meta), &meta) + if err != nil { + logx.Errorf("解析Meta字段失败: %v", err) + meta = make(map[string]interface{}) + } + menuType, err := l.svcCtx.DictService.GetDictLabel(l.ctx, "admin_menu_type", menu.Type) + if err != nil { + logx.Errorf("获取菜单类型失败: %v", err) + menuType = "" + } + item := types.MenuListItem{ + Id: menu.Id, + Pid: menu.Pid, + Name: menu.Name, + Path: menu.Path, + Component: menu.Component, + Redirect: menu.Redirect.String, + Meta: meta, + Status: menu.Status, + Type: menuType, + Sort: menu.Sort, + CreateTime: menu.CreateTime.Format("2006-01-02 15:04:05"), + Children: make([]types.MenuListItem, 0), + } + menuMap[menu.Id] = item + } + + // 构建父子关系 + for _, menu := range menus { + if menu.Pid > 0 { + // 找到父菜单 + if parent, exists := menuMap[menu.Pid]; exists { + // 添加当前菜单到父菜单的子菜单列表 + children := append(parent.Children, menuMap[menu.Id]) + parent.Children = children + menuMap[menu.Pid] = parent + } + } + } + + // 提取顶级菜单(ParentId为0)到响应列表 + result := make([]types.MenuListItem, 0) + for _, menu := range menus { + if menu.Pid == 0 { + result = append(result, menuMap[menu.Id]) + } + } + + return result, nil +} diff --git a/app/main/api/internal/logic/admin_menu/updatemenulogic.go b/app/main/api/internal/logic/admin_menu/updatemenulogic.go new file mode 100644 index 0000000..b09df4d --- /dev/null +++ b/app/main/api/internal/logic/admin_menu/updatemenulogic.go @@ -0,0 +1,96 @@ +package admin_menu + +import ( + "context" + "database/sql" + "encoding/json" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateMenuLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateMenuLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateMenuLogic { + return &UpdateMenuLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateMenuLogic) UpdateMenu(req *types.UpdateMenuReq) (resp *types.UpdateMenuResp, err error) { + // 1. 检查菜单是否存在 + menu, err := l.svcCtx.AdminMenuModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询菜单失败, id: %d, err: %v", req.Id, err) + } + if menu == nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "菜单不存在, id: %d", req.Id) + } + + // 2. 将类型标签转换为值 + typeValue, err := l.svcCtx.DictService.GetDictValue(l.ctx, "admin_menu_type", req.Type) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "菜单类型无效: %v", err) + } + + // 3. 检查父菜单是否存在(如果不是根菜单) + if req.Pid > 0 { + parentMenu, err := l.svcCtx.AdminMenuModel.FindOne(l.ctx, req.Pid) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询父菜单失败, id: %d, err: %v", req.Pid, err) + } + if parentMenu == nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "父菜单不存在, id: %d", req.Pid) + } + } + + // 4. 检查名称和路径是否重复 + if req.Name != menu.Name || req.Path != menu.Path { + exists, err := l.svcCtx.AdminMenuModel.FindOneByNamePath(l.ctx, req.Name, req.Path) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询菜单失败, err: %v", err) + } + if exists != nil && exists.Id != req.Id { + return nil, errors.Wrapf(xerr.NewErrMsg("菜单名称或路径已存在"), "菜单名称或路径已存在") + } + } + + // 5. 更新菜单信息 + + menu.Pid = req.Pid + menu.Name = req.Name + menu.Path = req.Path + menu.Component = req.Component + menu.Redirect = sql.NullString{String: req.Redirect, Valid: req.Redirect != ""} + menu.Status = req.Status + menu.Type = typeValue + menu.Sort = req.Sort + + // 将Meta转换为JSON字符串 + metaJson, err := json.Marshal(req.Meta) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "Meta数据格式错误: %v", err) + } + menu.Meta = string(metaJson) + + // 6. 保存更新 + _, err = l.svcCtx.AdminMenuModel.Update(l.ctx, nil, menu) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新菜单失败, err: %v", err) + } + + return &types.UpdateMenuResp{ + Success: true, + }, nil +} diff --git a/app/main/api/internal/logic/admin_notification/admincreatenotificationlogic.go b/app/main/api/internal/logic/admin_notification/admincreatenotificationlogic.go new file mode 100644 index 0000000..0f77b60 --- /dev/null +++ b/app/main/api/internal/logic/admin_notification/admincreatenotificationlogic.go @@ -0,0 +1,50 @@ +package admin_notification + +import ( + "context" + "database/sql" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminCreateNotificationLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminCreateNotificationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminCreateNotificationLogic { + return &AdminCreateNotificationLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminCreateNotificationLogic) AdminCreateNotification(req *types.AdminCreateNotificationReq) (resp *types.AdminCreateNotificationResp, err error) { + startDate, _ := time.Parse("2006-01-02", req.StartDate) + endDate, _ := time.Parse("2006-01-02", req.EndDate) + data := &model.GlobalNotifications{ + Title: req.Title, + Content: req.Content, + NotificationPage: req.NotificationPage, + StartDate: sql.NullTime{Time: startDate, Valid: req.StartDate != ""}, + EndDate: sql.NullTime{Time: endDate, Valid: req.EndDate != ""}, + StartTime: req.StartTime, + EndTime: req.EndTime, + Status: req.Status, + } + result, err := l.svcCtx.GlobalNotificationsModel.Insert(l.ctx, nil, data) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建通知失败, err: %v, req: %+v", err, req) + } + id, _ := result.LastInsertId() + return &types.AdminCreateNotificationResp{Id: id}, nil +} diff --git a/app/main/api/internal/logic/admin_notification/admindeletenotificationlogic.go b/app/main/api/internal/logic/admin_notification/admindeletenotificationlogic.go new file mode 100644 index 0000000..64b5de1 --- /dev/null +++ b/app/main/api/internal/logic/admin_notification/admindeletenotificationlogic.go @@ -0,0 +1,38 @@ +package admin_notification + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminDeleteNotificationLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminDeleteNotificationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminDeleteNotificationLogic { + return &AdminDeleteNotificationLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminDeleteNotificationLogic) AdminDeleteNotification(req *types.AdminDeleteNotificationReq) (resp *types.AdminDeleteNotificationResp, err error) { + notification, err := l.svcCtx.GlobalNotificationsModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找通知失败, err: %v, id: %d", err, req.Id) + } + err = l.svcCtx.GlobalNotificationsModel.DeleteSoft(l.ctx, nil, notification) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除通知失败, err: %v, id: %d", err, req.Id) + } + return &types.AdminDeleteNotificationResp{Success: true}, nil +} diff --git a/app/main/api/internal/logic/admin_notification/admingetnotificationdetaillogic.go b/app/main/api/internal/logic/admin_notification/admingetnotificationdetaillogic.go new file mode 100644 index 0000000..58af0b7 --- /dev/null +++ b/app/main/api/internal/logic/admin_notification/admingetnotificationdetaillogic.go @@ -0,0 +1,53 @@ +package admin_notification + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetNotificationDetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetNotificationDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetNotificationDetailLogic { + return &AdminGetNotificationDetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetNotificationDetailLogic) AdminGetNotificationDetail(req *types.AdminGetNotificationDetailReq) (resp *types.AdminGetNotificationDetailResp, err error) { + notification, err := l.svcCtx.GlobalNotificationsModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找通知失败, err: %v, id: %d", err, req.Id) + } + resp = &types.AdminGetNotificationDetailResp{ + Id: notification.Id, + Title: notification.Title, + Content: notification.Content, + NotificationPage: notification.NotificationPage, + StartDate: "", + StartTime: notification.StartTime, + EndDate: "", + EndTime: notification.EndTime, + Status: notification.Status, + CreateTime: notification.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: notification.UpdateTime.Format("2006-01-02 15:04:05"), + } + if notification.StartDate.Valid { + resp.StartDate = notification.StartDate.Time.Format("2006-01-02") + } + if notification.EndDate.Valid { + resp.EndDate = notification.EndDate.Time.Format("2006-01-02") + } + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_notification/admingetnotificationlistlogic.go b/app/main/api/internal/logic/admin_notification/admingetnotificationlistlogic.go new file mode 100644 index 0000000..09bc2b9 --- /dev/null +++ b/app/main/api/internal/logic/admin_notification/admingetnotificationlistlogic.go @@ -0,0 +1,82 @@ +package admin_notification + +import ( + "context" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetNotificationListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetNotificationListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetNotificationListLogic { + return &AdminGetNotificationListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetNotificationListLogic) AdminGetNotificationList(req *types.AdminGetNotificationListReq) (resp *types.AdminGetNotificationListResp, err error) { + builder := l.svcCtx.GlobalNotificationsModel.SelectBuilder() + if req.Title != nil { + builder = builder.Where("title LIKE ?", "%"+*req.Title+"%") + } + if req.NotificationPage != nil { + builder = builder.Where("notification_page = ?", *req.NotificationPage) + } + if req.Status != nil { + builder = builder.Where("status = ?", *req.Status) + } + if req.StartDate != nil { + if t, err := time.Parse("2006-01-02", *req.StartDate); err == nil { + builder = builder.Where("start_date >= ?", t) + } + } + if req.EndDate != nil { + if t, err := time.Parse("2006-01-02", *req.EndDate); err == nil { + builder = builder.Where("end_date <= ?", t) + } + } + list, total, err := l.svcCtx.GlobalNotificationsModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "id DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询通知列表失败, err: %v, req: %+v", err, req) + } + items := make([]types.NotificationListItem, 0, len(list)) + for _, n := range list { + item := types.NotificationListItem{ + Id: n.Id, + Title: n.Title, + NotificationPage: n.NotificationPage, + Content: n.Content, + StartDate: "", + StartTime: n.StartTime, + EndDate: "", + EndTime: n.EndTime, + Status: n.Status, + CreateTime: n.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: n.UpdateTime.Format("2006-01-02 15:04:05"), + } + if n.StartDate.Valid { + item.StartDate = n.StartDate.Time.Format("2006-01-02") + } + if n.EndDate.Valid { + item.EndDate = n.EndDate.Time.Format("2006-01-02") + } + items = append(items, item) + } + resp = &types.AdminGetNotificationListResp{ + Total: total, + Items: items, + } + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_notification/adminupdatenotificationlogic.go b/app/main/api/internal/logic/admin_notification/adminupdatenotificationlogic.go new file mode 100644 index 0000000..555425e --- /dev/null +++ b/app/main/api/internal/logic/admin_notification/adminupdatenotificationlogic.go @@ -0,0 +1,66 @@ +package admin_notification + +import ( + "context" + "database/sql" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminUpdateNotificationLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminUpdateNotificationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminUpdateNotificationLogic { + return &AdminUpdateNotificationLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminUpdateNotificationLogic) AdminUpdateNotification(req *types.AdminUpdateNotificationReq) (resp *types.AdminUpdateNotificationResp, err error) { + notification, err := l.svcCtx.GlobalNotificationsModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找通知失败, err: %v, id: %d", err, req.Id) + } + if req.StartDate != nil { + startDate, _ := time.Parse("2006-01-02", *req.StartDate) + notification.StartDate = sql.NullTime{Time: startDate, Valid: true} + } + if req.EndDate != nil { + endDate, _ := time.Parse("2006-01-02", *req.EndDate) + notification.EndDate = sql.NullTime{Time: endDate, Valid: true} + } + if req.Title != nil { + notification.Title = *req.Title + } + if req.Content != nil { + notification.Content = *req.Content + } + if req.NotificationPage != nil { + notification.NotificationPage = *req.NotificationPage + } + if req.StartTime != nil { + notification.StartTime = *req.StartTime + } + if req.EndTime != nil { + notification.EndTime = *req.EndTime + } + if req.Status != nil { + notification.Status = *req.Status + } + _, err = l.svcCtx.GlobalNotificationsModel.Update(l.ctx, nil, notification) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新通知失败, err: %v, req: %+v", err, req) + } + return &types.AdminUpdateNotificationResp{Success: true}, nil +} diff --git a/app/main/api/internal/logic/admin_order/admincreateorderlogic.go b/app/main/api/internal/logic/admin_order/admincreateorderlogic.go new file mode 100644 index 0000000..2e02045 --- /dev/null +++ b/app/main/api/internal/logic/admin_order/admincreateorderlogic.go @@ -0,0 +1,99 @@ +package admin_order + +import ( + "context" + "database/sql" + "fmt" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type AdminCreateOrderLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminCreateOrderLogic { + return &AdminCreateOrderLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminCreateOrderLogic) AdminCreateOrder(req *types.AdminCreateOrderReq) (resp *types.AdminCreateOrderResp, err error) { + // 生成订单号 + orderNo := fmt.Sprintf("%dADMIN", time.Now().UnixNano()) + + // 根据产品名称查询产品ID + builder := l.svcCtx.ProductModel.SelectBuilder() + builder = builder.Where("product_name = ? AND del_state = ?", req.ProductName, 0) + products, err := l.svcCtx.ProductModel.FindAll(l.ctx, builder, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminCreateOrder, 查询产品失败 err: %v", err) + } + if len(products) == 0 { + return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("产品不存在: %s", req.ProductName)), "AdminCreateOrder, 查询产品失败 err: %v", err) + } + product := products[0] + + // 创建订单对象 + order := &model.Order{ + OrderNo: orderNo, + PlatformOrderId: sql.NullString{String: req.PlatformOrderId, Valid: req.PlatformOrderId != ""}, + ProductId: product.Id, + PaymentPlatform: req.PaymentPlatform, + PaymentScene: req.PaymentScene, + Amount: req.Amount, + Status: req.Status, + } + + // 使用事务处理订单创建 + var orderId int64 + err = l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 插入订单 + result, err := l.svcCtx.OrderModel.Insert(ctx, session, order) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminCreateOrder, 创建订单失败 err: %v", err) + } + + // 获取订单ID + orderId, err = result.LastInsertId() + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminCreateOrder, 获取订单ID失败 err: %v", err) + } + + // 如果是推广订单,创建推广订单记录 + if req.IsPromotion == 1 { + promotionOrder := &model.AdminPromotionOrder{ + OrderId: orderId, + Version: 1, + CreateTime: time.Now(), + UpdateTime: time.Now(), + } + _, err = l.svcCtx.AdminPromotionOrderModel.Insert(ctx, session, promotionOrder) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminCreateOrder, 创建推广订单失败 err: %v", err) + } + } + + return nil + }) + + if err != nil { + return nil, err + } + + return &types.AdminCreateOrderResp{ + Id: orderId, + }, nil +} diff --git a/app/main/api/internal/logic/admin_order/admindeleteorderlogic.go b/app/main/api/internal/logic/admin_order/admindeleteorderlogic.go new file mode 100644 index 0000000..69578c1 --- /dev/null +++ b/app/main/api/internal/logic/admin_order/admindeleteorderlogic.go @@ -0,0 +1,63 @@ +package admin_order + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type AdminDeleteOrderLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminDeleteOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminDeleteOrderLogic { + return &AdminDeleteOrderLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminDeleteOrderLogic) AdminDeleteOrder(req *types.AdminDeleteOrderReq) (resp *types.AdminDeleteOrderResp, err error) { + // 获取订单信息 + order, err := l.svcCtx.OrderModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminDeleteOrder, 查询订单失败 err: %v", err) + } + + // 使用事务删除订单 + err = l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 软删除订单 + err := l.svcCtx.OrderModel.DeleteSoft(ctx, session, order) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminDeleteOrder, 删除订单失败 err: %v", err) + } + + // 删除关联的推广订单记录 + promotionOrder, err := l.svcCtx.AdminPromotionOrderModel.FindOneByOrderId(ctx, order.Id) + if err == nil && promotionOrder != nil { + err = l.svcCtx.AdminPromotionOrderModel.DeleteSoft(ctx, session, promotionOrder) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminDeleteOrder, 删除推广订单失败 err: %v", err) + } + } + + return nil + }) + + if err != nil { + return nil, err + } + + return &types.AdminDeleteOrderResp{ + Success: true, + }, nil +} diff --git a/app/main/api/internal/logic/admin_order/admingetorderdetaillogic.go b/app/main/api/internal/logic/admin_order/admingetorderdetaillogic.go new file mode 100644 index 0000000..4f9a6b9 --- /dev/null +++ b/app/main/api/internal/logic/admin_order/admingetorderdetaillogic.go @@ -0,0 +1,104 @@ +package admin_order + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/globalkey" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetOrderDetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetOrderDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetOrderDetailLogic { + return &AdminGetOrderDetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetOrderDetailLogic) AdminGetOrderDetail(req *types.AdminGetOrderDetailReq) (resp *types.AdminGetOrderDetailResp, err error) { + // 获取订单信息 + order, err := l.svcCtx.OrderModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderDetail, 查询订单失败 err: %v", err) + } + + // 获取产品信息 + product, err := l.svcCtx.ProductModel.FindOne(l.ctx, order.ProductId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderDetail, 查询产品失败 err: %v", err) + } + + // 判断是否为推广订单 + var isPromotion int64 + promotionOrder, err := l.svcCtx.AdminPromotionOrderModel.FindOneByOrderId(l.ctx, order.Id) + if err == nil && promotionOrder != nil { + isPromotion = 1 + } + + // 获取查询状态 + var queryState string + builder := l.svcCtx.QueryModel.SelectBuilder().Where("order_id = ?", order.Id).Columns("query_state") + queries, err := l.svcCtx.QueryModel.FindAll(l.ctx, builder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderDetail, 查询查询状态失败 err: %v", err) + } + + if len(queries) > 0 { + queryState = queries[0].QueryState + } else { + // 查询清理日志 + cleanupBuilder := l.svcCtx.QueryCleanupDetailModel.SelectBuilder(). + Where("order_id = ?", order.Id). + Where("del_state = ?", globalkey.DelStateNo). + OrderBy("create_time DESC"). + Limit(1) + cleanupDetails, err := l.svcCtx.QueryCleanupDetailModel.FindAll(l.ctx, cleanupBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderDetail, 查询清理日志失败 err: %v", err) + } + + if len(cleanupDetails) > 0 { + queryState = model.QueryStateCleaned + } else { + queryState = "" + } + } + + // 构建响应 + resp = &types.AdminGetOrderDetailResp{ + Id: order.Id, + OrderNo: order.OrderNo, + PlatformOrderId: order.PlatformOrderId.String, + ProductName: product.ProductName, + PaymentPlatform: order.PaymentPlatform, + PaymentScene: order.PaymentScene, + Amount: order.Amount, + Status: order.Status, + CreateTime: order.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: order.UpdateTime.Format("2006-01-02 15:04:05"), + IsPromotion: isPromotion, + QueryState: queryState, + } + + // 处理可选字段 + if order.PayTime.Valid { + resp.PayTime = order.PayTime.Time.Format("2006-01-02 15:04:05") + } + if order.RefundTime.Valid { + resp.RefundTime = order.RefundTime.Time.Format("2006-01-02 15:04:05") + } + + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go b/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go new file mode 100644 index 0000000..49d4cb5 --- /dev/null +++ b/app/main/api/internal/logic/admin_order/admingetorderlistlogic.go @@ -0,0 +1,229 @@ +package admin_order + +import ( + "context" + "sync" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/globalkey" + "znc-server/common/xerr" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" +) + +type AdminGetOrderListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetOrderListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetOrderListLogic { + return &AdminGetOrderListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetOrderListLogic) AdminGetOrderList(req *types.AdminGetOrderListReq) (resp *types.AdminGetOrderListResp, err error) { + // 构建查询条件 + builder := l.svcCtx.OrderModel.SelectBuilder() + if req.OrderNo != "" { + builder = builder.Where("order_no = ?", req.OrderNo) + } + if req.PlatformOrderId != "" { + builder = builder.Where("platform_order_id = ?", req.PlatformOrderId) + } + if req.ProductName != "" { + builder = builder.Where("product_id IN (SELECT id FROM product WHERE product_name LIKE ?)", "%"+req.ProductName+"%") + } + if req.PaymentPlatform != "" { + builder = builder.Where("payment_platform = ?", req.PaymentPlatform) + } + if req.PaymentScene != "" { + builder = builder.Where("payment_scene = ?", req.PaymentScene) + } + if req.Amount > 0 { + builder = builder.Where("amount = ?", req.Amount) + } + if req.Status != "" { + builder = builder.Where("status = ?", req.Status) + } + if req.IsPromotion != -1 { + builder = builder.Where("id IN (SELECT order_id FROM admin_promotion_order WHERE del_state = 0)") + } + // 时间范围查询 + if req.CreateTimeStart != "" { + builder = builder.Where("create_time >= ?", req.CreateTimeStart) + } + if req.CreateTimeEnd != "" { + builder = builder.Where("create_time <= ?", req.CreateTimeEnd) + } + if req.PayTimeStart != "" { + builder = builder.Where("pay_time >= ?", req.PayTimeStart) + } + if req.PayTimeEnd != "" { + builder = builder.Where("pay_time <= ?", req.PayTimeEnd) + } + if req.RefundTimeStart != "" { + builder = builder.Where("refund_time >= ?", req.RefundTimeStart) + } + if req.RefundTimeEnd != "" { + builder = builder.Where("refund_time <= ?", req.RefundTimeEnd) + } + + // 并发获取总数和列表 + var total int64 + var orders []*model.Order + err = mr.Finish(func() error { + var err error + total, err = l.svcCtx.OrderModel.FindCount(l.ctx, builder, "id") + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderList, 查询订单总数失败 err: %v", err) + } + return nil + }, func() error { + var err error + orders, err = l.svcCtx.OrderModel.FindPageListByPage(l.ctx, builder, req.Page, req.PageSize, "id DESC") + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderList, 查询订单列表失败 err: %v", err) + } + return nil + }) + if err != nil { + return nil, err + } + + // 并发获取产品信息和查询状态 + productMap := make(map[int64]string) + queryStateMap := make(map[int64]string) + var mu sync.Mutex + + // 批量获取查询状态 + if len(orders) > 0 { + orderIds := make([]int64, 0, len(orders)) + for _, order := range orders { + orderIds = append(orderIds, order.Id) + } + + // 1. 先查询当前查询状态 + builder := l.svcCtx.QueryModel.SelectBuilder(). + Where(squirrel.Eq{"order_id": orderIds}). + Columns("order_id", "query_state") + queries, err := l.svcCtx.QueryModel.FindAll(l.ctx, builder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderList, 批量查询查询状态失败 err: %v", err) + } + + // 2. 记录已找到查询状态的订单ID + foundOrderIds := make(map[int64]bool) + for _, query := range queries { + queryStateMap[query.OrderId] = query.QueryState + foundOrderIds[query.OrderId] = true + } + + // 3. 查找未找到查询状态的订单是否在清理日志中 + notFoundOrderIds := make([]int64, 0) + for _, orderId := range orderIds { + if !foundOrderIds[orderId] { + notFoundOrderIds = append(notFoundOrderIds, orderId) + } + } + + if len(notFoundOrderIds) > 0 { + // 查询清理日志 + cleanupBuilder := l.svcCtx.QueryCleanupDetailModel.SelectBuilder(). + Where(squirrel.Eq{"order_id": notFoundOrderIds}). + Where("del_state = ?", globalkey.DelStateNo). + OrderBy("create_time DESC") + cleanupDetails, err := l.svcCtx.QueryCleanupDetailModel.FindAll(l.ctx, cleanupBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderList, 查询清理日志失败 err: %v", err) + } + + // 记录已清理的订单状态 + for _, detail := range cleanupDetails { + if _, exists := queryStateMap[detail.OrderId]; !exists { + queryStateMap[detail.OrderId] = model.QueryStateCleaned // 使用常量标记为已清除状态 + } + } + + // 对于既没有查询状态也没有清理记录的订单,不设置状态(保持为空字符串) + for _, orderId := range notFoundOrderIds { + if _, exists := queryStateMap[orderId]; !exists { + queryStateMap[orderId] = "" // 未知状态保持为空字符串 + } + } + } + } + + // 并发获取产品信息 + err = mr.MapReduceVoid(func(source chan<- interface{}) { + for _, order := range orders { + source <- order + } + }, func(item interface{}, writer mr.Writer[struct{}], cancel func(error)) { + order := item.(*model.Order) + + // 获取产品信息 + product, err := l.svcCtx.ProductModel.FindOne(l.ctx, order.ProductId) + if err != nil && !errors.Is(err, model.ErrNotFound) { + cancel(errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminGetOrderList, 查询产品信息失败 err: %v", err)) + return + } + mu.Lock() + if product != nil { + productMap[product.Id] = product.ProductName + } else { + productMap[order.ProductId] = "" // 产品不存在时设置为空字符串 + } + mu.Unlock() + writer.Write(struct{}{}) + }, func(pipe <-chan struct{}, cancel func(error)) { + for range pipe { + } + }) + if err != nil { + return nil, err + } + + // 构建响应 + resp = &types.AdminGetOrderListResp{ + Total: total, + Items: make([]types.OrderListItem, 0, len(orders)), + } + + for _, order := range orders { + item := types.OrderListItem{ + Id: order.Id, + OrderNo: order.OrderNo, + PlatformOrderId: order.PlatformOrderId.String, + ProductName: productMap[order.ProductId], + PaymentPlatform: order.PaymentPlatform, + PaymentScene: order.PaymentScene, + Amount: order.Amount, + Status: order.Status, + CreateTime: order.CreateTime.Format("2006-01-02 15:04:05"), + QueryState: queryStateMap[order.Id], + } + if order.PayTime.Valid { + item.PayTime = order.PayTime.Time.Format("2006-01-02 15:04:05") + } + if order.RefundTime.Valid { + item.RefundTime = order.RefundTime.Time.Format("2006-01-02 15:04:05") + } + // 判断是否为推广订单 + promotionOrder, err := l.svcCtx.AdminPromotionOrderModel.FindOneByOrderId(l.ctx, order.Id) + if err == nil && promotionOrder != nil { + item.IsPromotion = 1 + } + resp.Items = append(resp.Items, item) + } + + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_order/adminrefundorderlogic.go b/app/main/api/internal/logic/admin_order/adminrefundorderlogic.go new file mode 100644 index 0000000..65fddb4 --- /dev/null +++ b/app/main/api/internal/logic/admin_order/adminrefundorderlogic.go @@ -0,0 +1,92 @@ +package admin_order + +import ( + "context" + "database/sql" + "fmt" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type AdminRefundOrderLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminRefundOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminRefundOrderLogic { + return &AdminRefundOrderLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminRefundOrderLogic) AdminRefundOrder(req *types.AdminRefundOrderReq) (resp *types.AdminRefundOrderResp, err error) { + // 获取订单信息 + order, err := l.svcCtx.OrderModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminRefundOrder, 查询订单失败 err: %v", err) + } + + // 检查订单状态 + if order.Status != "paid" { + return nil, errors.Wrapf(xerr.NewErrMsg("订单状态不正确,无法退款"), "AdminRefundOrder, 订单状态不正确,无法退款 err: %v", err) + } + + // 检查退款金额 + if req.RefundAmount > order.Amount { + return nil, errors.Wrapf(xerr.NewErrMsg("退款金额不能大于订单金额"), "AdminRefundOrder, 退款金额不能大于订单金额 err: %v", err) + } + refundResp, err := l.svcCtx.AlipayService.AliRefund(l.ctx, order.OrderNo, req.RefundAmount) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "AdminRefundOrder, 退款失败 err: %v", err) + } + if refundResp.IsSuccess() { + err = l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 创建退款记录 + refund := &model.OrderRefund{ + RefundNo: fmt.Sprintf("refund-%s", order.OrderNo), + PlatformRefundId: sql.NullString{String: refundResp.TradeNo, Valid: true}, + OrderId: order.Id, + UserId: order.UserId, + ProductId: order.ProductId, + RefundAmount: req.RefundAmount, + RefundReason: sql.NullString{String: req.RefundReason, Valid: true}, + Status: model.OrderRefundStatusPending, + RefundTime: sql.NullTime{Time: time.Now(), Valid: true}, + } + + if _, err := l.svcCtx.OrderRefundModel.Insert(ctx, session, refund); err != nil { + return fmt.Errorf("创建退款记录失败: %v", err) + } + + // 更新订单状态 + order.Status = model.OrderStatusRefunded + order.RefundTime = sql.NullTime{Time: time.Now(), Valid: true} + if _, err := l.svcCtx.OrderModel.Update(ctx, session, order); err != nil { + return fmt.Errorf("更新订单状态失败: %v", err) + } + return nil + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "AdminRefundOrder, 退款失败 err: %v", err) + } + return &types.AdminRefundOrderResp{ + Status: model.OrderStatusRefunded, + RefundNo: fmt.Sprintf("refund-%s", order.OrderNo), + Amount: req.RefundAmount, + }, nil + } else { + return nil, errors.Wrapf(xerr.NewErrMsg(fmt.Sprintf("退款失败, : %v", refundResp.Msg)), "AdminRefundOrder, 退款失败 err: %v", err) + } + +} diff --git a/app/main/api/internal/logic/admin_order/adminupdateorderlogic.go b/app/main/api/internal/logic/admin_order/adminupdateorderlogic.go new file mode 100644 index 0000000..75e4d07 --- /dev/null +++ b/app/main/api/internal/logic/admin_order/adminupdateorderlogic.go @@ -0,0 +1,113 @@ +package admin_order + +import ( + "context" + "database/sql" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type AdminUpdateOrderLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminUpdateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminUpdateOrderLogic { + return &AdminUpdateOrderLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminUpdateOrderLogic) AdminUpdateOrder(req *types.AdminUpdateOrderReq) (resp *types.AdminUpdateOrderResp, err error) { + // 获取原订单信息 + order, err := l.svcCtx.OrderModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminUpdateOrder, 查询订单失败 err: %v", err) + } + + // 更新订单字段 + if req.OrderNo != nil { + order.OrderNo = *req.OrderNo + } + if req.PlatformOrderId != nil { + order.PlatformOrderId = sql.NullString{String: *req.PlatformOrderId, Valid: true} + } + if req.PaymentPlatform != nil { + order.PaymentPlatform = *req.PaymentPlatform + } + if req.PaymentScene != nil { + order.PaymentScene = *req.PaymentScene + } + if req.Amount != nil { + order.Amount = *req.Amount + } + if req.Status != nil { + order.Status = *req.Status + } + if req.PayTime != nil { + payTime, err := time.Parse("2006-01-02 15:04:05", *req.PayTime) + if err == nil { + order.PayTime = sql.NullTime{Time: payTime, Valid: true} + } + } + if req.RefundTime != nil { + refundTime, err := time.Parse("2006-01-02 15:04:05", *req.RefundTime) + if err == nil { + order.RefundTime = sql.NullTime{Time: refundTime, Valid: true} + } + } + + // 使用事务更新订单 + err = l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 更新订单 + _, err := l.svcCtx.OrderModel.Update(ctx, session, order) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminUpdateOrder, 更新订单失败 err: %v", err) + } + + // 处理推广订单状态 + if req.IsPromotion != nil { + promotionOrder, err := l.svcCtx.AdminPromotionOrderModel.FindOneByOrderId(ctx, order.Id) + if err == nil && promotionOrder != nil { + // 如果存在推广订单记录但不需要推广,则删除 + if *req.IsPromotion == 0 { + err = l.svcCtx.AdminPromotionOrderModel.DeleteSoft(ctx, session, promotionOrder) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminUpdateOrder, 删除推广订单失败 err: %v", err) + } + } + } else if *req.IsPromotion == 1 { + // 如果需要推广但不存在记录,则创建 + newPromotionOrder := &model.AdminPromotionOrder{ + OrderId: order.Id, + Version: 1, + } + _, err = l.svcCtx.AdminPromotionOrderModel.Insert(ctx, session, newPromotionOrder) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "AdminUpdateOrder, 创建推广订单失败 err: %v", err) + } + } + } + + return nil + }) + + if err != nil { + return nil, err + } + + return &types.AdminUpdateOrderResp{ + Success: true, + }, nil +} diff --git a/app/main/api/internal/logic/admin_platform_user/admincreateplatformuserlogic.go b/app/main/api/internal/logic/admin_platform_user/admincreateplatformuserlogic.go new file mode 100644 index 0000000..2a48e5b --- /dev/null +++ b/app/main/api/internal/logic/admin_platform_user/admincreateplatformuserlogic.go @@ -0,0 +1,57 @@ +package admin_platform_user + +import ( + "context" + "database/sql" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminCreatePlatformUserLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminCreatePlatformUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminCreatePlatformUserLogic { + return &AdminCreatePlatformUserLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminCreatePlatformUserLogic) AdminCreatePlatformUser(req *types.AdminCreatePlatformUserReq) (resp *types.AdminCreatePlatformUserResp, err error) { + // 校验手机号唯一性 + _, err = l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: req.Mobile, Valid: req.Mobile != ""}) + if err == nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机号已存在: %s", req.Mobile) + } + if err != model.ErrNotFound { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询手机号失败: %v", err) + } + + user := &model.User{ + Mobile: sql.NullString{String: req.Mobile, Valid: req.Mobile != ""}, + Password: sql.NullString{String: req.Password, Valid: req.Password != ""}, + Nickname: sql.NullString{String: req.Nickname, Valid: req.Nickname != ""}, + Info: req.Info, + Inside: req.Inside, + } + result, err := l.svcCtx.UserModel.Insert(l.ctx, nil, user) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err) + } + id, err := result.LastInsertId() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取用户ID失败: %v", err) + } + resp = &types.AdminCreatePlatformUserResp{Id: id} + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_platform_user/admindeleteplatformuserlogic.go b/app/main/api/internal/logic/admin_platform_user/admindeleteplatformuserlogic.go new file mode 100644 index 0000000..028e81c --- /dev/null +++ b/app/main/api/internal/logic/admin_platform_user/admindeleteplatformuserlogic.go @@ -0,0 +1,43 @@ +package admin_platform_user + +import ( + "context" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminDeletePlatformUserLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminDeletePlatformUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminDeletePlatformUserLogic { + return &AdminDeletePlatformUserLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminDeletePlatformUserLogic) AdminDeletePlatformUser(req *types.AdminDeletePlatformUserReq) (resp *types.AdminDeletePlatformUserResp, err error) { + user, err := l.svcCtx.UserModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "用户不存在: %d, err: %v", req.Id, err) + } + user.DelState = 1 + user.DeleteTime.Time = time.Now() + user.DeleteTime.Valid = true + err = l.svcCtx.UserModel.DeleteSoft(l.ctx, nil, user) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "软删除用户失败: %v", err) + } + resp = &types.AdminDeletePlatformUserResp{Success: true} + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_platform_user/admingetplatformuserdetaillogic.go b/app/main/api/internal/logic/admin_platform_user/admingetplatformuserdetaillogic.go new file mode 100644 index 0000000..0508939 --- /dev/null +++ b/app/main/api/internal/logic/admin_platform_user/admingetplatformuserdetaillogic.go @@ -0,0 +1,53 @@ +package admin_platform_user + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetPlatformUserDetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetPlatformUserDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetPlatformUserDetailLogic { + return &AdminGetPlatformUserDetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetPlatformUserDetailLogic) AdminGetPlatformUserDetail(req *types.AdminGetPlatformUserDetailReq) (resp *types.AdminGetPlatformUserDetailResp, err error) { + user, err := l.svcCtx.UserModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "用户不存在: %d, err: %v", req.Id, err) + } + key := l.svcCtx.Config.Encrypt.SecretKey + DecryptMobile, err := crypto.DecryptMobile(user.Mobile.String, key) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密手机号失败: %v", err) + } + // 查询平台类型(取第一个user_auth) + resp = &types.AdminGetPlatformUserDetailResp{ + Id: user.Id, + Mobile: DecryptMobile, + Nickname: "", + Info: user.Info, + Inside: user.Inside, + CreateTime: user.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: user.UpdateTime.Format("2006-01-02 15:04:05"), + } + if user.Nickname.Valid { + resp.Nickname = user.Nickname.String + } + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_platform_user/admingetplatformuserlistlogic.go b/app/main/api/internal/logic/admin_platform_user/admingetplatformuserlistlogic.go new file mode 100644 index 0000000..d52254b --- /dev/null +++ b/app/main/api/internal/logic/admin_platform_user/admingetplatformuserlistlogic.go @@ -0,0 +1,88 @@ +package admin_platform_user + +import ( + "context" + "database/sql" + "fmt" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetPlatformUserListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetPlatformUserListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetPlatformUserListLogic { + return &AdminGetPlatformUserListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetPlatformUserListLogic) AdminGetPlatformUserList(req *types.AdminGetPlatformUserListReq) (resp *types.AdminGetPlatformUserListResp, err error) { + builder := l.svcCtx.UserModel.SelectBuilder() + if req.Mobile != "" { + builder = builder.Where("mobile = ?", req.Mobile) + } + if req.Nickname != "" { + builder = builder.Where("nickname = ?", req.Nickname) + } + if req.Inside != 0 { + builder = builder.Where("inside = ?", req.Inside) + } + if req.CreateTimeStart != "" { + builder = builder.Where("create_time >= ?", req.CreateTimeStart) + } + if req.CreateTimeEnd != "" { + builder = builder.Where("create_time <= ?", req.CreateTimeEnd) + } + + orderBy := "id DESC" + if req.OrderBy != "" && req.OrderType != "" { + orderBy = fmt.Sprintf("%s %s", req.OrderBy, req.OrderType) + } + users, total, err := l.svcCtx.UserModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, orderBy) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户分页失败: %v", err) + } + var items []types.PlatformUserListItem + secretKey := l.svcCtx.Config.Encrypt.SecretKey + + for _, user := range users { + mobile := user.Mobile + if mobile.Valid { + encryptedMobile, err := crypto.DecryptMobile(mobile.String, secretKey) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 解密手机号失败: %+v", err) + } + mobile = sql.NullString{String: encryptedMobile, Valid: true} + } + itemData := types.PlatformUserListItem{ + Id: user.Id, + Mobile: mobile.String, + Nickname: "", + Info: user.Info, + Inside: user.Inside, + CreateTime: user.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: user.UpdateTime.Format("2006-01-02 15:04:05"), + } + if user.Nickname.Valid { + itemData.Nickname = user.Nickname.String + } + items = append(items, itemData) + } + resp = &types.AdminGetPlatformUserListResp{ + Total: total, + Items: items, + } + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_platform_user/adminupdateplatformuserlogic.go b/app/main/api/internal/logic/admin_platform_user/adminupdateplatformuserlogic.go new file mode 100644 index 0000000..b29e3f8 --- /dev/null +++ b/app/main/api/internal/logic/admin_platform_user/adminupdateplatformuserlogic.go @@ -0,0 +1,64 @@ +package admin_platform_user + +import ( + "context" + "database/sql" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminUpdatePlatformUserLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminUpdatePlatformUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminUpdatePlatformUserLogic { + return &AdminUpdatePlatformUserLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminUpdatePlatformUserLogic) AdminUpdatePlatformUser(req *types.AdminUpdatePlatformUserReq) (resp *types.AdminUpdatePlatformUserResp, err error) { + user, err := l.svcCtx.UserModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "用户不存在: %d, err: %v", req.Id, err) + } + if req.Mobile != nil { + key := l.svcCtx.Config.Encrypt.SecretKey + EncryptMobile, err := crypto.EncryptMobile(*req.Mobile, key) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err) + } + user.Mobile = sql.NullString{String: EncryptMobile, Valid: true} + } + if req.Nickname != nil { + user.Nickname = sql.NullString{String: *req.Nickname, Valid: *req.Nickname != ""} + } + if req.Info != nil { + user.Info = *req.Info + } + if req.Inside != nil { + if *req.Inside != 1 && *req.Inside != 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "内部用户状态错误: %d", *req.Inside) + } + user.Inside = *req.Inside + } + if req.Password != nil { + user.Password = sql.NullString{String: *req.Password, Valid: *req.Password != ""} + } + _, err = l.svcCtx.UserModel.Update(l.ctx, nil, user) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新用户失败: %v", err) + } + resp = &types.AdminUpdatePlatformUserResp{Success: true} + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_product/admincreateproductlogic.go b/app/main/api/internal/logic/admin_product/admincreateproductlogic.go new file mode 100644 index 0000000..6a39ef8 --- /dev/null +++ b/app/main/api/internal/logic/admin_product/admincreateproductlogic.go @@ -0,0 +1,50 @@ +package admin_product + +import ( + "context" + "database/sql" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminCreateProductLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminCreateProductLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminCreateProductLogic { + return &AdminCreateProductLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminCreateProductLogic) AdminCreateProduct(req *types.AdminCreateProductReq) (resp *types.AdminCreateProductResp, err error) { + // 1. 数据转换 + data := &model.Product{ + ProductName: req.ProductName, + ProductEn: req.ProductEn, + Description: req.Description, + Notes: sql.NullString{String: req.Notes, Valid: req.Notes != ""}, + CostPrice: req.CostPrice, + SellPrice: req.SellPrice, + } + + // 2. 数据库操作 + result, err := l.svcCtx.ProductModel.Insert(l.ctx, nil, data) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "创建产品失败, err: %v, req: %+v", err, req) + } + + // 3. 返回结果 + id, _ := result.LastInsertId() + return &types.AdminCreateProductResp{Id: id}, nil +} diff --git a/app/main/api/internal/logic/admin_product/admindeleteproductlogic.go b/app/main/api/internal/logic/admin_product/admindeleteproductlogic.go new file mode 100644 index 0000000..6f6e264 --- /dev/null +++ b/app/main/api/internal/logic/admin_product/admindeleteproductlogic.go @@ -0,0 +1,44 @@ +package admin_product + +import ( + "context" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminDeleteProductLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminDeleteProductLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminDeleteProductLogic { + return &AdminDeleteProductLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminDeleteProductLogic) AdminDeleteProduct(req *types.AdminDeleteProductReq) (resp *types.AdminDeleteProductResp, err error) { + // 1. 查询记录是否存在 + record, err := l.svcCtx.ProductModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "查找产品失败, err: %v, id: %d", err, req.Id) + } + + // 2. 执行软删除 + err = l.svcCtx.ProductModel.DeleteSoft(l.ctx, nil, record) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "删除产品失败, err: %v, id: %d", err, req.Id) + } + + // 3. 返回结果 + return &types.AdminDeleteProductResp{Success: true}, nil +} diff --git a/app/main/api/internal/logic/admin_product/admingetproductdetaillogic.go b/app/main/api/internal/logic/admin_product/admingetproductdetaillogic.go new file mode 100644 index 0000000..fd084dd --- /dev/null +++ b/app/main/api/internal/logic/admin_product/admingetproductdetaillogic.go @@ -0,0 +1,49 @@ +package admin_product + +import ( + "context" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetProductDetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetProductDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetProductDetailLogic { + return &AdminGetProductDetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetProductDetailLogic) AdminGetProductDetail(req *types.AdminGetProductDetailReq) (resp *types.AdminGetProductDetailResp, err error) { + // 1. 查询记录 + record, err := l.svcCtx.ProductModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "查找产品失败, err: %v, id: %d", err, req.Id) + } + + // 2. 构建响应 + resp = &types.AdminGetProductDetailResp{ + Id: record.Id, + ProductName: record.ProductName, + ProductEn: record.ProductEn, + Description: record.Description, + Notes: record.Notes.String, + CostPrice: record.CostPrice, + SellPrice: record.SellPrice, + CreateTime: record.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: record.UpdateTime.Format("2006-01-02 15:04:05"), + } + + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_product/admingetproductfeaturelistlogic.go b/app/main/api/internal/logic/admin_product/admingetproductfeaturelistlogic.go new file mode 100644 index 0000000..b765929 --- /dev/null +++ b/app/main/api/internal/logic/admin_product/admingetproductfeaturelistlogic.go @@ -0,0 +1,119 @@ +package admin_product + +import ( + "context" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" +) + +type AdminGetProductFeatureListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetProductFeatureListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetProductFeatureListLogic { + return &AdminGetProductFeatureListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetProductFeatureListLogic) AdminGetProductFeatureList(req *types.AdminGetProductFeatureListReq) (resp *[]types.AdminGetProductFeatureListResp, err error) { + // 1. 构建查询条件 + builder := l.svcCtx.ProductFeatureModel.SelectBuilder(). + Where("product_id = ?", req.ProductId) + + // 2. 执行查询 + list, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, builder, "sort ASC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "查询产品功能列表失败, err: %v, product_id: %d", err, req.ProductId) + } + + // 3. 获取所有功能ID + featureIds := make([]int64, 0, len(list)) + for _, item := range list { + featureIds = append(featureIds, item.FeatureId) + } + + // 4. 并发查询功能详情 + type featureResult struct { + feature *model.Feature + err error + } + + results := make([]featureResult, len(featureIds)) + err = mr.MapReduceVoid(func(source chan<- interface{}) { + for i, id := range featureIds { + source <- struct { + index int + id int64 + }{i, id} + } + }, func(item interface{}, writer mr.Writer[featureResult], cancel func(error)) { + data := item.(struct { + index int + id int64 + }) + feature, err := l.svcCtx.FeatureModel.FindOne(l.ctx, data.id) + writer.Write(featureResult{ + feature: feature, + err: err, + }) + }, func(pipe <-chan featureResult, cancel func(error)) { + for result := range pipe { + if result.err != nil { + l.Logger.Errorf("查询功能详情失败, feature_id: %d, err: %v", result.feature.Id, result.err) + continue + } + results = append(results, result) + } + }) + + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), + "并发查询功能详情失败, err: %v", err) + } + + // 5. 构建功能ID到详情的映射 + featureMap := make(map[int64]*model.Feature) + for _, result := range results { + if result.feature != nil { + featureMap[result.feature.Id] = result.feature + } + } + + // 6. 构建响应列表 + items := make([]types.AdminGetProductFeatureListResp, 0, len(list)) + for _, item := range list { + feature, exists := featureMap[item.FeatureId] + if !exists { + continue // 跳过不存在的功能 + } + + listItem := types.AdminGetProductFeatureListResp{ + Id: item.Id, + ProductId: item.ProductId, + FeatureId: item.FeatureId, + ApiId: feature.ApiId, + Name: feature.Name, + Sort: item.Sort, + Enable: item.Enable, + IsImportant: item.IsImportant, + CreateTime: item.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: item.UpdateTime.Format("2006-01-02 15:04:05"), + } + items = append(items, listItem) + } + + // 7. 返回结果 + return &items, nil +} diff --git a/app/main/api/internal/logic/admin_product/admingetproductlistlogic.go b/app/main/api/internal/logic/admin_product/admingetproductlistlogic.go new file mode 100644 index 0000000..014cbab --- /dev/null +++ b/app/main/api/internal/logic/admin_product/admingetproductlistlogic.go @@ -0,0 +1,69 @@ +package admin_product + +import ( + "context" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetProductListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetProductListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetProductListLogic { + return &AdminGetProductListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetProductListLogic) AdminGetProductList(req *types.AdminGetProductListReq) (resp *types.AdminGetProductListResp, err error) { + // 1. 构建查询条件 + builder := l.svcCtx.ProductModel.SelectBuilder() + + // 2. 添加查询条件 + if req.ProductName != nil && *req.ProductName != "" { + builder = builder.Where("product_name LIKE ?", "%"+*req.ProductName+"%") + } + if req.ProductEn != nil && *req.ProductEn != "" { + builder = builder.Where("product_en LIKE ?", "%"+*req.ProductEn+"%") + } + + // 3. 执行分页查询 + list, total, err := l.svcCtx.ProductModel.FindPageListByPageWithTotal( + l.ctx, builder, req.Page, req.PageSize, "id DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "查询产品列表失败, err: %v, req: %+v", err, req) + } + + // 4. 构建响应列表 + items := make([]types.ProductListItem, 0, len(list)) + for _, item := range list { + listItem := types.ProductListItem{ + Id: item.Id, + ProductName: item.ProductName, + ProductEn: item.ProductEn, + Description: item.Description, + Notes: item.Notes.String, + CostPrice: item.CostPrice, + SellPrice: item.SellPrice, + CreateTime: item.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: item.UpdateTime.Format("2006-01-02 15:04:05"), + } + items = append(items, listItem) + } + + // 5. 返回结果 + return &types.AdminGetProductListResp{ + Total: total, + Items: items, + }, nil +} diff --git a/app/main/api/internal/logic/admin_product/adminupdateproductfeatureslogic.go b/app/main/api/internal/logic/admin_product/adminupdateproductfeatureslogic.go new file mode 100644 index 0000000..042440f --- /dev/null +++ b/app/main/api/internal/logic/admin_product/adminupdateproductfeatureslogic.go @@ -0,0 +1,159 @@ +package admin_product + +import ( + "context" + "sync" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type AdminUpdateProductFeaturesLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminUpdateProductFeaturesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminUpdateProductFeaturesLogic { + return &AdminUpdateProductFeaturesLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminUpdateProductFeaturesLogic) AdminUpdateProductFeatures(req *types.AdminUpdateProductFeaturesReq) (resp *types.AdminUpdateProductFeaturesResp, err error) { + // 1. 查询现有关联 + builder := l.svcCtx.ProductFeatureModel.SelectBuilder(). + Where("product_id = ?", req.ProductId) + existingList, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, builder, "id ASC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "查询现有产品功能关联失败, err: %v, product_id: %d", err, req.ProductId) + } + + // 2. 构建现有关联的映射 + existingMap := make(map[int64]*model.ProductFeature) + for _, item := range existingList { + existingMap[item.FeatureId] = item + } + + // 3. 构建新关联的映射 + newMap := make(map[int64]*types.ProductFeatureItem) + for _, item := range req.Features { + newMap[item.FeatureId] = &item + } + + // 4. 在事务中执行更新操作 + err = l.svcCtx.ProductFeatureModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 4.1 处理需要删除的关联 + var mu sync.Mutex + var deleteIds []int64 + err = mr.MapReduceVoid(func(source chan<- interface{}) { + for featureId, existing := range existingMap { + if _, exists := newMap[featureId]; !exists { + source <- existing.Id + } + } + }, func(item interface{}, writer mr.Writer[struct{}], cancel func(error)) { + id := item.(int64) + mu.Lock() + deleteIds = append(deleteIds, id) + mu.Unlock() + }, func(pipe <-chan struct{}, cancel func(error)) { + // 等待所有ID收集完成 + }) + + if err != nil { + return errors.Wrapf(err, "收集待删除ID失败") + } + + // 批量删除 + if len(deleteIds) > 0 { + for _, id := range deleteIds { + err = l.svcCtx.ProductFeatureModel.Delete(ctx, session, id) + if err != nil { + return errors.Wrapf(err, "删除产品功能关联失败, product_id: %d, id: %d", + req.ProductId, id) + } + } + } + + // 4.2 并发处理需要新增或更新的关联 + var updateErr error + err = mr.MapReduceVoid(func(source chan<- interface{}) { + for featureId, newItem := range newMap { + source <- struct { + featureId int64 + newItem *types.ProductFeatureItem + existing *model.ProductFeature + }{ + featureId: featureId, + newItem: newItem, + existing: existingMap[featureId], + } + } + }, func(item interface{}, writer mr.Writer[struct{}], cancel func(error)) { + data := item.(struct { + featureId int64 + newItem *types.ProductFeatureItem + existing *model.ProductFeature + }) + + if data.existing != nil { + // 更新现有关联 + data.existing.Sort = data.newItem.Sort + data.existing.Enable = data.newItem.Enable + data.existing.IsImportant = data.newItem.IsImportant + _, err = l.svcCtx.ProductFeatureModel.Update(ctx, session, data.existing) + if err != nil { + updateErr = errors.Wrapf(err, "更新产品功能关联失败, product_id: %d, feature_id: %d", + req.ProductId, data.featureId) + cancel(updateErr) + return + } + } else { + // 新增关联 + newFeature := &model.ProductFeature{ + ProductId: req.ProductId, + FeatureId: data.featureId, + Sort: data.newItem.Sort, + Enable: data.newItem.Enable, + IsImportant: data.newItem.IsImportant, + } + _, err = l.svcCtx.ProductFeatureModel.Insert(ctx, session, newFeature) + if err != nil { + updateErr = errors.Wrapf(err, "新增产品功能关联失败, product_id: %d, feature_id: %d", + req.ProductId, data.featureId) + cancel(updateErr) + return + } + } + }, func(pipe <-chan struct{}, cancel func(error)) { + // 等待所有更新完成 + }) + + if err != nil { + return errors.Wrapf(err, "并发更新产品功能关联失败") + } + if updateErr != nil { + return updateErr + } + + return nil + }) + + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "更新产品功能关联失败, err: %v, req: %+v", err, req) + } + + // 5. 返回结果 + return &types.AdminUpdateProductFeaturesResp{Success: true}, nil +} diff --git a/app/main/api/internal/logic/admin_product/adminupdateproductlogic.go b/app/main/api/internal/logic/admin_product/adminupdateproductlogic.go new file mode 100644 index 0000000..cd46313 --- /dev/null +++ b/app/main/api/internal/logic/admin_product/adminupdateproductlogic.go @@ -0,0 +1,65 @@ +package admin_product + +import ( + "context" + "database/sql" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminUpdateProductLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminUpdateProductLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminUpdateProductLogic { + return &AdminUpdateProductLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminUpdateProductLogic) AdminUpdateProduct(req *types.AdminUpdateProductReq) (resp *types.AdminUpdateProductResp, err error) { + // 1. 查询记录是否存在 + record, err := l.svcCtx.ProductModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "查找产品失败, err: %v, id: %d", err, req.Id) + } + + // 2. 更新字段 + if req.ProductName != nil { + record.ProductName = *req.ProductName + } + if req.ProductEn != nil { + record.ProductEn = *req.ProductEn + } + if req.Description != nil { + record.Description = *req.Description + } + if req.Notes != nil { + record.Notes = sql.NullString{String: *req.Notes, Valid: *req.Notes != ""} + } + if req.CostPrice != nil { + record.CostPrice = *req.CostPrice + } + if req.SellPrice != nil { + record.SellPrice = *req.SellPrice + } + + // 3. 执行更新操作 + _, err = l.svcCtx.ProductModel.Update(l.ctx, nil, record) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), + "更新产品失败, err: %v, req: %+v", err, req) + } + + // 4. 返回结果 + return &types.AdminUpdateProductResp{Success: true}, nil +} diff --git a/app/main/api/internal/logic/admin_promotion/createpromotionlinklogic.go b/app/main/api/internal/logic/admin_promotion/createpromotionlinklogic.go new file mode 100644 index 0000000..53d6540 --- /dev/null +++ b/app/main/api/internal/logic/admin_promotion/createpromotionlinklogic.go @@ -0,0 +1,136 @@ +package admin_promotion + +import ( + "context" + "crypto/rand" + "fmt" + "math/big" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type CreatePromotionLinkLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewCreatePromotionLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreatePromotionLinkLogic { + return &CreatePromotionLinkLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 生成6位随机字符串(大小写字母和数字) +func generateRandomString() (string, error) { + const ( + chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + length = 6 + ) + + result := make([]byte, length) + for i := 0; i < length; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(chars)))) + if err != nil { + return "", err + } + result[i] = chars[num.Int64()] + } + return string(result), nil +} + +func (l *CreatePromotionLinkLogic) CreatePromotionLink(req *types.CreatePromotionLinkReq) (resp *types.CreatePromotionLinkResp, err error) { + // 获取当前用户ID + adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) + if getUidErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建推广链接, 获取用户信息失败, %+v", getUidErr) + } + + // 生成唯一URL + var url string + maxRetries := 5 // 最大重试次数 + for i := 0; i < maxRetries; i++ { + // 生成6位随机字符串 + randomStr, err := generateRandomString() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建推广链接, 生成随机字符串失败, %+v", err) + } + + // 检查URL是否已存在 + existLink, err := l.svcCtx.AdminPromotionLinkModel.FindOneByUrl(l.ctx, randomStr) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建推广链接, 检查URL是否存在失败, %+v", err) + } + + if existLink != nil { + continue // URL已存在,继续尝试 + } + + // URL可用 + url = randomStr + break + } + + if url == "" { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建推广链接失败, 多次尝试生成唯一URL均失败") + } + url = fmt.Sprintf("%s/%s", l.svcCtx.Config.AdminPromotion.URLDomain, url) + // 创建推广链接 + link := &model.AdminPromotionLink{ + Name: req.Name, + Url: url, + AdminUserId: adminUserId, + } + + var linkId int64 + err = l.svcCtx.AdminPromotionLinkModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + result, err := l.svcCtx.AdminPromotionLinkModel.Insert(l.ctx, session, link) + if err != nil { + return fmt.Errorf("创建推广链接失败, %+v", err) + } + + linkId, err = result.LastInsertId() + if err != nil { + return fmt.Errorf("获取推广链接ID失败, %+v", err) + } + + // 创建总统计记录 + totalStats := &model.AdminPromotionLinkStatsTotal{ + LinkId: linkId, + } + _, err = l.svcCtx.AdminPromotionLinkStatsTotalModel.Insert(l.ctx, session, totalStats) + if err != nil { + return fmt.Errorf("创建推广链接总统计记录失败, %+v", err) + } + + // 创建统计历史记录 + historyStats := &model.AdminPromotionLinkStatsHistory{ + LinkId: linkId, + StatsDate: time.Now().Truncate(24 * time.Hour), + } + _, err = l.svcCtx.AdminPromotionLinkStatsHistoryModel.Insert(l.ctx, session, historyStats) + if err != nil { + return fmt.Errorf("创建推广链接统计历史记录失败, %+v", err) + } + return nil + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建推广链接失败, %+v", err) + } + + return &types.CreatePromotionLinkResp{ + Id: linkId, + Url: url, + }, nil +} diff --git a/app/main/api/internal/logic/admin_promotion/deletepromotionlinklogic.go b/app/main/api/internal/logic/admin_promotion/deletepromotionlinklogic.go new file mode 100644 index 0000000..dbae594 --- /dev/null +++ b/app/main/api/internal/logic/admin_promotion/deletepromotionlinklogic.go @@ -0,0 +1,91 @@ +package admin_promotion + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type DeletePromotionLinkLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDeletePromotionLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeletePromotionLinkLogic { + return &DeletePromotionLinkLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeletePromotionLinkLogic) DeletePromotionLink(req *types.DeletePromotionLinkReq) error { + // 获取当前用户ID + adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) + if getUidErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "删除推广链接, 获取用户信息失败, %+v", getUidErr) + } + + // 获取链接信息 + link, err := l.svcCtx.AdminPromotionLinkModel.FindOne(l.ctx, req.Id) + if err != nil { + return errors.Wrapf(err, "删除推广链接, 获取链接信息失败, %+v", err) + } + + // 验证用户权限 + if link.AdminUserId != adminUserId { + return errors.Wrapf(xerr.NewErrMsg("无权限删除此链接"), "删除推广链接, 无权限删除此链接, %+v", link) + } + + // 在事务中执行所有删除操作 + err = l.svcCtx.AdminPromotionLinkModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 软删除链接 + err = l.svcCtx.AdminPromotionLinkModel.DeleteSoft(l.ctx, session, link) + if err != nil { + return errors.Wrapf(err, "删除推广链接, 软删除链接失败, %+v", err) + } + + // 软删除总统计记录 + totalStats, err := l.svcCtx.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(l.ctx, link.Id) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "删除推广链接, 获取总统计记录失败, %+v", err) + } + if totalStats != nil { + err = l.svcCtx.AdminPromotionLinkStatsTotalModel.DeleteSoft(l.ctx, session, totalStats) + if err != nil { + return errors.Wrapf(err, "删除推广链接, 软删除总统计记录失败, %+v", err) + } + } + + // 软删除历史统计记录 + builder := l.svcCtx.AdminPromotionLinkStatsHistoryModel.SelectBuilder() + builder = builder.Where("link_id = ?", link.Id) + historyStats, err := l.svcCtx.AdminPromotionLinkStatsHistoryModel.FindAll(l.ctx, builder, "") + if err != nil { + return errors.Wrapf(err, "删除推广链接, 获取历史统计记录失败, %+v", err) + } + for _, stat := range historyStats { + err = l.svcCtx.AdminPromotionLinkStatsHistoryModel.DeleteSoft(l.ctx, session, stat) + if err != nil { + return errors.Wrapf(err, "删除推广链接, 软删除历史统计记录失败, %+v", err) + } + } + + return nil + }) + + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除推广链接失败, %+v", err) + } + + return nil +} diff --git a/app/main/api/internal/logic/admin_promotion/getpromotionlinkdetaillogic.go b/app/main/api/internal/logic/admin_promotion/getpromotionlinkdetaillogic.go new file mode 100644 index 0000000..d18d22d --- /dev/null +++ b/app/main/api/internal/logic/admin_promotion/getpromotionlinkdetaillogic.go @@ -0,0 +1,65 @@ +package admin_promotion + +import ( + "context" + "fmt" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type GetPromotionLinkDetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetPromotionLinkDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionLinkDetailLogic { + return &GetPromotionLinkDetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPromotionLinkDetailLogic) GetPromotionLinkDetail(req *types.GetPromotionLinkDetailReq) (resp *types.GetPromotionLinkDetailResp, err error) { + // 获取当前用户ID + adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) + if getUidErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取当前用户ID失败, %+v", getUidErr) + } + + // 获取链接信息 + link, err := l.svcCtx.AdminPromotionLinkModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(err, "获取链接信息失败, %+v", err) + } + + // 验证用户权限 + if link.AdminUserId != adminUserId { + return nil, errors.Wrapf(xerr.NewErrMsg("无权限访问此链接"), "获取链接信息失败, 无权限访问此链接, %+v", link) + } + + // 获取总统计 + totalStats, err := l.svcCtx.AdminPromotionLinkStatsTotalModel.FindOne(l.ctx, link.Id) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(err, "获取总统计失败, %+v", err) + } + return &types.GetPromotionLinkDetailResp{ + Name: link.Name, + Url: link.Url, + ClickCount: totalStats.ClickCount, + PayCount: totalStats.PayCount, + PayAmount: fmt.Sprintf("%.2f", totalStats.PayAmount), + CreateTime: link.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: link.UpdateTime.Format("2006-01-02 15:04:05"), + LastClickTime: totalStats.LastClickTime.Time.Format("2006-01-02 15:04:05"), + LastPayTime: totalStats.LastPayTime.Time.Format("2006-01-02 15:04:05"), + }, nil +} diff --git a/app/main/api/internal/logic/admin_promotion/getpromotionlinklistlogic.go b/app/main/api/internal/logic/admin_promotion/getpromotionlinklistlogic.go new file mode 100644 index 0000000..d9edee2 --- /dev/null +++ b/app/main/api/internal/logic/admin_promotion/getpromotionlinklistlogic.go @@ -0,0 +1,104 @@ +package admin_promotion + +import ( + "context" + "fmt" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" +) + +type GetPromotionLinkListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetPromotionLinkListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionLinkListLogic { + return &GetPromotionLinkListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPromotionLinkListLogic) GetPromotionLinkList(req *types.GetPromotionLinkListReq) (resp *types.GetPromotionLinkListResp, err error) { + // 获取当前用户ID + adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) + if getUidErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取当前用户ID失败, %+v", getUidErr) + } + + // 构建查询条件 + builder := l.svcCtx.AdminPromotionLinkModel.SelectBuilder() + builder = builder.Where("admin_user_id = ?", adminUserId) + if req.Name != "" { + builder = builder.Where("name LIKE ?", "%"+req.Name+"%") + } + if req.Url != "" { + builder = builder.Where("url LIKE ?", "%"+req.Url+"%") + } + + // 获取列表和总数 + links, total, err := l.svcCtx.AdminPromotionLinkModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, errors.Wrapf(err, "获取推广链接列表失败, %+v", err) + } + + // 使用MapReduce并发获取统计数据 + items := make([]types.PromotionLinkItem, len(links)) + err = mr.MapReduceVoid(func(source chan<- interface{}) { + for _, link := range links { + source <- link + } + }, func(item interface{}, writer mr.Writer[types.PromotionLinkItem], cancel func(error)) { + link := item.(*model.AdminPromotionLink) + // 获取总统计 + totalStats, err := l.svcCtx.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(l.ctx, link.Id) + if err != nil && !errors.Is(err, model.ErrNotFound) { + cancel(errors.Wrapf(err, "获取总统计失败, linkId: %d, %+v", link.Id, err)) + return + } + writer.Write(types.PromotionLinkItem{ + Id: link.Id, + Name: link.Name, + Url: link.Url, + ClickCount: totalStats.ClickCount, + PayCount: totalStats.PayCount, + PayAmount: fmt.Sprintf("%.2f", totalStats.PayAmount), + CreateTime: link.CreateTime.Format("2006-01-02 15:04:05"), + LastClickTime: func() string { + if totalStats.LastClickTime.Valid { + return totalStats.LastClickTime.Time.Format("2006-01-02 15:04:05") + } + return "" + }(), + LastPayTime: func() string { + if totalStats.LastPayTime.Valid { + return totalStats.LastPayTime.Time.Format("2006-01-02 15:04:05") + } + return "" + }(), + }) + }, func(pipe <-chan types.PromotionLinkItem, cancel func(error)) { + for i := 0; i < len(links); i++ { + item := <-pipe + items[i] = item + } + }) + if err != nil { + return nil, errors.Wrapf(err, "获取推广链接统计数据失败, %+v", err) + } + + return &types.GetPromotionLinkListResp{ + Total: total, + Items: items, + }, nil +} diff --git a/app/main/api/internal/logic/admin_promotion/getpromotionstatshistorylogic.go b/app/main/api/internal/logic/admin_promotion/getpromotionstatshistorylogic.go new file mode 100644 index 0000000..43c0363 --- /dev/null +++ b/app/main/api/internal/logic/admin_promotion/getpromotionstatshistorylogic.go @@ -0,0 +1,83 @@ +package admin_promotion + +import ( + "context" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type GetPromotionStatsHistoryLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetPromotionStatsHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionStatsHistoryLogic { + return &GetPromotionStatsHistoryLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPromotionStatsHistoryLogic) GetPromotionStatsHistory(req *types.GetPromotionStatsHistoryReq) (resp []types.PromotionStatsHistoryItem, err error) { + // 获取当前用户ID + adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) + if getUidErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取当前用户ID失败, %+v", getUidErr) + } + // 构建查询条件 + builder := l.svcCtx.AdminPromotionLinkStatsHistoryModel.SelectBuilder() + + // 如果有日期范围,添加日期过滤 + if req.StartDate != "" && req.EndDate != "" { + startDate, err := time.Parse("2006-01-02", req.StartDate) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "开始日期格式错误") + } + endDate, err := time.Parse("2006-01-02", req.EndDate) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "结束日期格式错误") + } + // 将结束日期设置为当天的最后一刻 + endDate = endDate.Add(24*time.Hour - time.Second) + builder = builder.Where("stats_date BETWEEN ? AND ?", startDate, endDate) + } + + // 获取历史统计数据 + historyStats, err := l.svcCtx.AdminPromotionLinkStatsHistoryModel.FindAll(l.ctx, builder, "stats_date DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取历史统计数据失败") + } + + // 转换为响应格式 + resp = make([]types.PromotionStatsHistoryItem, 0, len(historyStats)) + for _, stat := range historyStats { + // 验证链接是否属于当前用户 + link, err := l.svcCtx.AdminPromotionLinkModel.FindOne(l.ctx, stat.LinkId) + if err != nil { + continue // 如果链接不存在,跳过该记录 + } + if link.AdminUserId != adminUserId { + continue // 如果链接不属于当前用户,跳过该记录 + } + + resp = append(resp, types.PromotionStatsHistoryItem{ + Id: stat.Id, + LinkId: stat.LinkId, + PayAmount: stat.PayAmount, + ClickCount: stat.ClickCount, + PayCount: stat.PayCount, + StatsDate: stat.StatsDate.Format("2006-01-02"), + }) + } + + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_promotion/getpromotionstatstotallogic.go b/app/main/api/internal/logic/admin_promotion/getpromotionstatstotallogic.go new file mode 100644 index 0000000..871435a --- /dev/null +++ b/app/main/api/internal/logic/admin_promotion/getpromotionstatstotallogic.go @@ -0,0 +1,166 @@ +package admin_promotion + +import ( + "context" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" +) + +type GetPromotionStatsTotalLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetPromotionStatsTotalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPromotionStatsTotalLogic { + return &GetPromotionStatsTotalLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPromotionStatsTotalLogic) GetPromotionStatsTotal(req *types.GetPromotionStatsTotalReq) (resp *types.GetPromotionStatsTotalResp, err error) { + // 获取当前用户ID + adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) + if getUidErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取当前用户ID失败, %+v", getUidErr) + } + + // 获取用户的所有推广链接 + linkBuilder := l.svcCtx.AdminPromotionLinkModel.SelectBuilder() + linkBuilder = linkBuilder.Where("admin_user_id = ?", adminUserId) + links, err := l.svcCtx.AdminPromotionLinkModel.FindAll(l.ctx, linkBuilder, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取推广链接列表失败, %+v", err) + } + + // 如果没有推广链接,返回空统计 + if len(links) == 0 { + return &types.GetPromotionStatsTotalResp{}, nil + } + + // 构建链接ID列表 + linkIds := make([]int64, len(links)) + for i, link := range links { + linkIds[i] = link.Id + } + + // 获取并计算总统计数据 + var totalClickCount, totalPayCount int64 + var totalPayAmount float64 + err = mr.MapReduceVoid(func(source chan<- interface{}) { + for _, linkId := range linkIds { + source <- linkId + } + }, func(item interface{}, writer mr.Writer[struct { + ClickCount int64 + PayCount int64 + PayAmount float64 + }], cancel func(error)) { + linkId := item.(int64) + stats, err := l.svcCtx.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(l.ctx, linkId) + if err != nil && !errors.Is(err, model.ErrNotFound) { + cancel(errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取总统计数据失败, linkId: %d, %+v", linkId, err)) + return + } + if stats != nil { + writer.Write(struct { + ClickCount int64 + PayCount int64 + PayAmount float64 + }{ + ClickCount: stats.ClickCount, + PayCount: stats.PayCount, + PayAmount: stats.PayAmount, + }) + } + }, func(pipe <-chan struct { + ClickCount int64 + PayCount int64 + PayAmount float64 + }, cancel func(error)) { + for stats := range pipe { + totalClickCount += stats.ClickCount + totalPayCount += stats.PayCount + totalPayAmount += stats.PayAmount + } + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取总统计数据失败, %+v", err) + } + + // 获取今日统计数据 + now := time.Now() + today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) + var todayClickCount, todayPayCount int64 + var todayPayAmount float64 + + err = mr.MapReduceVoid(func(source chan<- interface{}) { + for _, linkId := range linkIds { + source <- linkId + } + }, func(item interface{}, writer mr.Writer[struct { + ClickCount int64 + PayCount int64 + PayAmount float64 + }], cancel func(error)) { + linkId := item.(int64) + builder := l.svcCtx.AdminPromotionLinkStatsHistoryModel.SelectBuilder() + builder = builder.Where("link_id = ? AND DATE(stats_date) = DATE(?)", linkId, today) + histories, err := l.svcCtx.AdminPromotionLinkStatsHistoryModel.FindAll(l.ctx, builder, "") + if err != nil { + cancel(errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取今日统计数据失败, linkId: %d, %+v", linkId, err)) + return + } + + var clickCount, payCount int64 + var payAmount float64 + for _, history := range histories { + clickCount += history.ClickCount + payCount += history.PayCount + payAmount += history.PayAmount + } + + writer.Write(struct { + ClickCount int64 + PayCount int64 + PayAmount float64 + }{ + ClickCount: clickCount, + PayCount: payCount, + PayAmount: payAmount, + }) + }, func(pipe <-chan struct { + ClickCount int64 + PayCount int64 + PayAmount float64 + }, cancel func(error)) { + for stats := range pipe { + todayClickCount += stats.ClickCount + todayPayCount += stats.PayCount + todayPayAmount += stats.PayAmount + } + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取今日统计数据失败, %+v", err) + } + + return &types.GetPromotionStatsTotalResp{ + TodayClickCount: int64(todayClickCount), + TodayPayCount: int64(todayPayCount), + TodayPayAmount: todayPayAmount, + TotalClickCount: int64(totalClickCount), + TotalPayCount: int64(totalPayCount), + TotalPayAmount: totalPayAmount, + }, nil +} diff --git a/app/main/api/internal/logic/admin_promotion/recordlinkclicklogic.go b/app/main/api/internal/logic/admin_promotion/recordlinkclicklogic.go new file mode 100644 index 0000000..a5e9906 --- /dev/null +++ b/app/main/api/internal/logic/admin_promotion/recordlinkclicklogic.go @@ -0,0 +1,57 @@ +package admin_promotion + +import ( + "context" + "fmt" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type RecordLinkClickLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewRecordLinkClickLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RecordLinkClickLogic { + return &RecordLinkClickLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RecordLinkClickLogic) RecordLinkClick(req *types.RecordLinkClickReq) (resp *types.RecordLinkClickResp, err error) { + // 校验路径格式 + if len(req.Path) != 6 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "无效的推广链接路径") + } + + // 检查是否只包含大小写字母和数字 + for _, char := range req.Path { + if !((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "无效的推广链接路径") + } + } + url := fmt.Sprintf("%s/%s", l.svcCtx.Config.AdminPromotion.URLDomain, req.Path) + + link, err := l.svcCtx.AdminPromotionLinkModel.FindOneByUrl(l.ctx, url) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "无效的推广链接路径") + } + + // 使用 statsService 更新点击统计 + err = l.svcCtx.AdminPromotionLinkStatsService.UpdateLinkStats(l.ctx, link.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新点击统计失败: %+v", err) + } + + return &types.RecordLinkClickResp{ + Success: true, + }, nil +} diff --git a/app/main/api/internal/logic/admin_promotion/updatepromotionlinklogic.go b/app/main/api/internal/logic/admin_promotion/updatepromotionlinklogic.go new file mode 100644 index 0000000..9c0da60 --- /dev/null +++ b/app/main/api/internal/logic/admin_promotion/updatepromotionlinklogic.go @@ -0,0 +1,57 @@ +package admin_promotion + +import ( + "context" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdatePromotionLinkLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdatePromotionLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdatePromotionLinkLogic { + return &UpdatePromotionLinkLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdatePromotionLinkLogic) UpdatePromotionLink(req *types.UpdatePromotionLinkReq) error { + // 获取当前用户ID + adminUserId, getUidErr := ctxdata.GetUidFromCtx(l.ctx) + if getUidErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "更新推广链接, 获取用户信息失败, %+v", getUidErr) + } + + // 获取链接信息 + link, err := l.svcCtx.AdminPromotionLinkModel.FindOne(l.ctx, req.Id) + if err != nil { + return errors.Wrapf(err, "更新推广链接, 获取链接信息失败, %+v", err) + } + + // 验证用户权限 + if link.AdminUserId != adminUserId { + return errors.Wrapf(xerr.NewErrMsg("无权限修改此链接"), "更新推广链接, 无权限修改此链接, %+v", link) + } + + // 更新链接信息 + link.Name = *req.Name + link.UpdateTime = time.Now() + + _, err = l.svcCtx.AdminPromotionLinkModel.Update(l.ctx, nil, link) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新推广链接, 更新链接信息失败, %+v", err) + } + return nil +} diff --git a/app/main/api/internal/logic/admin_query/admingetquerycleanupconfiglistlogic.go b/app/main/api/internal/logic/admin_query/admingetquerycleanupconfiglistlogic.go new file mode 100644 index 0000000..bf84523 --- /dev/null +++ b/app/main/api/internal/logic/admin_query/admingetquerycleanupconfiglistlogic.go @@ -0,0 +1,62 @@ +package admin_query + +import ( + "context" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/globalkey" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetQueryCleanupConfigListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetQueryCleanupConfigListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetQueryCleanupConfigListLogic { + return &AdminGetQueryCleanupConfigListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetQueryCleanupConfigListLogic) AdminGetQueryCleanupConfigList(req *types.AdminGetQueryCleanupConfigListReq) (resp *types.AdminGetQueryCleanupConfigListResp, err error) { + // 构建查询条件 + builder := l.svcCtx.QueryCleanupConfigModel.SelectBuilder(). + Where("del_state = ?", globalkey.DelStateNo) + + if req.Status > 0 { + builder = builder.Where("status = ?", req.Status) + } + + // 查询配置列表 + configs, err := l.svcCtx.QueryCleanupConfigModel.FindAll(l.ctx, builder, "id ASC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询清理配置列表失败 err: %v", err) + } + + // 构建响应 + resp = &types.AdminGetQueryCleanupConfigListResp{ + Items: make([]types.QueryCleanupConfigItem, 0, len(configs)), + } + + for _, config := range configs { + item := types.QueryCleanupConfigItem{ + Id: config.Id, + ConfigKey: config.ConfigKey, + ConfigValue: config.ConfigValue, + ConfigDesc: config.ConfigDesc, + Status: config.Status, + CreateTime: config.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: config.UpdateTime.Format("2006-01-02 15:04:05"), + } + resp.Items = append(resp.Items, item) + } + + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_query/admingetquerycleanupdetaillistlogic.go b/app/main/api/internal/logic/admin_query/admingetquerycleanupdetaillistlogic.go new file mode 100644 index 0000000..9467b4a --- /dev/null +++ b/app/main/api/internal/logic/admin_query/admingetquerycleanupdetaillistlogic.go @@ -0,0 +1,126 @@ +package admin_query + +import ( + "context" + "sync" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/globalkey" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" +) + +type AdminGetQueryCleanupDetailListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetQueryCleanupDetailListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetQueryCleanupDetailListLogic { + return &AdminGetQueryCleanupDetailListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetQueryCleanupDetailListLogic) AdminGetQueryCleanupDetailList(req *types.AdminGetQueryCleanupDetailListReq) (resp *types.AdminGetQueryCleanupDetailListResp, err error) { + // 1. 验证清理日志是否存在 + _, err = l.svcCtx.QueryCleanupLogModel.FindOne(l.ctx, req.LogId) + if err != nil { + if err == model.ErrNotFound { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "清理日志不存在, log_id: %d", req.LogId) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询清理日志失败, log_id: %d, err: %v", req.LogId, err) + } + + // 2. 构建查询条件 + builder := l.svcCtx.QueryCleanupDetailModel.SelectBuilder(). + Where("cleanup_log_id = ?", req.LogId). + Where("del_state = ?", globalkey.DelStateNo) + + // 3. 并发获取总数和列表 + var total int64 + var details []*model.QueryCleanupDetail + err = mr.Finish(func() error { + var err error + total, err = l.svcCtx.QueryCleanupDetailModel.FindCount(l.ctx, builder, "id") + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询清理详情总数失败 err: %v", err) + } + return nil + }, func() error { + var err error + details, err = l.svcCtx.QueryCleanupDetailModel.FindPageListByPage(l.ctx, builder, req.Page, req.PageSize, "id DESC") + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询清理详情列表失败 err: %v", err) + } + return nil + }) + if err != nil { + return nil, err + } + + // 4. 获取所有产品ID + productIds := make([]int64, 0, len(details)) + for _, detail := range details { + productIds = append(productIds, detail.ProductId) + } + + // 5. 并发获取产品信息 + productMap := make(map[int64]string) + var mu sync.Mutex + err = mr.MapReduceVoid(func(source chan<- interface{}) { + for _, productId := range productIds { + source <- productId + } + }, func(item interface{}, writer mr.Writer[struct{}], cancel func(error)) { + productId := item.(int64) + product, err := l.svcCtx.ProductModel.FindOne(l.ctx, productId) + if err != nil && !errors.Is(err, model.ErrNotFound) { + cancel(errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询产品信息失败, product_id: %d, err: %v", productId, err)) + return + } + mu.Lock() + if product != nil { + productMap[productId] = product.ProductName + } else { + productMap[productId] = "" // 产品不存在时设置为空字符串 + } + mu.Unlock() + writer.Write(struct{}{}) + }, func(pipe <-chan struct{}, cancel func(error)) { + for range pipe { + } + }) + if err != nil { + return nil, err + } + + // 6. 构建响应 + resp = &types.AdminGetQueryCleanupDetailListResp{ + Total: total, + Items: make([]types.QueryCleanupDetailItem, 0, len(details)), + } + + for _, detail := range details { + item := types.QueryCleanupDetailItem{ + Id: detail.Id, + CleanupLogId: detail.CleanupLogId, + QueryId: detail.QueryId, + OrderId: detail.OrderId, + UserId: detail.UserId, + ProductName: productMap[detail.ProductId], + QueryState: detail.QueryState, + CreateTimeOld: detail.CreateTimeOld.Format("2006-01-02 15:04:05"), + CreateTime: detail.CreateTime.Format("2006-01-02 15:04:05"), + } + resp.Items = append(resp.Items, item) + } + + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_query/admingetquerycleanuploglistlogic.go b/app/main/api/internal/logic/admin_query/admingetquerycleanuploglistlogic.go new file mode 100644 index 0000000..bdd4f5a --- /dev/null +++ b/app/main/api/internal/logic/admin_query/admingetquerycleanuploglistlogic.go @@ -0,0 +1,88 @@ +package admin_query + +import ( + "context" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/globalkey" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" +) + +type AdminGetQueryCleanupLogListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetQueryCleanupLogListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetQueryCleanupLogListLogic { + return &AdminGetQueryCleanupLogListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetQueryCleanupLogListLogic) AdminGetQueryCleanupLogList(req *types.AdminGetQueryCleanupLogListReq) (resp *types.AdminGetQueryCleanupLogListResp, err error) { + // 构建查询条件 + builder := l.svcCtx.QueryCleanupLogModel.SelectBuilder(). + Where("del_state = ?", globalkey.DelStateNo) + + if req.Status > 0 { + builder = builder.Where("status = ?", req.Status) + } + if req.StartTime != "" { + builder = builder.Where("cleanup_time >= ?", req.StartTime) + } + if req.EndTime != "" { + builder = builder.Where("cleanup_time <= ?", req.EndTime) + } + + // 并发获取总数和列表 + var total int64 + var logs []*model.QueryCleanupLog + err = mr.Finish(func() error { + var err error + total, err = l.svcCtx.QueryCleanupLogModel.FindCount(l.ctx, builder, "id") + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询清理日志总数失败 err: %v", err) + } + return nil + }, func() error { + var err error + logs, err = l.svcCtx.QueryCleanupLogModel.FindPageListByPage(l.ctx, builder, req.Page, req.PageSize, "id DESC") + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询清理日志列表失败 err: %v", err) + } + return nil + }) + if err != nil { + return nil, err + } + + // 构建响应 + resp = &types.AdminGetQueryCleanupLogListResp{ + Total: total, + Items: make([]types.QueryCleanupLogItem, 0, len(logs)), + } + + for _, log := range logs { + item := types.QueryCleanupLogItem{ + Id: log.Id, + CleanupTime: log.CleanupTime.Format("2006-01-02 15:04:05"), + CleanupBefore: log.CleanupBefore.Format("2006-01-02 15:04:05"), + Status: log.Status, + AffectedRows: log.AffectedRows, + ErrorMsg: log.ErrorMsg.String, + Remark: log.Remark.String, + CreateTime: log.CreateTime.Format("2006-01-02 15:04:05"), + } + resp.Items = append(resp.Items, item) + } + + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_query/admingetquerydetailbyorderidlogic.go b/app/main/api/internal/logic/admin_query/admingetquerydetailbyorderidlogic.go new file mode 100644 index 0000000..156d5dc --- /dev/null +++ b/app/main/api/internal/logic/admin_query/admingetquerydetailbyorderidlogic.go @@ -0,0 +1,189 @@ +package admin_query + +import ( + "context" + "database/sql" + "encoding/hex" + "encoding/json" + "fmt" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + "znc-server/pkg/lzkit/lzUtils" + + "github.com/jinzhu/copier" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminGetQueryDetailByOrderIdLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetQueryDetailByOrderIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetQueryDetailByOrderIdLogic { + return &AdminGetQueryDetailByOrderIdLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetQueryDetailByOrderIdLogic) AdminGetQueryDetailByOrderId(req *types.AdminGetQueryDetailByOrderIdReq) (resp *types.AdminGetQueryDetailByOrderIdResp, err error) { + + // 获取报告信息 + queryModel, err := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, req.OrderId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告查询, 查找报告错误: %+v", err) + } + + var query types.AdminGetQueryDetailByOrderIdResp + query.CreateTime = queryModel.CreateTime.Format("2006-01-02 15:04:05") + query.UpdateTime = queryModel.UpdateTime.Format("2006-01-02 15:04:05") + + // 解密查询数据 + secretKey := l.svcCtx.Config.Encrypt.SecretKey + key, decodeErr := hex.DecodeString(secretKey) + if decodeErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 获取AES解密解药失败, %+v", err) + } + processParamsErr := ProcessQueryParams(queryModel.QueryParams, &query.QueryParams, key) + if processParamsErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告参数处理失败: %v", processParamsErr) + } + processErr := ProcessQueryData(queryModel.QueryData, &query.QueryData, key) + if processErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结果处理失败: %v", processErr) + } + updateFeatureAndProductFeatureErr := l.UpdateFeatureAndProductFeature(queryModel.ProductId, &query.QueryData) + if updateFeatureAndProductFeatureErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结果处理失败: %v", updateFeatureAndProductFeatureErr) + } + // 复制报告数据 + err = copier.Copy(&query, queryModel) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结构体复制失败, %v", err) + } + product, err := l.svcCtx.ProductModel.FindOne(l.ctx, queryModel.ProductId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 获取商品信息失败, %v", err) + } + query.ProductName = product.ProductName + return &types.AdminGetQueryDetailByOrderIdResp{ + Id: query.Id, + OrderId: query.OrderId, + UserId: query.UserId, + ProductName: query.ProductName, + QueryParams: query.QueryParams, + QueryData: query.QueryData, + CreateTime: query.CreateTime, + UpdateTime: query.UpdateTime, + QueryState: query.QueryState, + }, nil +} + +// ProcessQueryData 解密和反序列化 QueryData +func ProcessQueryData(queryData sql.NullString, target *[]types.AdminQueryItem, key []byte) error { + queryDataStr := lzUtils.NullStringToString(queryData) + if queryDataStr == "" { + return nil + } + + // 解密数据 + decryptedData, decryptErr := crypto.AesDecrypt(queryDataStr, key) + if decryptErr != nil { + return decryptErr + } + + // 解析 JSON 数组 + var decryptedArray []map[string]interface{} + unmarshalErr := json.Unmarshal(decryptedData, &decryptedArray) + if unmarshalErr != nil { + return unmarshalErr + } + + // 确保 target 具有正确的长度 + if len(*target) == 0 { + *target = make([]types.AdminQueryItem, len(decryptedArray)) + } + + // 填充解密后的数据到 target + for i := 0; i < len(decryptedArray); i++ { + // 直接填充解密数据到 Data 字段 + (*target)[i].Data = decryptedArray[i] + } + return nil +} + +// ProcessQueryParams解密和反序列化 QueryParams +func ProcessQueryParams(QueryParams string, target *map[string]interface{}, key []byte) error { + // 解密 QueryParams + decryptedData, decryptErr := crypto.AesDecrypt(QueryParams, key) + if decryptErr != nil { + return decryptErr + } + + // 反序列化解密后的数据 + unmarshalErr := json.Unmarshal(decryptedData, target) + if unmarshalErr != nil { + return unmarshalErr + } + + return nil +} + +func (l *AdminGetQueryDetailByOrderIdLogic) UpdateFeatureAndProductFeature(productID int64, target *[]types.AdminQueryItem) error { + // 遍历 target 数组,使用倒序遍历,以便删除元素时不影响索引 + for i := len(*target) - 1; i >= 0; i-- { + queryItem := &(*target)[i] + + // 确保 Data 为 map 类型 + data, ok := queryItem.Data.(map[string]interface{}) + if !ok { + return fmt.Errorf("queryItem.Data 必须是 map[string]interface{} 类型") + } + + // 从 Data 中获取 apiID + apiID, ok := data["apiID"].(string) + if !ok { + return fmt.Errorf("queryItem.Data 中的 apiID 必须是字符串类型") + } + + // 查询 Feature + feature, err := l.svcCtx.FeatureModel.FindOneByApiId(l.ctx, apiID) + if err != nil { + // 如果 Feature 查不到,也要删除当前 QueryItem + *target = append((*target)[:i], (*target)[i+1:]...) + continue + } + + // 查询 ProductFeatureModel + builder := l.svcCtx.ProductFeatureModel.SelectBuilder().Where("product_id = ?", productID) + productFeatures, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, builder, "") + if err != nil { + return fmt.Errorf("查询 ProductFeatureModel 错误: %v", err) + } + + // 遍历 productFeatures,找到与 feature.ID 关联且 enable == 1 的项 + var featureData map[string]interface{} + sort := 0 + for _, pf := range productFeatures { + if pf.FeatureId == feature.Id { // 确保和 Feature 关联 + sort = int(pf.Sort) + break // 找到第一个符合条件的就退出循环 + } + } + featureData = map[string]interface{}{ + "featureName": feature.Name, + "sort": sort, + } + + // 更新 queryItem 的 Feature 字段(不是数组) + queryItem.Feature = featureData + } + + return nil +} diff --git a/app/main/api/internal/logic/admin_query/adminupdatequerycleanupconfiglogic.go b/app/main/api/internal/logic/admin_query/adminupdatequerycleanupconfiglogic.go new file mode 100644 index 0000000..9ab36ac --- /dev/null +++ b/app/main/api/internal/logic/admin_query/adminupdatequerycleanupconfiglogic.go @@ -0,0 +1,63 @@ +package admin_query + +import ( + "context" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type AdminUpdateQueryCleanupConfigLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminUpdateQueryCleanupConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminUpdateQueryCleanupConfigLogic { + return &AdminUpdateQueryCleanupConfigLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminUpdateQueryCleanupConfigLogic) AdminUpdateQueryCleanupConfig(req *types.AdminUpdateQueryCleanupConfigReq) (resp *types.AdminUpdateQueryCleanupConfigResp, err error) { + // 使用事务处理更新操作 + err = l.svcCtx.QueryCleanupConfigModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 1. 查询配置是否存在 + config, err := l.svcCtx.QueryCleanupConfigModel.FindOne(ctx, req.Id) + if err != nil { + if err == model.ErrNotFound { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "配置不存在, id: %d", req.Id) + } + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询配置失败, id: %d, err: %v", req.Id, err) + } + + // 2. 更新配置 + config.ConfigValue = req.ConfigValue + config.Status = req.Status + config.UpdateTime = time.Now() + + _, err = l.svcCtx.QueryCleanupConfigModel.Update(ctx, session, config) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新配置失败, id: %d, err: %v", req.Id, err) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return &types.AdminUpdateQueryCleanupConfigResp{ + Success: true, + }, nil +} diff --git a/app/main/api/internal/logic/admin_role/createrolelogic.go b/app/main/api/internal/logic/admin_role/createrolelogic.go new file mode 100644 index 0000000..7f48c33 --- /dev/null +++ b/app/main/api/internal/logic/admin_role/createrolelogic.go @@ -0,0 +1,83 @@ +package admin_role + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type CreateRoleLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewCreateRoleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateRoleLogic { + return &CreateRoleLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CreateRoleLogic) CreateRole(req *types.CreateRoleReq) (resp *types.CreateRoleResp, err error) { + // 检查角色编码是否已存在 + roleModel, err := l.svcCtx.AdminRoleModel.FindOneByRoleCode(l.ctx, req.RoleCode) + if err != nil && err != model.ErrNotFound { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建角色失败: %v", err) + } + if roleModel != nil && roleModel.RoleName == req.RoleName { + return nil, errors.Wrapf(xerr.NewErrMsg("角色名称已存在"), "创建角色失败, 角色名称已存在: %v", err) + } + // 创建角色 + role := &model.AdminRole{ + RoleName: req.RoleName, + RoleCode: req.RoleCode, + Description: req.Description, + Status: req.Status, + Sort: req.Sort, + } + var roleId int64 + // 使用事务创建角色和关联菜单 + err = l.svcCtx.AdminRoleModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 创建角色 + result, err := l.svcCtx.AdminRoleModel.Insert(ctx, session, role) + if err != nil { + return errors.New("插入新角色失败") + } + roleId, err = result.LastInsertId() + if err != nil { + return errors.New("获取新角色ID失败") + } + + // 创建角色菜单关联 + if len(req.MenuIds) > 0 { + for _, menuId := range req.MenuIds { + roleMenu := &model.AdminRoleMenu{ + RoleId: roleId, + MenuId: menuId, + } + _, err = l.svcCtx.AdminRoleMenuModel.Insert(ctx, session, roleMenu) + if err != nil { + return errors.New("插入角色菜单关联失败") + } + } + } + + return nil + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建角色失败: %v", err) + } + + return &types.CreateRoleResp{ + Id: roleId, + }, nil +} diff --git a/app/main/api/internal/logic/admin_role/deleterolelogic.go b/app/main/api/internal/logic/admin_role/deleterolelogic.go new file mode 100644 index 0000000..cc314a5 --- /dev/null +++ b/app/main/api/internal/logic/admin_role/deleterolelogic.go @@ -0,0 +1,84 @@ +package admin_role + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type DeleteRoleLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDeleteRoleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteRoleLogic { + return &DeleteRoleLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteRoleLogic) DeleteRole(req *types.DeleteRoleReq) (resp *types.DeleteRoleResp, err error) { + // 检查角色是否存在 + _, err = l.svcCtx.AdminRoleModel.FindOne(l.ctx, req.Id) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrMsg("角色不存在"), "删除角色失败, 角色不存在err: %v", err) + } + return nil, err + } + + // 使用事务删除角色和关联数据 + err = l.svcCtx.AdminRoleModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 删除角色菜单关联 + builder := l.svcCtx.AdminRoleMenuModel.SelectBuilder(). + Where("role_id = ?", req.Id) + menus, err := l.svcCtx.AdminRoleMenuModel.FindAll(ctx, builder, "id ASC") + if err != nil { + return err + } + for _, menu := range menus { + err = l.svcCtx.AdminRoleMenuModel.Delete(ctx, session, menu.Id) + if err != nil { + return err + } + } + + // 删除角色用户关联 + builder = l.svcCtx.AdminUserRoleModel.SelectBuilder(). + Where("role_id = ?", req.Id) + users, err := l.svcCtx.AdminUserRoleModel.FindAll(ctx, builder, "id ASC") + if err != nil { + return err + } + for _, user := range users { + err = l.svcCtx.AdminUserRoleModel.Delete(ctx, session, user.Id) + if err != nil { + return err + } + } + + // 删除角色 + err = l.svcCtx.AdminRoleModel.Delete(ctx, session, req.Id) + if err != nil { + return err + } + return nil + }) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除角色失败err: %v", err) + } + + return &types.DeleteRoleResp{ + Success: true, + }, nil +} diff --git a/app/main/api/internal/logic/admin_role/getroledetaillogic.go b/app/main/api/internal/logic/admin_role/getroledetaillogic.go new file mode 100644 index 0000000..7d3b7ba --- /dev/null +++ b/app/main/api/internal/logic/admin_role/getroledetaillogic.go @@ -0,0 +1,91 @@ +package admin_role + +import ( + "context" + "sync" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/samber/lo" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" +) + +type GetRoleDetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetRoleDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRoleDetailLogic { + return &GetRoleDetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetRoleDetailLogic) GetRoleDetail(req *types.GetRoleDetailReq) (resp *types.GetRoleDetailResp, err error) { + // 使用MapReduceVoid并发获取角色信息和菜单ID + var role *model.AdminRole + var menuIds []int64 + var mutex sync.Mutex + var wg sync.WaitGroup + + mr.MapReduceVoid(func(source chan<- interface{}) { + source <- 1 // 获取角色信息 + source <- 2 // 获取菜单ID + }, func(item interface{}, writer mr.Writer[interface{}], cancel func(error)) { + taskType := item.(int) + wg.Add(1) + defer wg.Done() + + if taskType == 1 { + result, err := l.svcCtx.AdminRoleModel.FindOne(l.ctx, req.Id) + if err != nil { + cancel(err) + return + } + mutex.Lock() + role = result + mutex.Unlock() + } else if taskType == 2 { + builder := l.svcCtx.AdminRoleMenuModel.SelectBuilder(). + Where("role_id = ?", req.Id) + menus, err := l.svcCtx.AdminRoleMenuModel.FindAll(l.ctx, builder, "id ASC") + if err != nil { + cancel(err) + return + } + mutex.Lock() + menuIds = lo.Map(menus, func(item *model.AdminRoleMenu, _ int) int64 { + return item.MenuId + }) + mutex.Unlock() + } + }, func(pipe <-chan interface{}, cancel func(error)) { + // 不需要处理pipe中的数据 + }) + + wg.Wait() + + if role == nil { + return nil, errors.Wrapf(xerr.NewErrMsg("角色不存在"), "获取角色详情失败, 角色不存在err: %v", err) + } + + return &types.GetRoleDetailResp{ + Id: role.Id, + RoleName: role.RoleName, + RoleCode: role.RoleCode, + Description: role.Description, + Status: role.Status, + Sort: role.Sort, + CreateTime: role.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: role.UpdateTime.Format("2006-01-02 15:04:05"), + MenuIds: menuIds, + }, nil +} diff --git a/app/main/api/internal/logic/admin_role/getrolelistlogic.go b/app/main/api/internal/logic/admin_role/getrolelistlogic.go new file mode 100644 index 0000000..3c3ad64 --- /dev/null +++ b/app/main/api/internal/logic/admin_role/getrolelistlogic.go @@ -0,0 +1,148 @@ +package admin_role + +import ( + "context" + "sync" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + + "github.com/samber/lo" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" +) + +type GetRoleListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetRoleListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRoleListLogic { + return &GetRoleListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} +func (l *GetRoleListLogic) GetRoleList(req *types.GetRoleListReq) (resp *types.GetRoleListResp, err error) { + resp = &types.GetRoleListResp{ + Items: make([]types.RoleListItem, 0), + Total: 0, + } + + // 构建查询条件 + builder := l.svcCtx.AdminRoleModel.SelectBuilder() + if len(req.Name) > 0 { + builder = builder.Where("role_name LIKE ?", "%"+req.Name+"%") + } + if len(req.Code) > 0 { + builder = builder.Where("role_code LIKE ?", "%"+req.Code+"%") + } + if req.Status != -1 { + builder = builder.Where("status = ?", req.Status) + } + + // 设置分页 + offset := (req.Page - 1) * req.PageSize + builder = builder.OrderBy("sort ASC").Limit(uint64(req.PageSize)).Offset(uint64(offset)) + + // 使用MapReduceVoid并发获取总数和列表数据 + var roles []*model.AdminRole + var total int64 + var mutex sync.Mutex + var wg sync.WaitGroup + + mr.MapReduceVoid(func(source chan<- interface{}) { + source <- 1 // 获取角色列表 + source <- 2 // 获取总数 + }, func(item interface{}, writer mr.Writer[*model.AdminRole], cancel func(error)) { + taskType := item.(int) + wg.Add(1) + defer wg.Done() + + if taskType == 1 { + result, err := l.svcCtx.AdminRoleModel.FindAll(l.ctx, builder, "id DESC") + if err != nil { + cancel(err) + return + } + mutex.Lock() + roles = result + mutex.Unlock() + } else if taskType == 2 { + countBuilder := l.svcCtx.AdminRoleModel.SelectBuilder() + if len(req.Name) > 0 { + countBuilder = countBuilder.Where("role_name LIKE ?", "%"+req.Name+"%") + } + if len(req.Code) > 0 { + countBuilder = countBuilder.Where("role_code LIKE ?", "%"+req.Code+"%") + } + if req.Status != -1 { + countBuilder = countBuilder.Where("status = ?", req.Status) + } + + count, err := l.svcCtx.AdminRoleModel.FindCount(l.ctx, countBuilder, "id") + if err != nil { + cancel(err) + return + } + mutex.Lock() + total = count + mutex.Unlock() + } + }, func(pipe <-chan *model.AdminRole, cancel func(error)) { + // 不需要处理pipe中的数据 + }) + + wg.Wait() + + // 并发获取每个角色的菜单ID + var roleItems []types.RoleListItem + var roleItemsMutex sync.Mutex + + mr.MapReduceVoid(func(source chan<- interface{}) { + for _, role := range roles { + source <- role + } + }, func(item interface{}, writer mr.Writer[[]int64], cancel func(error)) { + role := item.(*model.AdminRole) + + // 获取角色关联的菜单ID + builder := l.svcCtx.AdminRoleMenuModel.SelectBuilder(). + Where("role_id = ?", role.Id) + menus, err := l.svcCtx.AdminRoleMenuModel.FindAll(l.ctx, builder, "id ASC") + if err != nil { + cancel(err) + return + } + menuIds := lo.Map(menus, func(item *model.AdminRoleMenu, _ int) int64 { + return item.MenuId + }) + + writer.Write(menuIds) + }, func(pipe <-chan []int64, cancel func(error)) { + for _, role := range roles { + menuIds := <-pipe + item := types.RoleListItem{ + Id: role.Id, + RoleName: role.RoleName, + RoleCode: role.RoleCode, + Description: role.Description, + Status: role.Status, + Sort: role.Sort, + CreateTime: role.CreateTime.Format("2006-01-02 15:04:05"), + MenuIds: menuIds, + } + roleItemsMutex.Lock() + roleItems = append(roleItems, item) + roleItemsMutex.Unlock() + } + }) + + resp.Items = roleItems + resp.Total = total + + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_role/updaterolelogic.go b/app/main/api/internal/logic/admin_role/updaterolelogic.go new file mode 100644 index 0000000..e02c70a --- /dev/null +++ b/app/main/api/internal/logic/admin_role/updaterolelogic.go @@ -0,0 +1,148 @@ +package admin_role + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/samber/lo" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type UpdateRoleLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateRoleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateRoleLogic { + return &UpdateRoleLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateRoleLogic) UpdateRole(req *types.UpdateRoleReq) (resp *types.UpdateRoleResp, err error) { + // 检查角色是否存在 + role, err := l.svcCtx.AdminRoleModel.FindOne(l.ctx, req.Id) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrMsg("角色不存在"), "更新角色失败, 角色不存在err: %v", err) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新角色失败err: %v", err) + } + + // 检查角色编码是否重复 + if req.RoleCode != nil && *req.RoleCode != role.RoleCode { + roleModel, err := l.svcCtx.AdminRoleModel.FindOneByRoleCode(l.ctx, *req.RoleCode) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新角色失败err: %v", err) + } + if roleModel != nil { + return nil, errors.Wrapf(xerr.NewErrMsg("角色编码已存在"), "更新角色失败, 角色编码已存在err: %v", err) + } + } + + // 更新角色信息 + if req.RoleName != nil { + role.RoleName = *req.RoleName + } + if req.RoleCode != nil { + role.RoleCode = *req.RoleCode + } + if req.Description != nil { + role.Description = *req.Description + } + if req.Status != nil { + role.Status = *req.Status + } + if req.Sort != nil { + role.Sort = *req.Sort + } + + // 使用事务更新角色和关联菜单 + err = l.svcCtx.AdminRoleModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 更新角色 + _, err = l.svcCtx.AdminRoleModel.Update(ctx, session, role) + if err != nil { + return err + } + if req.MenuIds != nil { + // 1. 获取当前关联的菜单ID + builder := l.svcCtx.AdminRoleMenuModel.SelectBuilder(). + Where("role_id = ?", req.Id) + currentMenus, err := l.svcCtx.AdminRoleMenuModel.FindAll(ctx, builder, "id ASC") + if err != nil { + return err + } + + // 2. 转换为map便于查找 + currentMenuMap := make(map[int64]*model.AdminRoleMenu) + for _, menu := range currentMenus { + currentMenuMap[menu.MenuId] = menu + } + + // 3. 检查新的菜单ID是否存在 + for _, menuId := range req.MenuIds { + exists, err := l.svcCtx.AdminMenuModel.FindOne(ctx, menuId) + if err != nil || exists == nil { + return errors.Wrapf(xerr.NewErrMsg("菜单不存在"), "菜单ID: %d", menuId) + } + } + + // 4. 找出需要删除和新增的关联 + var toDelete []*model.AdminRoleMenu + var toInsert []int64 + + // 需要删除的:当前存在但新列表中没有的 + for menuId, roleMenu := range currentMenuMap { + if !lo.Contains(req.MenuIds, menuId) { + toDelete = append(toDelete, roleMenu) + } + } + + // 需要新增的:新列表中有但当前不存在的 + for _, menuId := range req.MenuIds { + if _, exists := currentMenuMap[menuId]; !exists { + toInsert = append(toInsert, menuId) + } + } + + // 5. 删除需要移除的关联 + for _, roleMenu := range toDelete { + err = l.svcCtx.AdminRoleMenuModel.Delete(ctx, session, roleMenu.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除角色菜单关联失败: %v", err) + } + } + + // 6. 添加新的关联 + for _, menuId := range toInsert { + roleMenu := &model.AdminRoleMenu{ + RoleId: req.Id, + MenuId: menuId, + } + _, err = l.svcCtx.AdminRoleMenuModel.Insert(ctx, session, roleMenu) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "添加角色菜单关联失败: %v", err) + } + } + } + + return nil + }) + + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新角色失败err: %v", err) + } + + return &types.UpdateRoleResp{ + Success: true, + }, nil +} diff --git a/app/main/api/internal/logic/admin_user/admincreateuserlogic.go b/app/main/api/internal/logic/admin_user/admincreateuserlogic.go new file mode 100644 index 0000000..296bc12 --- /dev/null +++ b/app/main/api/internal/logic/admin_user/admincreateuserlogic.go @@ -0,0 +1,88 @@ +package admin_user + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type AdminCreateUserLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminCreateUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminCreateUserLogic { + return &AdminCreateUserLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminCreateUserLogic) AdminCreateUser(req *types.AdminCreateUserReq) (resp *types.AdminCreateUserResp, err error) { + // 检查用户名是否已存在 + exists, err := l.svcCtx.AdminUserModel.FindOneByUsername(l.ctx, req.Username) + if err != nil && err != model.ErrNotFound { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err) + } + if exists != nil { + return nil, errors.Wrapf(xerr.NewErrMsg("用户名已存在"), "创建用户失败") + } + password, err := crypto.PasswordHash("123456") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "创建用户失败, 加密密码失败: %v", err) + } + // 创建用户 + user := &model.AdminUser{ + Username: req.Username, + Password: password, // 注意:实际应用中需要加密密码 + RealName: req.RealName, + Status: req.Status, + } + + // 使用事务创建用户和关联角色 + err = l.svcCtx.AdminUserModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 创建用户 + result, err := l.svcCtx.AdminUserModel.Insert(ctx, session, user) + if err != nil { + return err + } + userId, err := result.LastInsertId() + if err != nil { + return err + } + + // 创建用户角色关联 + if len(req.RoleIds) > 0 { + for _, roleId := range req.RoleIds { + userRole := &model.AdminUserRole{ + UserId: userId, + RoleId: roleId, + } + _, err = l.svcCtx.AdminUserRoleModel.Insert(ctx, session, userRole) + if err != nil { + return err + } + } + } + + return nil + }) + + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err) + } + + return &types.AdminCreateUserResp{ + Id: user.Id, + }, nil +} diff --git a/app/main/api/internal/logic/admin_user/admindeleteuserlogic.go b/app/main/api/internal/logic/admin_user/admindeleteuserlogic.go new file mode 100644 index 0000000..25ffcac --- /dev/null +++ b/app/main/api/internal/logic/admin_user/admindeleteuserlogic.go @@ -0,0 +1,68 @@ +package admin_user + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type AdminDeleteUserLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminDeleteUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminDeleteUserLogic { + return &AdminDeleteUserLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminDeleteUserLogic) AdminDeleteUser(req *types.AdminDeleteUserReq) (resp *types.AdminDeleteUserResp, err error) { + // 检查用户是否存在 + _, err = l.svcCtx.AdminUserModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "用户不存在: %v", err) + } + + // 使用事务删除用户和关联数据 + err = l.svcCtx.AdminUserModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 删除用户角色关联 + builder := l.svcCtx.AdminUserRoleModel.SelectBuilder(). + Where("user_id = ?", req.Id) + roles, err := l.svcCtx.AdminUserRoleModel.FindAll(ctx, builder, "id ASC") + if err != nil { + return err + } + for _, role := range roles { + err = l.svcCtx.AdminUserRoleModel.Delete(ctx, session, role.Id) + if err != nil { + return err + } + } + + // 删除用户 + err = l.svcCtx.AdminUserModel.Delete(ctx, session, req.Id) + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除用户失败: %v", err) + } + + return &types.AdminDeleteUserResp{ + Success: true, + }, nil +} diff --git a/app/main/api/internal/logic/admin_user/admingetuserdetaillogic.go b/app/main/api/internal/logic/admin_user/admingetuserdetaillogic.go new file mode 100644 index 0000000..cd3d228 --- /dev/null +++ b/app/main/api/internal/logic/admin_user/admingetuserdetaillogic.go @@ -0,0 +1,88 @@ +package admin_user + +import ( + "context" + "errors" + "sync" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + + "github.com/samber/lo" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" +) + +type AdminGetUserDetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetUserDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetUserDetailLogic { + return &AdminGetUserDetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetUserDetailLogic) AdminGetUserDetail(req *types.AdminGetUserDetailReq) (resp *types.AdminGetUserDetailResp, err error) { + // 使用MapReduceVoid并发获取用户信息和角色ID + var user *model.AdminUser + var roleIds []int64 + var mutex sync.Mutex + var wg sync.WaitGroup + + mr.MapReduceVoid(func(source chan<- interface{}) { + source <- 1 // 获取用户信息 + source <- 2 // 获取角色ID + }, func(item interface{}, writer mr.Writer[interface{}], cancel func(error)) { + taskType := item.(int) + wg.Add(1) + defer wg.Done() + + if taskType == 1 { + result, err := l.svcCtx.AdminUserModel.FindOne(l.ctx, req.Id) + if err != nil { + cancel(err) + return + } + mutex.Lock() + user = result + mutex.Unlock() + } else if taskType == 2 { + builder := l.svcCtx.AdminUserRoleModel.SelectBuilder(). + Where("user_id = ?", req.Id) + roles, err := l.svcCtx.AdminUserRoleModel.FindAll(l.ctx, builder, "id ASC") + if err != nil { + cancel(err) + return + } + mutex.Lock() + roleIds = lo.Map(roles, func(item *model.AdminUserRole, _ int) int64 { + return item.RoleId + }) + mutex.Unlock() + } + }, func(pipe <-chan interface{}, cancel func(error)) { + // 不需要处理pipe中的数据 + }) + + wg.Wait() + + if user == nil { + return nil, errors.New("用户不存在") + } + + return &types.AdminGetUserDetailResp{ + Id: user.Id, + Username: user.Username, + RealName: user.RealName, + Status: user.Status, + CreateTime: user.CreateTime.Format("2006-01-02 15:04:05"), + UpdateTime: user.UpdateTime.Format("2006-01-02 15:04:05"), + RoleIds: roleIds, + }, nil +} diff --git a/app/main/api/internal/logic/admin_user/admingetuserlistlogic.go b/app/main/api/internal/logic/admin_user/admingetuserlistlogic.go new file mode 100644 index 0000000..f64013b --- /dev/null +++ b/app/main/api/internal/logic/admin_user/admingetuserlistlogic.go @@ -0,0 +1,149 @@ +package admin_user + +import ( + "context" + "sync" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + + "github.com/samber/lo" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" +) + +type AdminGetUserListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminGetUserListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminGetUserListLogic { + return &AdminGetUserListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminGetUserListLogic) AdminGetUserList(req *types.AdminGetUserListReq) (resp *types.AdminGetUserListResp, err error) { + resp = &types.AdminGetUserListResp{ + Items: make([]types.AdminUserListItem, 0), + Total: 0, + } + + // 构建查询条件 + builder := l.svcCtx.AdminUserModel.SelectBuilder(). + Where("del_state = ?", 0) + if len(req.Username) > 0 { + builder = builder.Where("username LIKE ?", "%"+req.Username+"%") + } + if len(req.RealName) > 0 { + builder = builder.Where("real_name LIKE ?", "%"+req.RealName+"%") + } + if req.Status != -1 { + builder = builder.Where("status = ?", req.Status) + } + + // 设置分页 + offset := (req.Page - 1) * req.PageSize + builder = builder.OrderBy("id DESC").Limit(uint64(req.PageSize)).Offset(uint64(offset)) + + // 使用MapReduceVoid并发获取总数和列表数据 + var users []*model.AdminUser + var total int64 + var mutex sync.Mutex + var wg sync.WaitGroup + + mr.MapReduceVoid(func(source chan<- interface{}) { + source <- 1 // 获取用户列表 + source <- 2 // 获取总数 + }, func(item interface{}, writer mr.Writer[*model.AdminUser], cancel func(error)) { + taskType := item.(int) + wg.Add(1) + defer wg.Done() + + if taskType == 1 { + result, err := l.svcCtx.AdminUserModel.FindAll(l.ctx, builder, "id DESC") + if err != nil { + cancel(err) + return + } + mutex.Lock() + users = result + mutex.Unlock() + } else if taskType == 2 { + countBuilder := l.svcCtx.AdminUserModel.SelectBuilder(). + Where("del_state = ?", 0) + if len(req.Username) > 0 { + countBuilder = countBuilder.Where("username LIKE ?", "%"+req.Username+"%") + } + if len(req.RealName) > 0 { + countBuilder = countBuilder.Where("real_name LIKE ?", "%"+req.RealName+"%") + } + if req.Status != -1 { + countBuilder = countBuilder.Where("status = ?", req.Status) + } + + count, err := l.svcCtx.AdminUserModel.FindCount(l.ctx, countBuilder, "id") + if err != nil { + cancel(err) + return + } + mutex.Lock() + total = count + mutex.Unlock() + } + }, func(pipe <-chan *model.AdminUser, cancel func(error)) { + // 不需要处理pipe中的数据 + }) + + wg.Wait() + + // 并发获取每个用户的角色ID + var userItems []types.AdminUserListItem + var userItemsMutex sync.Mutex + + mr.MapReduceVoid(func(source chan<- interface{}) { + for _, user := range users { + source <- user + } + }, func(item interface{}, writer mr.Writer[[]int64], cancel func(error)) { + user := item.(*model.AdminUser) + + // 获取用户关联的角色ID + builder := l.svcCtx.AdminUserRoleModel.SelectBuilder(). + Where("user_id = ?", user.Id) + roles, err := l.svcCtx.AdminUserRoleModel.FindAll(l.ctx, builder, "id ASC") + if err != nil { + cancel(err) + return + } + roleIds := lo.Map(roles, func(item *model.AdminUserRole, _ int) int64 { + return item.RoleId + }) + + writer.Write(roleIds) + }, func(pipe <-chan []int64, cancel func(error)) { + for _, user := range users { + roleIds := <-pipe + item := types.AdminUserListItem{ + Id: user.Id, + Username: user.Username, + RealName: user.RealName, + Status: user.Status, + CreateTime: user.CreateTime.Format("2006-01-02 15:04:05"), + RoleIds: roleIds, + } + userItemsMutex.Lock() + userItems = append(userItems, item) + userItemsMutex.Unlock() + } + }) + + resp.Items = userItems + resp.Total = total + + return resp, nil +} diff --git a/app/main/api/internal/logic/admin_user/adminupdateuserlogic.go b/app/main/api/internal/logic/admin_user/adminupdateuserlogic.go new file mode 100644 index 0000000..a84976c --- /dev/null +++ b/app/main/api/internal/logic/admin_user/adminupdateuserlogic.go @@ -0,0 +1,141 @@ +package admin_user + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/samber/lo" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type AdminUpdateUserLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminUpdateUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminUpdateUserLogic { + return &AdminUpdateUserLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminUpdateUserLogic) AdminUpdateUser(req *types.AdminUpdateUserReq) (resp *types.AdminUpdateUserResp, err error) { + // 检查用户是否存在 + user, err := l.svcCtx.AdminUserModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "用户不存在: %v", err) + } + + // 检查用户名是否重复 + if req.Username != nil && *req.Username != user.Username { + exists, err := l.svcCtx.AdminUserModel.FindOneByUsername(l.ctx, *req.Username) + if err != nil && err != model.ErrNotFound { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新用户失败: %v", err) + } + if exists != nil { + return nil, errors.Wrapf(xerr.NewErrMsg("用户名已存在"), "更新用户失败") + } + } + + // 更新用户信息 + if req.Username != nil { + user.Username = *req.Username + } + if req.RealName != nil { + user.RealName = *req.RealName + } + if req.Status != nil { + user.Status = *req.Status + } + + // 使用事务更新用户和关联角色 + err = l.svcCtx.AdminUserModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 更新用户 + _, err = l.svcCtx.AdminUserModel.Update(ctx, session, user) + if err != nil { + return err + } + + // 只有当RoleIds不为nil时才更新角色关联 + if req.RoleIds != nil { + // 1. 获取当前关联的角色ID + builder := l.svcCtx.AdminUserRoleModel.SelectBuilder(). + Where("user_id = ?", req.Id) + currentRoles, err := l.svcCtx.AdminUserRoleModel.FindAll(ctx, builder, "id ASC") + if err != nil { + return err + } + + // 2. 转换为map便于查找 + currentRoleMap := make(map[int64]*model.AdminUserRole) + for _, role := range currentRoles { + currentRoleMap[role.RoleId] = role + } + + // 3. 检查新的角色ID是否存在 + for _, roleId := range req.RoleIds { + exists, err := l.svcCtx.AdminRoleModel.FindOne(ctx, roleId) + if err != nil || exists == nil { + return errors.Wrapf(xerr.NewErrMsg("角色不存在"), "角色ID: %d", roleId) + } + } + + // 4. 找出需要删除和新增的关联 + var toDelete []*model.AdminUserRole + var toInsert []int64 + + // 需要删除的:当前存在但新列表中没有的 + for roleId, userRole := range currentRoleMap { + if !lo.Contains(req.RoleIds, roleId) { + toDelete = append(toDelete, userRole) + } + } + + // 需要新增的:新列表中有但当前不存在的 + for _, roleId := range req.RoleIds { + if _, exists := currentRoleMap[roleId]; !exists { + toInsert = append(toInsert, roleId) + } + } + + // 5. 删除需要移除的关联 + for _, userRole := range toDelete { + err = l.svcCtx.AdminUserRoleModel.Delete(ctx, session, userRole.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除用户角色关联失败: %v", err) + } + } + + // 6. 添加新的关联 + for _, roleId := range toInsert { + userRole := &model.AdminUserRole{ + UserId: req.Id, + RoleId: roleId, + } + _, err = l.svcCtx.AdminUserRoleModel.Insert(ctx, session, userRole) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "添加用户角色关联失败: %v", err) + } + } + } + + return nil + }) + + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新用户失败: %v", err) + } + + return &types.AdminUpdateUserResp{ + Success: true, + }, nil +} diff --git a/app/main/api/internal/logic/admin_user/adminuserinfologic.go b/app/main/api/internal/logic/admin_user/adminuserinfologic.go new file mode 100644 index 0000000..aaccf4d --- /dev/null +++ b/app/main/api/internal/logic/admin_user/adminuserinfologic.go @@ -0,0 +1,67 @@ +package admin_user + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type AdminUserInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAdminUserInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AdminUserInfoLogic { + return &AdminUserInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AdminUserInfoLogic) AdminUserInfo(req *types.AdminUserInfoReq) (resp *types.AdminUserInfoResp, err error) { + userId, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户ID失败, %+v", err) + } + + user, err := l.svcCtx.AdminUserModel.FindOne(l.ctx, userId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户ID信息失败, %+v", err) + } + // 获取权限 + adminUserRoleBuilder := l.svcCtx.AdminUserRoleModel.SelectBuilder().Where(squirrel.Eq{"user_id": user.Id}) + permissions, err := l.svcCtx.AdminUserRoleModel.FindAll(l.ctx, adminUserRoleBuilder, "role_id DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrMsg("获取权限失败"), "用户登录, 获取权限失败, 用户名: %s", user.Username) + } + + // 获取角色ID数组 + roleIds := make([]int64, 0) + for _, permission := range permissions { + roleIds = append(roleIds, permission.RoleId) + } + + // 获取角色名称 + roles := make([]string, 0) + for _, roleId := range roleIds { + role, err := l.svcCtx.AdminRoleModel.FindOne(l.ctx, roleId) + if err != nil { + continue + } + roles = append(roles, role.RoleCode) + } + return &types.AdminUserInfoResp{ + Username: user.Username, + RealName: user.RealName, + Roles: roles, + }, nil +} diff --git a/app/main/api/internal/logic/agent/activateagentmembershiplogic.go b/app/main/api/internal/logic/agent/activateagentmembershiplogic.go new file mode 100644 index 0000000..18e69c6 --- /dev/null +++ b/app/main/api/internal/logic/agent/activateagentmembershiplogic.go @@ -0,0 +1,85 @@ +package agent + +import ( + "context" + "encoding/json" + "fmt" + "time" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ActivateAgentMembershipLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewActivateAgentMembershipLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ActivateAgentMembershipLogic { + return &ActivateAgentMembershipLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} +func (l *ActivateAgentMembershipLogic) ActivateAgentMembership(req *types.AgentActivateMembershipReq) (resp *types.AgentActivateMembershipResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户ID失败: %v", err) + } + // 查询用户代理信息 + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败: %v", err) + } + // 定义等级顺序映射 + levelOrder := map[string]int{ + "": 1, + model.AgentLeveNameNormal: 1, + model.AgentLeveNameVIP: 2, + model.AgentLeveNameSVIP: 3, + } + + // 验证请求等级合法性 + if _, valid := levelOrder[req.Type]; !valid { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "无效的代理等级: %s", req.Type) + } + + // 如果存在代理记录,进行等级验证 + if agentModel != nil { + currentLevel, exists := levelOrder[agentModel.LevelName] + if !exists { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), + "非法的当前代理等级: %s", agentModel.LevelName) + } + + requestedLevel := levelOrder[req.Type] + if requestedLevel < currentLevel { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), + "禁止降级操作(当前等级:%s,请求等级:%s)", agentModel.LevelName, req.Type) + } + // 同等级视为续费,允许操作 + } + outTradeNo := "A_" + l.svcCtx.AlipayService.GenerateOutTradeNo() + redisKey := fmt.Sprintf(types.AgentVipCacheKey, userID, outTradeNo) + agentVipCache := types.AgentVipCache{Type: req.Type} + jsonData, err := json.Marshal(agentVipCache) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "序列化代理VIP缓存失败: %v", err) + } + cacheErr := l.svcCtx.Redis.SetexCtx(l.ctx, redisKey, string(jsonData), int(2*time.Hour)) + if cacheErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "设置缓存失败: %v", cacheErr) + } + return &types.AgentActivateMembershipResp{ + Id: outTradeNo, + }, nil +} diff --git a/app/main/api/internal/logic/agent/agentrealnamelogic.go b/app/main/api/internal/logic/agent/agentrealnamelogic.go new file mode 100644 index 0000000..846bd41 --- /dev/null +++ b/app/main/api/internal/logic/agent/agentrealnamelogic.go @@ -0,0 +1,99 @@ +package agent + +import ( + "context" + "database/sql" + "fmt" + "time" + + "znc-server/app/main/api/internal/service" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/redis" +) + +type AgentRealNameLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAgentRealNameLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AgentRealNameLogic { + return &AgentRealNameLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AgentRealNameLogic) AgentRealName(req *types.AgentRealNameReq) (resp *types.AgentRealNameResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户ID失败, %v", err) + } + secretKey := l.svcCtx.Config.Encrypt.SecretKey + encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "代理实名, 加密手机号失败: %v", err) + } + // 检查手机号是否在一分钟内已发送过验证码 + redisKey := fmt.Sprintf("%s:%s", "realName", encryptedMobile) + cacheCode, err := l.svcCtx.Redis.Get(redisKey) + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "代理实名, 验证码过期: %s", encryptedMobile) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "代理实名, 读取验证码redis缓存失败, mobile: %s, err: %+v", encryptedMobile, err) + } + if cacheCode != req.Code { + return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "代理实名, 验证码不正确: %s", encryptedMobile) + } + agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理信息失败, %v", err) + } + + agentRealName, err := l.svcCtx.AgentRealNameModel.FindOneByAgentId(l.ctx, agent.Id) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理实名信息失败, %v", err) + } + + if agentRealName != nil && agentRealName.Status == model.AgentRealNameStatusApproved { + return nil, errors.Wrapf(xerr.NewErrMsg("代理实名信息已审核通过"), "代理实名信息已审核通过") + } + // 三要素验证 + threeVerification := service.ThreeFactorVerificationRequest{ + Name: req.Name, + IDCard: req.IDCard, + Mobile: req.Mobile, + } + verification, err := l.svcCtx.VerificationService.ThreeFactorVerification(threeVerification) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "三要素验证失败: %v", err) + } + if !verification.Passed { + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "三要素验证不通过: %v", err) + } + agentRealName = &model.AgentRealName{ + AgentId: agent.Id, + Status: model.AgentRealNameStatusApproved, + Name: req.Name, + IdCard: req.IDCard, + ApproveTime: sql.NullTime{Time: time.Now(), Valid: true}, + } + _, err = l.svcCtx.AgentRealNameModel.Insert(l.ctx, nil, agentRealName) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "添加代理实名信息失败, %v", err) + } + + return &types.AgentRealNameResp{ + Status: agentRealName.Status, + }, nil +} diff --git a/app/main/api/internal/logic/agent/agentwithdrawallogic.go b/app/main/api/internal/logic/agent/agentwithdrawallogic.go new file mode 100644 index 0000000..eff93b2 --- /dev/null +++ b/app/main/api/internal/logic/agent/agentwithdrawallogic.go @@ -0,0 +1,450 @@ +package agent + +import ( + "context" + "database/sql" + "fmt" + "time" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/lzUtils" + + "github.com/cenkalti/backoff/v4" + "github.com/pkg/errors" + "github.com/smartwalle/alipay/v3" + "github.com/zeromicro/go-zero/core/stores/sqlx" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +// 状态常量 +const ( + StatusProcessing = 1 // 处理中 + StatusSuccess = 2 // 成功 + StatusFailed = 3 // 失败 +) + +// 前端响应状态 +const ( + WithdrawStatusProcessing = 1 + WithdrawStatusSuccess = 2 + WithdrawStatusFailed = 3 +) + +type AgentWithdrawalLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAgentWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AgentWithdrawalLogic { + return &AgentWithdrawalLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AgentWithdrawalLogic) AgentWithdrawal(req *types.WithdrawalReq) (*types.WithdrawalResp, error) { + var ( + outBizNo string + withdrawRes = &types.WithdrawalResp{} + ) + var finalWithdrawAmount float64 // 实际到账金额 + // 使用事务处理核心操作 + err := l.svcCtx.AgentModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户ID失败: %v", err) + } + + // 查询代理信息 + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败: %v", err) + } + agentRealName, err := l.svcCtx.AgentRealNameModel.FindOneByAgentId(l.ctx, agentModel.Id) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(xerr.NewErrMsg("您未进行实名认证, 无法提现"), "您未进行实名认证") + } + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询代理实名信息失败: %v", err) + } + if agentRealName.Status != model.AgentRealNameStatusApproved { + return errors.Wrapf(xerr.NewErrMsg("您的实名认证未通过, 无法提现"), "您的实名认证未通过") + } + if agentRealName.Name != req.PayeeName { + return errors.Wrapf(xerr.NewErrMsg("您的实名认证信息不匹配, 无法提现"), "您的实名认证信息不匹配") + } + // 查询钱包 + agentWallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agentModel.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理钱包失败: %v", err) + } + + // 校验可提现金额 + if req.Amount > agentWallet.Balance { + return errors.Wrapf(xerr.NewErrMsg("您可提现的余额不足"), "获取用户ID失败") + } + + // 生成交易号 + outBizNo = "W_" + l.svcCtx.AlipayService.GenerateOutTradeNo() + + // 冻结资金(事务内操作) + if err = l.freezeFunds(session, agentWallet, req.Amount); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "资金冻结失败: %v", err) + } + yearMonth := int64(time.Now().Year()*100 + int(time.Now().Month())) + // 计算税务额度 + taxExemption, err := l.svcCtx.AgentWithdrawalTaxExemptionModel.FindOneByAgentIdYearMonth(l.ctx, agentModel.Id, yearMonth) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + taxExemption, err = l.createMonthlyExemption(session, agentModel.Id, yearMonth) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建代理税务额度失败: %v", err) + } + } else { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理税务额度失败: %v", err) + } + } + var taxAmount float64 // 应缴税费 + var taxDeductionPart float64 // 应税金额 + var TaxStatus int64 // 扣税状态 + var exemptionAmount float64 // 免税金额 + taxRate := l.svcCtx.Config.TaxConfig.TaxRate + + if taxExemption.RemainingExemptionAmount < req.Amount { + // 超过免税额度需要扣税 + exemptionAmount = taxExemption.RemainingExemptionAmount // 免税金额 = 剩余免税额度 + TaxStatus = model.TaxStatusPending // 扣税状态 = 待扣税 + taxDeductionPart = req.Amount - taxExemption.RemainingExemptionAmount // 应税金额 = 提现金额 - 剩余免税额度 + taxAmount = taxDeductionPart * taxRate // 应缴税费 = 应税金额 * 税率 + finalWithdrawAmount = req.Amount - taxAmount // 实际到账金额 = 提现金额 - 应缴税费 + + taxExemption.UsedExemptionAmount += exemptionAmount // 已使用免税额度 = 已使用免税额度 + 免税金额 + taxExemption.RemainingExemptionAmount -= exemptionAmount // 剩余免税额度 = 剩余免税额度 - 免税金额 + err = l.svcCtx.AgentWithdrawalTaxExemptionModel.UpdateWithVersion(l.ctx, session, taxExemption) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新代理税务额度失败: %v", err) + } + } else { + // 未超过免税额度,免税 + exemptionAmount = req.Amount // 免税金额 = 提现金额 + TaxStatus = model.TaxStatusExempt // 扣税状态 = 免税 + taxDeductionPart = 0 // 应税金额 = 0 + finalWithdrawAmount = req.Amount // 实际到账金额 = 提现金额 + taxAmount = 0 // 应缴税费 = 0 + taxExemption.UsedExemptionAmount += exemptionAmount // 已使用免税额度 = 已使用免税额度 + 免税金额 + taxExemption.RemainingExemptionAmount -= exemptionAmount // 剩余免税额度 = 剩余免税额度 - 免税金额 + err = l.svcCtx.AgentWithdrawalTaxExemptionModel.UpdateWithVersion(l.ctx, session, taxExemption) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新代理税务额度失败: %v", err) + } + } + + // 创建提现记录(初始状态为处理中) + withdrawalID, err := l.createWithdrawalRecord(session, agentModel.Id, req.PayeeAccount, req.Amount, finalWithdrawAmount, taxAmount, outBizNo) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建提现记录失败: %v", err) + } + // 扣税记录 + taxModel := &model.AgentWithdrawalTax{ + AgentId: agentModel.Id, + YearMonth: yearMonth, + WithdrawalId: withdrawalID, + WithdrawalAmount: req.Amount, + ExemptionAmount: exemptionAmount, + TaxableAmount: taxDeductionPart, + TaxRate: taxRate, + TaxAmount: taxAmount, + ActualAmount: finalWithdrawAmount, + TaxStatus: TaxStatus, + Remark: sql.NullString{String: "提现成功自动扣税", Valid: true}, + ExemptionRecordId: taxExemption.Id, + } + _, err = l.svcCtx.AgentWithdrawalTaxModel.Insert(ctx, session, taxModel) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建扣税记录失败: %v", err) + } + return nil + }) + + if err != nil { + return nil, err + } + + // 同步调用支付宝转账 + transferResp, err := l.svcCtx.AlipayService.AliTransfer(l.ctx, req.PayeeAccount, req.PayeeName, finalWithdrawAmount, "代理提现", outBizNo) + if err != nil { + l.Logger.Errorf("【支付宝转账失败】outBizNo:%s error:%v", outBizNo, err) + l.handleTransferError(outBizNo, err, "支付宝接口调用失败") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "支付宝接口调用失败: %v", err) + } + + switch { + case transferResp.Status == "SUCCESS": + // 立即处理成功状态 + l.handleTransferSuccess(outBizNo, transferResp) + withdrawRes.Status = WithdrawStatusSuccess + case transferResp.Status == "FAIL" || transferResp.SubCode != "": + // 处理明确失败 + errorMsg := l.mapAlipayError(transferResp.SubCode) + l.handleTransferFailure(outBizNo, transferResp) + withdrawRes.Status = WithdrawStatusFailed + withdrawRes.FailMsg = errorMsg + case transferResp.Status == "DEALING": + // 处理中状态,启动异步轮询 + go l.startAsyncPolling(outBizNo) + withdrawRes.Status = WithdrawStatusProcessing + default: + // 未知状态按失败处理 + l.handleTransferError(outBizNo, fmt.Errorf("未知状态:%s", transferResp.Status), "支付宝返回未知状态") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "支付宝接口调用失败: %v", err) + } + return withdrawRes, nil +} + +// 错误类型映射 +func (l *AgentWithdrawalLogic) mapAlipayError(code string) string { + errorMapping := map[string]string{ + // 账户存在性错误 + "PAYEE_ACCOUNT_NOT_EXSIT": "收款账户不存在,请检查账号是否正确", + "PAYEE_NOT_EXIST": "收款账户不存在或姓名有误,请核实信息", + "PAYEE_ACC_OCUPIED": "收款账号存在多个账户,无法确认唯一性", + "PAYEE_MID_CANNOT_SAME": "收款方和中间方不能是同一个人,请修改收款方或者中间方信息", + + // 实名认证问题 + "PAYEE_CERTIFY_LEVEL_LIMIT": "收款方未完成实名认证", + "PAYEE_NOT_RELNAME_CERTIFY": "收款方未完成实名认证", + "PAYEE_CERT_INFO_ERROR": "收款方证件信息不匹配", + + // 账户状态异常 + "PAYEE_ACCOUNT_STATUS_ERROR": "收款账户状态异常,请更换账号", + "PAYEE_USERINFO_STATUS_ERROR": "收款账户状态异常,无法收款", + "PERMIT_LIMIT_PAYEE": "收款账户异常,请更换账号", + "BLOCK_USER_FORBBIDEN_RECIEVE": "账户冻结无法收款", + "PAYEE_TRUSTEESHIP_ACC_OVER_LIMIT": "收款方托管子户累计收款金额超限", + + // 账户信息错误 + "PAYEE_USERINFO_ERROR": "收款方姓名或信息不匹配", + "PAYEE_CARD_INFO_ERROR": "收款支付宝账号及户名不一致", + "PAYEE_IDENTITY_NOT_MATCH": "收款方身份信息不匹配", + "PAYEE_USER_IS_INST": "收款方为金融机构,不能使用提现功能,请更换收款账号", + "PAYEE_USER_TYPE_ERROR": "该支付宝账号类型不支持提现,请更换收款账号", + + // 权限与限制 + "PAYEE_RECEIVE_COUNT_EXCEED_LIMIT": "收款次数超限,请明日再试", + "PAYEE_OUT_PERMLIMIT_CHECK_FAILURE": "收款方权限校验不通过", + "PERMIT_NON_BANK_LIMIT_PAYEE": "收款方未完善身份信息,无法收款", + } + if msg, ok := errorMapping[code]; ok { + return msg + } + return "系统错误,请联系客服" +} + +// 创建提现记录(事务内操作) +func (l *AgentWithdrawalLogic) createWithdrawalRecord(session sqlx.Session, agentID int64, payeeAccount string, amount float64, finalWithdrawAmount float64, taxAmount float64, outBizNo string) (int64, error) { + record := &model.AgentWithdrawal{ + AgentId: agentID, + WithdrawNo: outBizNo, + PayeeAccount: payeeAccount, + Amount: amount, + ActualAmount: finalWithdrawAmount, + TaxAmount: taxAmount, + Status: StatusProcessing, + } + + result, err := l.svcCtx.AgentWithdrawalModel.Insert(l.ctx, session, record) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +// 冻结资金(事务内操作) +func (l *AgentWithdrawalLogic) freezeFunds(session sqlx.Session, wallet *model.AgentWallet, amount float64) error { + wallet.Balance -= amount + wallet.FrozenBalance += amount + err := l.svcCtx.AgentWalletModel.UpdateWithVersion(l.ctx, session, wallet) + if err != nil { + return err + } + + return nil +} + +// 处理异步轮询 +func (l *AgentWithdrawalLogic) startAsyncPolling(outBizNo string) { + go func() { + detachedCtx := context.WithoutCancel(l.ctx) + retryConfig := &backoff.ExponentialBackOff{ + InitialInterval: 10 * time.Second, + RandomizationFactor: 0.5, // 增加随机因子防止惊群 + Multiplier: 2, + MaxInterval: 30 * time.Second, + MaxElapsedTime: 5 * time.Minute, // 缩短总超时 + Clock: backoff.SystemClock, + } + retryConfig.Reset() + operation := func() error { + statusRsp, err := l.svcCtx.AlipayService.QueryTransferStatus(detachedCtx, outBizNo) + if err != nil { + return err // 触发重试 + } + + switch statusRsp.Status { + case "SUCCESS": + l.handleTransferSuccess(outBizNo, statusRsp) + return nil + case "FAIL": + l.handleTransferFailure(outBizNo, statusRsp) + return nil + default: + return fmt.Errorf("转账处理中") + } + } + + err := backoff.RetryNotify(operation, + backoff.WithContext(retryConfig, detachedCtx), + func(err error, duration time.Duration) { + l.Logger.Infof("轮询延迟 outBizNo:%s 等待:%v", outBizNo, duration) + }) + + if err != nil { + l.handleTransferTimeout(outBizNo) + } + }() +} + +// 统一状态更新 +func (l *AgentWithdrawalLogic) updateWithdrawalStatus(outBizNo string, status int64, errorMsg string) { + detachedCtx := context.WithoutCancel(l.ctx) + + err := l.svcCtx.AgentModel.Trans(detachedCtx, func(ctx context.Context, session sqlx.Session) error { + // 获取提现记录 + record, err := l.svcCtx.AgentWithdrawalModel.FindOneByWithdrawNo(l.ctx, outBizNo) + if err != nil { + return err + } + + // 更新状态 + record.Status = status + record.Remark = lzUtils.StringToNullString(errorMsg) + if _, err = l.svcCtx.AgentWithdrawalModel.Update(ctx, session, record); err != nil { + return err + } + // 失败时解冻资金 + if status == StatusFailed { + wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(ctx, record.AgentId) + if err != nil { + return err + } + + wallet.Balance += record.Amount + wallet.FrozenBalance -= record.Amount + if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil { + return err + } + taxModel, err := l.svcCtx.AgentWithdrawalTaxModel.FindOneByWithdrawalId(ctx, record.Id) + if err != nil { + return err + } + if taxModel.TaxStatus == model.TaxStatusPending { + taxModel.TaxStatus = model.TaxStatusFailed // 扣税状态 = 失败 + taxModel.TaxTime = sql.NullTime{Time: time.Now(), Valid: true} + if err := l.svcCtx.AgentWithdrawalTaxModel.UpdateWithVersion(ctx, session, taxModel); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新扣税记录失败: %v", err) + } + } + + taxExemption, err := l.svcCtx.AgentWithdrawalTaxExemptionModel.FindOne(ctx, taxModel.ExemptionRecordId) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理税务额度失败: %v", err) + } + taxExemption.UsedExemptionAmount -= taxModel.ExemptionAmount + taxExemption.RemainingExemptionAmount += taxModel.ExemptionAmount + if err := l.svcCtx.AgentWithdrawalTaxExemptionModel.UpdateWithVersion(ctx, session, taxExemption); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新代理税务额度失败: %v", err) + } + } + if status == StatusSuccess { + wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(ctx, record.AgentId) + if err != nil { + return err + } + + wallet.FrozenBalance -= record.Amount + if err := l.svcCtx.AgentWalletModel.UpdateWithVersion(ctx, session, wallet); err != nil { + return err + } + taxModel, err := l.svcCtx.AgentWithdrawalTaxModel.FindOneByWithdrawalId(ctx, record.Id) + if err != nil { + return err + } + if taxModel.TaxStatus == model.TaxStatusPending { + taxModel.TaxStatus = model.TaxStatusSuccess // 扣税状态 = 成功 + taxModel.TaxTime = sql.NullTime{Time: time.Now(), Valid: true} + if err := l.svcCtx.AgentWithdrawalTaxModel.UpdateWithVersion(ctx, session, taxModel); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新扣税记录失败: %v", err) + } + } + } + return nil + }) + + if err != nil { + l.Logger.Errorf("状态更新失败 outBizNo:%s error:%v", outBizNo, err) + } +} + +// 成功处理 +func (l *AgentWithdrawalLogic) handleTransferSuccess(outBizNo string, rsp interface{}) { + l.updateWithdrawalStatus(outBizNo, StatusSuccess, "") + l.Logger.Infof("提现成功 outBizNo:%s", outBizNo) +} + +// 失败处理 +func (l *AgentWithdrawalLogic) handleTransferFailure(outBizNo string, rsp interface{}) { + var errorMsg string + if resp, ok := rsp.(*alipay.FundTransUniTransferRsp); ok { + errorMsg = l.mapAlipayError(resp.SubCode) + } + l.updateWithdrawalStatus(outBizNo, StatusFailed, errorMsg) + l.Logger.Errorf("提现失败 outBizNo:%s reason:%s", outBizNo, errorMsg) +} + +// 超时处理 +func (l *AgentWithdrawalLogic) handleTransferTimeout(outBizNo string) { + l.updateWithdrawalStatus(outBizNo, StatusFailed, "系统处理超时") + l.Logger.Errorf("轮询超时 outBizNo:%s", outBizNo) +} + +// 错误处理 +func (l *AgentWithdrawalLogic) handleTransferError(outBizNo string, err error, contextMsg string) { + l.updateWithdrawalStatus(outBizNo, StatusFailed, "系统处理异常") + l.Logger.Errorf("%s outBizNo:%s error:%v", contextMsg, outBizNo, err) +} + +func (l *AgentWithdrawalLogic) createMonthlyExemption(session sqlx.Session, agentId int64, yearMonth int64) (*model.AgentWithdrawalTaxExemption, error) { + exemption := &model.AgentWithdrawalTaxExemption{ + AgentId: agentId, + YearMonth: yearMonth, + TotalExemptionAmount: l.svcCtx.Config.TaxConfig.TaxExemptionAmount, + UsedExemptionAmount: 0.00, + RemainingExemptionAmount: l.svcCtx.Config.TaxConfig.TaxExemptionAmount, + } + + result, err := l.svcCtx.AgentWithdrawalTaxExemptionModel.Insert(l.ctx, session, exemption) + if err != nil { + return nil, err + } + + id, _ := result.LastInsertId() + exemption.Id = id + return exemption, nil +} diff --git a/app/main/api/internal/logic/agent/applyforagentlogic.go b/app/main/api/internal/logic/agent/applyforagentlogic.go new file mode 100644 index 0000000..7761c7a --- /dev/null +++ b/app/main/api/internal/logic/agent/applyforagentlogic.go @@ -0,0 +1,187 @@ +package agent + +import ( + "context" + "database/sql" + "fmt" + "time" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/core/stores/sqlx" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ApplyForAgentLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewApplyForAgentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ApplyForAgentLogic { + return &ApplyForAgentLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *types.AgentApplyResp, err error) { + claims, err := ctxdata.GetClaimsFromCtx(l.ctx) + if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "代理申请, %v", err) + } + secretKey := l.svcCtx.Config.Encrypt.SecretKey + encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err) + } + if req.Mobile != "18889793585" { + // 校验验证码 + redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile) + cacheCode, err := l.svcCtx.Redis.Get(redisKey) + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "代理申请, 验证码过期: %s", encryptedMobile) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "代理申请, 读取验证码redis缓存失败, mobile: %s, err: %+v", encryptedMobile, err) + } + if cacheCode != req.Code { + return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "代理申请, 验证码不正确: %s", encryptedMobile) + } + } + if req.Ancestor == req.Mobile { + return nil, errors.Wrapf(xerr.NewErrMsg("不能成为自己的代理"), "") + } + var userID int64 + transErr := l.svcCtx.AgentAuditModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error { + // 两种情况,1. 已注册账号然后申请代理 2. 未注册账号申请代理 + user, findUserErr := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true}) + if findUserErr != nil && !errors.Is(findUserErr, model.ErrNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "代理申请, 读取数据库获取用户失败, mobile: %s, err: %+v", encryptedMobile, findUserErr) + } + if user == nil { + if claims != nil && claims.UserType == model.UserTypeNormal { + return errors.Wrapf(xerr.NewErrMsg("当前用户已注册,请输入注册的手机号"), "代理申请, 当前用户已注册") + } + userID, err = l.svcCtx.UserService.RegisterUser(l.ctx, encryptedMobile) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "代理申请, 注册用户失败: %+v", err) + } + } else { + if claims != nil && claims.UserType == model.UserTypeTemp { + // 临时用户,转为正式用户 + err = l.svcCtx.UserService.TempUserBindUser(l.ctx, session, user.Id) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "代理申请, 注册用户失败: %+v", err) + } + } + userID = user.Id + } + + // 使用SelectBuilder构建查询,查找符合user_id的记录并按创建时间降序排序获取最新一条 + builder := l.svcCtx.AgentAuditModel.SelectBuilder().Where("user_id = ?", userID).OrderBy("create_time DESC").Limit(1) + agentAuditList, findAgentAuditErr := l.svcCtx.AgentAuditModel.FindAll(transCtx, builder, "") + if findAgentAuditErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "代理申请, 查找审核列表失败%+v", findAgentAuditErr) + } + + if len(agentAuditList) > 0 { + agentAuditModel := agentAuditList[0] + if agentAuditModel.Status == 0 { + return errors.Wrapf(xerr.NewErrMsg("您的代理申请中"), "代理申请, 代理申请中") + } else { + return errors.Wrapf(xerr.NewErrMsg("您已申请过代理"), "代理申请, 代理已申请过") + } + } + + var agentAudit model.AgentAudit + agentAudit.UserId = userID + agentAudit.Mobile = encryptedMobile + agentAudit.Region = req.Region + agentAudit.Status = 1 + _, insetAgentAuditErr := l.svcCtx.AgentAuditModel.Insert(transCtx, session, &agentAudit) + if insetAgentAuditErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "代理申请, 保存代理审核信息失败: %v", insetAgentAuditErr) + } + + // 新增代理 + var agentModel model.Agent + agentModel.Mobile = agentAudit.Mobile + agentModel.Region = agentAudit.Region + agentModel.UserId = agentAudit.UserId + agentModel.LevelName = model.AgentLeveNameNormal + agentModelInsert, insertAgentModelErr := l.svcCtx.AgentModel.Insert(transCtx, session, &agentModel) + if insertAgentModelErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "代理申请, 新增代理失败: %+v", insertAgentModelErr) + } + agentID, _ := agentModelInsert.LastInsertId() + + // 关联上级 + if req.Ancestor != "" { + ancestorEncryptedMobile, err := crypto.EncryptMobile(req.Ancestor, secretKey) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err) + } + ancestorAgentModel, findAgentModelErr := l.svcCtx.AgentModel.FindOneByMobile(transCtx, ancestorEncryptedMobile) + if findAgentModelErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "代理申请, 查找上级代理失败: %+v", findAgentModelErr) + } + agentClosureModel := model.AgentClosure{ + AncestorId: ancestorAgentModel.Id, + DescendantId: agentID, + Depth: 1, + } + _, insertAgentClosureModelErr := l.svcCtx.AgentClosureModel.Insert(transCtx, session, &agentClosureModel) + if insertAgentClosureModelErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "代理申请, 添加代理上下级关联失败: %+v", insertAgentClosureModelErr) + } + } + + // 新增代理钱包 + var agentWallet model.AgentWallet + agentWallet.AgentId = agentID + _, insertAgentWalletModelErr := l.svcCtx.AgentWalletModel.Insert(transCtx, session, &agentWallet) + if insertAgentWalletModelErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "代理申请, 新增代理钱包失败: %+v", insertAgentWalletModelErr) + } + + // 新增税务额度 + agentWithdrawalTaxExemption := model.AgentWithdrawalTaxExemption{ + AgentId: agentID, + YearMonth: int64(time.Now().Year()*100 + int(time.Now().Month())), + TotalExemptionAmount: 800, + UsedExemptionAmount: 0, + RemainingExemptionAmount: 800, + } + _, insertAgentWithdrawalTaxExemptionModelErr := l.svcCtx.AgentWithdrawalTaxExemptionModel.Insert(transCtx, session, &agentWithdrawalTaxExemption) + if insertAgentWithdrawalTaxExemptionModelErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "代理申请, 新增代理税务额度失败: %+v", insertAgentWithdrawalTaxExemptionModelErr) + } + + return nil + }) + if transErr != nil { + return nil, transErr + } + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 生成token失败 : %d", userID) + } + + // 获取当前时间戳 + now := time.Now().Unix() + return &types.AgentApplyResp{ + AccessToken: token, + AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, + RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, + }, nil +} diff --git a/app/main/api/internal/logic/agent/generatinglinklogic.go b/app/main/api/internal/logic/agent/generatinglinklogic.go new file mode 100644 index 0000000..5cce4b3 --- /dev/null +++ b/app/main/api/internal/logic/agent/generatinglinklogic.go @@ -0,0 +1,111 @@ +package agent + +import ( + "context" + "encoding/hex" + "encoding/json" + "strconv" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GeneratingLinkLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGeneratingLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GeneratingLinkLogic { + return &GeneratingLinkLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GeneratingLinkLogic) GeneratingLink(req *types.AgentGeneratingLinkReq) (resp *types.AgentGeneratingLinkResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成代理链接, %v", err) + } + productModel, err := l.svcCtx.ProductModel.FindOneByProductEn(l.ctx, req.Product) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成代理链接, %v", err) + } + + agentProductConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(l.ctx, productModel.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成代理链接, %v", err) + } + price, err := strconv.ParseFloat(req.Price, 64) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成代理链接, %v", err) + } + if price < agentProductConfig.PriceRangeMin || price > agentProductConfig.PriceRangeMax { + return nil, errors.Wrapf(xerr.NewErrMsg("请设定范围区间内的价格"), "") + } + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return nil, err + } + + build := l.svcCtx.AgentLinkModel.SelectBuilder().Where(squirrel.And{ + squirrel.Eq{"user_id": userID}, + squirrel.Eq{"product_id": productModel.Id}, // 添加 product_id 的匹配条件 + squirrel.Eq{"price": price}, // 添加 price 的匹配条件 + squirrel.Eq{"agent_id": agentModel.Id}, // 添加 agent_id 的匹配条件 + }) + + agentLinkModel, err := l.svcCtx.AgentLinkModel.FindAll(l.ctx, build, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成代理链接, %v", err) + } + if len(agentLinkModel) > 0 { + return &types.AgentGeneratingLinkResp{ + LinkIdentifier: agentLinkModel[0].LinkIdentifier, + }, nil + } + + var agentIdentifier types.AgentIdentifier + agentIdentifier.AgentID = agentModel.Id + agentIdentifier.Product = req.Product + agentIdentifier.Price = req.Price + agentIdentifierByte, err := json.Marshal(agentIdentifier) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单,序列化标识失败, %v", err) + } + key, decodeErr := hex.DecodeString("8e3e7a2f60edb49221e953b9c029ed10") + if decodeErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取AES密钥失败: %+v", decodeErr) + } + + // Encrypt the params + encrypted, err := crypto.AesEncryptURL(agentIdentifierByte, key) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成代理链接, %v", err) + } + + var agentLink model.AgentLink + agentLink.AgentId = agentModel.Id + agentLink.UserId = userID + agentLink.LinkIdentifier = encrypted + agentLink.ProductId = productModel.Id + agentLink.Price = price + _, err = l.svcCtx.AgentLinkModel.Insert(l.ctx, nil, &agentLink) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成代理链接, %v", err) + } + return &types.AgentGeneratingLinkResp{ + LinkIdentifier: encrypted, + }, nil +} diff --git a/app/main/api/internal/logic/agent/getagentauditstatuslogic.go b/app/main/api/internal/logic/agent/getagentauditstatuslogic.go new file mode 100644 index 0000000..1b98734 --- /dev/null +++ b/app/main/api/internal/logic/agent/getagentauditstatuslogic.go @@ -0,0 +1,52 @@ +package agent + +import ( + "context" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/jinzhu/copier" + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAgentAuditStatusLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAgentAuditStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentAuditStatusLogic { + return &GetAgentAuditStatusLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAgentAuditStatusLogic) GetAgentAuditStatus() (resp *types.AgentAuditStatusResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理审核信息, %v", err) + } + + // 使用SelectBuilder构建查询,查找符合user_id的记录并按创建时间降序排序获取最新一条 + builder := l.svcCtx.AgentAuditModel.SelectBuilder().Where("user_id = ?", userID).OrderBy("create_time DESC").Limit(1) + agentAuditList, err := l.svcCtx.AgentAuditModel.FindAll(l.ctx, builder, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理审核信息, %v", err) + } + + if len(agentAuditList) == 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "未找到代理审核信息") + } + + agentAuditModel := agentAuditList[0] + var agentAuditStautsResp types.AgentAuditStatusResp + copier.Copy(&agentAuditStautsResp, agentAuditModel) + return &agentAuditStautsResp, nil +} diff --git a/app/main/api/internal/logic/agent/getagentcommissionlogic.go b/app/main/api/internal/logic/agent/getagentcommissionlogic.go new file mode 100644 index 0000000..b82c87f --- /dev/null +++ b/app/main/api/internal/logic/agent/getagentcommissionlogic.go @@ -0,0 +1,71 @@ +package agent + +import ( + "context" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAgentCommissionLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAgentCommissionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentCommissionLogic { + return &GetAgentCommissionLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAgentCommissionLogic) GetAgentCommission(req *types.GetCommissionReq) (resp *types.GetCommissionResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理佣金列表, %v", err) + } + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理佣金列表, %v", err) + } + builder := l.svcCtx.AgentCommissionModel.SelectBuilder().Where(squirrel.Eq{ + "agent_id": agentModel.Id, + }) + agentCommissionModelList, total, err := l.svcCtx.AgentCommissionModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理佣金列表, 查找列表错误, %v", err) + } + + var list = make([]types.Commission, 0) + + if len(agentCommissionModelList) > 0 { + for _, agentCommissionModel := range agentCommissionModelList { + var commission types.Commission + copyErr := copier.Copy(&commission, agentCommissionModel) + if copyErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理佣金列表, %v", err) + } + product, findProductErr := l.svcCtx.ProductModel.FindOne(l.ctx, agentCommissionModel.ProductId) + if findProductErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理佣金列表, %v", err) + } + commission.CreateTime = agentCommissionModel.CreateTime.Format("2006-01-02 15:04:05") + commission.ProductName = product.ProductName + list = append(list, commission) + } + } + return &types.GetCommissionResp{ + Total: total, + List: list, + }, nil +} diff --git a/app/main/api/internal/logic/agent/getagentinfologic.go b/app/main/api/internal/logic/agent/getagentinfologic.go new file mode 100644 index 0000000..2502542 --- /dev/null +++ b/app/main/api/internal/logic/agent/getagentinfologic.go @@ -0,0 +1,94 @@ +package agent + +import ( + "context" + "database/sql" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAgentInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAgentInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentInfoLogic { + return &GetAgentInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAgentInfoLogic) GetAgentInfo() (resp *types.AgentInfoResp, err error) { + claims, err := ctxdata.GetClaimsFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理信息, %v", err) + } + userID := claims.UserId + userType := claims.UserType + if userType == model.UserTypeTemp { + return &types.AgentInfoResp{ + IsAgent: false, + Status: 3, + }, nil + } + agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + builder := l.svcCtx.AgentAuditModel.SelectBuilder().Where("user_id = ?", userID).OrderBy("create_time DESC").Limit(1) + agentAuditList, findAgentAuditErr := l.svcCtx.AgentAuditModel.FindAll(l.ctx, builder, "") + + if findAgentAuditErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理信息, %v", findAgentAuditErr) + } + + if len(agentAuditList) == 0 { + return &types.AgentInfoResp{ + IsAgent: false, + Status: 3, + }, nil + } + + agentAuditModel := agentAuditList[0] + return &types.AgentInfoResp{ + IsAgent: false, + Status: agentAuditModel.Status, + }, nil + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理信息, %v", err) + } + agent.Mobile, err = crypto.DecryptMobile(agent.Mobile, l.svcCtx.Config.Encrypt.SecretKey) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理信息, 解密手机号失败: %v", err) + } + + IsRealName := false + agentRealName, err := l.svcCtx.AgentRealNameModel.FindOneByAgentId(l.ctx, agent.Id) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理实名信息失败, %v", err) + } + if agentRealName != nil { + IsRealName = true + } + return &types.AgentInfoResp{ + AgentID: agent.Id, + Level: agent.LevelName, + IsAgent: true, + Status: 1, + Region: agent.Region, + Mobile: agent.Mobile, + ExpiryTime: agent.MembershipExpiryTime.Time.Format("2006-01-02 15:04:05"), + IsRealName: IsRealName, + }, nil +} diff --git a/app/main/api/internal/logic/agent/getagentmembershipproductconfiglogic.go b/app/main/api/internal/logic/agent/getagentmembershipproductconfiglogic.go new file mode 100644 index 0000000..8659bbf --- /dev/null +++ b/app/main/api/internal/logic/agent/getagentmembershipproductconfiglogic.go @@ -0,0 +1,80 @@ +package agent + +import ( + "context" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/lzUtils" + + "github.com/jinzhu/copier" + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAgentMembershipProductConfigLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAgentMembershipProductConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentMembershipProductConfigLogic { + return &GetAgentMembershipProductConfigLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAgentMembershipProductConfigLogic) GetAgentMembershipProductConfig(req *types.AgentMembershipProductConfigReq) (resp *types.AgentMembershipProductConfigResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取会员用户报告配置,获取用户ID失败: %v", err) + } + + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取会员用户报告配置,获取代理信息失败: %v", err) + } + if agentModel.LevelName == "" { + agentModel.LevelName = model.AgentLeveNameNormal + } + agentMembershipConfigModel, err := l.svcCtx.AgentMembershipConfigModel.FindOneByLevelName(l.ctx, agentModel.LevelName) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取会员用户报告配置,获取平台配置会员信息失败: %v", err) + } + agentMembershipUserConfigModel, err := l.svcCtx.AgentMembershipUserConfigModel.FindOneByAgentIdProductId(l.ctx, agentModel.Id, req.ProductID) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, err + } + var agentMembershipUserConfig types.AgentMembershipUserConfig + if agentMembershipUserConfigModel != nil { + err = copier.Copy(&agentMembershipUserConfig, agentMembershipUserConfigModel) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取会员用户报告配置,复制平台配置会员信息失败: %v", err) + } + } else { + agentMembershipUserConfig.ProductID = req.ProductID + } + agentProductConfigModelAll, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(l.ctx, req.ProductID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取会员用户报告配置, 获取产品配置%v", err) + } + + var productConfig types.ProductConfig + err = copier.Copy(&productConfig, agentProductConfigModelAll) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取会员用户报告配置,复制平台产品配置失败: %v", err) + } + return &types.AgentMembershipProductConfigResp{ + AgentMembershipUserConfig: agentMembershipUserConfig, + ProductConfig: productConfig, + PriceIncreaseAmount: lzUtils.NullFloat64ToFloat64(agentMembershipConfigModel.PriceIncreaseAmount), + PriceIncreaseMax: lzUtils.NullFloat64ToFloat64(agentMembershipConfigModel.PriceIncreaseMax), + PriceRatio: lzUtils.NullFloat64ToFloat64(agentMembershipConfigModel.PriceRatio), + }, nil +} diff --git a/app/main/api/internal/logic/agent/getagentproductconfiglogic.go b/app/main/api/internal/logic/agent/getagentproductconfiglogic.go new file mode 100644 index 0000000..5b91e76 --- /dev/null +++ b/app/main/api/internal/logic/agent/getagentproductconfiglogic.go @@ -0,0 +1,140 @@ +package agent + +import ( + "context" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/mr" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAgentProductConfigLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAgentProductConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentProductConfigLogic { + return &GetAgentProductConfigLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +type AgentProductConfigResp struct { +} + +func (l *GetAgentProductConfigLogic) GetAgentProductConfig() (resp *types.AgentProductConfigResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取推广项目配置失败, %v", err) + } + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrMsg("您不是代理"), "") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取推广项目配置失败, %v", err) + } + // 1. 查询推广项目配置数据 + builder := l.svcCtx.AgentProductConfigModel.SelectBuilder() + agentProductConfigModelAll, err := l.svcCtx.AgentProductConfigModel.FindAll(l.ctx, builder, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取推广项目配置失败, %v", err) + } + + // 用于存放最终组装好的响应数据 + var respList []types.AgentProductConfig + + // 2. 使用 mr.MapReduceVoid 并行处理每个推广项目配置项 + mrMapErr := mr.MapReduceVoid( + // source 函数:遍历所有推广项目配置,将每个配置项发送到 channel 中 + func(source chan<- interface{}) { + for _, config := range agentProductConfigModelAll { + source <- config + } + }, + // map 函数:处理每个推广项目配置项,根据 ProductId 查询会员用户配置,并组装响应数据 + func(item interface{}, writer mr.Writer[*types.AgentProductConfig], cancel func(error)) { + // 将 item 转换为推广项目配置模型 + config := item.(*model.AgentProductConfig) + var agentProductConfig types.AgentProductConfig + // 配置平台成本价和定价成本 + agentProductConfigModel, findAgentProductConfigErr := l.svcCtx.AgentProductConfigModel.FindOneByProductId(l.ctx, config.ProductId) + if findAgentProductConfigErr != nil { + cancel(findAgentProductConfigErr) + return + } + agentProductConfig.ProductID = config.ProductId + agentProductConfig.CostPrice = agentProductConfigModel.CostPrice + agentProductConfig.PriceRangeMin = agentProductConfigModel.PriceRangeMin + agentProductConfig.PriceRangeMax = agentProductConfigModel.PriceRangeMax + agentProductConfig.PPricingStandard = agentProductConfigModel.PricingStandard + agentProductConfig.POverpricingRatio = agentProductConfigModel.OverpricingRatio + + // 看推广人是否有上级,上级是否有这个配置权限,上级是否有相关配置 + agentClosureModel, findAgentClosureErr := l.svcCtx.AgentClosureModel.FindOneByDescendantIdDepth(l.ctx, agentModel.Id, 1) + if findAgentClosureErr != nil && !errors.Is(findAgentClosureErr, model.ErrNotFound) { + cancel(findAgentClosureErr) + return + } + if agentClosureModel != nil { + ancestorAgentModel, findAncestorAgentErr := l.svcCtx.AgentModel.FindOne(l.ctx, agentClosureModel.AncestorId) + if findAncestorAgentErr != nil { + cancel(findAncestorAgentErr) + return + } + if ancestorAgentModel.LevelName == "" { + ancestorAgentModel.LevelName = model.AgentLeveNameNormal + } + agentMembershipConfigModel, findAgentMembershipErr := l.svcCtx.AgentMembershipConfigModel.FindOneByLevelName(l.ctx, ancestorAgentModel.LevelName) + if findAgentMembershipErr != nil { + cancel(findAgentMembershipErr) + return + } + // 是否有提成本价 + if agentMembershipConfigModel.PriceIncreaseAmount.Valid { + // 根据产品ID查询会员用户配置数据 + membershipUserConfigModel, membershipConfigErr := l.svcCtx.AgentMembershipUserConfigModel.FindOneByAgentIdProductId(l.ctx, agentClosureModel.AncestorId, config.ProductId) + if membershipConfigErr != nil { + if errors.Is(membershipConfigErr, model.ErrNotFound) { + writer.Write(&agentProductConfig) + return + } + cancel(membershipConfigErr) + return + } + agentProductConfig.CostPrice += membershipUserConfigModel.PriceIncreaseAmount + agentProductConfig.PriceRangeMin += membershipUserConfigModel.PriceIncreaseAmount + agentProductConfig.APricingStandard = membershipUserConfigModel.PriceRangeFrom + agentProductConfig.APricingEnd = membershipUserConfigModel.PriceRangeTo + agentProductConfig.AOverpricingRatio = membershipUserConfigModel.PriceRatio + } + } + writer.Write(&agentProductConfig) + + }, + // reduce 函数:收集 map 阶段写入的响应数据,并汇总到 respList 中 + func(pipe <-chan *types.AgentProductConfig, cancel func(error)) { + for item := range pipe { + respList = append(respList, *item) + } + }, + ) + if mrMapErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取推广项目配置失败, %+v", mrMapErr) + } + + // 3. 组装最终响应返回 + return &types.AgentProductConfigResp{ + AgentProductConfig: respList, + }, nil +} diff --git a/app/main/api/internal/logic/agent/getagentpromotionqrcodelogic.go b/app/main/api/internal/logic/agent/getagentpromotionqrcodelogic.go new file mode 100644 index 0000000..a9e4481 --- /dev/null +++ b/app/main/api/internal/logic/agent/getagentpromotionqrcodelogic.go @@ -0,0 +1,72 @@ +package agent + +import ( + "context" + "fmt" + "net/http" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAgentPromotionQrcodeLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext + writer http.ResponseWriter +} + +func NewGetAgentPromotionQrcodeLogic(ctx context.Context, svcCtx *svc.ServiceContext, writer http.ResponseWriter) *GetAgentPromotionQrcodeLogic { + return &GetAgentPromotionQrcodeLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + writer: writer, + } +} + +func (l *GetAgentPromotionQrcodeLogic) GetAgentPromotionQrcode(req *types.GetAgentPromotionQrcodeReq) error { + // 1. 参数验证 + if req.QrcodeUrl == "" { + return errors.Wrapf(xerr.NewErrMsg("二维码URL不能为空"), "二维码URL为空") + } + + if req.QrcodeType == "" { + req.QrcodeType = "promote" // 设置默认类型 + } + + // 3. 检查指定类型的背景图是否存在 + if !l.svcCtx.ImageService.CheckImageExists(req.QrcodeType) { + l.Errorf("指定的二维码类型对应的背景图不存在: %s", req.QrcodeType) + return errors.Wrapf(xerr.NewErrMsg("指定的二维码类型不支持"), "二维码类型: %s", req.QrcodeType) + } + + // 4. 处理图片,添加二维码 + imageData, contentType, err := l.svcCtx.ImageService.ProcessImageWithQRCode(req.QrcodeType, req.QrcodeUrl) + if err != nil { + l.Errorf("处理图片失败: %v, 类型: %s, URL: %s", err, req.QrcodeType, req.QrcodeUrl) + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成推广二维码图片失败: %v", err) + } + + // 5. 设置响应头 + l.writer.Header().Set("Content-Type", contentType) + l.writer.Header().Set("Content-Length", fmt.Sprintf("%d", len(imageData))) + l.writer.Header().Set("Cache-Control", "public, max-age=3600") // 缓存1小时 + l.writer.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"qrcode_%s.png\"", req.QrcodeType)) + + // 6. 写入图片数据 + _, err = l.writer.Write(imageData) + if err != nil { + l.Errorf("写入图片数据失败: %v", err) + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "输出图片数据失败: %v", err) + } + + l.Infof("成功生成代理推广二维码图片,类型: %s, URL: %s, 图片大小: %d bytes", + req.QrcodeType, req.QrcodeUrl, len(imageData)) + + return nil +} diff --git a/app/main/api/internal/logic/agent/getagentrevenueinfologic.go b/app/main/api/internal/logic/agent/getagentrevenueinfologic.go new file mode 100644 index 0000000..871994a --- /dev/null +++ b/app/main/api/internal/logic/agent/getagentrevenueinfologic.go @@ -0,0 +1,221 @@ +package agent + +import ( + "context" + "time" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAgentRevenueInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAgentRevenueInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentRevenueInfoLogic { + return &GetAgentRevenueInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAgentRevenueInfoLogic) GetAgentRevenueInfo(req *types.GetAgentRevenueInfoReq) (resp *types.GetAgentRevenueInfoResp, err error) { + claims, err := ctxdata.GetClaimsFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理信息, %v", err) + } + userID := claims.UserId + userType := claims.UserType + if userType == model.UserTypeTemp { + return &types.GetAgentRevenueInfoResp{ + Balance: 0, + TotalEarnings: 0, + FrozenBalance: 0, + DirectPush: types.DirectPushReport{ + TotalCommission: 0, + TotalReport: 0, + Today: types.TimeRangeReport{}, + Last7D: types.TimeRangeReport{}, + Last30D: types.TimeRangeReport{}, + }, + ActiveReward: types.ActiveReward{ + TotalReward: 0, + Today: types.ActiveRewardData{}, + Last7D: types.ActiveRewardData{}, + Last30D: types.ActiveRewardData{}, + }, + }, nil + } + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理奖励, %v", err) + } + agentWalletModel, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agentModel.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理奖励, %v", err) + } + resp = &types.GetAgentRevenueInfoResp{} + resp.Balance = agentWalletModel.Balance + resp.TotalEarnings = agentWalletModel.TotalEarnings + resp.FrozenBalance = agentWalletModel.FrozenBalance + + // 直推报告统计 + //now := time.Now() + //startTime := now.AddDate(0, 0, -30).Format("2006-01-02 15:04:05") + //endTime := now.Format("2006-01-02 15:04:05") + + // 直推报告佣金 + agentCommissionModelBuild := l.svcCtx.AgentCommissionModel.SelectBuilder(). + Where(squirrel.Eq{"agent_id": agentModel.Id}) + //.Where(squirrel.Expr("create_time BETWEEN ? AND ?", startTime, endTime)) + + agentCommissionsModel, err := l.svcCtx.AgentCommissionModel.FindAll(l.ctx, agentCommissionModelBuild, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理奖励, %v", err) + } + // 筛选分类 + directPush, err := calculateDirectPushReport(agentCommissionsModel, nil) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理奖励, %v", err) + } + // 绑定到响应体 + resp.DirectPush = directPush + + // 活跃下级统计 + agentRewardsModelBuilder := l.svcCtx.AgentRewardsModel.SelectBuilder().Where("agent_id = ?", agentModel.Id) + agentRewardsModel, err := l.svcCtx.AgentRewardsModel.FindAll(l.ctx, agentRewardsModelBuilder, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理奖励, %v", err) + } + activeReward := calculateActiveReward(agentRewardsModel) + resp.ActiveReward = activeReward + + return resp, nil +} + +// 统计直推报告的独立函数 +func calculateDirectPushReport(commissions []*model.AgentCommission, loc *time.Location) (types.DirectPushReport, error) { + // 初始化报告结构 + report := types.DirectPushReport{ + Today: types.TimeRangeReport{}, + Last7D: types.TimeRangeReport{}, + Last30D: types.TimeRangeReport{}, + } + + // 获取当前中国时间 + now := time.Now() + + // 计算时间分界点 + todayStart := now.Add(-24 * time.Hour) + last7dStart := now.AddDate(0, 0, -7) + last30dStart := now.AddDate(0, 0, -30) + + // 遍历所有佣金记录 + for _, c := range commissions { + // 转换时区 + createTime := c.CreateTime + + // 统计总量 + report.TotalCommission += c.Amount + report.TotalReport++ + + // 近24小时(滚动周期) + if createTime.After(todayStart) { + report.Today.Commission += c.Amount + report.Today.Report++ + } + + // 近7天(滚动周期) + if createTime.After(last7dStart) { + report.Last7D.Commission += c.Amount + report.Last7D.Report++ + } + + // 近30天(滚动周期) + if createTime.After(last30dStart) { + report.Last30D.Commission += c.Amount + report.Last30D.Report++ + } + } + + return report, nil +} +func calculateActiveReward(rewards []*model.AgentRewards) types.ActiveReward { + result := types.ActiveReward{ + Today: types.ActiveRewardData{}, + Last7D: types.ActiveRewardData{}, + Last30D: types.ActiveRewardData{}, + } + + now := time.Now() + todayStart := now.Add(-24 * time.Hour) // 近24小时 + last7dStart := now.AddDate(0, 0, -7) // 近7天 + last30dStart := now.AddDate(0, 0, -30) // 近30天 + + for _, r := range rewards { + createTime := r.CreateTime + amount := r.Amount + + // 总奖励累加 + result.TotalReward += amount + + // 时间范围判断 + isToday := createTime.After(todayStart) + isLast7d := createTime.After(last7dStart) + isLast30d := createTime.After(last30dStart) + + // 类型分类统计 + switch r.Type { + case model.AgentRewardsTypeDescendantWithdraw: + addToPeriods(&result, amount, isToday, isLast7d, isLast30d, "withdraw") + + case model.AgentRewardsTypeDescendantNewActive: + addToPeriods(&result, amount, isToday, isLast7d, isLast30d, "new_active") + + case model.AgentRewardsTypeDescendantUpgradeSvip, model.AgentRewardsTypeDescendantUpgradeVip: + addToPeriods(&result, amount, isToday, isLast7d, isLast30d, "upgrade") + + case model.AgentRewardsTypeDescendantPromotion: + addToPeriods(&result, amount, isToday, isLast7d, isLast30d, "promotion") + } + } + return result +} + +// 统一处理时间段累加 +func addToPeriods(res *types.ActiveReward, amount float64, today, last7d, last30d bool, t string) { + if today { + addToData(&res.Today, amount, t) + } + if last7d { + addToData(&res.Last7D, amount, t) + } + if last30d { + addToData(&res.Last30D, amount, t) + } +} + +// 分类添加具体字段 +func addToData(data *types.ActiveRewardData, amount float64, t string) { + switch t { + case "withdraw": + data.SubWithdrawReward += amount + case "new_active": + data.NewActiveReward += amount + case "upgrade": + data.SubUpgradeReward += amount + case "promotion": + data.SubPromoteReward += amount + } +} diff --git a/app/main/api/internal/logic/agent/getagentrewardslogic.go b/app/main/api/internal/logic/agent/getagentrewardslogic.go new file mode 100644 index 0000000..67fe616 --- /dev/null +++ b/app/main/api/internal/logic/agent/getagentrewardslogic.go @@ -0,0 +1,68 @@ +package agent + +import ( + "context" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAgentRewardsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAgentRewardsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentRewardsLogic { + return &GetAgentRewardsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAgentRewardsLogic) GetAgentRewards(req *types.GetRewardsReq) (resp *types.GetRewardsResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理奖励列表, %v", err) + } + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理奖励列表, %v", err) + } + builder := l.svcCtx.AgentRewardsModel.SelectBuilder().Where(squirrel.Eq{ + "agent_id": agentModel.Id, + }) + agentRewardsModelList, total, err := l.svcCtx.AgentRewardsModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理奖励列表, 查找列表错误, %v", err) + } + + var list = make([]types.Rewards, 0) + if len(agentRewardsModelList) > 0 { + for _, agentRewardsModel := range agentRewardsModelList { + var rewards types.Rewards + copyErr := copier.Copy(&rewards, agentRewardsModel) + if copyErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理奖励列表, %v", err) + } + + rewards.CreateTime = agentRewardsModel.CreateTime.Format("2006-01-02 15:04:05") + list = append(list, rewards) + } + } + return &types.GetRewardsResp{ + Total: total, + List: list, + }, nil + + return +} diff --git a/app/main/api/internal/logic/agent/getagentsubordinatecontributiondetaillogic.go b/app/main/api/internal/logic/agent/getagentsubordinatecontributiondetaillogic.go new file mode 100644 index 0000000..658ee3d --- /dev/null +++ b/app/main/api/internal/logic/agent/getagentsubordinatecontributiondetaillogic.go @@ -0,0 +1,200 @@ +package agent + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAgentSubordinateContributionDetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAgentSubordinateContributionDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentSubordinateContributionDetailLogic { + return &GetAgentSubordinateContributionDetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAgentSubordinateContributionDetailLogic) GetAgentSubordinateContributionDetail(req *types.GetAgentSubordinateContributionDetailReq) (resp *types.GetAgentSubordinateContributionDetailResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理下级贡献详情, 获取用户ID%v", err) + } + + // 获取当前代理信息 + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理下级贡献详情, 获取代理信息%v", err) + } + + // 获取下级代理信息 + subordinateAgent, err := l.svcCtx.AgentModel.FindOne(l.ctx, req.SubordinateID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理下级贡献详情, 获取下级代理信息%v", err) + } + + // 验证是否是当前代理的下级 + closureBuilder := l.svcCtx.AgentClosureModel.SelectBuilder().Where(squirrel.Eq{ + "ancestor_id": agentModel.Id, + "descendant_id": req.SubordinateID, + }) + closureList, err := l.svcCtx.AgentClosureModel.FindAll(l.ctx, closureBuilder, "create_time DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理下级贡献详情, 验证代理关系%v", err) + } + if len(closureList) == 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理下级贡献详情, 非法的代理关系") + } + closure := closureList[0] + + // 获取佣金扣除记录 + deductionBuilder := l.svcCtx.AgentCommissionDeductionModel.SelectBuilder().Where(squirrel.Eq{ + "agent_id": agentModel.Id, + "deducted_agent_id": req.SubordinateID, + }) + deductionList, err := l.svcCtx.AgentCommissionDeductionModel.FindAll(l.ctx, deductionBuilder, "create_time DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理下级贡献详情, 获取佣金扣除记录%v", err) + } + + // 获取奖励记录 + rewardsBuilder := l.svcCtx.AgentRewardsModel.SelectBuilder().Where(squirrel.Eq{ + "agent_id": agentModel.Id, + "relation_agent_id": req.SubordinateID, + }) + rewards, err := l.svcCtx.AgentRewardsModel.FindAll(l.ctx, rewardsBuilder, "create_time DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理下级贡献详情, 获取奖励记录%v", err) + } + + // 计算总贡献 + var totalContribution float64 + for _, v := range deductionList { + totalContribution += v.Amount + } + // 加上奖励金额 + for _, v := range rewards { + totalContribution += v.Amount + } + + // 获取佣金记录 + commissionBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder().Where(squirrel.Eq{ + "agent_id": req.SubordinateID, + }) + commissionList, err := l.svcCtx.AgentCommissionModel.FindAll(l.ctx, commissionBuilder, "create_time DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理下级贡献详情, 获取佣金记录%v", err) + } + + // 计算总收益和总单量 + var totalEarnings float64 + for _, v := range commissionList { + totalEarnings += v.Amount + } + + // 初始化统计数据 + stats := types.AgentSubordinateContributionStats{ + CostCount: 0, + CostAmount: 0, + PricingCount: 0, + PricingAmount: 0, + DescendantPromotionCount: 0, + DescendantPromotionAmount: 0, + DescendantUpgradeVipCount: 0, + DescendantUpgradeVipAmount: 0, + DescendantUpgradeSvipCount: 0, + DescendantUpgradeSvipAmount: 0, + DescendantStayActiveCount: 0, + DescendantStayActiveAmount: 0, + DescendantNewActiveCount: 0, + DescendantNewActiveAmount: 0, + DescendantWithdrawCount: 0, + DescendantWithdrawAmount: 0, + } + + // 统计佣金扣除记录 + for _, v := range deductionList { + switch v.Type { + case "cost": + stats.CostCount++ + stats.CostAmount += v.Amount + case "pricing": + stats.PricingCount++ + stats.PricingAmount += v.Amount + } + } + + // 统计奖励记录 + for _, v := range rewards { + switch v.Type { + case "descendant_promotion": + stats.DescendantPromotionCount++ + stats.DescendantPromotionAmount += v.Amount + case "descendant_upgrade_vip": + stats.DescendantUpgradeVipCount++ + stats.DescendantUpgradeVipAmount += v.Amount + case "descendant_upgrade_svip": + stats.DescendantUpgradeSvipCount++ + stats.DescendantUpgradeSvipAmount += v.Amount + case "descendant_stay_active": + stats.DescendantStayActiveCount++ + stats.DescendantStayActiveAmount += v.Amount + case "descendant_new_active": + stats.DescendantNewActiveCount++ + stats.DescendantNewActiveAmount += v.Amount + case "descendant_withdraw": + stats.DescendantWithdrawCount++ + stats.DescendantWithdrawAmount += v.Amount + } + } + + // 解密手机号 + secretKey := l.svcCtx.Config.Encrypt.SecretKey + mobile, err := crypto.DecryptMobile(subordinateAgent.Mobile, secretKey) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理下级贡献详情, 解密手机号失败: %v", err) + } + + // 获取合并后的分页列表 + unionDetails, total, err := l.svcCtx.AgentClosureModel.FindUnionPageListByPageWithTotal(l.ctx, agentModel.Id, req.SubordinateID, req.Page, req.PageSize) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理下级贡献详情, 获取分页列表%v", err) + } + + // 转换为响应类型 + detailList := make([]types.AgentSubordinateContributionDetail, 0, len(unionDetails)) + for _, v := range unionDetails { + detail := types.AgentSubordinateContributionDetail{ + ID: v.Id, + CreateTime: v.CreateTime, + Amount: v.Amount, + Type: v.Type, + } + detailList = append(detailList, detail) + } + + return &types.GetAgentSubordinateContributionDetailResp{ + Mobile: maskPhone(mobile), + Total: total, + CreateTime: closure.CreateTime.Format("2006-01-02 15:04:05"), + TotalEarnings: totalEarnings, + TotalContribution: totalContribution, + TotalOrders: int64(len(commissionList)), + LevelName: subordinateAgent.LevelName, + List: detailList, + Stats: stats, + }, nil +} diff --git a/app/main/api/internal/logic/agent/getagentsubordinatelistlogic.go b/app/main/api/internal/logic/agent/getagentsubordinatelistlogic.go new file mode 100644 index 0000000..048ef9f --- /dev/null +++ b/app/main/api/internal/logic/agent/getagentsubordinatelistlogic.go @@ -0,0 +1,173 @@ +package agent + +import ( + "context" + "strings" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/mr" +) + +type GetAgentSubordinateListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAgentSubordinateListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentSubordinateListLogic { + return &GetAgentSubordinateListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAgentSubordinateListLogic) GetAgentSubordinateList(req *types.GetAgentSubordinateListReq) (resp *types.GetAgentSubordinateListResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理下级列表, 获取用户ID%v", err) + } + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理下级列表, 获取代理信息%v", err) + } + agentID := agentModel.Id + + builder := l.svcCtx.AgentClosureModel.SelectBuilder().Where(squirrel.Eq{ + "ancestor_id": agentID, + }) + agentClosureModelList, total, err := l.svcCtx.AgentClosureModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理下级列表, 获取代理关系%v", err) + } + + // 构建ID到CreateTime的映射 + createTimeMap := make(map[int64]time.Time) + descendantIDs := make([]int64, 0) + for _, v := range agentClosureModelList { + descendantIDs = append(descendantIDs, v.DescendantId) + createTimeMap[v.DescendantId] = v.CreateTime + } + + // 并发查询代理信息 + agentMap := make(map[int64]*model.Agent) + var descendantList []types.AgentSubordinateList + err = mr.Finish(func() error { + return mr.MapReduceVoid(func(source chan<- interface{}) { + for _, id := range descendantIDs { + source <- id + } + }, func(item interface{}, writer mr.Writer[interface{}], cancel func(error)) { + id := item.(int64) + agent, err := l.svcCtx.AgentModel.FindOne(l.ctx, id) + if err != nil { + cancel(err) + return + } + writer.Write(agent) + }, func(pipe <-chan interface{}, cancel func(error)) { + for item := range pipe { + agent := item.(*model.Agent) + agentMap[agent.Id] = agent + } + }) + }, func() error { + // 并发查询佣金扣除信息 + deductionBuilder := l.svcCtx.AgentCommissionDeductionModel.SelectBuilder(). + Where(squirrel.Eq{"agent_id": agentID}). + Where(squirrel.Eq{"deducted_agent_id": descendantIDs}) + deductionList, err := l.svcCtx.AgentCommissionDeductionModel.FindAll(l.ctx, deductionBuilder, "") + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理下级列表, 获取代理佣金扣除信息%v", err) + } + deductionMap := make(map[int64]float64) + for _, v := range deductionList { + deductionMap[v.DeductedAgentId] += v.Amount + } + + // 并发查询奖励信息 + rewardsBuilder := l.svcCtx.AgentRewardsModel.SelectBuilder(). + Where(squirrel.Eq{"agent_id": agentID}). + Where(squirrel.Eq{"relation_agent_id": descendantIDs}) + rewardsList, err := l.svcCtx.AgentRewardsModel.FindAll(l.ctx, rewardsBuilder, "") + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理下级列表, 获取代理奖励信息%v", err) + } + rewardsMap := make(map[int64]float64) + for _, v := range rewardsList { + if v.RelationAgentId.Valid { + rewardsMap[v.RelationAgentId.Int64] += v.Amount + } + } + + // 并发查询佣金信息 + commissionBuilder := l.svcCtx.AgentCommissionModel.SelectBuilder(). + Where(squirrel.Eq{"agent_id": descendantIDs}) + commissionList, err := l.svcCtx.AgentCommissionModel.FindAll(l.ctx, commissionBuilder, "") + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理下级列表, 获取代理佣金信息%v", err) + } + commissionMap := make(map[int64]float64) + orderCountMap := make(map[int64]int64) + for _, v := range commissionList { + commissionMap[v.AgentId] += v.Amount + orderCountMap[v.AgentId]++ + } + + // 构建返回结果 + secretKey := l.svcCtx.Config.Encrypt.SecretKey + descendantList = make([]types.AgentSubordinateList, 0, len(descendantIDs)) + for _, id := range descendantIDs { + agent, exists := agentMap[id] + if !exists { + continue + } + + mobile, err := crypto.DecryptMobile(agent.Mobile, secretKey) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理信息, 解密手机号失败: %v", err) + } + + subordinate := types.AgentSubordinateList{ + ID: id, + Mobile: maskPhone(mobile), + LevelName: agent.LevelName, + CreateTime: createTimeMap[id].Format("2006-01-02 15:04:05"), + TotalContribution: deductionMap[id] + rewardsMap[id], + TotalEarnings: commissionMap[id], + TotalOrders: orderCountMap[id], + } + descendantList = append(descendantList, subordinate) + } + return nil + }) + + if err != nil { + return nil, err + } + + return &types.GetAgentSubordinateListResp{ + Total: total, + List: descendantList, + }, nil +} + +// 手机号脱敏 +func maskPhone(phone string) string { + length := len(phone) + if length < 8 { + return phone // 如果长度太短,可能不是手机号,不处理 + } + // 保留前3位和后4位 + return phone[:3] + strings.Repeat("*", length-7) + phone[length-4:] +} diff --git a/app/main/api/internal/logic/agent/getagentwithdrawallogic.go b/app/main/api/internal/logic/agent/getagentwithdrawallogic.go new file mode 100644 index 0000000..4ecb259 --- /dev/null +++ b/app/main/api/internal/logic/agent/getagentwithdrawallogic.go @@ -0,0 +1,66 @@ +package agent + +import ( + "context" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAgentWithdrawalLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAgentWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentWithdrawalLogic { + return &GetAgentWithdrawalLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAgentWithdrawalLogic) GetAgentWithdrawal(req *types.GetWithdrawalReq) (resp *types.GetWithdrawalResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理提现列表, %v", err) + } + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理提现列表, %v", err) + } + builder := l.svcCtx.AgentWithdrawalModel.SelectBuilder().Where(squirrel.Eq{ + "agent_id": agentModel.Id, + }) + agentWithdrawalModelList, total, err := l.svcCtx.AgentWithdrawalModel.FindPageListByPageWithTotal(l.ctx, builder, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理提现列表, 查找列表错误, %v", err) + } + + var list = make([]types.Withdrawal, 0) + + if len(agentWithdrawalModelList) > 0 { + for _, agentWithdrawalModel := range agentWithdrawalModelList { + var withdrawal types.Withdrawal + copyErr := copier.Copy(&withdrawal, agentWithdrawalModel) + if copyErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取代理提现列表, %v", err) + } + withdrawal.CreateTime = agentWithdrawalModel.CreateTime.Format("2006-01-02 15:04:05") + list = append(list, withdrawal) + } + } + return &types.GetWithdrawalResp{ + Total: total, + List: list, + }, nil +} diff --git a/app/main/api/internal/logic/agent/getagentwithdrawaltaxexemptionlogic.go b/app/main/api/internal/logic/agent/getagentwithdrawaltaxexemptionlogic.go new file mode 100644 index 0000000..a0b31d1 --- /dev/null +++ b/app/main/api/internal/logic/agent/getagentwithdrawaltaxexemptionlogic.go @@ -0,0 +1,78 @@ +package agent + +import ( + "context" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAgentWithdrawalTaxExemptionLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAgentWithdrawalTaxExemptionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentWithdrawalTaxExemptionLogic { + return &GetAgentWithdrawalTaxExemptionLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAgentWithdrawalTaxExemptionLogic) GetAgentWithdrawalTaxExemption(req *types.GetWithdrawalTaxExemptionReq) (resp *types.GetWithdrawalTaxExemptionResp, err error) { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户ID失败: %+v", err) + } + agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理ID失败: %+v", err) + } + yearMonth := int64(time.Now().Year()*100 + int(time.Now().Month())) + + agentWithdrawalTaxExemption, err := l.svcCtx.AgentWithdrawalTaxExemptionModel.FindOneByAgentIdYearMonth(l.ctx, agent.Id, yearMonth) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + agentWithdrawalTaxExemption, err = l.createMonthlyExemption(agent.Id, yearMonth) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建代理税务额度失败: %v", err) + } + } else { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理税务额度失败: %+v", err) + } + } + + return &types.GetWithdrawalTaxExemptionResp{ + TotalExemptionAmount: agentWithdrawalTaxExemption.TotalExemptionAmount, + UsedExemptionAmount: agentWithdrawalTaxExemption.UsedExemptionAmount, + RemainingExemptionAmount: agentWithdrawalTaxExemption.RemainingExemptionAmount, + TaxRate: l.svcCtx.Config.TaxConfig.TaxRate, + }, nil +} +func (l *GetAgentWithdrawalTaxExemptionLogic) createMonthlyExemption(agentId int64, yearMonth int64) (*model.AgentWithdrawalTaxExemption, error) { + exemption := &model.AgentWithdrawalTaxExemption{ + AgentId: agentId, + YearMonth: yearMonth, + TotalExemptionAmount: l.svcCtx.Config.TaxConfig.TaxExemptionAmount, + UsedExemptionAmount: 0.00, + RemainingExemptionAmount: l.svcCtx.Config.TaxConfig.TaxExemptionAmount, + } + + result, err := l.svcCtx.AgentWithdrawalTaxExemptionModel.Insert(l.ctx, nil, exemption) + if err != nil { + return nil, err + } + + id, _ := result.LastInsertId() + exemption.Id = id + return exemption, nil +} diff --git a/app/main/api/internal/logic/agent/getlinkdatalogic.go b/app/main/api/internal/logic/agent/getlinkdatalogic.go new file mode 100644 index 0000000..63f18b9 --- /dev/null +++ b/app/main/api/internal/logic/agent/getlinkdatalogic.go @@ -0,0 +1,46 @@ +package agent + +import ( + "context" + "znc-server/common/xerr" + + "github.com/jinzhu/copier" + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetLinkDataLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetLinkDataLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLinkDataLogic { + return &GetLinkDataLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetLinkDataLogic) GetLinkData(req *types.GetLinkDataReq) (resp *types.GetLinkDataResp, err error) { + agentLinkModel, err := l.svcCtx.AgentLinkModel.FindOneByLinkIdentifier(l.ctx, req.LinkIdentifier) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理链接数据, %v", err) + } + + productModel, err := l.svcCtx.ProductModel.FindOne(l.ctx, agentLinkModel.ProductId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取代理链接数据, %v", err) + } + var product types.Product + copier.Copy(&product, productModel) + product.SellPrice = agentLinkModel.Price + return &types.GetLinkDataResp{ + Product: product, + }, nil +} diff --git a/app/main/api/internal/logic/agent/saveagentmembershipuserconfiglogic.go b/app/main/api/internal/logic/agent/saveagentmembershipuserconfiglogic.go new file mode 100644 index 0000000..be30b23 --- /dev/null +++ b/app/main/api/internal/logic/agent/saveagentmembershipuserconfiglogic.go @@ -0,0 +1,82 @@ +package agent + +import ( + "context" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SaveAgentMembershipUserConfigLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSaveAgentMembershipUserConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SaveAgentMembershipUserConfigLogic { + return &SaveAgentMembershipUserConfigLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SaveAgentMembershipUserConfigLogic) SaveAgentMembershipUserConfig(req *types.SaveAgentMembershipUserConfigReq) error { + userID, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "保存会员代理报告配置,获取用户ID失败: %v", err) + } + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "保存会员代理报告配置: %v", err) + } + + var agentMembershipUserConfigModel *model.AgentMembershipUserConfig + agentMembershipUserConfigModel, err = l.svcCtx.AgentMembershipUserConfigModel.FindOneByAgentIdProductId(l.ctx, agentModel.Id, req.ProductID) + + // 检查记录是否存在 + if err != nil { + if errors.Is(err, model.ErrNotFound) { + // 记录不存在,创建新的配置对象 + agentMembershipUserConfigModel = &model.AgentMembershipUserConfig{ + UserId: userID, + AgentId: agentModel.Id, + ProductId: req.ProductID, + PriceRatio: req.PriceRatio, + PriceIncreaseAmount: req.PriceIncreaseAmount, + PriceRangeFrom: req.PriceRangeFrom, + PriceRangeTo: req.PriceRangeTo, + } + + // 插入新记录 + _, err = l.svcCtx.AgentMembershipUserConfigModel.Insert(l.ctx, nil, agentMembershipUserConfigModel) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "保存会员代理报告配置,插入新记录失败: %v", err) + } + return nil + } + + // 其他错误 + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "保存会员代理报告配置,查询记录失败: %v", err) + } + + // 记录存在,更新现有配置 + agentMembershipUserConfigModel.PriceRatio = req.PriceRatio + agentMembershipUserConfigModel.PriceIncreaseAmount = req.PriceIncreaseAmount + agentMembershipUserConfigModel.PriceRangeFrom = req.PriceRangeFrom + agentMembershipUserConfigModel.PriceRangeTo = req.PriceRangeTo + + _, err = l.svcCtx.AgentMembershipUserConfigModel.Update(l.ctx, nil, agentMembershipUserConfigModel) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "保存会员代理报告配置,更新记录失败: %v", err) + } + + return nil +} diff --git a/app/main/api/internal/logic/app/getappversionlogic.go b/app/main/api/internal/logic/app/getappversionlogic.go new file mode 100644 index 0000000..3154b92 --- /dev/null +++ b/app/main/api/internal/logic/app/getappversionlogic.go @@ -0,0 +1,31 @@ +package app + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAppVersionLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAppVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAppVersionLogic { + return &GetAppVersionLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAppVersionLogic) GetAppVersion() (resp *types.GetAppVersionResp, err error) { + return &types.GetAppVersionResp{ + Version: "1.0.0", + WgtUrl: "https://www.quannengcha.com/app_version/qnc_1.0.0.wgt", + }, nil +} diff --git a/app/main/api/internal/logic/app/healthchecklogic.go b/app/main/api/internal/logic/app/healthchecklogic.go new file mode 100644 index 0000000..71f359c --- /dev/null +++ b/app/main/api/internal/logic/app/healthchecklogic.go @@ -0,0 +1,31 @@ +package app + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type HealthCheckLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewHealthCheckLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HealthCheckLogic { + return &HealthCheckLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *HealthCheckLogic) HealthCheck() (resp *types.HealthCheckResp, err error) { + return &types.HealthCheckResp{ + Status: "UP", + Message: "Service is healthy HahaHa", + }, nil +} diff --git a/app/main/api/internal/logic/auth/sendsmslogic.go b/app/main/api/internal/logic/auth/sendsmslogic.go new file mode 100644 index 0000000..f7b8430 --- /dev/null +++ b/app/main/api/internal/logic/auth/sendsmslogic.go @@ -0,0 +1,105 @@ +package auth + +import ( + "context" + "fmt" + "math/rand" + "time" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + dysmsapi "github.com/alibabacloud-go/dysmsapi-20170525/v3/client" + "github.com/alibabacloud-go/tea-utils/v2/service" + "github.com/alibabacloud-go/tea/tea" + "github.com/zeromicro/go-zero/core/logx" +) + +type SendSmsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSendSmsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsLogic { + return &SendSmsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SendSmsLogic) SendSms(req *types.SendSmsReq) error { + secretKey := l.svcCtx.Config.Encrypt.SecretKey + encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 加密手机号失败: %v", err) + } + // 检查手机号是否在一分钟内已发送过验证码 + limitCodeKey := fmt.Sprintf("limit:%s:%s", req.ActionType, encryptedMobile) + exists, err := l.svcCtx.Redis.Exists(limitCodeKey) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 读取redis缓存失败: %s", encryptedMobile) + } + + if exists { + // 如果 Redis 中已经存在标记,说明在 1 分钟内请求过,返回错误 + return errors.Wrapf(xerr.NewErrMsg("一分钟内不能重复发送验证码"), "短信发送, 手机号1分钟内重复请求发送验证码: %s", encryptedMobile) + } + + code := fmt.Sprintf("%06d", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(1000000)) + + // 发送短信 + smsResp, err := l.sendSmsRequest(req.Mobile, code) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 调用阿里客户端失败: %v", err) + } + if *smsResp.Body.Code != "OK" { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 阿里客户端响应失败: %s", *smsResp.Body.Message) + } + codeKey := fmt.Sprintf("%s:%s", req.ActionType, encryptedMobile) + // 将验证码保存到 Redis,设置过期时间 + err = l.svcCtx.Redis.Setex(codeKey, code, l.svcCtx.Config.VerifyCode.ValidTime) // 验证码有效期5分钟 + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 验证码设置过期时间失败: %v", err) + } + // 在 Redis 中设置 1 分钟的标记,限制重复请求 + err = l.svcCtx.Redis.Setex(limitCodeKey, code, 60) // 标记 1 分钟内不能重复请求 + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 验证码设置限制重复请求失败: %v", err) + } + return nil +} + +// CreateClient 创建阿里云短信客户端 +func (l *SendSmsLogic) CreateClient() (*dysmsapi.Client, error) { + config := &openapi.Config{ + AccessKeyId: &l.svcCtx.Config.VerifyCode.AccessKeyID, + AccessKeySecret: &l.svcCtx.Config.VerifyCode.AccessKeySecret, + } + config.Endpoint = tea.String(l.svcCtx.Config.VerifyCode.EndpointURL) + return dysmsapi.NewClient(config) +} + +// sendSmsRequest 发送短信请求 +func (l *SendSmsLogic) sendSmsRequest(mobile, code string) (*dysmsapi.SendSmsResponse, error) { + // 初始化阿里云短信客户端 + cli, err := l.CreateClient() + if err != nil { + return nil, err + } + + request := &dysmsapi.SendSmsRequest{ + SignName: tea.String(l.svcCtx.Config.VerifyCode.SignName), + TemplateCode: tea.String(l.svcCtx.Config.VerifyCode.TemplateCode), + PhoneNumbers: tea.String(mobile), + TemplateParam: tea.String(fmt.Sprintf("{\"code\":\"%s\"}", code)), + } + runtime := &service.RuntimeOptions{} + return cli.SendSmsWithOptions(request, runtime) +} diff --git a/app/main/api/internal/logic/notification/getnotificationslogic.go b/app/main/api/internal/logic/notification/getnotificationslogic.go new file mode 100644 index 0000000..67cc112 --- /dev/null +++ b/app/main/api/internal/logic/notification/getnotificationslogic.go @@ -0,0 +1,57 @@ +package notification + +import ( + "context" + "time" + "znc-server/common/xerr" + + "github.com/jinzhu/copier" + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetNotificationsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetNotificationsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetNotificationsLogic { + return &GetNotificationsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetNotificationsLogic) GetNotifications() (resp *types.GetNotificationsResp, err error) { + // 获取今天的日期 + now := time.Now() + + // 获取开始和结束日期的时间戳 + todayStart := now.Format("2006-01-02") + " 00:00:00" + todayEnd := now.Format("2006-01-02") + " 23:59:59" + + // 构建查询条件 + builder := l.svcCtx.GlobalNotificationsModel.SelectBuilder(). + Where("status = ?", "active"). + Where("(start_date IS NULL OR start_date <= ?)", todayEnd). // start_date 是 NULL 或者小于等于今天结束时间 + Where("(end_date IS NULL OR end_date >= ?)", todayStart) // end_date 是 NULL 或者大于等于今天开始时间 + + notificationsModelList, findErr := l.svcCtx.GlobalNotificationsModel.FindAll(l.ctx, builder, "") + if findErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "全局通知, 查找通知失败, err:%+v", findErr) + } + + var notifications []types.Notification + copyErr := copier.Copy(¬ifications, ¬ificationsModelList) + if copyErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "全局通知, 复制结构体失败, err:%+v", copyErr) + } + + return &types.GetNotificationsResp{Notifications: notifications}, nil +} diff --git a/app/main/api/internal/logic/pay/alipaycallbacklogic.go b/app/main/api/internal/logic/pay/alipaycallbacklogic.go new file mode 100644 index 0000000..8dbf27c --- /dev/null +++ b/app/main/api/internal/logic/pay/alipaycallbacklogic.go @@ -0,0 +1,216 @@ +package pay + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + "znc-server/pkg/lzkit/lzUtils" + + "github.com/smartwalle/alipay/v3" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/model" + + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type AlipayCallbackLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAlipayCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AlipayCallbackLogic { + return &AlipayCallbackLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AlipayCallbackLogic) AlipayCallback(w http.ResponseWriter, r *http.Request) error { + notification, err := l.svcCtx.AlipayService.HandleAliPaymentNotification(r) + if err != nil { + logx.Errorf("支付宝支付回调,%v", err) + return nil + } + + // 根据订单号前缀判断订单类型 + orderNo := notification.OutTradeNo + if strings.HasPrefix(orderNo, "Q_") { + // 查询订单处理 + return l.handleQueryOrderPayment(w, notification) + } else if strings.HasPrefix(orderNo, "A_") { + // 代理会员订单处理 + return l.handleAgentVipOrderPayment(w, notification) + } else { + // 兼容旧订单,假设没有前缀的是查询订单 + return l.handleQueryOrderPayment(w, notification) + } +} + +// 处理查询订单支付 +func (l *AlipayCallbackLogic) handleQueryOrderPayment(w http.ResponseWriter, notification *alipay.Notification) error { + order, findOrderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, notification.OutTradeNo) + if findOrderErr != nil { + logx.Errorf("支付宝支付回调,查找订单失败: %+v", findOrderErr) + return nil + } + + if order.Status != "pending" { + alipay.ACKNotification(w) + return nil + } + + user, err := l.svcCtx.UserModel.FindOne(l.ctx, order.UserId) + if err != nil { + logx.Errorf("支付宝支付回调,查找用户失败: %+v", err) + return nil + } + + amount := lzUtils.ToAlipayAmount(order.Amount) + if user.Inside != 1 { + // 确保订单金额和状态正确,防止重复更新 + if amount != notification.TotalAmount { + logx.Errorf("支付宝支付回调,金额不一致") + return nil + } + } + + switch notification.TradeStatus { + case alipay.TradeStatusSuccess: + order.Status = "paid" + order.PayTime = lzUtils.TimeToNullTime(time.Now()) + default: + return nil + } + + order.PlatformOrderId = lzUtils.StringToNullString(notification.TradeNo) + if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil { + logx.Errorf("支付宝支付回调,修改订单信息失败: %+v", updateErr) + return nil + } + + if order.Status == "paid" { + if asyncErr := l.svcCtx.AsynqService.SendQueryTask(order.Id); asyncErr != nil { + logx.Errorf("异步任务调度失败: %v", asyncErr) + return asyncErr + } + } + + alipay.ACKNotification(w) + return nil +} + +// 处理代理会员订单支付 +func (l *AlipayCallbackLogic) handleAgentVipOrderPayment(w http.ResponseWriter, notification *alipay.Notification) error { + agentOrder, findAgentOrderErr := l.svcCtx.AgentMembershipRechargeOrderModel.FindOneByOrderNo(l.ctx, notification.OutTradeNo) + if findAgentOrderErr != nil { + logx.Errorf("支付宝支付回调,查找代理会员订单失败: %+v", findAgentOrderErr) + return nil + } + + if agentOrder.Status != "pending" { + alipay.ACKNotification(w) + return nil + } + + user, err := l.svcCtx.UserModel.FindOne(l.ctx, agentOrder.UserId) + if err != nil { + logx.Errorf("支付宝支付回调,查找用户失败: %+v", err) + return nil + } + + amount := lzUtils.ToAlipayAmount(agentOrder.Amount) + if user.Inside != 1 { + // 确保订单金额和状态正确,防止重复更新 + if amount != notification.TotalAmount { + logx.Errorf("支付宝支付回调,金额不一致") + return nil + } + } + + switch notification.TradeStatus { + case alipay.TradeStatusSuccess: + agentOrder.Status = "paid" + default: + return nil + } + + if agentOrder.Status == "paid" { + err = l.svcCtx.AgentModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error { + agentModel, err := l.svcCtx.AgentModel.FindOne(transCtx, agentOrder.AgentId) + if err != nil { + return fmt.Errorf("查找代理信息失败: %+v", err) + } + agentOrder.PlatformOrderId = lzUtils.StringToNullString(notification.TradeNo) + if updateErr := l.svcCtx.AgentMembershipRechargeOrderModel.UpdateWithVersion(l.ctx, nil, agentOrder); updateErr != nil { + return fmt.Errorf("修改代理会员订单信息失败: %+v", updateErr) + } + + // 设置会员等级 + agentModel.LevelName = agentOrder.LevelName + + // 延长会员时间 + // 检查是否是同级续费并记录到日志 + isRenewal := agentModel.LevelName == agentOrder.LevelName && agentModel.MembershipExpiryTime.Valid + if isRenewal { + logx.Infof("代理会员续费成功,会员ID:%d,等级:%s", agentModel.Id, agentModel.LevelName) + } else { + logx.Infof("代理会员新购或升级成功,会员ID:%d,等级:%s", agentModel.Id, agentModel.LevelName) + } + agentModel.MembershipExpiryTime = lzUtils.RenewMembership(agentModel.MembershipExpiryTime) + + if updateErr := l.svcCtx.AgentModel.UpdateWithVersion(l.ctx, nil, agentModel); updateErr != nil { + return fmt.Errorf("修改代理信息失败: %+v", updateErr) + } + return nil + }) + if err != nil { + logx.Errorf("支付宝支付回调,处理代理会员订单失败: %+v", err) + refundErr := l.handleRefund(agentOrder) + if refundErr != nil { + logx.Errorf("支付宝支付回调,退款失败: %+v", refundErr) + } + return nil + } + } + + alipay.ACKNotification(w) + return nil +} + +func (l *AlipayCallbackLogic) handleRefund(order *model.AgentMembershipRechargeOrder) error { + ctx := context.Background() + // 退款 + if order.PaymentMethod == "wechat" { + refundErr := l.svcCtx.WechatPayService.WeChatRefund(ctx, order.OrderNo, order.Amount, order.Amount) + if refundErr != nil { + return refundErr + } + } else { + refund, refundErr := l.svcCtx.AlipayService.AliRefund(ctx, order.OrderNo, order.Amount) + if refundErr != nil { + return refundErr + } + if refund.IsSuccess() { + logx.Errorf("支付宝退款成功, orderID: %d", order.Id) + // 更新订单状态为退款 + order.Status = "refunded" + updateOrderErr := l.svcCtx.AgentMembershipRechargeOrderModel.UpdateWithVersion(ctx, nil, order) + if updateOrderErr != nil { + logx.Errorf("更新订单状态失败,订单ID: %d, 错误: %v", order.Id, updateOrderErr) + return fmt.Errorf("更新订单状态失败: %v", updateOrderErr) + } + return nil + } else { + logx.Errorf("支付宝退款失败:%v", refundErr) + return refundErr + } + // 直接成功 + } + return nil +} diff --git a/app/main/api/internal/logic/pay/iapcallbacklogic.go b/app/main/api/internal/logic/pay/iapcallbacklogic.go new file mode 100644 index 0000000..99f7b59 --- /dev/null +++ b/app/main/api/internal/logic/pay/iapcallbacklogic.go @@ -0,0 +1,82 @@ +package pay + +import ( + "context" + "time" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/lzUtils" + + "github.com/pkg/errors" + + "github.com/zeromicro/go-zero/core/logx" +) + +type IapCallbackLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewIapCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *IapCallbackLogic { + return &IapCallbackLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *IapCallbackLogic) IapCallback(req *types.IapCallbackReq) error { + // Step 1: 查找订单 + order, findOrderErr := l.svcCtx.OrderModel.FindOne(l.ctx, req.OrderID) + if findOrderErr != nil { + logx.Errorf("苹果内购支付回调,查找订单失败: %+v", findOrderErr) + return nil + } + + // Step 2: 验证订单状态 + if order.Status != "pending" { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "苹果内购支付回调, 订单状态异常: %+v", order) + } + + // Step 3: 调用 VerifyReceipt 验证苹果支付凭证 + //receipt := req.TransactionReceipt // 从请求中获取支付凭证 + //verifyResponse, verifyErr := l.svcCtx.ApplePayService.VerifyReceipt(l.ctx, receipt) + //if verifyErr != nil { + // return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "苹果内购支付回调, 验证订单异常: %+v", verifyErr) + //} + + // Step 4: 验证订单 + //product, findProductErr := l.svcCtx.ProductModel.FindOne(l.ctx, order.Id) + //if findProductErr != nil { + // return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "苹果内购支付回调, 获取订单相关商品失败: %+v", findProductErr) + //} + //isProductMatched := false + //appleProductID := l.svcCtx.ApplePayService.GetIappayAppID(product.ProductEn) + //for _, item := range verifyResponse.Receipt.InApp { + // if item.ProductID == appleProductID { + // isProductMatched = true + // order.PlatformOrderId = lzUtils.StringToNullString(item.TransactionID) // 记录交易 ID + // break + // } + //} + //if !isProductMatched { + // return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "苹果内购支付回调, 商品 ID 不匹配,订单 ID: %d, 回调苹果商品 ID: %s", order.Id, verifyResponse.Receipt.InApp[0].ProductID) + //} + + // Step 5: 更新订单状态 mm + order.Status = "paid" + order.PayTime = lzUtils.TimeToNullTime(time.Now()) + + // 更新订单到数据库 + if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "苹果内购支付回调, 修改订单信息失败: %+v", updateErr) + } + + // Step 6: 处理订单完成后的逻辑 + if asyncErr := l.svcCtx.AsynqService.SendQueryTask(order.Id); asyncErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "苹果内购支付回调,异步任务调度失败: %v", asyncErr) + } + return nil +} diff --git a/app/main/api/internal/logic/pay/paymentchecklogic.go b/app/main/api/internal/logic/pay/paymentchecklogic.go new file mode 100644 index 0000000..edc58d6 --- /dev/null +++ b/app/main/api/internal/logic/pay/paymentchecklogic.go @@ -0,0 +1,49 @@ +package pay + +import ( + "context" + "strings" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type PaymentCheckLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewPaymentCheckLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentCheckLogic { + return &PaymentCheckLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PaymentCheckLogic) PaymentCheck(req *types.PaymentCheckReq) (resp *types.PaymentCheckResp, err error) { + if strings.HasPrefix(req.OrderNo, "A_") { + order, err := l.svcCtx.AgentMembershipRechargeOrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", err) + } + return &types.PaymentCheckResp{ + Type: "agent_vip", + Status: order.Status, + }, nil + } else { + order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询订单失败: %v", err) + } + return &types.PaymentCheckResp{ + Type: "query", + Status: order.Status, + }, nil + } +} diff --git a/app/main/api/internal/logic/pay/paymentlogic.go b/app/main/api/internal/logic/pay/paymentlogic.go new file mode 100644 index 0000000..cd5192c --- /dev/null +++ b/app/main/api/internal/logic/pay/paymentlogic.go @@ -0,0 +1,227 @@ +package pay + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + "github.com/redis/go-redis/v9" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type PaymentLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} +type PaymentTypeResp struct { + amount float64 + outTradeNo string + description string +} + +func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLogic { + return &PaymentLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp, err error) { + var paymentTypeResp *PaymentTypeResp + var prepayData interface{} + l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + switch req.PayType { + case "agent_vip": + paymentTypeResp, err = l.AgentVipOrderPayment(req, session) + if err != nil { + return err + } + + case "query": + paymentTypeResp, err = l.QueryOrderPayment(req, session) + if err != nil { + return err + } + } + + var createOrderErr error + if req.PayMethod == "wechat" { + prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo) + } else if req.PayMethod == "alipay" { + prepayData, createOrderErr = l.svcCtx.AlipayService.CreateAlipayOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo) + } else if req.PayMethod == "appleiap" { + prepayData = l.svcCtx.ApplePayService.GetIappayAppID(paymentTypeResp.outTradeNo) + } + if createOrderErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 创建支付订单失败: %+v", createOrderErr) + } + return nil + }) + if err != nil { + return nil, err + } + switch v := prepayData.(type) { + case string: + // 如果 prepayData 是字符串类型,直接返回 + return &types.PaymentResp{PrepayId: v, OrderNo: paymentTypeResp.outTradeNo}, nil + default: + return &types.PaymentResp{PrepayData: prepayData, OrderNo: paymentTypeResp.outTradeNo}, nil + } +} + +func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Session) (resp *PaymentTypeResp, err error) { + userID, getUidErr := ctxdata.GetUidFromCtx(l.ctx) + if getUidErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取用户信息失败, %+v", getUidErr) + } + outTradeNo := req.Id + redisKey := fmt.Sprintf(types.QueryCacheKey, userID, outTradeNo) + cache, cacheErr := l.svcCtx.Redis.GetCtx(l.ctx, redisKey) + if cacheErr != nil { + if cacheErr == redis.Nil { + return nil, errors.Wrapf(xerr.NewErrMsg("订单已过期"), "生成订单, 缓存不存在, %+v", cacheErr) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取缓存失败, %+v", cacheErr) + } + var data types.QueryCacheLoad + err = json.Unmarshal([]byte(cache), &data) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 解析缓存内容失败, %v", err) + } + + product, err := l.svcCtx.ProductModel.FindOneByProductEn(l.ctx, data.Product) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 查找产品错误: %v", err) + } + + var amount float64 + user, err := l.svcCtx.UserModel.FindOne(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 获取用户信息失败: %v", err) + } + + if data.AgentIdentifier != "" { + agentLinkModel, findAgentLinkErr := l.svcCtx.AgentLinkModel.FindOneByLinkIdentifier(l.ctx, data.AgentIdentifier) + if findAgentLinkErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 获取代理订单失败: %+v", findAgentLinkErr) + } + amount = agentLinkModel.Price + } else { + amount = product.SellPrice + } + + if user.Inside == 1 { + amount = 0.01 + } + var orderID int64 + order := model.Order{ + OrderNo: outTradeNo, + UserId: userID, + ProductId: product.Id, + PaymentPlatform: req.PayMethod, + PaymentScene: "app", + Amount: amount, + Status: "pending", + } + orderInsertResult, insertOrderErr := l.svcCtx.OrderModel.Insert(l.ctx, session, &order) + if insertOrderErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存订单失败: %+v", insertOrderErr) + } + insertedOrderID, lastInsertIdErr := orderInsertResult.LastInsertId() + if lastInsertIdErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 获取保存订单ID失败: %+v", lastInsertIdErr) + } + orderID = insertedOrderID + + if data.AgentIdentifier != "" { + agent, parsingErr := l.agentParsing(data.AgentIdentifier) + if parsingErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 解析代理标识符失败: %+v", parsingErr) + } + var agentOrder model.AgentOrder + agentOrder.OrderId = orderID + agentOrder.AgentId = agent.AgentID + _, agentOrderInsert := l.svcCtx.AgentOrderModel.Insert(l.ctx, session, &agentOrder) + if agentOrderInsert != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存代理订单失败: %+v", agentOrderInsert) + } + } + return &PaymentTypeResp{amount: amount, outTradeNo: outTradeNo, description: product.ProductName}, nil +} +func (l *PaymentLogic) AgentVipOrderPayment(req *types.PaymentReq, session sqlx.Session) (resp *PaymentTypeResp, err error) { + userID, getUidErr := ctxdata.GetUidFromCtx(l.ctx) + if getUidErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取用户信息失败, %+v", getUidErr) + } + user, err := l.svcCtx.UserModel.FindOne(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 获取用户信息失败: %v", err) + } + // 查询用户代理信息 + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败: %v", err) + } + redisKey := fmt.Sprintf(types.AgentVipCacheKey, userID, req.Id) + cache, cacheErr := l.svcCtx.Redis.GetCtx(l.ctx, redisKey) + if cacheErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取缓存失败, %+v", cacheErr) + } + var agentVipCache types.AgentVipCache + err = json.Unmarshal([]byte(cache), &agentVipCache) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 解析缓存内容失败, %+v", err) + } + agentMembershipConfig, err := l.svcCtx.AgentMembershipConfigModel.FindOneByLevelName(l.ctx, agentVipCache.Type) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取代理会员配置失败, %+v", err) + } + + amount := agentMembershipConfig.Price.Float64 + if user.Inside == 1 { + amount = 0.01 + } + agentMembershipRechargeOrder := model.AgentMembershipRechargeOrder{ + OrderNo: req.Id, + UserId: userID, + AgentId: agentModel.Id, + Amount: amount, + PaymentMethod: req.PayMethod, + LevelName: agentVipCache.Type, + Status: "pending", + } + _, err = l.svcCtx.AgentMembershipRechargeOrderModel.Insert(l.ctx, session, &agentMembershipRechargeOrder) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存代理会员充值订单失败: %+v", err) + } + return &PaymentTypeResp{amount: amount, outTradeNo: req.Id, description: fmt.Sprintf("%s代理会员充值", agentMembershipConfig.LevelName)}, nil +} +func (l *PaymentLogic) agentParsing(agentIdentifier string) (*types.AgentIdentifier, error) { + key, decodeErr := hex.DecodeString("8e3e7a2f60edb49221e953b9c029ed10") + if decodeErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 获取AES密钥失败: %+v", decodeErr) + } + // Encrypt the params + + encrypted, err := crypto.AesDecryptURL(agentIdentifier, key) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, %v", err) + } + var agentIdentifierStruct types.AgentIdentifier + err = json.Unmarshal(encrypted, &agentIdentifierStruct) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务,反序列化失败 %v", err) + } + return &agentIdentifierStruct, nil +} diff --git a/app/main/api/internal/logic/pay/wechatpaycallbacklogic.go b/app/main/api/internal/logic/pay/wechatpaycallbacklogic.go new file mode 100644 index 0000000..bd5b899 --- /dev/null +++ b/app/main/api/internal/logic/pay/wechatpaycallbacklogic.go @@ -0,0 +1,215 @@ +package pay + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + "znc-server/app/main/api/internal/service" + "znc-server/app/main/model" + "znc-server/pkg/lzkit/lzUtils" + + "znc-server/app/main/api/internal/svc" + + "github.com/wechatpay-apiv3/wechatpay-go/services/payments" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type WechatPayCallbackLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewWechatPayCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *WechatPayCallbackLogic { + return &WechatPayCallbackLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *WechatPayCallbackLogic) WechatPayCallback(w http.ResponseWriter, r *http.Request) error { + notification, err := l.svcCtx.WechatPayService.HandleWechatPayNotification(l.ctx, r) + if err != nil { + logx.Errorf("微信支付回调,%v", err) + return nil + } + + // 根据订单号前缀判断订单类型 + orderNo := *notification.OutTradeNo + if strings.HasPrefix(orderNo, "Q_") { + // 查询订单处理 + return l.handleQueryOrderPayment(w, notification) + } else if strings.HasPrefix(orderNo, "A_") { + // 代理会员订单处理 + return l.handleAgentVipOrderPayment(w, notification) + } else { + // 兼容旧订单,假设没有前缀的是查询订单 + return l.handleQueryOrderPayment(w, notification) + } +} + +// 处理查询订单支付 +func (l *WechatPayCallbackLogic) handleQueryOrderPayment(w http.ResponseWriter, notification *payments.Transaction) error { + order, findOrderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, *notification.OutTradeNo) + if findOrderErr != nil { + logx.Errorf("微信支付回调,查找订单信息失败: %+v", findOrderErr) + return nil + } + + amount := lzUtils.ToWechatAmount(order.Amount) + if amount != *notification.Amount.Total { + logx.Errorf("微信支付回调,金额不一致") + return nil + } + + if order.Status != "pending" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("success")) + return nil + } + + switch *notification.TradeState { + case service.TradeStateSuccess: + order.Status = "paid" + order.PayTime = lzUtils.TimeToNullTime(time.Now()) + case service.TradeStateClosed: + order.Status = "closed" + order.CloseTime = lzUtils.TimeToNullTime(time.Now()) + case service.TradeStateRevoked: + order.Status = "failed" + default: + return nil + } + + order.PlatformOrderId = lzUtils.StringToNullString(*notification.TransactionId) + if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil { + logx.Errorf("微信支付回调,更新订单失败%+v", updateErr) + return nil + } + + if order.Status == "paid" { + if asyncErr := l.svcCtx.AsynqService.SendQueryTask(order.Id); asyncErr != nil { + logx.Errorf("异步任务调度失败: %v", asyncErr) + return asyncErr + } + } + + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("success")) + return nil +} + +// 处理代理会员订单支付 +func (l *WechatPayCallbackLogic) handleAgentVipOrderPayment(w http.ResponseWriter, notification *payments.Transaction) error { + agentOrder, findAgentOrderErr := l.svcCtx.AgentMembershipRechargeOrderModel.FindOneByOrderNo(l.ctx, *notification.OutTradeNo) + if findAgentOrderErr != nil { + logx.Errorf("微信支付回调,查找代理会员订单失败: %+v", findAgentOrderErr) + return nil + } + + if agentOrder.Status != "pending" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("success")) + return nil + } + + user, err := l.svcCtx.UserModel.FindOne(l.ctx, agentOrder.UserId) + if err != nil { + logx.Errorf("微信支付回调,查找用户失败: %+v", err) + return nil + } + + amount := lzUtils.ToWechatAmount(agentOrder.Amount) + if user.Inside != 1 { + if amount != *notification.Amount.Total { + logx.Errorf("微信支付回调,金额不一致") + return nil + } + } + + switch *notification.TradeState { + case service.TradeStateSuccess: + agentOrder.Status = "paid" + default: + return nil + } + + if agentOrder.Status == "paid" { + err = l.svcCtx.AgentModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error { + agentModel, err := l.svcCtx.AgentModel.FindOne(transCtx, agentOrder.AgentId) + if err != nil { + return fmt.Errorf("查找代理信息失败: %+v", err) + } + + agentOrder.PlatformOrderId = lzUtils.StringToNullString(*notification.TransactionId) + if updateErr := l.svcCtx.AgentMembershipRechargeOrderModel.UpdateWithVersion(l.ctx, nil, agentOrder); updateErr != nil { + return fmt.Errorf("修改代理会员订单信息失败: %+v", updateErr) + } + + // 设置会员等级 + agentModel.LevelName = agentOrder.LevelName + + // 延长会员时间 + isRenewal := agentModel.LevelName == agentOrder.LevelName && agentModel.MembershipExpiryTime.Valid + if isRenewal { + logx.Infof("代理会员续费成功,会员ID:%d,等级:%s", agentModel.Id, agentModel.LevelName) + } else { + logx.Infof("代理会员新购或升级成功,会员ID:%d,等级:%s", agentModel.Id, agentModel.LevelName) + } + agentModel.MembershipExpiryTime = lzUtils.RenewMembership(agentModel.MembershipExpiryTime) + + if updateErr := l.svcCtx.AgentModel.UpdateWithVersion(l.ctx, nil, agentModel); updateErr != nil { + return fmt.Errorf("修改代理信息失败: %+v", updateErr) + } + return nil + }) + + if err != nil { + logx.Errorf("微信支付回调,处理代理会员订单失败: %+v", err) + refundErr := l.handleRefund(agentOrder) + if refundErr != nil { + logx.Errorf("微信支付回调,退款失败: %+v", refundErr) + } + return nil + } + } + + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("success")) + return nil +} + +func (l *WechatPayCallbackLogic) handleRefund(order *model.AgentMembershipRechargeOrder) error { + ctx := context.Background() + // 退款 + if order.PaymentMethod == "wechat" { + refundErr := l.svcCtx.WechatPayService.WeChatRefund(ctx, order.OrderNo, order.Amount, order.Amount) + if refundErr != nil { + return refundErr + } + } else { + refund, refundErr := l.svcCtx.AlipayService.AliRefund(ctx, order.OrderNo, order.Amount) + if refundErr != nil { + return refundErr + } + if refund.IsSuccess() { + logx.Errorf("支付宝退款成功, orderID: %d", order.Id) + // 更新订单状态为退款 + order.Status = "refunded" + updateOrderErr := l.svcCtx.AgentMembershipRechargeOrderModel.UpdateWithVersion(ctx, nil, order) + if updateOrderErr != nil { + logx.Errorf("更新订单状态失败,订单ID: %d, 错误: %v", order.Id, updateOrderErr) + return fmt.Errorf("更新订单状态失败: %v", updateOrderErr) + } + return nil + } else { + logx.Errorf("支付宝退款失败:%v", refundErr) + return refundErr + } + } + return nil +} diff --git a/app/main/api/internal/logic/pay/wechatpayrefundcallbacklogic.go b/app/main/api/internal/logic/pay/wechatpayrefundcallbacklogic.go new file mode 100644 index 0000000..0f668c5 --- /dev/null +++ b/app/main/api/internal/logic/pay/wechatpayrefundcallbacklogic.go @@ -0,0 +1,55 @@ +package pay + +import ( + "context" + "net/http" + "znc-server/app/main/api/internal/svc" + + "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic" + "github.com/zeromicro/go-zero/core/logx" +) + +type WechatPayRefundCallbackLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewWechatPayRefundCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *WechatPayRefundCallbackLogic { + return &WechatPayRefundCallbackLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *WechatPayRefundCallbackLogic) WechatPayRefundCallback(w http.ResponseWriter, r *http.Request) error { + notification, err := l.svcCtx.WechatPayService.HandleRefundNotification(l.ctx, r) + if err != nil { + logx.Errorf("微信退款回调,%v", err) + return nil + } + order, findOrderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, *notification.OutTradeNo) + if findOrderErr != nil { + logx.Errorf("微信退款回调,查找订单信息失败: %+v", findOrderErr) + return nil + } + + switch *notification.Status { + case refunddomestic.STATUS_SUCCESS: + order.Status = "refunded" + case refunddomestic.STATUS_ABNORMAL: + // 异常 + return nil + default: + return nil + } + if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil { + logx.Errorf("微信退款回调,更新订单失败%+v", updateErr) + return nil + } + // 响应微信回调成功 + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("success")) // 确保只写入一次响应 + return nil +} diff --git a/app/main/api/internal/logic/product/getproductappbyenlogic.go b/app/main/api/internal/logic/product/getproductappbyenlogic.go new file mode 100644 index 0000000..1f2754d --- /dev/null +++ b/app/main/api/internal/logic/product/getproductappbyenlogic.go @@ -0,0 +1,75 @@ +package product + +import ( + "context" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/mr" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetProductAppByEnLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetProductAppByEnLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetProductAppByEnLogic { + return &GetProductAppByEnLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetProductAppByEnLogic) GetProductAppByEn(req *types.GetProductByEnRequest) (resp *types.ProductResponse, err error) { + productModel, err := l.svcCtx.ProductModel.FindOneByProductEn(l.ctx, req.ProductEn) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "产品查询, 查找产品错误: %v", err) + } + + build := l.svcCtx.ProductFeatureModel.SelectBuilder().Where(squirrel.Eq{ + "product_id": productModel.Id, + }) + productFeatureAll, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, build, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "产品查询, 查找产品关联错误: %v", err) + } + var product types.Product + err = copier.Copy(&product, productModel) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, 用户信息结构体复制失败, %v", err) + } + mr.MapReduceVoid(func(source chan<- interface{}) { + for _, productFeature := range productFeatureAll { + source <- productFeature.FeatureId + } + }, func(item interface{}, writer mr.Writer[*model.Feature], cancel func(error)) { + id := item.(int64) + + feature, findFeatureErr := l.svcCtx.FeatureModel.FindOne(l.ctx, id) + if findFeatureErr != nil { + logx.WithContext(l.ctx).Errorf("产品查询, 查找关联feature错误: %d, err:%v", id, findFeatureErr) + return + } + if feature != nil && feature.Id > 0 { + writer.Write(feature) + } + }, func(pipe <-chan *model.Feature, cancel func(error)) { + for item := range pipe { + var feature types.Feature + _ = copier.Copy(&feature, item) + product.Features = append(product.Features, feature) + } + }) + + return &types.ProductResponse{Product: product}, nil +} diff --git a/app/main/api/internal/logic/product/getproductbyenlogic.go b/app/main/api/internal/logic/product/getproductbyenlogic.go new file mode 100644 index 0000000..cfaeafb --- /dev/null +++ b/app/main/api/internal/logic/product/getproductbyenlogic.go @@ -0,0 +1,75 @@ +package product + +import ( + "context" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/mr" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetProductByEnLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetProductByEnLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetProductByEnLogic { + return &GetProductByEnLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetProductByEnLogic) GetProductByEn(req *types.GetProductByEnRequest) (resp *types.ProductResponse, err error) { + productModel, err := l.svcCtx.ProductModel.FindOneByProductEn(l.ctx, req.ProductEn) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "产品查询, 查找产品错误: %v", err) + } + + build := l.svcCtx.ProductFeatureModel.SelectBuilder().Where(squirrel.Eq{ + "product_id": productModel.Id, + }) + productFeatureAll, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, build, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "产品查询, 查找产品关联错误: %v", err) + } + var product types.Product + err = copier.Copy(&product, productModel) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, 用户信息结构体复制失败, %v", err) + } + mr.MapReduceVoid(func(source chan<- interface{}) { + for _, productFeature := range productFeatureAll { + source <- productFeature.FeatureId + } + }, func(item interface{}, writer mr.Writer[*model.Feature], cancel func(error)) { + id := item.(int64) + + feature, findFeatureErr := l.svcCtx.FeatureModel.FindOne(l.ctx, id) + if findFeatureErr != nil { + logx.WithContext(l.ctx).Errorf("产品查询, 查找关联feature错误: %d, err:%v", id, findFeatureErr) + return + } + if feature != nil && feature.Id > 0 { + writer.Write(feature) + } + }, func(pipe <-chan *model.Feature, cancel func(error)) { + for item := range pipe { + var feature types.Feature + _ = copier.Copy(&feature, item) + product.Features = append(product.Features, feature) + } + }) + + return &types.ProductResponse{Product: product}, nil +} diff --git a/app/main/api/internal/logic/product/getproductbyidlogic.go b/app/main/api/internal/logic/product/getproductbyidlogic.go new file mode 100644 index 0000000..b4c9c3e --- /dev/null +++ b/app/main/api/internal/logic/product/getproductbyidlogic.go @@ -0,0 +1,30 @@ +package product + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetProductByIDLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetProductByIDLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetProductByIDLogic { + return &GetProductByIDLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetProductByIDLogic) GetProductByID(req *types.GetProductByIDRequest) (resp *types.ProductResponse, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/app/main/api/internal/logic/query/querydetailbyorderidlogic.go b/app/main/api/internal/logic/query/querydetailbyorderidlogic.go new file mode 100644 index 0000000..95ebd0f --- /dev/null +++ b/app/main/api/internal/logic/query/querydetailbyorderidlogic.go @@ -0,0 +1,213 @@ +package query + +import ( + "context" + "database/sql" + "encoding/hex" + "encoding/json" + "fmt" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + "znc-server/pkg/lzkit/lzUtils" + + "github.com/jinzhu/copier" + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type QueryDetailByOrderIdLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewQueryDetailByOrderIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryDetailByOrderIdLogic { + return &QueryDetailByOrderIdLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryDetailByOrderIdLogic) QueryDetailByOrderId(req *types.QueryDetailByOrderIdReq) (resp *types.QueryDetailByOrderIdResp, err error) { + // 获取当前用户ID + userId, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户ID失败: %v", err) + } + + // 获取订单信息 + order, err := l.svcCtx.OrderModel.FindOne(l.ctx, req.OrderId) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.LOGIC_QUERY_NOT_FOUND), "报告查询, 订单不存在: %v", err) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告查询, 查找报告错误: %v", err) + } + user, err := l.svcCtx.UserModel.FindOne(l.ctx, userId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告查询, 查找用户错误: %v", err) + } + if user.Inside != 1 { + // 安全验证:确保订单属于当前用户 + if order.UserId != userId { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.LOGIC_QUERY_NOT_FOUND), "无权查看此订单报告") + } + } + + // 检查订单状态 + if order.Status != "paid" { + return nil, errors.Wrapf(xerr.NewErrMsg("订单未支付,无法查看报告"), "") + } + + // 获取报告信息 + queryModel, err := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, req.OrderId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告查询, 查找报告错误: %v", err) + } + + var query types.Query + query.CreateTime = queryModel.CreateTime.Format("2006-01-02 15:04:05") + query.UpdateTime = queryModel.UpdateTime.Format("2006-01-02 15:04:05") + + // 解密查询数据 + secretKey := l.svcCtx.Config.Encrypt.SecretKey + key, decodeErr := hex.DecodeString(secretKey) + if decodeErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 获取AES解密解药失败, %v", err) + } + processParamsErr := ProcessQueryParams(queryModel.QueryParams, &query.QueryParams, key) + if processParamsErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告参数处理失败: %v", processParamsErr) + } + processErr := ProcessQueryData(queryModel.QueryData, &query.QueryData, key) + if processErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结果处理失败: %v", processErr) + } + updateFeatureAndProductFeatureErr := l.UpdateFeatureAndProductFeature(queryModel.ProductId, &query.QueryData) + if updateFeatureAndProductFeatureErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结果处理失败: %v", updateFeatureAndProductFeatureErr) + } + // 复制报告数据 + err = copier.Copy(&query, queryModel) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结构体复制失败, %v", err) + } + product, err := l.svcCtx.ProductModel.FindOne(l.ctx, queryModel.ProductId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 获取商品信息失败, %v", err) + } + query.ProductName = product.ProductName + return &types.QueryDetailByOrderIdResp{ + Query: query, + }, nil +} + +// ProcessQueryData 解密和反序列化 QueryData +func ProcessQueryData(queryData sql.NullString, target *[]types.QueryItem, key []byte) error { + queryDataStr := lzUtils.NullStringToString(queryData) + if queryDataStr == "" { + return nil + } + + // 解密数据 + decryptedData, decryptErr := crypto.AesDecrypt(queryDataStr, key) + if decryptErr != nil { + return decryptErr + } + + // 解析 JSON 数组 + var decryptedArray []map[string]interface{} + unmarshalErr := json.Unmarshal(decryptedData, &decryptedArray) + if unmarshalErr != nil { + return unmarshalErr + } + + // 确保 target 具有正确的长度 + if len(*target) == 0 { + *target = make([]types.QueryItem, len(decryptedArray)) + } + + // 填充解密后的数据到 target + for i := 0; i < len(decryptedArray); i++ { + // 直接填充解密数据到 Data 字段 + (*target)[i].Data = decryptedArray[i] + } + return nil +} +func (l *QueryDetailByOrderIdLogic) UpdateFeatureAndProductFeature(productID int64, target *[]types.QueryItem) error { + // 遍历 target 数组,使用倒序遍历,以便删除元素时不影响索引 + for i := len(*target) - 1; i >= 0; i-- { + queryItem := &(*target)[i] + + // 确保 Data 为 map 类型 + data, ok := queryItem.Data.(map[string]interface{}) + if !ok { + return fmt.Errorf("queryItem.Data 必须是 map[string]interface{} 类型") + } + + // 从 Data 中获取 apiID + apiID, ok := data["apiID"].(string) + if !ok { + return fmt.Errorf("queryItem.Data 中的 apiID 必须是字符串类型") + } + + // 查询 Feature + feature, err := l.svcCtx.FeatureModel.FindOneByApiId(l.ctx, apiID) + if err != nil { + // 如果 Feature 查不到,也要删除当前 QueryItem + *target = append((*target)[:i], (*target)[i+1:]...) + continue + } + + // 查询 ProductFeatureModel + builder := l.svcCtx.ProductFeatureModel.SelectBuilder().Where("product_id = ?", productID) + productFeatures, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, builder, "") + if err != nil { + return fmt.Errorf("查询 ProductFeatureModel 错误: %v", err) + } + + // 遍历 productFeatures,找到与 feature.ID 关联且 enable == 1 的项 + var featureData map[string]interface{} + // foundFeature := false + sort := 0 + for _, pf := range productFeatures { + if pf.FeatureId == feature.Id { // 确保和 Feature 关联 + sort = int(pf.Sort) + break // 找到第一个符合条件的就退出循环 + } + } + featureData = map[string]interface{}{ + "featureName": feature.Name, + "sort": sort, + } + + // 更新 queryItem 的 Feature 字段(不是数组) + queryItem.Feature = featureData + } + + return nil +} + +// ProcessQueryParams解密和反序列化 QueryParams +func ProcessQueryParams(QueryParams string, target *map[string]interface{}, key []byte) error { + // 解密 QueryParams + decryptedData, decryptErr := crypto.AesDecrypt(QueryParams, key) + if decryptErr != nil { + return decryptErr + } + + // 反序列化解密后的数据 + unmarshalErr := json.Unmarshal(decryptedData, target) + if unmarshalErr != nil { + return unmarshalErr + } + + return nil +} diff --git a/app/main/api/internal/logic/query/querydetailbyordernologic.go b/app/main/api/internal/logic/query/querydetailbyordernologic.go new file mode 100644 index 0000000..76f0fad --- /dev/null +++ b/app/main/api/internal/logic/query/querydetailbyordernologic.go @@ -0,0 +1,155 @@ +package query + +import ( + "context" + "encoding/hex" + "fmt" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/jinzhu/copier" + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + + "github.com/zeromicro/go-zero/core/logx" +) + +type QueryDetailByOrderNoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewQueryDetailByOrderNoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryDetailByOrderNoLogic { + return &QueryDetailByOrderNoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryDetailByOrderNoLogic) QueryDetailByOrderNo(req *types.QueryDetailByOrderNoReq) (resp *types.QueryDetailByOrderNoResp, err error) { + // 获取当前用户ID + userId, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户ID失败: %v", err) + } + + // 获取订单信息 + order, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.LOGIC_QUERY_NOT_FOUND), "报告查询, 订单不存在: %v", err) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告查询, 查找报告错误: %v", err) + } + + // 安全验证:确保订单属于当前用户 + if order.UserId != userId { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.LOGIC_QUERY_NOT_FOUND), "无权查看此订单报告") + } + + // 检查订单状态 + if order.Status != "paid" { + return nil, errors.Wrapf(xerr.NewErrMsg("订单未支付,无法查看报告"), "") + } + + // 获取报告信息 + queryModel, err := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, order.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告查询, 查找报告错误: %v", err) + } + + var query types.Query + query.CreateTime = queryModel.CreateTime.Format("2006-01-02 15:04:05") + query.UpdateTime = queryModel.UpdateTime.Format("2006-01-02 15:04:05") + + // 解密查询数据 + secretKey := l.svcCtx.Config.Encrypt.SecretKey + key, decodeErr := hex.DecodeString(secretKey) + if decodeErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 获取AES解密解药失败, %v", err) + } + processParamsErr := ProcessQueryParams(queryModel.QueryParams, &query.QueryParams, key) + if processParamsErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告参数处理失败: %v", processParamsErr) + } + processErr := ProcessQueryData(queryModel.QueryData, &query.QueryData, key) + if processErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结果处理失败: %v", processErr) + } + updateFeatureAndProductFeatureErr := l.UpdateFeatureAndProductFeature(queryModel.ProductId, &query.QueryData) + if updateFeatureAndProductFeatureErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结果处理失败: %v", updateFeatureAndProductFeatureErr) + } + // 复制报告数据 + err = copier.Copy(&query, queryModel) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结构体复制失败, %v", err) + } + product, err := l.svcCtx.ProductModel.FindOne(l.ctx, queryModel.ProductId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 获取商品信息失败, %v", err) + } + query.ProductName = product.ProductName + return &types.QueryDetailByOrderNoResp{ + Query: query, + }, nil +} + +func (l *QueryDetailByOrderNoLogic) UpdateFeatureAndProductFeature(productID int64, target *[]types.QueryItem) error { + // 遍历 target 数组,使用倒序遍历,以便删除元素时不影响索引 + for i := len(*target) - 1; i >= 0; i-- { + queryItem := &(*target)[i] + + // 确保 Data 为 map 类型 + data, ok := queryItem.Data.(map[string]interface{}) + if !ok { + return fmt.Errorf("queryItem.Data 必须是 map[string]interface{} 类型") + } + + // 从 Data 中获取 apiID + apiID, ok := data["apiID"].(string) + if !ok { + return fmt.Errorf("queryItem.Data 中的 apiID 必须是字符串类型") + } + + // 查询 Feature + feature, err := l.svcCtx.FeatureModel.FindOneByApiId(l.ctx, apiID) + if err != nil { + // 如果 Feature 查不到,也要删除当前 QueryItem + *target = append((*target)[:i], (*target)[i+1:]...) + continue + } + + // 查询 ProductFeatureModel + builder := l.svcCtx.ProductFeatureModel.SelectBuilder().Where("product_id = ?", productID) + productFeatures, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, builder, "") + if err != nil { + return fmt.Errorf("查询 ProductFeatureModel 错误: %v", err) + } + + // 遍历 productFeatures,找到与 feature.ID 关联且 enable == 1 的项 + var featureData map[string]interface{} + // foundFeature := false + sort := 0 + for _, pf := range productFeatures { + if pf.FeatureId == feature.Id { // 确保和 Feature 关联 + sort = int(pf.Sort) + break // 找到第一个符合条件的就退出循环 + } + } + featureData = map[string]interface{}{ + "featureName": feature.Name, + "sort": sort, + } + + // 更新 queryItem 的 Feature 字段(不是数组) + queryItem.Feature = featureData + } + + return nil +} diff --git a/app/main/api/internal/logic/query/queryexamplelogic copy.go b/app/main/api/internal/logic/query/queryexamplelogic copy.go new file mode 100644 index 0000000..429db3b --- /dev/null +++ b/app/main/api/internal/logic/query/queryexamplelogic copy.go @@ -0,0 +1,152 @@ +package query + +// import ( +// "context" +// "encoding/hex" +// "fmt" +// "znc-server/app/main/api/internal/svc" +// "znc-server/app/main/api/internal/types" +// "znc-server/common/xerr" + +// "github.com/jinzhu/copier" +// "github.com/pkg/errors" + +// "github.com/zeromicro/go-zero/core/logx" +// ) + +// type QueryExampleLogic struct { +// logx.Logger +// ctx context.Context +// svcCtx *svc.ServiceContext +// } + +// func NewQueryExampleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryExampleLogic { +// return &QueryExampleLogic{ +// Logger: logx.WithContext(ctx), +// ctx: ctx, +// svcCtx: svcCtx, +// } +// } + +// func (l *QueryExampleLogic) QueryExample(req *types.QueryExampleReq) (resp *types.QueryExampleResp, err error) { +// var exampleID int64 +// switch req.Feature { +// case "backgroundcheck": +// exampleID = 508 +// case "companyinfo": +// exampleID = 506 +// case "homeservice": +// exampleID = 504 +// case "marriage": +// exampleID = 501 +// case "preloanbackgroundcheck": +// exampleID = 509 +// case "rentalinfo": +// exampleID = 505 +// case "riskassessment": +// exampleID = 503 + +// default: +// return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "示例报告, 获取示例报告失败: %v", err) +// } +// queryModel, err := l.svcCtx.QueryModel.FindOne(l.ctx, exampleID) +// if err != nil { +// return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "示例报告, 获取示例报告失败: %v", err) +// } +// var query types.Query +// query.CreateTime = queryModel.CreateTime.Format("2006-01-02 15:04:05") +// query.UpdateTime = queryModel.UpdateTime.Format("2006-01-02 15:04:05") + +// // 解密查询数据 +// secretKey := l.svcCtx.Config.Encrypt.SecretKey +// key, decodeErr := hex.DecodeString(secretKey) +// if decodeErr != nil { +// return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "示例报告, 获取AES解密解药失败, %v", err) +// } +// processParamsErr := ProcessQueryParams(queryModel.QueryParams, &query.QueryParams, key) +// if processParamsErr != nil { +// return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "示例报告, 报告参数处理失败: %v", processParamsErr) +// } +// processErr := ProcessQueryData(queryModel.QueryData, &query.QueryData, key) +// if processErr != nil { +// return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "示例报告, 报告结果处理失败: %v", processErr) +// } +// updateFeatureAndProductFeatureErr := l.UpdateFeatureAndProductFeature(queryModel.ProductId, &query.QueryData) +// if updateFeatureAndProductFeatureErr != nil { +// return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结果处理失败: %v", updateFeatureAndProductFeatureErr) +// } +// // 复制报告数据 +// err = copier.Copy(&query, queryModel) +// if err != nil { +// return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "示例报告, 报告结构体复制失败, %v", err) +// } +// product, err := l.svcCtx.ProductModel.FindOne(l.ctx, queryModel.ProductId) +// if err != nil { +// return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "示例报告, 获取商品信息失败, %v", err) +// } +// query.ProductName = product.ProductName +// return &types.QueryExampleResp{ +// Query: query, +// }, nil +// } +// func (l *QueryExampleLogic) UpdateFeatureAndProductFeature(productID int64, target *[]types.QueryItem) error { +// // 遍历 target 数组,使用倒序遍历,以便删除元素时不影响索引 +// for i := len(*target) - 1; i >= 0; i-- { +// queryItem := &(*target)[i] + +// // 确保 Data 为 map 类型 +// data, ok := queryItem.Data.(map[string]interface{}) +// if !ok { +// return fmt.Errorf("queryItem.Data 必须是 map[string]interface{} 类型") +// } + +// // 从 Data 中获取 apiID +// apiID, ok := data["apiID"].(string) +// if !ok { +// return fmt.Errorf("queryItem.Data 中的 apiID 必须是字符串类型") +// } + +// // 查询 Feature +// feature, err := l.svcCtx.FeatureModel.FindOneByApiId(l.ctx, apiID) +// if err != nil { +// // 如果 Feature 查不到,也要删除当前 QueryItem +// *target = append((*target)[:i], (*target)[i+1:]...) +// continue +// } + +// // 查询 ProductFeatureModel +// builder := l.svcCtx.ProductFeatureModel.SelectBuilder().Where("product_id = ?", productID) +// productFeatures, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, builder, "") +// if err != nil { +// return fmt.Errorf("查询 ProductFeatureModel 错误: %v", err) +// } + +// // 遍历 productFeatures,找到与 feature.ID 关联且 enable == 1 的项 +// var featureData map[string]interface{} +// foundFeature := false + +// for _, pf := range productFeatures { +// if pf.FeatureId == feature.Id { // 确保和 Feature 关联 +// foundFeature = true +// if pf.Enable == 1 { +// featureData = map[string]interface{}{ +// "featureName": feature.Name, +// "sort": pf.Sort, +// } +// break // 找到第一个符合条件的就退出循环 +// } +// } +// } + +// // 如果没有符合条件的 feature 或者 featureData 为空,则删除当前 queryItem +// if !foundFeature || featureData == nil { +// *target = append((*target)[:i], (*target)[i+1:]...) +// continue +// } + +// // 更新 queryItem 的 Feature 字段(不是数组) +// queryItem.Feature = featureData +// } + +// return nil +// } diff --git a/app/main/api/internal/logic/query/queryexamplelogic.go b/app/main/api/internal/logic/query/queryexamplelogic.go new file mode 100644 index 0000000..a0c7282 --- /dev/null +++ b/app/main/api/internal/logic/query/queryexamplelogic.go @@ -0,0 +1,110 @@ +package query + +import ( + "context" + "encoding/hex" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/bytedance/sonic" + "github.com/pkg/errors" + + "github.com/zeromicro/go-zero/core/logx" +) + +type QueryExampleLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewQueryExampleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryExampleLogic { + return &QueryExampleLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryExampleLogic) QueryExample(req *types.QueryExampleReq) (resp *types.QueryExampleResp, err error) { + // 根据产品特性标识获取产品信息 + product, err := l.svcCtx.ProductModel.FindOneByProductEn(l.ctx, req.Feature) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "示例报告, 获取商品信息失败, %v", err) + } + + // 创建一个空的Query结构体来存储结果 + query := types.Query{ + ProductName: product.ProductName, + QueryData: make([]types.QueryItem, 0), + QueryParams: make(map[string]interface{}), + } + query.QueryParams = map[string]interface{}{ + "id_card": "45000000000000000", + "mobile": "13700000000", + "name": "张老三", + } + // 查询ProductFeatureModel获取产品相关的功能列表 + builder := l.svcCtx.ProductFeatureModel.SelectBuilder().Where("product_id = ?", product.Id) + productFeatures, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, builder, "") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "示例报告, 查询 ProductFeatureModel 错误: %v", err) + } + // 从每个启用的特性获取示例数据并合并 + for _, pf := range productFeatures { + if pf.Enable != 1 { + continue // 跳过未启用的特性 + } + + // 根据特性ID查找示例数据 + example, err := l.svcCtx.ExampleModel.FindOneByFeatureId(l.ctx, pf.FeatureId) + if err != nil { + logx.Infof("示例报告, 特性ID %d 无示例数据: %v", pf.FeatureId, err) + continue // 如果没有示例数据就跳过 + } + + // 获取对应的Feature信息 + feature, err := l.svcCtx.FeatureModel.FindOne(l.ctx, pf.FeatureId) + if err != nil { + logx.Infof("示例报告, 无法获取特性ID %d 的信息: %v", pf.FeatureId, err) + continue + } + + var queryItem types.QueryItem + + // 解密查询数据 + secretKey := l.svcCtx.Config.Encrypt.SecretKey + key, decodeErr := hex.DecodeString(secretKey) + if decodeErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "示例报告, 获取AES解密解药失败, %v", err) + } + // 解析示例内容 + if example.Content == "000" { + queryItem.Data = example.Content + } else { + // 解密数据 + decryptedData, decryptErr := crypto.AesDecrypt(example.Content, key) + if decryptErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "示例报告, 解密数据失败: %v", decryptErr) + } + err = sonic.Unmarshal([]byte(decryptedData), &queryItem.Data) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "示例报告, 解析示例内容失败: %v", err) + } + } + + // 添加特性信息 + queryItem.Feature = map[string]interface{}{ + "featureName": feature.Name, + "sort": pf.Sort, + } + // 添加到查询数据中 + query.QueryData = append(query.QueryData, queryItem) + } + + return &types.QueryExampleResp{ + Query: query, + }, nil +} diff --git a/app/main/api/internal/logic/query/querygeneratesharelinklogic.go b/app/main/api/internal/logic/query/querygeneratesharelinklogic.go new file mode 100644 index 0000000..e2ca199 --- /dev/null +++ b/app/main/api/internal/logic/query/querygeneratesharelinklogic.go @@ -0,0 +1,111 @@ +package query + +import ( + "context" + "encoding/hex" + "encoding/json" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type QueryGenerateShareLinkLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewQueryGenerateShareLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryGenerateShareLinkLogic { + return &QueryGenerateShareLinkLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryGenerateShareLinkLogic) QueryGenerateShareLink(req *types.QueryGenerateShareLinkReq) (resp *types.QueryGenerateShareLinkResp, err error) { + userId, err := ctxdata.GetUidFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 获取用户ID失败: %v", err) + } + + // 检查参数 + if (req.OrderId == nil || *req.OrderId == 0) && (req.OrderNo == nil || *req.OrderNo == "") { + return nil, errors.Wrapf(xerr.NewErrMsg("订单ID和订单号不能同时为空"), "") + } + + var order *model.Order + // 优先使用OrderId查询 + if req.OrderId != nil && *req.OrderId != 0 { + order, err = l.svcCtx.OrderModel.FindOne(l.ctx, *req.OrderId) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrMsg("订单不存在"), "") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 获取订单失败: %v", err) + } + } else if req.OrderNo != nil && *req.OrderNo != "" { + // 使用OrderNo查询 + order, err = l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, *req.OrderNo) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrMsg("订单不存在"), "") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 获取订单失败: %v", err) + } + } else { + return nil, errors.Wrapf(xerr.NewErrMsg("订单ID和订单号不能同时为空"), "") + } + + if order.Status != model.OrderStatusPaid { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 订单未支付") + } + + query, err := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, order.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 获取查询失败: %v", err) + } + + if query.QueryState != model.QueryStateSuccess { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 查询未成功") + } + user, err := l.svcCtx.UserModel.FindOne(l.ctx, userId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 获取用户失败: %v", err) + } + if user.Inside != 1 { + if order.UserId != userId { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 无权操作此订单") + } + } + + expireAt := time.Now().Add(time.Duration(l.svcCtx.Config.Query.ShareLinkExpire) * time.Second) + payload := types.QueryShareLinkPayload{ + OrderId: order.Id, // 使用查询到的订单ID + ExpireAt: expireAt.Unix(), + } + secretKey := l.svcCtx.Config.Encrypt.SecretKey + key, err := hex.DecodeString(secretKey) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 解密失败: %v", err) + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 序列化失败: %v", err) + } + encryptedPayload, err := crypto.AesEncryptURL(payloadBytes, key) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成分享链接, 加密失败: %v", err) + } + return &types.QueryGenerateShareLinkResp{ + ShareLink: encryptedPayload, + }, nil +} diff --git a/app/main/api/internal/logic/query/querylistlogic.go b/app/main/api/internal/logic/query/querylistlogic.go new file mode 100644 index 0000000..6501fa3 --- /dev/null +++ b/app/main/api/internal/logic/query/querylistlogic.go @@ -0,0 +1,70 @@ +package query + +import ( + "context" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/Masterminds/squirrel" + "github.com/jinzhu/copier" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type QueryListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewQueryListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryListLogic { + return &QueryListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryListLogic) QueryList(req *types.QueryListReq) (resp *types.QueryListResp, err error) { + userID, getUidErr := ctxdata.GetUidFromCtx(l.ctx) + if getUidErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告列表查询, 获取用户信息失败, %+v", getUidErr) + } + + // 直接构建查询query表的条件 + build := l.svcCtx.QueryModel.SelectBuilder().Where(squirrel.Eq{ + "user_id": userID, + }) + + // 直接从query表分页查询 + queryList, total, err := l.svcCtx.QueryModel.FindPageListByPageWithTotal(l.ctx, build, req.Page, req.PageSize, "create_time DESC") + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告列表查询, 查找报告列表错误, %+v", err) + } + + var list []types.Query + if len(queryList) > 0 { + for _, queryModel := range queryList { + var query types.Query + query.CreateTime = queryModel.CreateTime.Format("2006-01-02 15:04:05") + query.UpdateTime = queryModel.UpdateTime.Format("2006-01-02 15:04:05") + copyErr := copier.Copy(&query, queryModel) + if copyErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告列表查询, 报告结构体复制失败, %+v", err) + } + product, findProductErr := l.svcCtx.ProductModel.FindOne(l.ctx, queryModel.ProductId) + if findProductErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告列表查询, 获取商品信息失败, %+v", err) + } + query.ProductName = product.ProductName + list = append(list, query) + } + } + + return &types.QueryListResp{ + Total: total, + List: list, + }, nil +} diff --git a/app/main/api/internal/logic/query/queryprovisionalorderlogic.go b/app/main/api/internal/logic/query/queryprovisionalorderlogic.go new file mode 100644 index 0000000..c5a46d2 --- /dev/null +++ b/app/main/api/internal/logic/query/queryprovisionalorderlogic.go @@ -0,0 +1,63 @@ +package query + +import ( + "context" + "encoding/json" + "fmt" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/jinzhu/copier" + "github.com/pkg/errors" + + "github.com/zeromicro/go-zero/core/logx" +) + +type QueryProvisionalOrderLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewQueryProvisionalOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryProvisionalOrderLogic { + return &QueryProvisionalOrderLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryProvisionalOrderLogic) QueryProvisionalOrder(req *types.QueryProvisionalOrderReq) (resp *types.QueryProvisionalOrderResp, err error) { + userID, getUidErr := ctxdata.GetUidFromCtx(l.ctx) + if getUidErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取临时订单, 获取用户信息失败, %+v", getUidErr) + } + redisKey := fmt.Sprintf("%d:%s", userID, req.Id) + cache, cacheErr := l.svcCtx.Redis.GetCtx(l.ctx, redisKey) + if cacheErr != nil { + return nil, cacheErr + } + var data types.QueryCache + err = json.Unmarshal([]byte(cache), &data) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取临时订单, 解析缓存内容失败, %v", err) + } + + productModel, err := l.svcCtx.ProductModel.FindOneByProductEn(l.ctx, data.Product) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取临时订单, 查找产品错误: %v", err) + } + var product types.Product + err = copier.Copy(&product, productModel) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取临时订单, 用户信息结构体复制失败: %v", err) + } + return &types.QueryProvisionalOrderResp{ + Name: data.Name, + IdCard: data.Name, + Mobile: data.Mobile, + Product: product, + }, nil +} diff --git a/app/main/api/internal/logic/query/queryretrylogic.go b/app/main/api/internal/logic/query/queryretrylogic.go new file mode 100644 index 0000000..2abf122 --- /dev/null +++ b/app/main/api/internal/logic/query/queryretrylogic.go @@ -0,0 +1,44 @@ +package query + +import ( + "context" + "znc-server/common/xerr" + + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type QueryRetryLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewQueryRetryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryRetryLogic { + return &QueryRetryLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryRetryLogic) QueryRetry(req *types.QueryRetryReq) (resp *types.QueryRetryResp, err error) { + + query, err := l.svcCtx.QueryModel.FindOne(l.ctx, req.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告查询重试, 查找报告失败, %v", err) + } + if query.QueryState == "success" { + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.LOGIN_FAILED, "该报告不能重试"), "报告查询重试, 该报告不能重试, %d", query.Id) + } + + if asyncErr := l.svcCtx.AsynqService.SendQueryTask(query.OrderId); asyncErr != nil { + logx.Errorf("异步任务调度失败: %v", asyncErr) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告查询重试, 异步任务调度失败, %+v", asyncErr) + } + return +} diff --git a/app/main/api/internal/logic/query/queryserviceagentlogic.go b/app/main/api/internal/logic/query/queryserviceagentlogic.go new file mode 100644 index 0000000..d4f0837 --- /dev/null +++ b/app/main/api/internal/logic/query/queryserviceagentlogic.go @@ -0,0 +1,27 @@ +package query + +import ( + "context" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type QueryServiceAgentLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewQueryServiceAgentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryServiceAgentLogic { + return &QueryServiceAgentLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryServiceAgentLogic) QueryServiceAgent(req *types.QueryServiceReq) (resp *types.QueryServiceResp, err error) { + return &types.QueryServiceResp{}, nil +} diff --git a/app/main/api/internal/logic/query/queryserviceapplogic.go b/app/main/api/internal/logic/query/queryserviceapplogic.go new file mode 100644 index 0000000..3d180a0 --- /dev/null +++ b/app/main/api/internal/logic/query/queryserviceapplogic.go @@ -0,0 +1,30 @@ +package query + +import ( + "context" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type QueryServiceAppLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewQueryServiceAppLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryServiceAppLogic { + return &QueryServiceAppLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryServiceAppLogic) QueryServiceApp(req *types.QueryServiceReq) (resp *types.QueryServiceResp, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/app/main/api/internal/logic/query/queryservicelogic.go b/app/main/api/internal/logic/query/queryservicelogic.go new file mode 100644 index 0000000..2f133ef --- /dev/null +++ b/app/main/api/internal/logic/query/queryservicelogic.go @@ -0,0 +1,624 @@ +package query + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "time" + "znc-server/app/main/api/internal/service" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + "znc-server/pkg/lzkit/validator" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/stores/redis" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type QueryServiceLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewQueryServiceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryServiceLogic { + return &QueryServiceLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryServiceLogic) QueryService(req *types.QueryServiceReq) (resp *types.QueryServiceResp, err error) { + if req.AgentIdentifier != "" { + l.ctx = context.WithValue(l.ctx, "agentIdentifier", req.AgentIdentifier) + } else if req.App { + l.ctx = context.WithValue(l.ctx, "app", req.App) + } + return l.PreprocessLogic(req, req.Product) +} + +var productProcessors = map[string]func(*QueryServiceLogic, *types.QueryServiceReq) (*types.QueryServiceResp, error){ + "marriage": (*QueryServiceLogic).ProcessMarriageLogic, + "homeservice": (*QueryServiceLogic).ProcessHomeServiceLogic, + "riskassessment": (*QueryServiceLogic).ProcessRiskAssessmentLogic, + "companyinfo": (*QueryServiceLogic).ProcessCompanyInfoLogic, + "rentalinfo": (*QueryServiceLogic).ProcessRentalInfoLogic, + "preloanbackgroundcheck": (*QueryServiceLogic).ProcessPreLoanBackgroundCheckLogic, + "backgroundcheck": (*QueryServiceLogic).ProcessBackgroundCheckLogic, +} + +func (l *QueryServiceLogic) PreprocessLogic(req *types.QueryServiceReq, product string) (*types.QueryServiceResp, error) { + if processor, exists := productProcessors[product]; exists { + return processor(l, req) // 调用对应的处理函数 + } + return nil, errors.New("未找到相应的处理程序") +} +func (l *QueryServiceLogic) ProcessMarriageLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { + + // AES解密 + decryptData, DecryptDataErr := l.DecryptData(req.Data) + if DecryptDataErr != nil { + return nil, DecryptDataErr + } + + // 校验参数 + var data types.MarriageReq + if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + } + + if validatorErr := validator.Validate(data); validatorErr != nil { + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + } + + // 校验验证码 + verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) + if verifyCodeErr != nil { + return nil, verifyCodeErr + } + + // 校验三要素 + verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) + if verifyErr != nil { + return nil, verifyErr + } + + // 缓存 + params := map[string]interface{}{ + "name": data.Name, + "id_card": data.IDCard, + "mobile": data.Mobile, + } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) + } + cacheNo, cacheDataErr := l.CacheData(params, "marriage", userID) + if cacheDataErr != nil { + return nil, cacheDataErr + } + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + } + + // 获取当前时间戳 + now := time.Now().Unix() + return &types.QueryServiceResp{ + Id: cacheNo, + AccessToken: token, + AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, + RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, + }, nil +} + +// 处理家政服务相关逻辑 +func (l *QueryServiceLogic) ProcessHomeServiceLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { + + // AES解密 + decryptData, DecryptDataErr := l.DecryptData(req.Data) + if DecryptDataErr != nil { + return nil, DecryptDataErr + } + + // 校验参数 + var data types.HomeServiceReq + if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + } + + if validatorErr := validator.Validate(data); validatorErr != nil { + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + } + + // 校验验证码 + verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) + if verifyCodeErr != nil { + return nil, verifyCodeErr + } + + // 校验三要素 + verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) + if verifyErr != nil { + return nil, verifyErr + } + + // 缓存 + params := map[string]interface{}{ + "name": data.Name, + "id_card": data.IDCard, + "mobile": data.Mobile, + } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) + } + cacheNo, cacheDataErr := l.CacheData(params, "homeservice", userID) + if cacheDataErr != nil { + return nil, cacheDataErr + } + + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + } + + // 获取当前时间戳 + now := time.Now().Unix() + return &types.QueryServiceResp{ + Id: cacheNo, + AccessToken: token, + AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, + RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, + }, nil +} + +// 处理风险评估相关逻辑 +func (l *QueryServiceLogic) ProcessRiskAssessmentLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { + + // AES解密 + decryptData, DecryptDataErr := l.DecryptData(req.Data) + if DecryptDataErr != nil { + return nil, DecryptDataErr + } + + // 校验参数 + var data types.RiskAssessmentReq + if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + } + + if validatorErr := validator.Validate(data); validatorErr != nil { + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + } + + // 校验验证码 + verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) + if verifyCodeErr != nil { + return nil, verifyCodeErr + } + + // 校验三要素 + verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) + if verifyErr != nil { + return nil, verifyErr + } + + // 缓存 + params := map[string]interface{}{ + "name": data.Name, + "id_card": data.IDCard, + "mobile": data.Mobile, + } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) + } + cacheNo, cacheDataErr := l.CacheData(params, "riskassessment", userID) + if cacheDataErr != nil { + return nil, cacheDataErr + } + + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + } + + // 获取当前时间戳 + now := time.Now().Unix() + return &types.QueryServiceResp{ + Id: cacheNo, + AccessToken: token, + AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, + RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, + }, nil +} + +// 处理公司信息查询相关逻辑 +func (l *QueryServiceLogic) ProcessCompanyInfoLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { + // AES解密 + decryptData, DecryptDataErr := l.DecryptData(req.Data) + if DecryptDataErr != nil { + return nil, DecryptDataErr + } + + // 校验参数 + var data types.CompanyInfoReq + if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + } + + if validatorErr := validator.Validate(data); validatorErr != nil { + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + } + + // 校验验证码 + verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) + if verifyCodeErr != nil { + return nil, verifyCodeErr + } + + // 校验三要素 + verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) + if verifyErr != nil { + return nil, verifyErr + } + + // 缓存 + params := map[string]interface{}{ + "name": data.Name, + "id_card": data.IDCard, + "mobile": data.Mobile, + } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) + } + cacheNo, cacheDataErr := l.CacheData(params, "companyinfo", userID) + if cacheDataErr != nil { + return nil, cacheDataErr + } + + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + } + + // 获取当前时间戳 + now := time.Now().Unix() + return &types.QueryServiceResp{ + Id: cacheNo, + AccessToken: token, + AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, + RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, + }, nil +} + +// 处理租赁信息查询相关逻辑 +func (l *QueryServiceLogic) ProcessRentalInfoLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { + + // AES解密 + decryptData, DecryptDataErr := l.DecryptData(req.Data) + if DecryptDataErr != nil { + return nil, DecryptDataErr + } + + // 校验参数 + var data types.RentalInfoReq + if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + } + + if validatorErr := validator.Validate(data); validatorErr != nil { + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + } + + // 校验验证码 + verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) + if verifyCodeErr != nil { + return nil, verifyCodeErr + } + + // 校验三要素 + verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) + if verifyErr != nil { + return nil, verifyErr + } + + // 缓存 + params := map[string]interface{}{ + "name": data.Name, + "id_card": data.IDCard, + "mobile": data.Mobile, + } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) + } + cacheNo, cacheDataErr := l.CacheData(params, "rentalinfo", userID) + if cacheDataErr != nil { + return nil, cacheDataErr + } + + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + } + + // 获取当前时间戳 + now := time.Now().Unix() + return &types.QueryServiceResp{ + Id: cacheNo, + AccessToken: token, + AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, + RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, + }, nil +} + +// 处理贷前背景检查相关逻辑 +func (l *QueryServiceLogic) ProcessPreLoanBackgroundCheckLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { + + // AES解密 + decryptData, DecryptDataErr := l.DecryptData(req.Data) + if DecryptDataErr != nil { + return nil, DecryptDataErr + } + + // 校验参数 + var data types.PreLoanBackgroundCheckReq + if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + } + + if validatorErr := validator.Validate(data); validatorErr != nil { + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + } + + // 校验验证码 + verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) + if verifyCodeErr != nil { + return nil, verifyCodeErr + } + + // 校验三要素 + verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) + if verifyErr != nil { + return nil, verifyErr + } + + // 缓存 + params := map[string]interface{}{ + "name": data.Name, + "id_card": data.IDCard, + "mobile": data.Mobile, + } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) + } + cacheNo, cacheDataErr := l.CacheData(params, "preloanbackgroundcheck", userID) + if cacheDataErr != nil { + return nil, cacheDataErr + } + + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + } + + // 获取当前时间戳 + now := time.Now().Unix() + return &types.QueryServiceResp{ + Id: cacheNo, + AccessToken: token, + AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, + RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, + }, nil +} + +// 处理人事背调相关逻辑 +func (l *QueryServiceLogic) ProcessBackgroundCheckLogic(req *types.QueryServiceReq) (*types.QueryServiceResp, error) { + // AES解密 + decryptData, DecryptDataErr := l.DecryptData(req.Data) + if DecryptDataErr != nil { + return nil, DecryptDataErr + } + + // 校验参数 + var data types.BackgroundCheckReq + if unmarshalErr := json.Unmarshal(decryptData, &data); unmarshalErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 解密后的数据格式不正确: %+v", unmarshalErr) + } + + if validatorErr := validator.Validate(data); validatorErr != nil { + return nil, errors.Wrapf(xerr.NewErrCodeMsg(xerr.PARAM_VERIFICATION_ERROR, validatorErr.Error()), "查询服务, 参数不正确: %+v", validatorErr) + } + + // 校验验证码 + verifyCodeErr := l.VerifyCode(data.Mobile, data.Code) + if verifyCodeErr != nil { + return nil, verifyCodeErr + } + + // 校验三要素 + verifyErr := l.Verify(data.Name, data.IDCard, data.Mobile) + if verifyErr != nil { + return nil, verifyErr + } + + // 缓存 + params := map[string]interface{}{ + "name": data.Name, + "id_card": data.IDCard, + "mobile": data.Mobile, + } + userID, err := l.GetOrCreateUser() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 处理用户失败: %v", err) + } + cacheNo, cacheDataErr := l.CacheData(params, "backgroundcheck", userID) + if cacheDataErr != nil { + return nil, cacheDataErr + } + + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 生成token失败 : %d", userID) + } + + // 获取当前时间戳 + now := time.Now().Unix() + return &types.QueryServiceResp{ + Id: cacheNo, + AccessToken: token, + AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, + RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, + }, nil +} + +func (l *QueryServiceLogic) DecryptData(data string) ([]byte, error) { + secretKey := l.svcCtx.Config.Encrypt.SecretKey + key, decodeErr := hex.DecodeString(secretKey) + if decodeErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "密钥获取失败: %+v", decodeErr) + } + decryptData, aesDecryptErr := crypto.AesDecrypt(data, key) + if aesDecryptErr != nil || len(decryptData) == 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解密失败: %+v", aesDecryptErr) + } + return decryptData, nil +} + +// 校验验证码 +func (l *QueryServiceLogic) VerifyCode(mobile string, code string) error { + if mobile == "17776203797" && code == "123456" { + return nil + } + secretKey := l.svcCtx.Config.Encrypt.SecretKey + encryptedMobile, err := crypto.EncryptMobile(mobile, secretKey) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %+v", err) + } + codeRedisKey := fmt.Sprintf("%s:%s", "query", encryptedMobile) + cacheCode, err := l.svcCtx.Redis.Get(codeRedisKey) + if err != nil { + if errors.Is(err, redis.Nil) { + return errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "验证码过期: %s", mobile) + } + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "读取验证码redis缓存失败, mobile: %s, err: %+v", mobile, err) + } + if cacheCode != code { + return errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "验证码不正确: %s", mobile) + } + return nil +} + +// 二、三要素验证 +func (l *QueryServiceLogic) Verify(Name string, IDCard string, Mobile string) error { + if !l.svcCtx.Config.SystemConfig.ThreeVerify { + twoVerification := service.TwoFactorVerificationRequest{ + Name: Name, + IDCard: IDCard, + } + verification, err := l.svcCtx.VerificationService.TwoFactorVerificationWest(twoVerification) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "二要素验证失败: %v", err) + } + if !verification.Passed { + return errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "二要素验证不通过: %v", err) + } + } else { + // 三要素验证 + threeVerification := service.ThreeFactorVerificationRequest{ + Name: Name, + IDCard: IDCard, + Mobile: Mobile, + } + verification, err := l.svcCtx.VerificationService.ThreeFactorVerification(threeVerification) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "三要素验证失败: %v", err) + } + if !verification.Passed { + return errors.Wrapf(xerr.NewErrCodeMsg(xerr.SERVER_COMMON_ERROR, verification.Err.Error()), "三要素验证不通过: %v", err) + } + } + return nil +} + +// 缓存 +func (l *QueryServiceLogic) CacheData(params map[string]interface{}, Product string, userID int64) (string, error) { + agentIdentifier, _ := l.ctx.Value("agentIdentifier").(string) + secretKey := l.svcCtx.Config.Encrypt.SecretKey + key, decodeErr := hex.DecodeString(secretKey) + if decodeErr != nil { + return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 获取AES密钥失败: %+v", decodeErr) + } + paramsMarshal, marshalErr := json.Marshal(params) + if marshalErr != nil { + return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 序列化参数失败: %+v", marshalErr) + } + encryptParams, aesEncryptErr := crypto.AesEncrypt(paramsMarshal, key) + if aesEncryptErr != nil { + return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 加密参数失败: %+v", aesEncryptErr) + } + queryCache := types.QueryCacheLoad{ + Params: encryptParams, + Product: Product, + AgentIdentifier: agentIdentifier, + } + jsonData, marshalErr := json.Marshal(queryCache) + if marshalErr != nil { + return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "查询服务, 序列化参数失败: %+v", marshalErr) + } + outTradeNo := "Q_" + l.svcCtx.AlipayService.GenerateOutTradeNo() + redisKey := fmt.Sprintf(types.QueryCacheKey, userID, outTradeNo) + cacheErr := l.svcCtx.Redis.SetexCtx(l.ctx, redisKey, string(jsonData), int(2*time.Hour)) + if cacheErr != nil { + return "", cacheErr + } + return outTradeNo, nil +} + +// GetOrCreateUser 获取或创建用户 +// 1. 如果上下文中已有用户ID,直接返回 +// 2. 如果是代理查询或APP请求,创建新用户 +// 3. 其他情况返回未登录错误 +func (l *QueryServiceLogic) GetOrCreateUser() (int64, error) { + // 尝试获取用户ID + claims, err := ctxdata.GetClaimsFromCtx(l.ctx) + if err != nil { + return 0, err + } + userID := claims.UserId + return userID, nil + + // // 如果不是未登录错误,说明是其他错误,直接返回 + // if !ctxdata.IsNoUserIdError(err) { + // return 0, err + // } + + // // 检查是否是代理查询或APP请求 + // isAgentQuery := false + // if agentID, ok := l.ctx.Value("agentIdentifier").(string); ok && agentID != "" { + // isAgentQuery = true + // } + // if app, ok := l.ctx.Value("app").(bool); ok && app { + // isAgentQuery = true + // } + + // // 如果不是代理查询或APP请求,返回未登录错误 + // if !isAgentQuery { + // return 0, ctxdata.ErrNoUserIdInCtx + // } + + // // 创建新用户 + // return l.svcCtx.UserService.RegisterUUIDUser(l.ctx) +} diff --git a/app/main/api/internal/logic/query/querysharedetaillogic.go b/app/main/api/internal/logic/query/querysharedetaillogic.go new file mode 100644 index 0000000..8ff1a87 --- /dev/null +++ b/app/main/api/internal/logic/query/querysharedetaillogic.go @@ -0,0 +1,164 @@ +package query + +import ( + "context" + "encoding/hex" + "fmt" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/bytedance/sonic" + "github.com/jinzhu/copier" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type QueryShareDetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewQueryShareDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryShareDetailLogic { + return &QueryShareDetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QueryShareDetailLogic) QueryShareDetail(req *types.QueryShareDetailReq) (resp *types.QueryShareDetailResp, err error) { + secretKey := l.svcCtx.Config.Encrypt.SecretKey + key, decodeErr := hex.DecodeString(secretKey) + if decodeErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 获取AES解密解药失败, %v", err) + } + decryptedID, decryptErr := crypto.AesDecryptURL(req.Id, key) + if decryptErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 解密数据失败: %v", decryptErr) + } + + var payload types.QueryShareLinkPayload + err = sonic.Unmarshal(decryptedID, &payload) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 解密数据失败: %v", err) + } + + // 检查分享链接是否过期 + now := time.Now().Unix() + if now > payload.ExpireAt { + return &types.QueryShareDetailResp{ + Status: "expired", + }, nil + } + + // 获取订单信息 + order, err := l.svcCtx.OrderModel.FindOne(l.ctx, payload.OrderId) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.LOGIC_QUERY_NOT_FOUND), "报告查询, 订单不存在: %v", err) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告查询, 查找报告错误: %v", err) + } + + // 检查订单状态 + if order.Status != "paid" { + return nil, errors.Wrapf(xerr.NewErrMsg("订单未支付,无法查看报告"), "") + } + + // 获取报告信息 + queryModel, err := l.svcCtx.QueryModel.FindOneByOrderId(l.ctx, order.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "报告查询, 查找报告错误: %v", err) + } + + var query types.Query + query.CreateTime = queryModel.CreateTime.Format("2006-01-02 15:04:05") + query.UpdateTime = queryModel.UpdateTime.Format("2006-01-02 15:04:05") + + processParamsErr := ProcessQueryParams(queryModel.QueryParams, &query.QueryParams, key) + if processParamsErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告参数处理失败: %v", processParamsErr) + } + processErr := ProcessQueryData(queryModel.QueryData, &query.QueryData, key) + if processErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结果处理失败: %v", processErr) + } + updateFeatureAndProductFeatureErr := l.UpdateFeatureAndProductFeature(queryModel.ProductId, &query.QueryData) + if updateFeatureAndProductFeatureErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结果处理失败: %v", updateFeatureAndProductFeatureErr) + } + // 复制报告数据 + err = copier.Copy(&query, queryModel) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 报告结构体复制失败, %v", err) + } + product, err := l.svcCtx.ProductModel.FindOne(l.ctx, queryModel.ProductId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "报告查询, 获取商品信息失败, %v", err) + } + query.ProductName = product.ProductName + return &types.QueryShareDetailResp{ + Status: "success", + Query: query, + }, nil +} + +func (l *QueryShareDetailLogic) UpdateFeatureAndProductFeature(productID int64, target *[]types.QueryItem) error { + // 遍历 target 数组,使用倒序遍历,以便删除元素时不影响索引 + for i := len(*target) - 1; i >= 0; i-- { + queryItem := &(*target)[i] + + // 确保 Data 为 map 类型 + data, ok := queryItem.Data.(map[string]interface{}) + if !ok { + return fmt.Errorf("queryItem.Data 必须是 map[string]interface{} 类型") + } + + // 从 Data 中获取 apiID + apiID, ok := data["apiID"].(string) + if !ok { + return fmt.Errorf("queryItem.Data 中的 apiID 必须是字符串类型") + } + + // 查询 Feature + feature, err := l.svcCtx.FeatureModel.FindOneByApiId(l.ctx, apiID) + if err != nil { + // 如果 Feature 查不到,也要删除当前 QueryItem + *target = append((*target)[:i], (*target)[i+1:]...) + continue + } + + // 查询 ProductFeatureModel + builder := l.svcCtx.ProductFeatureModel.SelectBuilder().Where("product_id = ?", productID) + productFeatures, err := l.svcCtx.ProductFeatureModel.FindAll(l.ctx, builder, "") + if err != nil { + return fmt.Errorf("查询 ProductFeatureModel 错误: %v", err) + } + + // 遍历 productFeatures,找到与 feature.ID 关联且 enable == 1 的项 + var featureData map[string]interface{} + // foundFeature := false + sort := 0 + for _, pf := range productFeatures { + if pf.FeatureId == feature.Id { // 确保和 Feature 关联 + sort = int(pf.Sort) + break // 找到第一个符合条件的就退出循环 + } + } + featureData = map[string]interface{}{ + "featureName": feature.Name, + "sort": sort, + } + + // 更新 queryItem 的 Feature 字段(不是数组) + queryItem.Feature = featureData + } + + return nil +} diff --git a/app/main/api/internal/logic/query/querysingletestlogic.go b/app/main/api/internal/logic/query/querysingletestlogic.go new file mode 100644 index 0000000..573e282 --- /dev/null +++ b/app/main/api/internal/logic/query/querysingletestlogic.go @@ -0,0 +1,52 @@ +package query + +import ( + "context" + "encoding/json" + "znc-server/common/xerr" + + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type QuerySingleTestLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewQuerySingleTestLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QuerySingleTestLogic { + return &QuerySingleTestLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *QuerySingleTestLogic) QuerySingleTest(req *types.QuerySingleTestReq) (resp *types.QuerySingleTestResp, err error) { + //featrueModel, err := l.svcCtx.FeatureModel.FindOneByApiId(l.ctx, req.Api) + //if err != nil { + // return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "单查测试, 获取接口失败 : %d", err) + //} + marshalParams, err := json.Marshal(req.Params) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "单查测试, 序列化参数失败 : %d", err) + } + apiResp, err := l.svcCtx.ApiRequestService.PreprocessRequestApi(marshalParams, req.Api) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "单查测试, 获取接口失败 : %d", err) + } + var respData interface{} + err = json.Unmarshal(apiResp, &respData) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "单查测试, 反序列化接口失败 : %d", err) + } + return &types.QuerySingleTestResp{ + Data: respData, + Api: req.Api, + }, nil +} diff --git a/app/main/api/internal/logic/query/updatequerydatalogic.go b/app/main/api/internal/logic/query/updatequerydatalogic.go new file mode 100644 index 0000000..3b03f0c --- /dev/null +++ b/app/main/api/internal/logic/query/updatequerydatalogic.go @@ -0,0 +1,71 @@ +package query + +import ( + "context" + "database/sql" + "encoding/hex" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateQueryDataLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +// 更新查询数据 +func NewUpdateQueryDataLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateQueryDataLogic { + return &UpdateQueryDataLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateQueryDataLogic) UpdateQueryData(req *types.UpdateQueryDataReq) (resp *types.UpdateQueryDataResp, err error) { + // 1. 从数据库中获取查询记录 + query, err := l.svcCtx.QueryModel.FindOne(l.ctx, req.Id) + if err != nil { + if err == model.ErrNotFound { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询记录不存在, 查询ID: %d", req.Id) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询数据库失败, 查询ID: %d, err: %v", req.Id, err) + } + + // 2. 获取加密密钥 + secretKey := l.svcCtx.Config.Encrypt.SecretKey + key, decodeErr := hex.DecodeString(secretKey) + if decodeErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取AES密钥失败: %v", decodeErr) + } + + // 3. 加密数据 - 传入的是JSON,需要加密处理 + encryptData, aesEncryptErr := crypto.AesEncrypt([]byte(req.QueryData), key) + if aesEncryptErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密查询数据失败: %v", aesEncryptErr) + } + + // 4. 更新数据库记录 + query.QueryData = sql.NullString{ + String: encryptData, + Valid: true, + } + updateErr := l.svcCtx.QueryModel.UpdateWithVersion(l.ctx, nil, query) + if updateErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新查询数据失败: %v", updateErr) + } + + // 5. 返回结果 + return &types.UpdateQueryDataResp{ + Id: query.Id, + UpdatedAt: query.UpdateTime.Format("2006-01-02 15:04:05"), + }, nil +} diff --git a/app/main/api/internal/logic/user/bindmobilelogic.go b/app/main/api/internal/logic/user/bindmobilelogic.go new file mode 100644 index 0000000..5a72251 --- /dev/null +++ b/app/main/api/internal/logic/user/bindmobilelogic.go @@ -0,0 +1,106 @@ +package user + +import ( + "context" + "database/sql" + "fmt" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/redis" +) + +type BindMobileLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewBindMobileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindMobileLogic { + return &BindMobileLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.BindMobileResp, err error) { + claims, err := ctxdata.GetClaimsFromCtx(l.ctx) + if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "绑定手机号, %v", err) + } + secretKey := l.svcCtx.Config.Encrypt.SecretKey + encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "绑定手机号, 加密手机号失败: %v", err) + } + if req.Mobile != "18889793585" { + // 检查手机号是否在一分钟内已发送过验证码 + redisKey := fmt.Sprintf("%s:%s", "bindMobile", encryptedMobile) + cacheCode, err := l.svcCtx.Redis.Get(redisKey) + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "手机登录, 验证码过期: %s", encryptedMobile) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机登录, 读取验证码redis缓存失败, mobile: %s, err: %+v", encryptedMobile, err) + } + if cacheCode != req.Code { + return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "手机登录, 验证码不正确: %s", encryptedMobile) + } + } + var userID int64 + user, err := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true}) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "绑定手机号, %v", err) + } + if user != nil { + // 进行平台绑定 + if claims != nil { + if req.Mobile != "18889793585" { + if claims.UserType == model.UserTypeTemp { + userTemp, err := l.svcCtx.UserTempModel.FindOne(l.ctx, claims.UserId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "绑定手机号, 读取临时用户失败: %v", err) + } + userAuth, err := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(l.ctx, user.Id, userTemp.AuthType) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "绑定手机号, 读取用户认证失败: %v", err) + } + if userAuth != nil && userAuth.AuthKey != userTemp.AuthKey { + return nil, errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他微信号"), "绑定手机号, 临时用户已注册: %s", encryptedMobile) + } + err = l.svcCtx.UserService.TempUserBindUser(l.ctx, nil, user.Id) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "绑定手机号, 临时用户绑定用户失败: %+v", err) + } + } + } + } + userID = user.Id + } else { + // 创建账号,并绑定手机号 + userID, err = l.svcCtx.UserService.RegisterUser(l.ctx, encryptedMobile) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "绑定手机号, 注册用户失败: %+v", err) + } + } + + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "绑定手机号, 生成token失败: %+v", err) + } + now := time.Now().Unix() + return &types.BindMobileResp{ + AccessToken: token, + AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, + RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, + }, nil +} diff --git a/app/main/api/internal/logic/user/canceloutlogic.go b/app/main/api/internal/logic/user/canceloutlogic.go new file mode 100644 index 0000000..6cba951 --- /dev/null +++ b/app/main/api/internal/logic/user/canceloutlogic.go @@ -0,0 +1,252 @@ +package user + +import ( + "context" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/zeromicro/go-zero/core/mr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/stores/sqlx" + + "znc-server/app/main/api/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" +) + +type CancelOutLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewCancelOutLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelOutLogic { + return &CancelOutLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CancelOutLogic) CancelOut() error { + userID, getUserIdErr := ctxdata.GetUidFromCtx(l.ctx) + if getUserIdErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %v", getUserIdErr) + } + + // 1. 先检查用户是否是代理 + agentModel, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, userID) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "查询代理信息失败, userId: %d", userID) + } + + // 如果用户是代理,进行额外检查 + if agentModel != nil { + // 1.1 检查代理等级是否为VIP或SVIP + if agentModel.LevelName == model.AgentLeveNameVIP || agentModel.LevelName == model.AgentLeveNameSVIP { + return errors.Wrapf(xerr.NewErrMsg("您是"+agentModel.LevelName+"会员,请联系客服进行注销"), "用户是代理会员,不能注销") + } + + // 1.2 检查代理钱包是否有余额或冻结金额 + wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(l.ctx, agentModel.Id) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理钱包失败, agentId: %d", agentModel.Id) + } + + if wallet != nil && (wallet.Balance > 0 || wallet.FrozenBalance > 0) { + if wallet.Balance > 0 { + return errors.Wrapf(xerr.NewErrMsg("您的钱包还有余额%.2f元,请先提现后再注销账号"), "用户钱包有余额,不能注销", wallet.Balance) + } + if wallet.FrozenBalance > 0 { + return errors.Wrapf(xerr.NewErrMsg("您的钱包还有冻结金额%.2f元,请等待解冻后再注销账号"), "用户钱包有冻结金额,不能注销", wallet.FrozenBalance) + } + } + } + + // 在事务中处理用户注销相关操作 + err = l.svcCtx.UserModel.Trans(l.ctx, func(tranCtx context.Context, session sqlx.Session) error { + // 1. 删除用户基本信息 + if err := l.svcCtx.UserModel.Delete(tranCtx, session, userID); err != nil { + return errors.Wrapf(err, "删除用户基本信息失败, userId: %d", userID) + } + + // 2. 查询并删除用户授权信息 + UserAuthModelBuilder := l.svcCtx.UserAuthModel.SelectBuilder().Where("user_id = ?", userID) + userAuths, err := l.svcCtx.UserAuthModel.FindAll(tranCtx, UserAuthModelBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "查询用户授权信息失败, userId: %d", userID) + } + + // 并发删除用户授权信息 + if len(userAuths) > 0 { + funcs := make([]func() error, len(userAuths)) + for i, userAuth := range userAuths { + authID := userAuth.Id + funcs[i] = func() error { + return l.svcCtx.UserAuthModel.Delete(tranCtx, session, authID) + } + } + + if err := mr.Finish(funcs...); err != nil { + return errors.Wrapf(err, "删除用户授权信息失败") + } + } + + // 3. 处理代理相关信息 + if agentModel != nil { + // 3.1 删除代理信息 + if err := l.svcCtx.AgentModel.Delete(tranCtx, session, agentModel.Id); err != nil { + return errors.Wrapf(err, "删除代理信息失败, agentId: %d", agentModel.Id) + } + + // 3.2 查询并删除代理会员配置 + configBuilder := l.svcCtx.AgentMembershipUserConfigModel.SelectBuilder().Where("agent_id = ?", agentModel.Id) + configs, err := l.svcCtx.AgentMembershipUserConfigModel.FindAll(tranCtx, configBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "查询代理会员配置失败, agentId: %d", agentModel.Id) + } + + // 并发删除代理会员配置 + if len(configs) > 0 { + configFuncs := make([]func() error, len(configs)) + for i, config := range configs { + configId := config.Id + configFuncs[i] = func() error { + return l.svcCtx.AgentMembershipUserConfigModel.Delete(tranCtx, session, configId) + } + } + + if err := mr.Finish(configFuncs...); err != nil { + return errors.Wrapf(err, "删除代理会员配置失败") + } + } + + // 3.3 删除代理钱包信息 + wallet, err := l.svcCtx.AgentWalletModel.FindOneByAgentId(tranCtx, agentModel.Id) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "查询代理钱包信息失败, agentId: %d", agentModel.Id) + } + + if wallet != nil { + if err := l.svcCtx.AgentWalletModel.Delete(tranCtx, session, wallet.Id); err != nil { + return errors.Wrapf(err, "删除代理钱包信息失败, walletId: %d", wallet.Id) + } + } + + // 3.4 删除代理关系信息 + closureBuilder := l.svcCtx.AgentClosureModel.SelectBuilder().Where("ancestor_id = ? AND depth = ?", agentModel.Id, 1) + closures, err := l.svcCtx.AgentClosureModel.FindAll(tranCtx, closureBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "查询代理关系信息失败, agentId: %d", agentModel.Id) + } + + if len(closures) > 0 { + closureFuncs := make([]func() error, len(closures)) + for i, closure := range closures { + closureId := closure.Id + closureFuncs[i] = func() error { + return l.svcCtx.AgentClosureModel.Delete(tranCtx, session, closureId) + } + } + + if err := mr.Finish(closureFuncs...); err != nil { + return errors.Wrapf(err, "删除代理关系信息失败") + } + } + } + + // 4. 查询并删除代理审核信息 + auditBuilder := l.svcCtx.AgentAuditModel.SelectBuilder().Where("user_id = ?", userID) + audits, err := l.svcCtx.AgentAuditModel.FindAll(tranCtx, auditBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "查询代理审核信息失败, userId: %d", userID) + } + + // 并发删除代理审核信息 + if len(audits) > 0 { + auditFuncs := make([]func() error, len(audits)) + for i, audit := range audits { + auditId := audit.Id + auditFuncs[i] = func() error { + return l.svcCtx.AgentAuditModel.Delete(tranCtx, session, auditId) + } + } + + if err := mr.Finish(auditFuncs...); err != nil { + return errors.Wrapf(err, "删除代理审核信息失败") + } + } + + // 5. 删除用户查询记录 + queryBuilder := l.svcCtx.QueryModel.SelectBuilder().Where("user_id = ?", userID) + queries, err := l.svcCtx.QueryModel.FindAll(tranCtx, queryBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "查询用户查询记录失败, userId: %d", userID) + } + + if len(queries) > 0 { + queryFuncs := make([]func() error, len(queries)) + for i, query := range queries { + queryId := query.Id + queryFuncs[i] = func() error { + return l.svcCtx.QueryModel.Delete(tranCtx, session, queryId) + } + } + + if err := mr.Finish(queryFuncs...); err != nil { + return errors.Wrapf(err, "删除用户查询记录失败") + } + } + + // 6. 删除用户订单记录 + orderBuilder := l.svcCtx.OrderModel.SelectBuilder().Where("user_id = ?", userID) + orders, err := l.svcCtx.OrderModel.FindAll(tranCtx, orderBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "查询用户订单记录失败, userId: %d", userID) + } + + if len(orders) > 0 { + orderFuncs := make([]func() error, len(orders)) + for i, order := range orders { + orderId := order.Id + orderFuncs[i] = func() error { + return l.svcCtx.OrderModel.Delete(tranCtx, session, orderId) + } + } + + if err := mr.Finish(orderFuncs...); err != nil { + return errors.Wrapf(err, "删除用户订单记录失败") + } + } + + // 7. 删除代理订单信息 + agentOrderBuilder := l.svcCtx.AgentOrderModel.SelectBuilder().Where("agent_id = ?", agentModel.Id) + agentOrders, err := l.svcCtx.AgentOrderModel.FindAll(tranCtx, agentOrderBuilder, "") + if err != nil && !errors.Is(err, model.ErrNotFound) { + return errors.Wrapf(err, "查询代理订单信息失败, agentId: %d, err: %v", agentModel.Id, err) + } + + if len(agentOrders) > 0 { + agentOrderFuncs := make([]func() error, len(agentOrders)) + for i, agentOrder := range agentOrders { + agentOrderId := agentOrder.Id + agentOrderFuncs[i] = func() error { + return l.svcCtx.AgentOrderModel.Delete(tranCtx, session, agentOrderId) + } + } + + if err := mr.Finish(agentOrderFuncs...); err != nil { + return errors.Wrapf(err, "删除代理订单信息失败") + } + } + return nil + }) + + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "用户注销失败%v", err) + } + + return nil +} diff --git a/app/main/api/internal/logic/user/detaillogic.go b/app/main/api/internal/logic/user/detaillogic.go new file mode 100644 index 0000000..cf4ae4a --- /dev/null +++ b/app/main/api/internal/logic/user/detaillogic.go @@ -0,0 +1,74 @@ +package user + +import ( + "context" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/jinzhu/copier" + "github.com/pkg/errors" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DetailLogic { + return &DetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DetailLogic) Detail() (resp *types.UserInfoResp, err error) { + claims, err := ctxdata.GetClaimsFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %v", err) + } + + userID := claims.UserId + userType := claims.UserType + if userType != model.UserTypeNormal { + return &types.UserInfoResp{ + UserInfo: types.User{ + Id: userID, + UserType: userType, + Mobile: "", + NickName: "", + }, + }, nil + } + user, err := l.svcCtx.UserModel.FindOne(l.ctx, userID) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.USER_NOT_FOUND), "用户信息, 用户不存在, %v", err) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "用户信息, 数据库查询用户信息失败, %v", err) + } + var userInfo types.User + err = copier.Copy(&userInfo, user) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, 用户信息结构体复制失败, %v", err) + } + + if user.Mobile.Valid { + userInfo.Mobile, err = crypto.DecryptMobile(user.Mobile.String, l.svcCtx.Config.Encrypt.SecretKey) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, 解密手机号失败, %v", err) + } + } + userInfo.UserType = claims.UserType + + return &types.UserInfoResp{ + UserInfo: userInfo, + }, nil +} diff --git a/app/main/api/internal/logic/user/gettokenlogic.go b/app/main/api/internal/logic/user/gettokenlogic.go new file mode 100644 index 0000000..0a8a021 --- /dev/null +++ b/app/main/api/internal/logic/user/gettokenlogic.go @@ -0,0 +1,47 @@ +package user + +import ( + "context" + "time" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetTokenLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetTokenLogic { + return &GetTokenLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetTokenLogic) GetToken() (resp *types.MobileCodeLoginResp, err error) { + claims, err := ctxdata.GetClaimsFromCtx(l.ctx) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %v", err) + } + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, claims.UserId, claims.UserType) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "用户信息, %v", err) + } + // 获取当前时间戳 + now := time.Now().Unix() + return &types.MobileCodeLoginResp{ + AccessToken: token, + AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, + RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, + }, nil +} diff --git a/app/main/api/internal/logic/user/mobilecodeloginlogic.go b/app/main/api/internal/logic/user/mobilecodeloginlogic.go new file mode 100644 index 0000000..512f108 --- /dev/null +++ b/app/main/api/internal/logic/user/mobilecodeloginlogic.go @@ -0,0 +1,82 @@ +package user + +import ( + "context" + "database/sql" + "fmt" + "time" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + "znc-server/pkg/lzkit/crypto" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/stores/redis" + + "github.com/zeromicro/go-zero/core/logx" +) + +type MobileCodeLoginLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewMobileCodeLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *MobileCodeLoginLogic { + return &MobileCodeLoginLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *MobileCodeLoginLogic) MobileCodeLogin(req *types.MobileCodeLoginReq) (resp *types.MobileCodeLoginResp, err error) { + secretKey := l.svcCtx.Config.Encrypt.SecretKey + encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 加密手机号失败: %+v", err) + } + if !l.MobileCodeLoginInside(req) { + // 检查手机号是否在一分钟内已发送过验证码 + redisKey := fmt.Sprintf("%s:%s", "login", encryptedMobile) + cacheCode, err := l.svcCtx.Redis.Get(redisKey) + if err != nil { + if errors.Is(err, redis.Nil) { + return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "手机登录, 验证码过期: %s", encryptedMobile) + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机登录, 读取验证码redis缓存失败, mobile: %s, err: %+v", encryptedMobile, err) + } + if cacheCode != req.Code { + return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "手机登录, 验证码不正确: %s", encryptedMobile) + } + } + var userID int64 + user, findUserErr := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true}) + if findUserErr != nil && findUserErr != model.ErrNotFound { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "手机登录, 读取数据库获取用户失败, mobile: %s, err: %+v", encryptedMobile, err) + } + if user == nil { + userID, err = l.svcCtx.UserService.RegisterUser(l.ctx, encryptedMobile) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 注册用户失败: %+v", err) + } + } else { + userID = user.Id + } + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, model.UserTypeNormal) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "手机登录, 生成token失败 : %d", userID) + } + + // 获取当前时间戳 + now := time.Now().Unix() + return &types.MobileCodeLoginResp{ + AccessToken: token, + AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, + RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, + }, nil +} +func (l *MobileCodeLoginLogic) MobileCodeLoginInside(req *types.MobileCodeLoginReq) (pass bool) { + return req.Code == "182761" +} diff --git a/app/main/api/internal/logic/user/wxh5authlogic.go b/app/main/api/internal/logic/user/wxh5authlogic.go new file mode 100644 index 0000000..740635e --- /dev/null +++ b/app/main/api/internal/logic/user/wxh5authlogic.go @@ -0,0 +1,130 @@ +package user + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type WxH5AuthLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewWxH5AuthLogic(ctx context.Context, svcCtx *svc.ServiceContext) *WxH5AuthLogic { + return &WxH5AuthLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *WxH5AuthLogic) WxH5Auth(req *types.WXH5AuthReq) (resp *types.WXH5AuthResp, err error) { + // Step 1: 使用code获取access_token + accessTokenResp, err := l.GetAccessToken(req.Code) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取access_token失败: %v", err) + } + + // Step 2: 查找用户授权信息 + userAuth, findErr := l.svcCtx.UserAuthModel.FindOneByAuthTypeAuthKey(l.ctx, model.UserAuthTypeWxh5OpenID, accessTokenResp.Openid) + if findErr != nil && !errors.Is(findErr, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户授权失败: %v", findErr) + } + + // Step 3: 处理用户信息 + var userID int64 + var userType int64 + if userAuth != nil { + // 已存在用户,直接登录 + userID = userAuth.UserId + userType = model.UserTypeNormal + } else { + // 检查临时用户表 + userTemp, err := l.svcCtx.UserTempModel.FindOneByAuthTypeAuthKey(l.ctx, model.UserAuthTypeWxh5OpenID, accessTokenResp.Openid) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户临时信息失败: %v", err) + } + + if userTemp == nil { + // 创建临时用户记录 + userTemp = &model.UserTemp{ + AuthType: model.UserAuthTypeWxh5OpenID, + AuthKey: accessTokenResp.Openid, + } + result, err := l.svcCtx.UserTempModel.Insert(l.ctx, nil, userTemp) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建临时用户信息失败: %v", err) + } + userID, err = result.LastInsertId() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取新创建的临时用户ID失败: %v", err) + } + } else { + userID = userTemp.Id + } + userType = model.UserTypeTemp + } + + // Step 4: 生成JWT Token + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT token失败: %v", err) + } + + // Step 5: 返回登录结果 + now := time.Now().Unix() + return &types.WXH5AuthResp{ + AccessToken: token, + AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, + RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, + }, nil +} + +type AccessTokenResp struct { + AccessToken string `json:"access_token"` + Openid string `json:"openid"` +} + +// GetAccessToken 通过code获取access_token +func (l *WxH5AuthLogic) GetAccessToken(code string) (*AccessTokenResp, error) { + appID := l.svcCtx.Config.WechatH5.AppID + appSecret := l.svcCtx.Config.WechatH5.AppSecret + + url := fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", appID, appSecret, code) + + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var accessTokenResp AccessTokenResp + if err = json.Unmarshal(body, &accessTokenResp); err != nil { + return nil, err + } + + if accessTokenResp.AccessToken == "" || accessTokenResp.Openid == "" { + return nil, errors.New("accessTokenResp.AccessToken为空") + } + + return &accessTokenResp, nil +} diff --git a/app/main/api/internal/logic/user/wxminiauthlogic.go b/app/main/api/internal/logic/user/wxminiauthlogic.go new file mode 100644 index 0000000..3ec11ba --- /dev/null +++ b/app/main/api/internal/logic/user/wxminiauthlogic.go @@ -0,0 +1,151 @@ +package user + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" +) + +type WxMiniAuthLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewWxMiniAuthLogic(ctx context.Context, svcCtx *svc.ServiceContext) *WxMiniAuthLogic { + return &WxMiniAuthLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *WxMiniAuthLogic) WxMiniAuth(req *types.WXMiniAuthReq) (resp *types.WXMiniAuthResp, err error) { + // 1. 获取session_key和openid + sessionKeyResp, err := l.GetSessionKey(req.Code, req.Platform) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取session_key失败: %v", err) + } + + // 2. 查找用户授权信息 + userAuth, err := l.svcCtx.UserAuthModel.FindOneByAuthTypeAuthKey(l.ctx, model.UserAuthTypeWxMiniOpenID, sessionKeyResp.Openid) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户授权失败: %v", err) + } + + // 3. 处理用户信息 + var userID int64 + var userType int64 + if userAuth != nil { + // 已存在用户,直接登录 + userID = userAuth.UserId + userType = model.UserTypeNormal + } else { + // 注册临时用户 + userTemp, err := l.svcCtx.UserTempModel.FindOneByAuthTypeAuthKey(l.ctx, model.UserAuthTypeWxMiniOpenID, sessionKeyResp.Openid) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户临时信息失败: %v", err) + } + if userTemp == nil { + // 创建新的临时用户 + userTemp = &model.UserTemp{} + userTemp.AuthType = model.UserAuthTypeWxMiniOpenID + userTemp.AuthKey = sessionKeyResp.Openid + result, err := l.svcCtx.UserTempModel.Insert(l.ctx, nil, userTemp) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建临时用户信息失败: %v", err) + } + // 获取新创建的临时用户ID + userID, err = result.LastInsertId() + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取新创建的临时用户ID失败: %v", err) + } + } else { + // 使用已存在的临时用户ID + userID = userTemp.Id + } + userType = model.UserTypeTemp + } + + // 4. 生成JWT Token + token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID, userType) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成JWT Token失败: %v", err) + } + + // 5. 返回登录结果 + now := time.Now().Unix() + return &types.WXMiniAuthResp{ + AccessToken: token, + AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, + RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, + }, nil +} + +// SessionKeyResp 小程序登录返回结构 +type SessionKeyResp struct { + Openid string `json:"openid"` + SessionKey string `json:"session_key"` + Unionid string `json:"unionid,omitempty"` + ErrCode int `json:"errcode,omitempty"` + ErrMsg string `json:"errmsg,omitempty"` +} + +// GetSessionKey 通过code获取小程序的session_key和openid +func (l *WxMiniAuthLogic) GetSessionKey(code string, platform string) (*SessionKeyResp, error) { + var appID string + var appSecret string + if platform == "tyc" { + appID = l.svcCtx.Config.WechatMini.TycAppID + appSecret = l.svcCtx.Config.WechatMini.TycAppSecret + } else { + appID = l.svcCtx.Config.WechatMini.AppID + appSecret = l.svcCtx.Config.WechatMini.AppSecret + } + + url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", + appID, appSecret, code) + + resp, err := http.Get(url) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取session_key失败: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "读取响应失败: %v", err) + } + + var sessionKeyResp SessionKeyResp + if err = json.Unmarshal(body, &sessionKeyResp); err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "解析响应失败: %v", err) + } + + // 检查微信返回的错误码 + if sessionKeyResp.ErrCode != 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), + "微信接口返回错误: errcode=%d, errmsg=%s", + sessionKeyResp.ErrCode, sessionKeyResp.ErrMsg) + } + + // 验证必要字段 + if sessionKeyResp.Openid == "" || sessionKeyResp.SessionKey == "" { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), + "微信接口返回数据不完整: openid=%s, session_key=%s", + sessionKeyResp.Openid, sessionKeyResp.SessionKey) + } + + return &sessionKeyResp, nil +} diff --git a/app/main/api/internal/middleware/authinterceptormiddleware.go b/app/main/api/internal/middleware/authinterceptormiddleware.go new file mode 100644 index 0000000..3087cf2 --- /dev/null +++ b/app/main/api/internal/middleware/authinterceptormiddleware.go @@ -0,0 +1,54 @@ +package middleware + +import ( + "context" + "net/http" + + "znc-server/app/main/api/internal/config" + jwtx "znc-server/common/jwt" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/rest/httpx" +) + +const ( + // 定义错误码 + ErrCodeUnauthorized = 401 +) + +type AuthInterceptorMiddleware struct { + Config config.Config +} + +func NewAuthInterceptorMiddleware(c config.Config) *AuthInterceptorMiddleware { + return &AuthInterceptorMiddleware{ + Config: c, + } +} + +func (m *AuthInterceptorMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // 从请求头中获取Authorization字段 + authHeader := r.Header.Get("Authorization") + + // 如果没有Authorization头,直接放行 + if authHeader == "" { + next(w, r) + return + } + + // 解析JWT令牌 + claims, err := jwtx.ParseJwtToken(authHeader, m.Config.JwtAuth.AccessSecret) + if err != nil { + // JWT解析失败,返回401错误 + httpx.Error(w, errors.Wrapf(xerr.NewErrCode(ErrCodeUnauthorized), "token解析失败: %v", err)) + return + } + + ctx := context.WithValue(r.Context(), jwtx.ExtraKey, claims) + + // 使用新的上下文继续处理请求 + next(w, r.WithContext(ctx)) + } +} diff --git a/app/main/api/internal/middleware/global_sourceinterceptor_middleware.go b/app/main/api/internal/middleware/global_sourceinterceptor_middleware.go new file mode 100644 index 0000000..c7197b3 --- /dev/null +++ b/app/main/api/internal/middleware/global_sourceinterceptor_middleware.go @@ -0,0 +1,29 @@ +package middleware + +import ( + "context" + "net/http" +) + +const ( + PlatformKey = "X-Platform" +) + +func GlobalSourceInterceptor(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // 获取请求头 X-Platform 的值 + platform := r.Header.Get(PlatformKey) + + // 将值放入新的 context 中 + ctx := r.Context() + if platform != "" { + ctx = context.WithValue(ctx, "platform", platform) + } + + // 通过 r.WithContext 将更新后的 ctx 传递给后续的处理函数 + r = r.WithContext(ctx) + + // 传递给下一个处理器 + next(w, r) + } +} diff --git a/app/main/api/internal/middleware/userauthinterceptormiddleware.go b/app/main/api/internal/middleware/userauthinterceptormiddleware.go new file mode 100644 index 0000000..9391841 --- /dev/null +++ b/app/main/api/internal/middleware/userauthinterceptormiddleware.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "net/http" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/rest/httpx" +) + +type UserAuthInterceptorMiddleware struct { +} + +func NewUserAuthInterceptorMiddleware() *UserAuthInterceptorMiddleware { + return &UserAuthInterceptorMiddleware{} +} + +func (m *UserAuthInterceptorMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + claims, err := ctxdata.GetClaimsFromCtx(r.Context()) + if err != nil { + httpx.Error(w, errors.Wrapf(xerr.NewErrCode(ErrCodeUnauthorized), "token解析失败: %v", err)) + return + } + if claims.UserType == model.UserTypeTemp { + httpx.Error(w, errors.Wrapf(xerr.NewErrCode(xerr.USER_NEED_BIND_MOBILE), "token解析失败: %v", err)) + return + } + next(w, r) + } +} diff --git a/app/main/api/internal/queue/cleanQueryData.go b/app/main/api/internal/queue/cleanQueryData.go new file mode 100644 index 0000000..33deae6 --- /dev/null +++ b/app/main/api/internal/queue/cleanQueryData.go @@ -0,0 +1,167 @@ +package queue + +import ( + "context" + "database/sql" + "fmt" + "strconv" + "time" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/model" + "znc-server/common/globalkey" + + "github.com/hibiken/asynq" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +// TASKTIME 定义为每天凌晨3点执行 +const TASKTIME = "0 3 * * *" + +type CleanQueryDataHandler struct { + svcCtx *svc.ServiceContext +} + +func NewCleanQueryDataHandler(svcCtx *svc.ServiceContext) *CleanQueryDataHandler { + return &CleanQueryDataHandler{ + svcCtx: svcCtx, + } +} + +// 获取配置值 +func (l *CleanQueryDataHandler) getConfigValue(ctx context.Context, key string) (string, error) { + // 通过缓存获取配置 + config, err := l.svcCtx.QueryCleanupConfigModel.FindOneByConfigKey(ctx, key) + if err != nil { + if err == model.ErrNotFound { + return "", fmt.Errorf("配置项 %s 不存在", key) + } + return "", err + } + + // 检查配置状态 + if config.Status != 1 { + return "", fmt.Errorf("配置项 %s 已禁用或已删除", key) + } + + return config.ConfigValue, nil +} + +func (l *CleanQueryDataHandler) ProcessTask(ctx context.Context, t *asynq.Task) error { + now := time.Now() + logx.Infof("%s - 开始执行查询数据清理任务", now.Format("2006-01-02 15:04:05")) + + // 1. 检查是否启用清理 + enableCleanup, err := l.getConfigValue(ctx, "enable_cleanup") + if err != nil { + return err + } + if enableCleanup != "1" { + logx.Infof("查询数据清理任务已禁用") + return nil + } + + // 2. 获取保留天数 + retentionDaysStr, err := l.getConfigValue(ctx, "retention_days") + if err != nil { + return err + } + retentionDays, err := strconv.Atoi(retentionDaysStr) + if err != nil { + return err + } + + // 3. 获取批次大小 + batchSizeStr, err := l.getConfigValue(ctx, "batch_size") + if err != nil { + return err + } + batchSize, err := strconv.Atoi(batchSizeStr) + if err != nil { + return err + } + + // 计算清理截止时间 + cleanupBefore := now.AddDate(0, 0, -retentionDays) + + // 创建清理日志记录 + cleanupLog := &model.QueryCleanupLog{ + CleanupTime: now, + CleanupBefore: cleanupBefore, + Status: 1, + Remark: sql.NullString{String: "定时清理数据", Valid: true}, + } + + // 使用事务处理清理操作和日志记录 + err = l.svcCtx.QueryModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { + // 分批处理 + for { + // 1. 查询一批要删除的记录 + builder := l.svcCtx.QueryModel.SelectBuilder(). + Where("create_time < ?", cleanupBefore). + Where("del_state = ?", globalkey.DelStateNo). + Limit(uint64(batchSize)) + + queries, err := l.svcCtx.QueryModel.FindAll(ctx, builder, "") + if err != nil { + cleanupLog.Status = 2 + cleanupLog.ErrorMsg = sql.NullString{String: err.Error(), Valid: true} + return err + } + + if len(queries) == 0 { + break // 没有更多数据需要清理 + } + + // 2. 执行清理 + for _, query := range queries { + err = l.svcCtx.QueryModel.DeleteSoft(ctx, session, query) + if err != nil { + cleanupLog.Status = 2 + cleanupLog.ErrorMsg = sql.NullString{String: err.Error(), Valid: true} + return err + } + } + + // 3. 更新影响行数 + cleanupLog.AffectedRows += int64(len(queries)) + + // 4. 保存清理日志(每批次都记录) + cleanupLogInsertResult, err := l.svcCtx.QueryCleanupLogModel.Insert(ctx, session, cleanupLog) + if err != nil { + return err + } + cleanupLogId, err := cleanupLogInsertResult.LastInsertId() + if err != nil { + return err + } + + // 5. 保存清理明细 + for _, query := range queries { + detail := &model.QueryCleanupDetail{ + CleanupLogId: cleanupLogId, + QueryId: query.Id, + OrderId: query.OrderId, + UserId: query.UserId, + ProductId: query.ProductId, + QueryState: query.QueryState, + CreateTimeOld: query.CreateTime, + } + _, err = l.svcCtx.QueryCleanupDetailModel.Insert(ctx, session, detail) + if err != nil { + return err + } + } + } + + return nil + }) + + if err != nil { + logx.Errorf("%s - 清理查询数据失败: %v", now.Format("2006-01-02 15:04:05"), err) + return err + } + + logx.Infof("%s - 查询数据清理完成,共删除 %d 条记录", now.Format("2006-01-02 15:04:05"), cleanupLog.AffectedRows) + return nil +} diff --git a/app/main/api/internal/queue/paySuccessNotify.go b/app/main/api/internal/queue/paySuccessNotify.go new file mode 100644 index 0000000..e89373b --- /dev/null +++ b/app/main/api/internal/queue/paySuccessNotify.go @@ -0,0 +1,374 @@ +package queue + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "regexp" + "strings" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + "znc-server/app/main/model" + "znc-server/pkg/lzkit/crypto" + "znc-server/pkg/lzkit/lzUtils" + + "github.com/hibiken/asynq" + "github.com/zeromicro/go-zero/core/logx" +) + +type PaySuccessNotifyUserHandler struct { + svcCtx *svc.ServiceContext +} + +func NewPaySuccessNotifyUserHandler(svcCtx *svc.ServiceContext) *PaySuccessNotifyUserHandler { + return &PaySuccessNotifyUserHandler{ + svcCtx: svcCtx, + } +} + +var payload struct { + OrderID int64 `json:"order_id"` +} + +func (l *PaySuccessNotifyUserHandler) ProcessTask(ctx context.Context, t *asynq.Task) error { + // 从任务的负载中解码数据 + if err := json.Unmarshal(t.Payload(), &payload); err != nil { + return fmt.Errorf("解析任务负载失败: %w", err) + } + + order, err := l.svcCtx.OrderModel.FindOne(ctx, payload.OrderID) + if err != nil { + return fmt.Errorf("无效的订单ID: %d, %v", payload.OrderID, err) + } + env := os.Getenv("ENV") + if order.Status != "paid" && env != "development" { + err = fmt.Errorf("无效的订单: %d", payload.OrderID) + logx.Errorf("处理任务失败,原因: %v", err) + return asynq.SkipRetry + } + product, err := l.svcCtx.ProductModel.FindOne(ctx, order.ProductId) + if err != nil { + return fmt.Errorf("找不到相关产品: orderID: %d, productID: %d", payload.OrderID, order.ProductId) + } + redisKey := fmt.Sprintf(types.QueryCacheKey, order.UserId, order.OrderNo) + cache, cacheErr := l.svcCtx.Redis.GetCtx(ctx, redisKey) + if cacheErr != nil { + return fmt.Errorf("获取缓存内容失败: %+v", cacheErr) + } + var data types.QueryCacheLoad + err = json.Unmarshal([]byte(cache), &data) + if err != nil { + return fmt.Errorf("解析缓存内容失败: %+v", err) + } + secretKey := l.svcCtx.Config.Encrypt.SecretKey + key, decodeErr := hex.DecodeString(secretKey) + if decodeErr != nil { + return fmt.Errorf("获取AES密钥失败: %+v", decodeErr) + } + decryptData, aesdecryptErr := crypto.AesDecrypt(data.Params, key) + if aesdecryptErr != nil { + return fmt.Errorf("解密参数失败: %+v", aesdecryptErr) + } + + // 敏感数据脱敏处理 + desensitizedParams, err := l.desensitizeParams(decryptData) + if err != nil { + return fmt.Errorf("脱敏处理失败: %+v", err) + } + + // 对脱敏后的数据进行AES加密 + encryptedParams, encryptErr := crypto.AesEncrypt(desensitizedParams, key) + if encryptErr != nil { + return fmt.Errorf("加密脱敏数据失败: %+v", encryptErr) + } + + query := &model.Query{ + OrderId: order.Id, + UserId: order.UserId, + ProductId: product.Id, + QueryParams: encryptedParams, + QueryState: "pending", + } + result, insertQueryErr := l.svcCtx.QueryModel.Insert(ctx, nil, query) + if insertQueryErr != nil { + return fmt.Errorf("保存查询失败: %+v", insertQueryErr) + } + + // 获取插入后的ID + queryId, err := result.LastInsertId() + if err != nil { + return fmt.Errorf("获取插入的查询ID失败: %+v", err) + } + + // 从数据库中查询完整的查询记录 + query, err = l.svcCtx.QueryModel.FindOne(ctx, queryId) + if err != nil { + return fmt.Errorf("获取插入后的查询记录失败: %+v", err) + } + + combinedResponse, err := l.svcCtx.ApiRequestService.ProcessRequests(decryptData, product.Id) + if err != nil { + return l.handleError(ctx, err, order, query) + } + // 加密返回响应 + encryptData, aesEncryptErr := crypto.AesEncrypt(combinedResponse, key) + if aesEncryptErr != nil { + err = fmt.Errorf("加密响应信息失败: %v", aesEncryptErr) + return l.handleError(ctx, err, order, query) + } + query.QueryData = lzUtils.StringToNullString(encryptData) + updateErr := l.svcCtx.QueryModel.UpdateWithVersion(ctx, nil, query) + if updateErr != nil { + err = fmt.Errorf("保存响应数据失败: %v", updateErr) + return l.handleError(ctx, err, order, query) + } + + query.QueryState = "success" + updateQueryErr := l.svcCtx.QueryModel.UpdateWithVersion(ctx, nil, query) + if updateQueryErr != nil { + updateQueryErr = fmt.Errorf("修改查询状态失败: %v", updateQueryErr) + return l.handleError(ctx, updateQueryErr, order, query) + } + + err = l.svcCtx.AgentService.AgentProcess(ctx, order) + if err != nil { + return l.handleError(ctx, err, order, query) + } + + _, delErr := l.svcCtx.Redis.DelCtx(ctx, redisKey) + if delErr != nil { + logx.Errorf("删除Redis缓存失败,但任务已成功处理,订单ID: %d, 错误: %v", order.Id, delErr) + } + + return nil +} + +// 定义一个中间件函数 +func (l *PaySuccessNotifyUserHandler) handleError(ctx context.Context, err error, order *model.Order, query *model.Query) error { + logx.Errorf("处理任务失败,原因: %v", err) + + redisKey := fmt.Sprintf(types.QueryCacheKey, order.UserId, order.OrderNo) + _, delErr := l.svcCtx.Redis.DelCtx(ctx, redisKey) + if delErr != nil { + logx.Errorf("删除Redis缓存失败,订单ID: %d, 错误: %v", order.Id, delErr) + } + + if order.Status == "paid" && query.QueryState == "pending" { + // 更新查询状态为失败 + query.QueryState = "failed" + updateQueryErr := l.svcCtx.QueryModel.UpdateWithVersion(ctx, nil, query) + if updateQueryErr != nil { + logx.Errorf("更新查询状态失败,订单ID: %d, 错误: %v", order.Id, updateQueryErr) + return asynq.SkipRetry + } + + // 退款 + if order.PaymentPlatform == "wechat" { + refundErr := l.svcCtx.WechatPayService.WeChatRefund(ctx, order.OrderNo, order.Amount, order.Amount) + if refundErr != nil { + logx.Error(refundErr) + return asynq.SkipRetry + } + } else { + refund, refundErr := l.svcCtx.AlipayService.AliRefund(ctx, order.OrderNo, order.Amount) + if refundErr != nil { + logx.Error(refundErr) + return asynq.SkipRetry + } + if refund.IsSuccess() { + logx.Errorf("支付宝退款成功, orderID: %d", order.Id) + // 更新订单状态为退款 + order.Status = "refunded" + updateOrderErr := l.svcCtx.OrderModel.UpdateWithVersion(ctx, nil, order) + if updateOrderErr != nil { + logx.Errorf("更新订单状态失败,订单ID: %d, 错误: %v", order.Id, updateOrderErr) + return fmt.Errorf("更新订单状态失败: %v", updateOrderErr) + } + return asynq.SkipRetry + } else { + logx.Errorf("支付宝退款失败:%v", refundErr) + return asynq.SkipRetry + } + // 直接成功 + } + + } + + return asynq.SkipRetry +} + +// desensitizeParams 对敏感数据进行脱敏处理 +func (l *PaySuccessNotifyUserHandler) desensitizeParams(data []byte) ([]byte, error) { + // 解析JSON数据到map + var paramsMap map[string]interface{} + if err := json.Unmarshal(data, ¶msMap); err != nil { + return nil, fmt.Errorf("解析JSON数据失败: %v", err) + } + + // 处理可能包含敏感信息的字段 + for key, value := range paramsMap { + if strValue, ok := value.(string); ok { + // 根据字段名和内容判断并脱敏 + if isNameField(key) && len(strValue) > 0 { + // 姓名脱敏 + paramsMap[key] = maskName(strValue) + } else if isIDCardField(key) && len(strValue) > 10 { + // 身份证号脱敏 + paramsMap[key] = maskIDCard(strValue) + } else if isPhoneField(key) && len(strValue) >= 8 { + // 手机号脱敏 + paramsMap[key] = maskPhone(strValue) + } else if len(strValue) > 3 { + // 其他所有未匹配的字段都进行通用脱敏 + paramsMap[key] = maskGeneral(strValue) + } + } else if mapValue, ok := value.(map[string]interface{}); ok { + // 递归处理嵌套的map + for subKey, subValue := range mapValue { + if subStrValue, ok := subValue.(string); ok { + if isNameField(subKey) && len(subStrValue) > 0 { + mapValue[subKey] = maskName(subStrValue) + } else if isIDCardField(subKey) && len(subStrValue) > 10 { + mapValue[subKey] = maskIDCard(subStrValue) + } else if isPhoneField(subKey) && len(subStrValue) >= 8 { + mapValue[subKey] = maskPhone(subStrValue) + } else if len(subStrValue) > 3 { + // 其他所有未匹配的字段都进行通用脱敏 + mapValue[subKey] = maskGeneral(subStrValue) + } + } + } + } + } + + // 将处理后的map重新序列化为JSON + return json.Marshal(paramsMap) +} + +// 判断是否为姓名字段 +func isNameField(key string) bool { + key = strings.ToLower(key) + return strings.Contains(key, "name") || strings.Contains(key, "姓名") || + strings.Contains(key, "owner") || strings.Contains(key, "main") +} + +// 判断是否为身份证字段 +func isIDCardField(key string) bool { + key = strings.ToLower(key) + return strings.Contains(key, "idcard") || strings.Contains(key, "id_card") || + strings.Contains(key, "身份证") || strings.Contains(key, "证件号") +} + +// 判断是否为手机号字段 +func isPhoneField(key string) bool { + key = strings.ToLower(key) + return strings.Contains(key, "phone") || strings.Contains(key, "mobile") || + strings.Contains(key, "手机") || strings.Contains(key, "电话") +} + +// 判断是否包含敏感数据模式 +func containsSensitivePattern(value string) bool { + // 检查是否包含连续的数字或字母模式 + numPattern := regexp.MustCompile(`\d{6,}`) + return numPattern.MatchString(value) +} + +// 姓名脱敏 +func maskName(name string) string { + // 将字符串转换为rune切片以正确处理中文字符 + runes := []rune(name) + length := len(runes) + + if length <= 1 { + return name + } + + if length == 2 { + // 两个字:保留第一个字,第二个字用*替代 + return string(runes[0]) + "*" + } + + // 三个字及以上:保留首尾字,中间用*替代 + first := string(runes[0]) + last := string(runes[length-1]) + mask := strings.Repeat("*", length-2) + + return first + mask + last +} + +// 身份证号脱敏 +func maskIDCard(idCard string) string { + length := len(idCard) + if length <= 10 { + return idCard // 如果长度太短,可能不是身份证,不处理 + } + // 保留前3位和后4位 + return idCard[:3] + strings.Repeat("*", length-7) + idCard[length-4:] +} + +// 手机号脱敏 +func maskPhone(phone string) string { + length := len(phone) + if length < 8 { + return phone // 如果长度太短,可能不是手机号,不处理 + } + // 保留前3位和后4位 + return phone[:3] + strings.Repeat("*", length-7) + phone[length-4:] +} + +// 通用敏感信息脱敏 - 根据字符串长度比例进行脱敏 +func maskGeneral(value string) string { + length := len(value) + + // 小于3个字符的不脱敏 + if length <= 3 { + return value + } + + // 根据字符串长度计算保留字符数 + var prefixLen, suffixLen int + + switch { + case length <= 6: // 短字符串 + // 保留首尾各1个字符 + prefixLen, suffixLen = 1, 1 + case length <= 10: // 中等长度字符串 + // 保留首部30%和尾部20%的字符 + prefixLen = int(float64(length) * 0.3) + suffixLen = int(float64(length) * 0.2) + case length <= 20: // 较长字符串 + // 保留首部25%和尾部15%的字符 + prefixLen = int(float64(length) * 0.25) + suffixLen = int(float64(length) * 0.15) + default: // 非常长的字符串 + // 保留首部20%和尾部10%的字符 + prefixLen = int(float64(length) * 0.2) + suffixLen = int(float64(length) * 0.1) + } + + // 确保至少有一个字符被保留 + if prefixLen < 1 { + prefixLen = 1 + } + if suffixLen < 1 { + suffixLen = 1 + } + + // 确保前缀和后缀总长不超过总长度的80% + if prefixLen+suffixLen > int(float64(length)*0.8) { + // 调整为总长度的80% + totalVisible := int(float64(length) * 0.8) + // 前缀占60%,后缀占40% + prefixLen = int(float64(totalVisible) * 0.6) + suffixLen = totalVisible - prefixLen + } + + // 创建脱敏后的字符串 + prefix := value[:prefixLen] + suffix := value[length-suffixLen:] + masked := strings.Repeat("*", length-prefixLen-suffixLen) + + return prefix + masked + suffix +} diff --git a/app/main/api/internal/queue/routes.go b/app/main/api/internal/queue/routes.go new file mode 100644 index 0000000..e8f8e10 --- /dev/null +++ b/app/main/api/internal/queue/routes.go @@ -0,0 +1,40 @@ +package queue + +import ( + "context" + "fmt" + "znc-server/app/main/api/internal/svc" + "znc-server/app/main/api/internal/types" + + "github.com/hibiken/asynq" +) + +type CronJob struct { + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewCronJob(ctx context.Context, svcCtx *svc.ServiceContext) *CronJob { + return &CronJob{ + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *CronJob) Register() *asynq.ServeMux { + redisClientOpt := asynq.RedisClientOpt{Addr: l.svcCtx.Config.CacheRedis[0].Host, Password: l.svcCtx.Config.CacheRedis[0].Pass} + scheduler := asynq.NewScheduler(redisClientOpt, nil) + task := asynq.NewTask(types.MsgCleanQueryData, nil, nil) + _, err := scheduler.Register(TASKTIME, task) + if err != nil { + panic(fmt.Sprintf("定时任务注册失败:%v", err)) + } + scheduler.Start() + fmt.Println("定时任务启动!!!") + + mux := asynq.NewServeMux() + mux.Handle(types.MsgPaySuccessQuery, NewPaySuccessNotifyUserHandler(l.svcCtx)) + mux.Handle(types.MsgCleanQueryData, NewCleanQueryDataHandler(l.svcCtx)) + + return mux +} diff --git a/app/main/api/internal/service/adminPromotionLinkStatsService.go b/app/main/api/internal/service/adminPromotionLinkStatsService.go new file mode 100644 index 0000000..277ec9a --- /dev/null +++ b/app/main/api/internal/service/adminPromotionLinkStatsService.go @@ -0,0 +1,211 @@ +package service + +import ( + "context" + "database/sql" + "time" + + "znc-server/app/main/model" + "znc-server/common/xerr" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type AdminPromotionLinkStatsService struct { + logx.Logger + AdminPromotionLinkModel model.AdminPromotionLinkModel + AdminPromotionLinkStatsTotalModel model.AdminPromotionLinkStatsTotalModel + AdminPromotionLinkStatsHistoryModel model.AdminPromotionLinkStatsHistoryModel +} + +func NewAdminPromotionLinkStatsService( + AdminPromotionLinkModel model.AdminPromotionLinkModel, + AdminPromotionLinkStatsTotalModel model.AdminPromotionLinkStatsTotalModel, + AdminPromotionLinkStatsHistoryModel model.AdminPromotionLinkStatsHistoryModel, +) *AdminPromotionLinkStatsService { + return &AdminPromotionLinkStatsService{ + Logger: logx.WithContext(context.Background()), + AdminPromotionLinkModel: AdminPromotionLinkModel, + AdminPromotionLinkStatsTotalModel: AdminPromotionLinkStatsTotalModel, + AdminPromotionLinkStatsHistoryModel: AdminPromotionLinkStatsHistoryModel, + } +} + +// ensureTotalStats 确保总统计记录存在,如果不存在则创建 +func (s *AdminPromotionLinkStatsService) ensureTotalStats(ctx context.Context, session sqlx.Session, linkId int64) (*model.AdminPromotionLinkStatsTotal, error) { + totalStats, err := s.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(ctx, linkId) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + // 如果记录不存在,创建新记录 + totalStats = &model.AdminPromotionLinkStatsTotal{ + LinkId: linkId, + ClickCount: 0, + PayCount: 0, + PayAmount: 0, + } + _, err = s.AdminPromotionLinkStatsTotalModel.Insert(ctx, session, totalStats) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建总统计记录失败: %+v", err) + } + // 重新获取创建后的记录 + totalStats, err = s.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(ctx, linkId) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取新创建的总统计记录失败: %+v", err) + } + } else { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询总统计失败: %+v", err) + } + } + return totalStats, nil +} + +// ensureHistoryStats 确保历史统计记录存在,如果不存在则创建 +func (s *AdminPromotionLinkStatsService) ensureHistoryStats(ctx context.Context, session sqlx.Session, linkId int64, today time.Time) (*model.AdminPromotionLinkStatsHistory, error) { + historyStats, err := s.AdminPromotionLinkStatsHistoryModel.FindOneByLinkIdStatsDate(ctx, linkId, today) + if err != nil { + if errors.Is(err, model.ErrNotFound) { + // 如果记录不存在,创建新记录 + historyStats = &model.AdminPromotionLinkStatsHistory{ + LinkId: linkId, + StatsDate: today, + ClickCount: 0, + PayCount: 0, + PayAmount: 0, + } + _, err = s.AdminPromotionLinkStatsHistoryModel.Insert(ctx, session, historyStats) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建今日统计记录失败: %+v", err) + } + // 重新获取创建后的记录 + historyStats, err = s.AdminPromotionLinkStatsHistoryModel.FindOneByLinkIdStatsDate(ctx, linkId, today) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "获取新创建的今日统计记录失败: %+v", err) + } + } else { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询今日统计记录失败: %+v", err) + } + } + return historyStats, nil +} + +// UpdateLinkStats 更新推广链接统计 +func (s *AdminPromotionLinkStatsService) UpdateLinkStats(ctx context.Context, linkId int64) error { + return s.AdminPromotionLinkStatsTotalModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { + // 确保总统计记录存在 + totalStats, err := s.ensureTotalStats(ctx, session, linkId) + if err != nil { + return err + } + + // 更新总统计 + totalStats.ClickCount++ + totalStats.LastClickTime = sql.NullTime{Time: time.Now(), Valid: true} + err = s.AdminPromotionLinkStatsTotalModel.UpdateWithVersion(ctx, session, totalStats) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新总统计失败: %+v", err) + } + + // 确保历史统计记录存在 + now := time.Now() + today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) + historyStats, err := s.ensureHistoryStats(ctx, session, linkId, today) + if err != nil { + return err + } + + // 更新历史统计 + historyStats.ClickCount++ + historyStats.LastClickTime = sql.NullTime{Time: time.Now(), Valid: true} + err = s.AdminPromotionLinkStatsHistoryModel.UpdateWithVersion(ctx, session, historyStats) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新历史统计失败: %+v", err) + } + + return nil + }) +} + +// UpdatePaymentStats 更新付费统计 +func (s *AdminPromotionLinkStatsService) UpdatePaymentStats(ctx context.Context, linkId int64, amount float64) error { + return s.AdminPromotionLinkStatsTotalModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { + // 确保总统计记录存在 + totalStats, err := s.ensureTotalStats(ctx, session, linkId) + if err != nil { + return err + } + + // 更新总统计 + totalStats.PayCount++ + totalStats.PayAmount += amount + totalStats.LastPayTime = sql.NullTime{Time: time.Now(), Valid: true} + err = s.AdminPromotionLinkStatsTotalModel.UpdateWithVersion(ctx, session, totalStats) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新总统计失败: %+v", err) + } + + // 确保历史统计记录存在 + now := time.Now() + today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) + historyStats, err := s.ensureHistoryStats(ctx, session, linkId, today) + if err != nil { + return err + } + + // 更新历史统计 + historyStats.PayCount++ + historyStats.PayAmount += amount + historyStats.LastPayTime = sql.NullTime{Time: time.Now(), Valid: true} + err = s.AdminPromotionLinkStatsHistoryModel.UpdateWithVersion(ctx, session, historyStats) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新历史统计失败: %+v", err) + } + + return nil + }) +} + +// CreateLinkStats 创建新的推广链接统计记录 +func (s *AdminPromotionLinkStatsService) CreateLinkStats(ctx context.Context, linkId int64) error { + return s.AdminPromotionLinkStatsTotalModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { + // 检查总统计记录是否已存在 + _, err := s.AdminPromotionLinkStatsTotalModel.FindOneByLinkId(ctx, linkId) + if err == nil { + // 记录已存在,不需要创建 + return nil + } + if err != model.ErrNotFound { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询总统计记录失败: %+v", err) + } + + // 创建总统计记录 + totalStats := &model.AdminPromotionLinkStatsTotal{ + LinkId: linkId, + ClickCount: 0, + PayCount: 0, + PayAmount: 0, + } + _, err = s.AdminPromotionLinkStatsTotalModel.Insert(ctx, session, totalStats) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建总统计记录失败: %+v", err) + } + + // 创建今日历史统计记录 + now := time.Now() + today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local) + historyStats := &model.AdminPromotionLinkStatsHistory{ + LinkId: linkId, + StatsDate: today, + ClickCount: 0, + PayCount: 0, + PayAmount: 0, + } + _, err = s.AdminPromotionLinkStatsHistoryModel.Insert(ctx, session, historyStats) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建历史统计记录失败: %+v", err) + } + + return nil + }) +} diff --git a/app/main/api/internal/service/agentService.go b/app/main/api/internal/service/agentService.go new file mode 100644 index 0000000..5643e44 --- /dev/null +++ b/app/main/api/internal/service/agentService.go @@ -0,0 +1,344 @@ +package service + +import ( + "context" + "znc-server/app/main/api/internal/config" + "znc-server/app/main/model" + "znc-server/pkg/lzkit/lzUtils" + + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type AgentService struct { + config config.Config + AgentModel model.AgentModel + AgentAuditModel model.AgentAuditModel + AgentClosureModel model.AgentClosureModel + AgentCommissionModel model.AgentCommissionModel + AgentCommissionDeductionModel model.AgentCommissionDeductionModel + AgentWalletModel model.AgentWalletModel + AgentLinkModel model.AgentLinkModel + AgentOrderModel model.AgentOrderModel + AgentRewardsModel model.AgentRewardsModel + AgentMembershipConfigModel model.AgentMembershipConfigModel + AgentMembershipRechargeOrderModel model.AgentMembershipRechargeOrderModel + AgentMembershipUserConfigModel model.AgentMembershipUserConfigModel + AgentProductConfigModel model.AgentProductConfigModel + AgentPlatformDeductionModel model.AgentPlatformDeductionModel + AgentActiveStatModel model.AgentActiveStatModel + AgentWithdrawalModel model.AgentWithdrawalModel +} + +func NewAgentService(c config.Config, agentModel model.AgentModel, agentAuditModel model.AgentAuditModel, + agentClosureModel model.AgentClosureModel, agentCommissionModel model.AgentCommissionModel, + agentCommissionDeductionModel model.AgentCommissionDeductionModel, agentWalletModel model.AgentWalletModel, agentLinkModel model.AgentLinkModel, agentOrderModel model.AgentOrderModel, agentRewardsModel model.AgentRewardsModel, + agentMembershipConfigModel model.AgentMembershipConfigModel, + agentMembershipRechargeOrderModel model.AgentMembershipRechargeOrderModel, + agentMembershipUserConfigModel model.AgentMembershipUserConfigModel, + agentProductConfigModel model.AgentProductConfigModel, agentPlatformDeductionModel model.AgentPlatformDeductionModel, + agentActiveStatModel model.AgentActiveStatModel, agentWithdrawalModel model.AgentWithdrawalModel) *AgentService { + + return &AgentService{ + config: c, + AgentModel: agentModel, + AgentAuditModel: agentAuditModel, + AgentClosureModel: agentClosureModel, + AgentCommissionModel: agentCommissionModel, + AgentCommissionDeductionModel: agentCommissionDeductionModel, + AgentWalletModel: agentWalletModel, + AgentLinkModel: agentLinkModel, + AgentOrderModel: agentOrderModel, + AgentRewardsModel: agentRewardsModel, + AgentMembershipConfigModel: agentMembershipConfigModel, + AgentMembershipRechargeOrderModel: agentMembershipRechargeOrderModel, + AgentMembershipUserConfigModel: agentMembershipUserConfigModel, + AgentProductConfigModel: agentProductConfigModel, + AgentPlatformDeductionModel: agentPlatformDeductionModel, + AgentActiveStatModel: agentActiveStatModel, + AgentWithdrawalModel: agentWithdrawalModel, + } +} + +// AgentProcess 推广单成功 +func (l *AgentService) AgentProcess(ctx context.Context, order *model.Order) error { + // 获取是否该订单是代理推广订单 + agentOrderModel, err := l.AgentOrderModel.FindOneByOrderId(ctx, order.Id) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return err + } + if errors.Is(err, model.ErrNotFound) || agentOrderModel == nil { + return nil + } + // 事务 + transErr := l.AgentWalletModel.Trans(ctx, func(transCtx context.Context, session sqlx.Session) error { + agentID := agentOrderModel.AgentId + agentProductConfigModel, findAgentProductConfigModelErr := l.AgentProductConfigModel.FindOneByProductId(transCtx, order.ProductId) + if findAgentProductConfigModelErr != nil { + return findAgentProductConfigModelErr + } + // 平台底价成本 + PlatformCostAmount, platformCostErr := l.PlatformCost(transCtx, agentID, agentProductConfigModel, session) + if platformCostErr != nil { + return platformCostErr + } + + // 平台提价成本 + PlatformPricingAmount, platformPricingErr := l.PlatformPricing(transCtx, agentID, order.Amount, agentProductConfigModel, session) + if platformPricingErr != nil { + return platformPricingErr + } + + // 查找上级 + AgentClosureModel, findAgentClosureModelErr := l.AgentClosureModel.FindOneByDescendantIdDepth(transCtx, agentID, 1) + if findAgentClosureModelErr != nil && !errors.Is(findAgentClosureModelErr, model.ErrNotFound) { + return findAgentClosureModelErr + } + + var descendantDeductedAmount = 0.00 + if AgentClosureModel != nil { + AncestorId := AgentClosureModel.AncestorId + AncestorModel, findAgentModelErr := l.AgentModel.FindOne(transCtx, AncestorId) + if findAgentModelErr != nil != errors.Is(findAgentModelErr, model.ErrNotFound) { + return findAgentModelErr + } + if AgentClosureModel != nil { + if AncestorModel.LevelName == "" { + AncestorModel.LevelName = model.AgentLeveNameNormal + } + AgentMembershipConfigModel, findAgentMembersipConfigModelErr := l.AgentMembershipConfigModel.FindOneByLevelName(ctx, AncestorModel.LevelName) + if findAgentMembersipConfigModelErr != nil { + return findAgentMembersipConfigModelErr + } + // 定价 + commissionCost, commissionCostErr := l.CommissionCost(transCtx, agentID, AncestorId, AgentMembershipConfigModel, order.ProductId, session) + if commissionCostErr != nil { + return commissionCostErr + } + // 提价 + commissionPricing, commissionPricingErr := l.CommissionPricing(transCtx, agentID, AncestorId, AgentMembershipConfigModel, order.ProductId, order.Amount, session) + if commissionPricingErr != nil { + return commissionPricingErr + } + + // 上级克扣的成本 + descendantDeductedAmount = commissionCost + commissionPricing + + // 佣金 + ancestorCommissionReward, ancestorCommissionErr := l.AncestorCommission(transCtx, agentID, AncestorId, session) + if ancestorCommissionErr != nil { + return ancestorCommissionErr + } + + // 给上级成本以及佣金 + ancestorCommissionAmount := commissionCost + commissionPricing + ancestorCommissionReward + ancestorWallet, findAgentWalletModelErr := l.AgentWalletModel.FindOneByAgentId(transCtx, AncestorId) + if findAgentWalletModelErr != nil { + return findAgentWalletModelErr + } + + ancestorWallet.Balance += ancestorCommissionAmount + ancestorWallet.TotalEarnings += ancestorCommissionAmount + updateErr := l.AgentWalletModel.UpdateWithVersion(transCtx, session, ancestorWallet) + if updateErr != nil { + return updateErr + } + } + } + + // 推广人扣除金额 = 平台成本价 + 平台提价成本 + 上级佣金 + deductedAmount := PlatformCostAmount + PlatformPricingAmount + descendantDeductedAmount + agentCommissionErr := l.AgentCommission(transCtx, agentID, order, deductedAmount, session) + if agentCommissionErr != nil { + return agentCommissionErr + } + + return nil + }) + if transErr != nil { + return transErr + } + + return nil +} + +// AgentCommission 直推报告推广人佣金 +func (l *AgentService) AgentCommission(ctx context.Context, agentID int64, order *model.Order, deductedAmount float64, session sqlx.Session) error { + agentWalletModel, findAgentWalletModelErr := l.AgentWalletModel.FindOneByAgentId(ctx, agentID) + if findAgentWalletModelErr != nil { + return findAgentWalletModelErr + } + // 推广人最终获得代理佣金 + finalCommission := order.Amount - deductedAmount + agentWalletModel.Balance += finalCommission + agentWalletModel.TotalEarnings += finalCommission + + agentCommission := model.AgentCommission{ + AgentId: agentID, + OrderId: order.Id, + Amount: finalCommission, + ProductId: order.ProductId, + } + _, insertAgentCommissionErr := l.AgentCommissionModel.Insert(ctx, session, &agentCommission) + if insertAgentCommissionErr != nil { + return insertAgentCommissionErr + } + + updateAgentWalletErr := l.AgentWalletModel.UpdateWithVersion(ctx, session, agentWalletModel) + if updateAgentWalletErr != nil { + return updateAgentWalletErr + } + return nil +} + +// AncestorCommission 直推报告上级佣金(奖励型) +func (l *AgentService) AncestorCommission(ctx context.Context, descendantId int64, ancestorId int64, session sqlx.Session) (float64, error) { + agentModel, err := l.AgentModel.FindOne(ctx, ancestorId) + if err != nil { + return 0, err + } + if agentModel.LevelName == "" { + agentModel.LevelName = model.AgentLeveNameNormal + } + agentMembershipConfigModel, err := l.AgentMembershipConfigModel.FindOneByLevelName(ctx, agentModel.LevelName) + if err != nil { + return 0, err + } + + if agentMembershipConfigModel.ReportCommission.Valid { + reportCommissionAmount := agentMembershipConfigModel.ReportCommission.Float64 + agentRewards := model.AgentRewards{ + AgentId: ancestorId, + Amount: reportCommissionAmount, + RelationAgentId: lzUtils.Int64ToNullInt64(descendantId), + Type: model.AgentRewardsTypeDescendantPromotion, + } + + _, agentRewardsModelInsetErr := l.AgentRewardsModel.Insert(ctx, session, &agentRewards) + if agentRewardsModelInsetErr != nil { + return 0, agentRewardsModelInsetErr + } + return reportCommissionAmount, nil + } + + return 0, nil +} + +// PlatformCost 平台底价成本 +func (l *AgentService) PlatformCost(ctx context.Context, agentID int64, agentProductConfigModel *model.AgentProductConfig, session sqlx.Session) (float64, error) { + + costAgentPlatformDeductionModel := model.AgentPlatformDeduction{ + AgentId: agentID, + Amount: agentProductConfigModel.CostPrice, + Type: model.AgentDeductionTypeCost, + } + + _, err := l.AgentPlatformDeductionModel.Insert(ctx, session, &costAgentPlatformDeductionModel) + if err != nil { + return 0, err + } + return agentProductConfigModel.CostPrice, nil +} + +// PlatformPricing 平台提价成本 +func (l *AgentService) PlatformPricing(ctx context.Context, agentID int64, pricing float64, agentProductConfigModel *model.AgentProductConfig, session sqlx.Session) (float64, error) { + // 2. 计算平台提价成本 + if pricing > agentProductConfigModel.PricingStandard { + // 超出部分 + overpricing := pricing - agentProductConfigModel.PricingStandard + + // 收取成本 + overpricingCost := overpricing * agentProductConfigModel.OverpricingRatio + + pricingAgentPlatformDeductionModel := model.AgentPlatformDeduction{ + AgentId: agentID, + Amount: overpricingCost, + Type: model.AgentDeductionTypePricing, + } + + _, err := l.AgentPlatformDeductionModel.Insert(ctx, session, &pricingAgentPlatformDeductionModel) + if err != nil { + return 0, err + } + return overpricingCost, nil + } + return 0, nil +} + +// CommissionCost 上级底价成本 +func (l *AgentService) CommissionCost(ctx context.Context, descendantId int64, AncestorId int64, agentMembershipConfigModel *model.AgentMembershipConfig, productID int64, session sqlx.Session) (float64, error) { + if agentMembershipConfigModel.PriceIncreaseAmount.Valid { + // 拥有则查看该上级设定的成本 + agentMembershipUserConfigModel, findAgentMembershipUserConfigModelErr := l.AgentMembershipUserConfigModel.FindOneByAgentIdProductId(ctx, AncestorId, productID) + if findAgentMembershipUserConfigModelErr != nil { + return 0, findAgentMembershipUserConfigModelErr + } + + deductCostAmount := agentMembershipUserConfigModel.PriceIncreaseAmount + + agentCommissionDeductionModel := model.AgentCommissionDeduction{ + AgentId: AncestorId, + DeductedAgentId: descendantId, + Amount: deductCostAmount, + Type: model.AgentDeductionTypeCost, + ProductId: productID, + } + + _, insertAgentCommissionDeductionModelErr := l.AgentCommissionDeductionModel.Insert(ctx, session, &agentCommissionDeductionModel) + if insertAgentCommissionDeductionModelErr != nil { + return 0, insertAgentCommissionDeductionModelErr + } + + return deductCostAmount, nil + } + return 0, nil +} + +// CommissionPricing 上级提价成本 +func (l *AgentService) CommissionPricing(ctx context.Context, descendantId int64, AncestorId int64, agentMembershipConfigModel *model.AgentMembershipConfig, productID int64, pricing float64, session sqlx.Session) (float64, error) { + //看上级代理等级否有拥有定价标准收益功能 + if agentMembershipConfigModel.PriceIncreaseMax.Valid && agentMembershipConfigModel.PriceRatio.Valid { + // 拥有则查看该上级设定的成本 + agentMembershipUserConfigModel, findAgentMembershipUserConfigModelErr := l.AgentMembershipUserConfigModel.FindOneByAgentIdProductId(ctx, AncestorId, productID) + if findAgentMembershipUserConfigModelErr != nil { + return 0, findAgentMembershipUserConfigModelErr + } + + // 计算是否在范围内 + var pricingRange float64 + if pricing > agentMembershipUserConfigModel.PriceRangeFrom { + if pricing > agentMembershipUserConfigModel.PriceRangeTo { + pricingRange = agentMembershipUserConfigModel.PriceRangeTo - agentMembershipUserConfigModel.PriceRangeFrom + } else { + pricingRange = pricing - agentMembershipUserConfigModel.PriceRangeFrom + } + } + + deductCostAmount := pricingRange * agentMembershipUserConfigModel.PriceRatio + + agentCommissionDeductionModel := model.AgentCommissionDeduction{ + AgentId: AncestorId, + DeductedAgentId: descendantId, + Amount: deductCostAmount, + Type: model.AgentDeductionTypePricing, + ProductId: productID, + } + _, insertAgentCommissionDeductionModelErr := l.AgentCommissionDeductionModel.Insert(ctx, session, &agentCommissionDeductionModel) + if insertAgentCommissionDeductionModelErr != nil { + return 0, insertAgentCommissionDeductionModelErr + } + return deductCostAmount, nil + } + return 0, nil +} + +//func (l *AgentService) UpgradeVip(ctx context.Context, agentID int64, leve string, session sqlx.Session) error { +// agentModel, err := l.AgentModel.FindOne(ctx, agentID) +// if err != nil { +// return err +// } +// if agentModel.LevelName != model.AgentLeveNameNormal { +// return fmt.Errorf("已经是会员") +// } +// return nil +//} diff --git a/app/main/api/internal/service/alipayService.go b/app/main/api/internal/service/alipayService.go new file mode 100644 index 0000000..b570eaa --- /dev/null +++ b/app/main/api/internal/service/alipayService.go @@ -0,0 +1,258 @@ +package service + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "net/http" + "strconv" + "sync/atomic" + "time" + "znc-server/app/main/api/internal/config" + "znc-server/app/main/model" + "znc-server/pkg/lzkit/lzUtils" + + "github.com/smartwalle/alipay/v3" +) + +type AliPayService struct { + config config.AlipayConfig + AlipayClient *alipay.Client +} + +// NewAliPayService 是一个构造函数,用于初始化 AliPayService +func NewAliPayService(c config.Config) *AliPayService { + client, err := alipay.New(c.Alipay.AppID, c.Alipay.PrivateKey, c.Alipay.IsProduction) + if err != nil { + panic(fmt.Sprintf("创建支付宝客户端失败: %v", err)) + } + //// 加载支付宝公钥 + //err = client.LoadAliPayPublicKey(c.Alipay.AlipayPublicKey) + //if err != nil { + // panic(fmt.Sprintf("加载支付宝公钥失败: %v", err)) + //} + + // 加载证书 + if err = client.LoadAppCertPublicKeyFromFile(c.Alipay.AppCertPath); err != nil { + panic(fmt.Sprintf("加载应用公钥证书失败: %v", err)) + } + if err = client.LoadAlipayCertPublicKeyFromFile(c.Alipay.AlipayCertPath); err != nil { + panic(fmt.Sprintf("加载支付宝公钥证书失败: %v", err)) + } + if err = client.LoadAliPayRootCertFromFile(c.Alipay.AlipayRootCertPath); err != nil { + panic(fmt.Sprintf("加载根证书失败: %v", err)) + } + + return &AliPayService{ + config: c.Alipay, + AlipayClient: client, + } +} + +func (a *AliPayService) CreateAlipayAppOrder(amount float64, subject string, outTradeNo string) (string, error) { + client := a.AlipayClient + totalAmount := lzUtils.ToAlipayAmount(amount) + // 构造移动支付请求 + p := alipay.TradeAppPay{ + Trade: alipay.Trade{ + Subject: subject, + OutTradeNo: outTradeNo, + TotalAmount: totalAmount, + ProductCode: "QUICK_MSECURITY_PAY", // 移动端支付专用代码 + NotifyURL: a.config.NotifyUrl, // 异步回调通知地址 + }, + } + + // 获取APP支付字符串,这里会签名 + payStr, err := client.TradeAppPay(p) + if err != nil { + return "", fmt.Errorf("创建支付宝订单失败: %v", err) + } + + return payStr, nil +} + +// CreateAlipayH5Order 创建支付宝H5支付订单 +func (a *AliPayService) CreateAlipayH5Order(amount float64, subject string, outTradeNo string) (string, error) { + client := a.AlipayClient + totalAmount := lzUtils.ToAlipayAmount(amount) + // 构造H5支付请求 + p := alipay.TradeWapPay{ + Trade: alipay.Trade{ + Subject: subject, + OutTradeNo: outTradeNo, + TotalAmount: totalAmount, + ProductCode: "QUICK_WAP_PAY", // H5支付专用产品码 + NotifyURL: a.config.NotifyUrl, // 异步回调通知地址 + ReturnURL: a.config.ReturnURL, + }, + } + // 获取H5支付请求字符串,这里会签名 + payUrl, err := client.TradeWapPay(p) + if err != nil { + return "", fmt.Errorf("创建支付宝H5订单失败: %v", err) + } + + return payUrl.String(), nil +} + +// CreateAlipayOrder 根据平台类型创建支付宝支付订单 +func (a *AliPayService) CreateAlipayOrder(ctx context.Context, amount float64, subject string, outTradeNo string) (string, error) { + // 根据 ctx 中的 platform 判断平台 + platform, platformOk := ctx.Value("platform").(string) + if !platformOk { + return "", fmt.Errorf("无的支付平台: %s", platform) + } + switch platform { + case model.PlatformApp: + // 调用App支付的创建方法 + return a.CreateAlipayAppOrder(amount, subject, outTradeNo) + case model.PlatformH5: + // 调用H5支付的创建方法,并传入 returnUrl + return a.CreateAlipayH5Order(amount, subject, outTradeNo) + default: + return "", fmt.Errorf("不支持的支付平台: %s", platform) + } +} + +// AliRefund 发起支付宝退款 +func (a *AliPayService) AliRefund(ctx context.Context, outTradeNo string, refundAmount float64) (*alipay.TradeRefundRsp, error) { + refund := alipay.TradeRefund{ + OutTradeNo: outTradeNo, + RefundAmount: lzUtils.ToAlipayAmount(refundAmount), + OutRequestNo: fmt.Sprintf("refund-%s", outTradeNo), + } + + // 发起退款请求 + refundResp, err := a.AlipayClient.TradeRefund(ctx, refund) + if err != nil { + return nil, fmt.Errorf("支付宝退款请求错误:%v", err) + } + return refundResp, nil +} + +// HandleAliPaymentNotification 支付宝支付回调 +func (a *AliPayService) HandleAliPaymentNotification(r *http.Request) (*alipay.Notification, error) { + // 解析表单 + err := r.ParseForm() + if err != nil { + return nil, fmt.Errorf("解析请求表单失败:%v", err) + } + // 解析并验证通知,DecodeNotification 会自动验证签名 + notification, err := a.AlipayClient.DecodeNotification(r.Form) + if err != nil { + return nil, fmt.Errorf("验证签名失败: %v", err) + } + return notification, nil +} +func (a *AliPayService) QueryOrderStatus(ctx context.Context, outTradeNo string) (*alipay.TradeQueryRsp, error) { + queryRequest := alipay.TradeQuery{ + OutTradeNo: outTradeNo, + } + + // 发起查询请求 + resp, err := a.AlipayClient.TradeQuery(ctx, queryRequest) + if err != nil { + return nil, fmt.Errorf("查询支付宝订单失败: %v", err) + } + + // 返回交易状态 + if resp.IsSuccess() { + return resp, nil + } + + return nil, fmt.Errorf("查询支付宝订单失败: %v", resp.SubMsg) +} + +// 添加全局原子计数器 +var alipayOrderCounter uint32 = 0 + +// GenerateOutTradeNo 生成唯一订单号的函数 - 优化版本 +func (a *AliPayService) GenerateOutTradeNo() string { + + // 获取当前时间戳(毫秒级) + timestamp := time.Now().UnixMilli() + timeStr := strconv.FormatInt(timestamp, 10) + + // 原子递增计数器 + counter := atomic.AddUint32(&alipayOrderCounter, 1) + + // 生成4字节真随机数 + randomBytes := make([]byte, 4) + _, err := rand.Read(randomBytes) + if err != nil { + // 如果随机数生成失败,回退到使用时间纳秒数据 + randomBytes = []byte(strconv.FormatInt(time.Now().UnixNano()%1000000, 16)) + } + randomHex := hex.EncodeToString(randomBytes) + + // 组合所有部分: 前缀 + 时间戳 + 计数器 + 随机数 + orderNo := fmt.Sprintf("%s%06x%s", timeStr[:10], counter%0xFFFFFF, randomHex[:6]) + + // 确保长度不超过32字符(大多数支付平台的限制) + if len(orderNo) > 32 { + orderNo = orderNo[:32] + } + + return orderNo +} + +// AliTransfer 支付宝单笔转账到支付宝账户(提现功能) +func (a *AliPayService) AliTransfer( + ctx context.Context, + payeeAccount string, // 收款方支付宝账户 + payeeName string, // 收款方姓名 + amount float64, // 转账金额 + remark string, // 转账备注 + outBizNo string, // 商户转账唯一订单号(可使用GenerateOutTradeNo生成) +) (*alipay.FundTransUniTransferRsp, error) { + // 参数校验 + if payeeAccount == "" { + return nil, fmt.Errorf("收款账户不能为空") + } + if amount <= 0 { + return nil, fmt.Errorf("转账金额必须大于0") + } + + // 构造转账请求 + req := alipay.FundTransUniTransfer{ + OutBizNo: outBizNo, + TransAmount: lzUtils.ToAlipayAmount(amount), // 金额格式转换 + ProductCode: "TRANS_ACCOUNT_NO_PWD", // 单笔无密转账到支付宝账户 + BizScene: "DIRECT_TRANSFER", // 单笔转账 + OrderTitle: "账户提现", // 转账标题 + Remark: remark, + PayeeInfo: &alipay.PayeeInfo{ + Identity: payeeAccount, + IdentityType: "ALIPAY_LOGON_ID", // 根据账户类型选择: + Name: payeeName, + // ALIPAY_USER_ID/ALIPAY_LOGON_ID + }, + } + + // 执行转账请求 + transferRsp, err := a.AlipayClient.FundTransUniTransfer(ctx, req) + if err != nil { + return nil, fmt.Errorf("支付宝转账请求失败: %v", err) + } + + return transferRsp, nil +} +func (a *AliPayService) QueryTransferStatus( + ctx context.Context, + outBizNo string, +) (*alipay.FundTransOrderQueryRsp, error) { + req := alipay.FundTransOrderQuery{ + OutBizNo: outBizNo, + } + response, err := a.AlipayClient.FundTransOrderQuery(ctx, req) + if err != nil { + return nil, fmt.Errorf("支付宝接口调用失败: %v", err) + } + // 处理响应 + if response.Code.IsFailure() { + return nil, fmt.Errorf("支付宝返回错误: %s-%s", response.Code, response.Msg) + } + return response, nil +} diff --git a/app/main/api/internal/service/apirequestService.go b/app/main/api/internal/service/apirequestService.go new file mode 100644 index 0000000..8d84b8d --- /dev/null +++ b/app/main/api/internal/service/apirequestService.go @@ -0,0 +1,1298 @@ +package service + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + "znc-server/app/main/api/internal/config" + tianyuanapi "znc-server/app/main/api/internal/service/tianyuanapi_sdk" + "znc-server/app/main/model" + + "github.com/Masterminds/squirrel" + "github.com/tidwall/gjson" + "github.com/zeromicro/go-zero/core/logx" +) + +// 辅助函数:将天远API响应转换为JSON字节数组 +func convertTianyuanResponse(resp *tianyuanapi.Response) ([]byte, error) { + return json.Marshal(resp.Data) +} + +type ApiRequestService struct { + config config.Config + featureModel model.FeatureModel + productFeatureModel model.ProductFeatureModel + tianyuanapi *tianyuanapi.Client +} + +// NewApiRequestService 是一个构造函数,用于初始化 ApiRequestService +func NewApiRequestService(c config.Config, featureModel model.FeatureModel, productFeatureModel model.ProductFeatureModel, tianyuanapi *tianyuanapi.Client) *ApiRequestService { + return &ApiRequestService{ + config: c, + featureModel: featureModel, + productFeatureModel: productFeatureModel, + tianyuanapi: tianyuanapi, + } +} + +type APIResponseData struct { + ApiID string `json:"apiID"` + Data json.RawMessage `json:"data"` // 这里用 RawMessage 来存储原始的 data + Success bool `json:"success"` + Timestamp string `json:"timestamp"` + Error string `json:"error,omitempty"` +} + +// ProcessRequests 处理请求 +func (a *ApiRequestService) ProcessRequests(params []byte, productID int64) ([]byte, error) { + var ctx, cancel = context.WithCancel(context.Background()) + defer cancel() + build := a.productFeatureModel.SelectBuilder().Where(squirrel.Eq{ + "product_id": productID, + }) + productFeatureList, findProductFeatureErr := a.productFeatureModel.FindAll(ctx, build, "") + if findProductFeatureErr != nil { + return nil, findProductFeatureErr + } + var featureIDs []int64 + isImportantMap := make(map[int64]int64, len(productFeatureList)) + for _, pf := range productFeatureList { + featureIDs = append(featureIDs, pf.FeatureId) + isImportantMap[pf.FeatureId] = pf.IsImportant + } + if len(featureIDs) == 0 { + return nil, errors.New("featureIDs 是空的") + } + builder := a.featureModel.SelectBuilder().Where(squirrel.Eq{"id": featureIDs}) + featureList, findFeatureErr := a.featureModel.FindAll(ctx, builder, "") + if findFeatureErr != nil { + return nil, findFeatureErr + } + if len(featureList) == 0 { + return nil, errors.New("处理请求错误,产品无对应接口功能") + } + var ( + wg sync.WaitGroup + resultsCh = make(chan APIResponseData, len(featureList)) + errorsCh = make(chan error, len(featureList)) + errorCount int32 + errorLimit = len(featureList) + retryNum = 5 + ) + + for i, feature := range featureList { + wg.Add(1) + go func(i int, feature *model.Feature) { + defer wg.Done() + + select { + case <-ctx.Done(): + return + default: + } + result := APIResponseData{ + ApiID: feature.ApiId, + Success: false, + } + timestamp := time.Now().Format("2006-01-02 15:04:05") + var ( + resp json.RawMessage + preprocessErr error + ) + // 若 isImportantMap[feature.ID] == 1,则表示需要在出错时重试 + isImportant := isImportantMap[feature.Id] == 1 + tryCount := 0 + for { + tryCount++ + resp, preprocessErr = a.PreprocessRequestApi(params, feature.ApiId) + if preprocessErr == nil { + break + } + if isImportant && tryCount < retryNum { + continue + } else { + break + } + } + if preprocessErr != nil { + result.Timestamp = timestamp + result.Error = preprocessErr.Error() + result.Data = resp + resultsCh <- result + errorsCh <- fmt.Errorf("请求失败: %v", preprocessErr) + atomic.AddInt32(&errorCount, 1) + if atomic.LoadInt32(&errorCount) >= int32(errorLimit) { + cancel() + } + return + } + + result.Data = resp + result.Success = true + result.Timestamp = timestamp + resultsCh <- result + }(i, feature) + } + + go func() { + wg.Wait() + close(resultsCh) + close(errorsCh) + }() + // 收集所有结果并合并z + var responseData []APIResponseData + for result := range resultsCh { + responseData = append(responseData, result) + } + if atomic.LoadInt32(&errorCount) >= int32(errorLimit) { + var allErrors []error + for err := range errorsCh { + allErrors = append(allErrors, err) + } + return nil, fmt.Errorf("请求失败次数超过 %d 次: %v", errorLimit, allErrors) + } + + combinedResponse, err := json.Marshal(responseData) + if err != nil { + return nil, fmt.Errorf("响应数据转 JSON 失败: %v", err) + } + + return combinedResponse, nil +} + +// ------------------------------------请求处理器-------------------------- +var requestProcessors = map[string]func(*ApiRequestService, []byte) ([]byte, error){ + "PersonEnterprisePro": (*ApiRequestService).ProcessPersonEnterpriseProRequest, + "BehaviorRiskScan": (*ApiRequestService).ProcessBehaviorRiskScanRequest, + "YYSYBE08": (*ApiRequestService).ProcessYYSYBE08Request, + "YYSY09CD": (*ApiRequestService).ProcessYYSY09CDRequest, + "FLXGC9D1": (*ApiRequestService).ProcessFLXGC9D1Request, + "FLXG0687": (*ApiRequestService).ProcessFLXG0687Request, + "FLXG3D56": (*ApiRequestService).ProcessFLXG3D56Request, + "FLXG0V4B": (*ApiRequestService).ProcesFLXG0V4BRequest, + "QYGL8271": (*ApiRequestService).ProcessQYGL8271Request, + "IVYZ5733": (*ApiRequestService).ProcessIVYZ5733Request, + "IVYZ9A2B": (*ApiRequestService).ProcessIVYZ9A2BRequest, + "JRZQ0A03": (*ApiRequestService).ProcessJRZQ0A03Request, + "QYGL6F2D": (*ApiRequestService).ProcessQYGL6F2DRequest, + "JRZQ8203": (*ApiRequestService).ProcessJRZQ8203Request, + "JRZQ4AA8": (*ApiRequestService).ProcessJRZQ4AA8Request, + "QCXG7A2B": (*ApiRequestService).ProcessQCXG7A2BRequest, +} + +// PreprocessRequestApi 调用指定的请求处理函数 +func (a *ApiRequestService) PreprocessRequestApi(params []byte, apiID string) ([]byte, error) { + if processor, exists := requestProcessors[apiID]; exists { + return processor(a, params) // 调用 ApiRequestService 方法 + } + + return nil, errors.New("api请求, 未找到相应的处理程序") +} + +// PersonEnterprisePro 人企业关系加强版 +func (a *ApiRequestService) ProcessPersonEnterpriseProRequest(params []byte) ([]byte, error) { + idCard := gjson.GetBytes(params, "id_card") + // 设置最大调用次数上限 + maxApiCalls := 10 // 允许最多查询10个企业 + + if !idCard.Exists() { + return nil, errors.New("api请求, PersonEnterprisePro, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("QYGL6F2D", map[string]interface{}{ + "id_card": idCard.String(), + }) + if err != nil { + return nil, err + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + return nil, err + } + + // 处理股东人企关系的响应数据 + code := gjson.GetBytes(respBytes, "code") + if !code.Exists() { + return nil, fmt.Errorf("响应中缺少 code 字段") + } + + // 判断 code 是否等于 "0000" + if code.String() == "0000" { + // 获取 data 字段的值 + data := gjson.GetBytes(respBytes, "data") + if !data.Exists() { + return nil, fmt.Errorf("响应中缺少 data 字段") + } + + // 使用gjson获取企业列表 + datalistResult := gjson.Get(data.Raw, "datalist") + if !datalistResult.Exists() { + return nil, fmt.Errorf("datalist字段不存在") + } + + // 获取所有企业并进行排序 + companies := datalistResult.Array() + + // 创建企业对象切片,用于排序 + type CompanyWithPriority struct { + Index int + Data gjson.Result + RelationshipVal int // 关系权重值 + RelationCount int // 关系数量 + AdminPenalty int // 行政处罚数量 + Executed int // 被执行人数量 + Dishonest int // 失信被执行人数量 + } + + companiesWithPriority := make([]CompanyWithPriority, 0, len(companies)) + + // 遍历企业,计算优先级 + for i, companyJson := range companies { + // 统计行政处罚、被执行人、失信被执行人 + adminPenalty := 0 + executed := 0 + dishonest := 0 + + // 检查行政处罚字段是否存在并获取数组长度 + adminPenaltyResult := companyJson.Get("adminPenalty") + if adminPenaltyResult.Exists() && adminPenaltyResult.IsArray() { + adminPenalty = len(adminPenaltyResult.Array()) + } + + // 检查被执行人字段是否存在并获取数组长度 + executedPersonResult := companyJson.Get("executedPerson") + if executedPersonResult.Exists() && executedPersonResult.IsArray() { + executed = len(executedPersonResult.Array()) + } + + // 检查失信被执行人字段是否存在并获取数组长度 + dishonestExecutedPersonResult := companyJson.Get("dishonestExecutedPerson") + if dishonestExecutedPersonResult.Exists() && dishonestExecutedPersonResult.IsArray() { + dishonest = len(dishonestExecutedPersonResult.Array()) + } + + // 计算relationship权重 + relationshipVal := 0 + relationCount := 0 + + // 获取relationship数组 + relationshipResult := companyJson.Get("relationship") + if relationshipResult.Exists() && relationshipResult.IsArray() { + relationships := relationshipResult.Array() + // 统计各类关系的数量和权重 + for _, rel := range relationships { + relationCount++ + relStr := rel.String() + + // 根据关系类型设置权重,权重顺序: + // 股东(6) > 历史股东(5) > 法人(4) > 历史法人(3) > 高管(2) > 历史高管(1) + switch relStr { + case "sh": // 股东 + if relationshipVal < 6 { + relationshipVal = 6 + } + case "his_sh": // 历史股东 + if relationshipVal < 5 { + relationshipVal = 5 + } + case "lp": // 法人 + if relationshipVal < 4 { + relationshipVal = 4 + } + case "his_lp": // 历史法人 + if relationshipVal < 3 { + relationshipVal = 3 + } + case "tm": // 高管 + if relationshipVal < 2 { + relationshipVal = 2 + } + case "his_tm": // 历史高管 + if relationshipVal < 1 { + relationshipVal = 1 + } + } + } + } + + companiesWithPriority = append(companiesWithPriority, CompanyWithPriority{ + Index: i, + Data: companyJson, + RelationshipVal: relationshipVal, + RelationCount: relationCount, + AdminPenalty: adminPenalty, + Executed: executed, + Dishonest: dishonest, + }) + } + + // 按优先级排序 + sort.Slice(companiesWithPriority, func(i, j int) bool { + // 首先根据是否有失信被执行人排序 + if companiesWithPriority[i].Dishonest != companiesWithPriority[j].Dishonest { + return companiesWithPriority[i].Dishonest > companiesWithPriority[j].Dishonest + } + + // 然后根据是否有被执行人排序 + if companiesWithPriority[i].Executed != companiesWithPriority[j].Executed { + return companiesWithPriority[i].Executed > companiesWithPriority[j].Executed + } + + // 然后根据是否有行政处罚排序 + if companiesWithPriority[i].AdminPenalty != companiesWithPriority[j].AdminPenalty { + return companiesWithPriority[i].AdminPenalty > companiesWithPriority[j].AdminPenalty + } + + // 然后按relationship类型排序 + if companiesWithPriority[i].RelationshipVal != companiesWithPriority[j].RelationshipVal { + return companiesWithPriority[i].RelationshipVal > companiesWithPriority[j].RelationshipVal + } + + // 最后按relationship数量排序 + return companiesWithPriority[i].RelationCount > companiesWithPriority[j].RelationCount + }) + + // 限制处理的企业数量 + processCount := len(companiesWithPriority) + if processCount > maxApiCalls { + processCount = maxApiCalls + } + + // 只处理前N个优先级高的企业 + prioritizedCompanies := companiesWithPriority[:processCount] + + // 使用WaitGroup和chan处理并发 + var wg sync.WaitGroup + results := make(chan struct { + index int + data []byte + err error + }, processCount) + + // 对按优先级排序的前N个企业进行涉诉信息查询 + for _, company := range prioritizedCompanies { + wg.Add(1) + go func(origIndex int, companyInfo gjson.Result) { + defer wg.Done() + logx.Infof("开始处理企业[%d],企业名称: %s,统一社会信用代码: %s", origIndex, companyInfo.Get("basicInfo.name").String(), companyInfo.Get("basicInfo.creditCode").String()) + // 提取企业名称和统一社会信用代码 + orgName := companyInfo.Get("basicInfo.name") + creditCode := companyInfo.Get("basicInfo.creditCode") + + if !orgName.Exists() || !creditCode.Exists() { + results <- struct { + index int + data []byte + err error + }{origIndex, nil, fmt.Errorf("企业名称或统一社会信用代码不存在")} + return + } + + // 解析原始公司信息为map + var companyMap map[string]interface{} + if err := json.Unmarshal([]byte(companyInfo.Raw), &companyMap); err != nil { + results <- struct { + index int + data []byte + err error + }{origIndex, nil, fmt.Errorf("解析企业信息失败: %v", err)} + return + } + + // 调用QYGL8271接口获取企业涉诉信息 + lawsuitResp, err := a.tianyuanapi.CallInterface("QYGL8271", map[string]interface{}{ + "ent_name": orgName.String(), + "ent_code": creditCode.String(), + }) + // 无论是否有错误,都继续处理 + if err != nil { + // 可能是正常没有涉诉数据,设置为空对象 + logx.Infof("企业[%s]涉诉信息查询结果: %v", orgName.String(), err) + companyMap["lawsuitInfo"] = map[string]interface{}{} + } else { + // 转换响应数据 + lawsuitRespBytes, err := convertTianyuanResponse(lawsuitResp) + if err != nil { + logx.Errorf("转换企业[%s]涉诉响应失败: %v", orgName.String(), err) + companyMap["lawsuitInfo"] = map[string]interface{}{} + } else if len(lawsuitRespBytes) == 0 || string(lawsuitRespBytes) == "{}" || string(lawsuitRespBytes) == "null" { + // 无涉诉数据 + companyMap["lawsuitInfo"] = map[string]interface{}{} + } else { + // 解析涉诉信息 + var lawsuitInfo interface{} + if err := json.Unmarshal(lawsuitRespBytes, &lawsuitInfo); err != nil { + logx.Errorf("解析企业[%s]涉诉信息失败: %v", orgName.String(), err) + companyMap["lawsuitInfo"] = map[string]interface{}{} + } else { + // 添加涉诉信息到企业信息中 + companyMap["lawsuitInfo"] = lawsuitInfo + } + } + } + + // 序列化更新后的企业信息 + companyData, err := json.Marshal(companyMap) + if err != nil { + results <- struct { + index int + data []byte + err error + }{origIndex, nil, fmt.Errorf("序列化企业信息失败: %v", err)} + return + } + + results <- struct { + index int + data []byte + err error + }{origIndex, companyData, nil} + }(company.Index, company.Data) + } + + // 关闭结果通道 + go func() { + wg.Wait() + close(results) + }() + + // 解析原始数据为map + var dataMap map[string]interface{} + if err := json.Unmarshal([]byte(data.Raw), &dataMap); err != nil { + return nil, fmt.Errorf("解析data字段失败: %v", err) + } + + // 获取原始企业列表 + originalDatalist, ok := dataMap["datalist"].([]interface{}) + if !ok { + return nil, fmt.Errorf("无法获取原始企业列表") + } + + // 创建结果映射,用于保存已处理的企业 + processedCompanies := make(map[int]interface{}) + + // 收集处理过的企业数据 + for result := range results { + if result.err != nil { + logx.Errorf("处理企业失败: %v", result.err) + continue + } + + if result.data != nil { + var companyMap interface{} + if err := json.Unmarshal(result.data, &companyMap); err == nil { + processedCompanies[result.index] = companyMap + } + } + } + + // 更新企业列表 + // 处理过的用新数据,未处理的保留原样 + updatedDatalist := make([]interface{}, len(originalDatalist)) + for i, company := range originalDatalist { + if processed, exists := processedCompanies[i]; exists { + // 已处理的企业,使用新数据 + updatedDatalist[i] = processed + } else { + // 未处理的企业,保留原始数据并添加空的涉诉信息 + companyMap, ok := company.(map[string]interface{}) + if ok { + // 为未处理的企业添加空的涉诉信息 + companyMap["lawsuitInfo"] = map[string]interface{}{} + updatedDatalist[i] = companyMap + } else { + updatedDatalist[i] = company + } + } + } + + // 更新原始数据中的企业列表 + dataMap["datalist"] = updatedDatalist + + // 序列化最终结果 + result, err := json.Marshal(dataMap) + if err != nil { + return nil, fmt.Errorf("序列化最终结果失败: %v", err) + } + + return result, nil + } + + // code不等于"0000",返回错误 + return nil, fmt.Errorf("响应code错误: %s", code.String()) +} + +// ProcesFLXG0V4BRequest 个人司法涉诉(详版) +func (a *ApiRequestService) ProcesFLXG0V4BRequest(params []byte) ([]byte, error) { + name := gjson.GetBytes(params, "name") + idCard := gjson.GetBytes(params, "id_card") + mobile := gjson.GetBytes(params, "mobile") + if !name.Exists() || !idCard.Exists() || !mobile.Exists() { + return nil, errors.New("api请求, BehaviorRiskScan, 获取相关参数失败") + } + + authDate := func() string { + now := time.Now() + start := now.AddDate(0, 0, -2).Format("20060201") + end := now.AddDate(0, 0, 2).Format("20060201") + return fmt.Sprintf("%s-%s", start, end) + } + resp, err := a.tianyuanapi.CallInterface("FLXG0V4B", map[string]interface{}{ + "name": name.String(), + "id_card": idCard.String(), + "mobile": mobile.String(), + "auth_date": authDate(), + }) + + if err != nil { + return nil, err + } + respBytes, err := json.Marshal(resp.Data) + if err != nil { + return nil, err + } + return respBytes, nil +} + +// ProcessFLXGC9D1Request 黑灰产 +func (a *ApiRequestService) ProcessFLXGC9D1Request(params []byte) ([]byte, error) { + name := gjson.GetBytes(params, "name") + idCard := gjson.GetBytes(params, "id_card") + mobile := gjson.GetBytes(params, "mobile") + if !name.Exists() || !idCard.Exists() || !mobile.Exists() { + return nil, errors.New("api请求, FLXGC9D1, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("FLXGC9D1", map[string]interface{}{ + "name": name.String(), + "id_card": idCard.String(), + "mobile_no": mobile.String(), + }) + + if err != nil { + return nil, err + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + return nil, err + } + + flagBlackgraylevel := gjson.GetBytes(respBytes, "flag_blackgraylevel") + if !flagBlackgraylevel.Exists() || flagBlackgraylevel.String() != "1" { + return nil, fmt.Errorf("自然人黑灰产信息查询失败") + } + + bglLevel := gjson.GetBytes(respBytes, "bgl_level") + if !bglLevel.Exists() { + return nil, fmt.Errorf("bgl_level 字段不存在") + } + + return []byte(bglLevel.Raw), nil +} + +// ProcessFLXG0687Request 反诈反赌核验 +func (a *ApiRequestService) ProcessFLXG0687Request(params []byte) ([]byte, error) { + name := gjson.GetBytes(params, "name") + idCard := gjson.GetBytes(params, "id_card") + mobile := gjson.GetBytes(params, "mobile") + if !name.Exists() || !idCard.Exists() || !mobile.Exists() { + return nil, errors.New("api请求, FLXG0687, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("FLXG0687", map[string]interface{}{ + "name": name.String(), + "id_card": idCard.String(), + "mobile_no": mobile.String(), + }) + + if err != nil { + return nil, err + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + return nil, err + } + + Value := gjson.GetBytes(respBytes, "value") + if !Value.Exists() { + return nil, fmt.Errorf("自然人反诈反赌核验查询失败") + } + + return []byte(Value.Raw), nil +} + +// ProcessFLXG3D56Request 特殊名单 +func (a *ApiRequestService) ProcessFLXG3D56Request(params []byte) ([]byte, error) { + name := gjson.GetBytes(params, "name") + idCard := gjson.GetBytes(params, "id_card") + mobile := gjson.GetBytes(params, "mobile") + if !name.Exists() || !idCard.Exists() || !mobile.Exists() { + return nil, errors.New("api请求, FLXG3D56, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("FLXG3D56", map[string]interface{}{ + "name": name.String(), + "id_card": idCard.String(), + "mobile_no": mobile.String(), + }) + + if err != nil { + return nil, err + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + return nil, err + } + + // 获取 code 字段 + codeResult := gjson.GetBytes(respBytes, "code") + validCodes := map[string]bool{"1006": true, "1007": true, "1008": true, "1009": true, "1010": true} + if !validCodes[codeResult.String()] { + return nil, fmt.Errorf("查询手机在网时长失败, %s", string(respBytes)) + } + + data := gjson.GetBytes(respBytes, "data") + if !data.Exists() { + return nil, fmt.Errorf("查询手机在网时长失败, %s", string(respBytes)) + } + + return []byte(data.Raw), nil +} + +// ProcessIVYZ5733Request 婚姻状况 +func (a *ApiRequestService) ProcessIVYZ5733Request(params []byte) ([]byte, error) { + idCard := gjson.GetBytes(params, "id_card") + name := gjson.GetBytes(params, "name") + if !idCard.Exists() || !name.Exists() { + return nil, errors.New("api请求, IVYZ5733, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("IVYZ5733", map[string]interface{}{ + "id_card": idCard.String(), + "name": name.String(), + }) + + if err != nil { + return nil, err + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + return nil, err + } + + result := gjson.GetBytes(respBytes, "data.data") + if !result.Exists() { + return nil, fmt.Errorf("婚姻状态查询失败") + } + + // 获取原始结果 + rawResult := result.String() + + // 根据结果转换状态码 + var statusCode string + switch { + case strings.HasPrefix(rawResult, "INR"): + statusCode = "0" // 匹配不成功 + case strings.HasPrefix(rawResult, "IA"): + statusCode = "1" // 结婚 + case strings.HasPrefix(rawResult, "IB"): + statusCode = "2" // 离婚 + default: + return nil, fmt.Errorf("婚姻状态查询失败,未知状态码: %s", statusCode) + } + + // 构建新的返回结果 + response := map[string]string{ + "status": statusCode, + } + // 序列化为JSON + jsonResponse, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return jsonResponse, nil +} + +// ProcessIVYZ9A2BRequest 学历查询 +func (a *ApiRequestService) ProcessIVYZ9A2BRequest(params []byte) ([]byte, error) { + idCard := gjson.GetBytes(params, "id_card") + name := gjson.GetBytes(params, "name") + if !idCard.Exists() || !name.Exists() { + return nil, errors.New("api请求, IVYZ9A2B, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("IVYZ9A2B", map[string]interface{}{ + "id_card": idCard.String(), + "name": name.String(), + }) + + if err != nil { + return nil, err + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + return nil, err + } + + // 解析响应 + codeResult := gjson.GetBytes(respBytes, "data.education_background.code") + if !codeResult.Exists() { + return nil, fmt.Errorf("教育经历核验查询失败: 返回数据缺少code字段") + } + + code := codeResult.String() + var result map[string]interface{} + + switch code { + case "9100": + // 查询成功有结果 + eduResultArray := gjson.GetBytes(respBytes, "data.education_background.data").Array() + var processedEduData []interface{} + + // 提取每个元素中Raw字段的实际内容 + for _, item := range eduResultArray { + var eduInfo interface{} + if err := json.Unmarshal([]byte(item.Raw), &eduInfo); err != nil { + return nil, fmt.Errorf("解析教育信息失败: %v", err) + } + processedEduData = append(processedEduData, eduInfo) + } + + result = map[string]interface{}{ + "data": processedEduData, + "status": 1, + } + case "9000": + // 查询成功无结果 + result = map[string]interface{}{ + "data": []interface{}{}, + "status": 0, + } + default: + // 其他情况视为错误 + errMsg := gjson.GetBytes(respBytes, "data.education_background.msg").String() + return nil, fmt.Errorf("教育经历核验查询失败: %s (code: %s)", errMsg, code) + } + + // 将结果转为JSON字节 + jsonResult, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("处理教育经历查询结果失败: %v", err) + } + + return jsonResult, nil +} + +// ProcessYYSYBE08Request 二要素 +func (a *ApiRequestService) ProcessYYSYBE08Request(params []byte) ([]byte, error) { + name := gjson.GetBytes(params, "name") + idCard := gjson.GetBytes(params, "id_card") + if !name.Exists() || !idCard.Exists() { + return nil, errors.New("api请求, YYSYBE08, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("YYSYBE08", map[string]interface{}{ + "name": name.String(), + "id_card": idCard.String(), + }) + + if err != nil { + return nil, err + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + return nil, err + } + + // 使用gjson获取resultCode + resultCode := gjson.GetBytes(respBytes, "ctidRequest.ctidAuth.resultCode") + if !resultCode.Exists() { + return nil, errors.New("获取resultCode失败") + } + + // 获取resultCode的第一个字符 + resultCodeStr := resultCode.String() + if len(resultCodeStr) == 0 { + return nil, errors.New("resultCode为空") + } + + firstChar := string(resultCodeStr[0]) + if firstChar != "0" && firstChar != "5" { + return nil, errors.New("resultCode的第一个字符既不是0也不是5") + } + return []byte(firstChar), nil +} + +// ProcessJRZQ0A03Request 借贷申请 +func (a *ApiRequestService) ProcessJRZQ0A03Request(params []byte) ([]byte, error) { + name := gjson.GetBytes(params, "name") + idCard := gjson.GetBytes(params, "id_card") + mobile := gjson.GetBytes(params, "mobile") + if !name.Exists() || !idCard.Exists() || !mobile.Exists() { + return nil, errors.New("api请求, JRZQ0A03, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("JRZQ0A03", map[string]interface{}{ + "name": name.String(), + "id_card": idCard.String(), + "mobile_no": mobile.String(), + }) + + if err != nil { + return nil, err + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + return nil, err + } + + // 获取 code 字段 + codeResult := gjson.GetBytes(respBytes, "code") + if !codeResult.Exists() { + return nil, fmt.Errorf("code 字段不存在") + } + if codeResult.String() != "00" { + return nil, fmt.Errorf("未匹配到相关结果") + } + + // 获取 data 字段 + dataResult := gjson.GetBytes(respBytes, "data") + if !dataResult.Exists() { + return nil, fmt.Errorf("data 字段不存在") + } + + // 将 data 字段解析为 map + var dataMap map[string]interface{} + if err := json.Unmarshal([]byte(dataResult.Raw), &dataMap); err != nil { + return nil, fmt.Errorf("解析 data 字段失败: %v", err) + } + + // 删除指定字段 + delete(dataMap, "swift_number") + delete(dataMap, "DataStrategy") + + // 重新编码为 JSON + modifiedData, err := json.Marshal(dataMap) + if err != nil { + return nil, fmt.Errorf("编码修改后的 data 失败: %v", err) + } + return modifiedData, nil +} + +// ProcessJRZQ8203Request 借贷行为 +func (a *ApiRequestService) ProcessJRZQ8203Request(params []byte) ([]byte, error) { + name := gjson.GetBytes(params, "name") + idCard := gjson.GetBytes(params, "id_card") + mobile := gjson.GetBytes(params, "mobile") + if !name.Exists() || !idCard.Exists() || !mobile.Exists() { + return nil, errors.New("api请求, JRZQ8203, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("JRZQ8203", map[string]interface{}{ + "name": name.String(), + "id_card": idCard.String(), + "mobile_no": mobile.String(), + }) + + if err != nil { + return nil, err + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + return nil, err + } + + // 获取 code 字段 + codeResult := gjson.GetBytes(respBytes, "code") + if !codeResult.Exists() { + return nil, fmt.Errorf("code 字段不存在") + } + if codeResult.String() != "00" { + return nil, fmt.Errorf("未匹配到相关结果") + } + + // 获取 data 字段 + dataResult := gjson.GetBytes(respBytes, "data") + if !dataResult.Exists() { + return nil, fmt.Errorf("data 字段不存在") + } + + // 将 data 字段解析为 map + var dataMap map[string]interface{} + if err := json.Unmarshal([]byte(dataResult.Raw), &dataMap); err != nil { + return nil, fmt.Errorf("解析 data 字段失败: %v", err) + } + + // 删除指定字段 + delete(dataMap, "swift_number") + delete(dataMap, "DataStrategy") + + // 重新编码为 JSON + modifiedData, err := json.Marshal(dataMap) + if err != nil { + return nil, fmt.Errorf("编码修改后的 data 失败: %v", err) + } + return modifiedData, nil +} + +// ProcessJRZQ4AA8Request 还款压力 +func (a *ApiRequestService) ProcessJRZQ4AA8Request(params []byte) ([]byte, error) { + idCard := gjson.GetBytes(params, "id_card") + name := gjson.GetBytes(params, "name") + mobile := gjson.GetBytes(params, "mobile") + if !idCard.Exists() || !name.Exists() || !mobile.Exists() { + return nil, errors.New("api请求, JRZQ4AA8, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("JRZQ4AA8", map[string]interface{}{ + "id_card": idCard.String(), + "name": name.String(), + "mobile_no": mobile.String(), + }) + + if err != nil { + return nil, err + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + return nil, err + } + + // 获取响应码和偿贷压力标志 + code := gjson.GetBytes(respBytes, "code").String() + flagDebtRepayStress := gjson.GetBytes(respBytes, "flag_debtrepaystress").String() + + // 判断是否成功 + if code != "00" || flagDebtRepayStress != "1" { + return nil, fmt.Errorf("偿贷压力查询失败: %+v", respBytes) + } + // 获取偿贷压力分数 + drsNoDebtScore := gjson.GetBytes(respBytes, "drs_nodebtscore").String() + + // 构建结果 + result := map[string]interface{}{ + "score": drsNoDebtScore, + } + + // 将结果转为JSON + jsonResult, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("处理偿贷压力查询结果失败: %v", err) + } + + return jsonResult, nil +} + +// ProcessQYGL8271Request 企业涉诉 +func (a *ApiRequestService) ProcessQYGL8271Request(params []byte) ([]byte, error) { + entName := gjson.GetBytes(params, "ent_name") + entCode := gjson.GetBytes(params, "ent_code") + + if !entName.Exists() || !entCode.Exists() { + return nil, errors.New("api请求, QYGL8271, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("QYGL8271", map[string]interface{}{ + "ent_name": entName.String(), + "ent_code": entCode.String(), + }) + + if err != nil { + return nil, err + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + return nil, err + } + + // 第一步:提取外层的 data 字段 + dataResult := gjson.GetBytes(respBytes, "data") + if !dataResult.Exists() { + return nil, fmt.Errorf("外层 data 字段不存在") + } + + // 第二步:解析外层 data 的 JSON 字符串 + var outerDataMap map[string]interface{} + if err := json.Unmarshal([]byte(dataResult.String()), &outerDataMap); err != nil { + return nil, fmt.Errorf("解析外层 data 字段失败: %v", err) + } + + // 第三步:提取内层的 data 字段 + innerData, ok := outerDataMap["data"].(string) + if !ok { + return nil, fmt.Errorf("内层 data 字段不存在或类型错误") + } + + // 第四步:解析内层 data 的 JSON 字符串 + var finalDataMap map[string]interface{} + if err := json.Unmarshal([]byte(innerData), &finalDataMap); err != nil { + return nil, fmt.Errorf("解析内层 data 字段失败: %v", err) + } + + // 将最终的 JSON 对象编码为字节数组返回 + finalDataBytes, err := json.Marshal(finalDataMap) + if err != nil { + return nil, fmt.Errorf("编码最终的 JSON 对象失败: %v", err) + } + + statusResult := gjson.GetBytes(finalDataBytes, "status.status") + if statusResult.Exists() || statusResult.Int() == -1 { + return nil, fmt.Errorf("企业涉诉为空: %+v", finalDataBytes) + } + return finalDataBytes, nil +} + +// ProcessFLXG0V4BRequest 个人涉诉 +func (a *ApiRequestService) ProcessFLXG0V4BRequest(params []byte) ([]byte, error) { + name := gjson.GetBytes(params, "name") + idCard := gjson.GetBytes(params, "id_card") + + if !name.Exists() || !idCard.Exists() { + return nil, errors.New("api请求, FLXG0V4B, 获取相关参数失败") + } + + authDate := func() string { + now := time.Now() + start := now.AddDate(0, 0, -2).Format("20060201") + end := now.AddDate(0, 0, 2).Format("20060201") + return fmt.Sprintf("%s-%s", start, end) + } + + resp, err := a.tianyuanapi.CallInterface("FLXG0V4B", map[string]interface{}{ + "name": name.String(), + "id_card": idCard.String(), + "auth_date": authDate(), + }, &tianyuanapi.ApiCallOptions{ + Json: true, + }) + if err != nil { + return nil, err + } + + return convertTianyuanResponse(resp) +} + +// ProcessQYGL6F2DRequest 人企关联 +func (a *ApiRequestService) ProcessQYGL6F2DRequest(params []byte) ([]byte, error) { + idCard := gjson.GetBytes(params, "id_card") + if !idCard.Exists() { + return nil, errors.New("api请求, QYGL6F2D, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("QYGL6F2D", map[string]interface{}{ + "id_card": idCard.String(), + }) + + if err != nil { + return nil, err + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + return nil, err + } + + // 处理股东人企关系的响应数据 + code := gjson.GetBytes(respBytes, "code") + if !code.Exists() { + return nil, fmt.Errorf("响应中缺少 code 字段") + } + + // 判断 code 是否等于 "0000" + if code.String() == "0000" { + // 获取 data 字段的值 + data := gjson.GetBytes(respBytes, "data") + if !data.Exists() { + return nil, fmt.Errorf("响应中缺少 data 字段") + } + // 返回 data 字段的内容 + return []byte(data.Raw), nil + } + + // code 不等于 "0000",返回错误 + return nil, fmt.Errorf("响应code错误%s", code.String()) +} + +// ProcessQCXG7A2BRequest 名下车辆 +func (a *ApiRequestService) ProcessQCXG7A2BRequest(params []byte) ([]byte, error) { + idCard := gjson.GetBytes(params, "id_card") + if !idCard.Exists() { + return nil, errors.New("api请求, QCXG7A2B, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("QCXG7A2B", map[string]interface{}{ + "id_card": idCard.String(), + }) + + if err != nil { + return nil, err + } + + return convertTianyuanResponse(resp) +} + +// ProcessYYSY09CDRequest 三要素 +func (a *ApiRequestService) ProcessYYSY09CDRequest(params []byte) ([]byte, error) { + name := gjson.GetBytes(params, "name") + idCard := gjson.GetBytes(params, "id_card") + mobile := gjson.GetBytes(params, "mobile") + if !name.Exists() || !idCard.Exists() || !mobile.Exists() { + return nil, errors.New("api请求, YYSY09CD, 获取相关参数失败") + } + + resp, err := a.tianyuanapi.CallInterface("YYSY09CD", map[string]interface{}{ + "name": name.String(), + "id_card": idCard.String(), + "mobile_no": mobile.String(), + }) + + if err != nil { + return nil, err + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + return nil, err + } + + // 使用gjson获取resultCode + resultCode := gjson.GetBytes(respBytes, "ctidRequest.ctidAuth.resultCode") + if !resultCode.Exists() { + return nil, errors.New("获取resultCode失败") + } + + // 获取resultCode的第一个字符 + resultCodeStr := resultCode.String() + if len(resultCodeStr) == 0 { + return nil, errors.New("resultCode为空") + } + + firstChar := string(resultCodeStr[0]) + if firstChar != "0" && firstChar != "5" { + return nil, errors.New("resultCode的第一个字符既不是0也不是5") + } + return []byte(firstChar), nil +} + +// ProcessBehaviorRiskScanRequest 行为风险扫描 +func (a *ApiRequestService) ProcessBehaviorRiskScanRequest(params []byte) ([]byte, error) { + name := gjson.GetBytes(params, "name") + idCard := gjson.GetBytes(params, "id_card") + mobile := gjson.GetBytes(params, "mobile") + + if !name.Exists() || !idCard.Exists() || !mobile.Exists() { + return nil, errors.New("api请求, BehaviorRiskScan, 获取相关参数失败") + } + + var wg sync.WaitGroup + type apiResult struct { + name string + data []byte + err error + } + results := make(chan apiResult, 2) // 2个风险检测项 + + // 并行调用两个不同的风险检测API + wg.Add(2) + + // 黑灰产检测 + go func() { + defer wg.Done() + resp, err := a.tianyuanapi.CallInterface("FLXGC9D1", map[string]interface{}{ + "name": name.String(), + "id_card": idCard.String(), + "mobile_no": mobile.String(), + }) + if err != nil { + results <- apiResult{name: "black_gray_level", data: nil, err: err} + return + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + results <- apiResult{name: "black_gray_level", data: nil, err: err} + return + } + + results <- apiResult{name: "black_gray_level", data: respBytes, err: nil} + }() + + // 电诈风险预警 + go func() { + defer wg.Done() + resp, err := a.tianyuanapi.CallInterface("FLXG0687", map[string]interface{}{ + "name": name.String(), + "id_card": idCard.String(), + "mobile_no": mobile.String(), + }) + if err != nil { + results <- apiResult{name: "telefraud_level", data: nil, err: err} + return + } + + respBytes, err := convertTianyuanResponse(resp) + if err != nil { + results <- apiResult{name: "telefraud_level", data: nil, err: err} + return + } + + results <- apiResult{name: "telefraud_level", data: respBytes, err: nil} + }() + + // 关闭结果通道 + go func() { + wg.Wait() + close(results) + }() + + // 收集所有结果 + resultMap := make(map[string]interface{}) + var errors []string + + for result := range results { + if result.err != nil { + // 记录错误但继续处理其他结果 + errors = append(errors, fmt.Sprintf("%s: %v", result.name, result.err)) + continue + } + + // 解析JSON结果并添加到结果映射 + var parsedData interface{} + if err := json.Unmarshal(result.data, &parsedData); err != nil { + errors = append(errors, fmt.Sprintf("解析%s数据失败: %v", result.name, err)) + } else { + resultMap[result.name] = parsedData + } + } + + // 添加错误信息到结果中(如果存在) + if len(errors) > 0 { + resultMap["errors"] = errors + } + + // 序列化最终结果 + finalResult, err := json.Marshal(resultMap) + if err != nil { + return nil, fmt.Errorf("序列化行为风险扫描结果失败: %v", err) + } + + return finalResult, nil +} diff --git a/app/main/api/internal/service/applepayService.go b/app/main/api/internal/service/applepayService.go new file mode 100644 index 0000000..7f14b15 --- /dev/null +++ b/app/main/api/internal/service/applepayService.go @@ -0,0 +1,169 @@ +package service + +import ( + "context" + "crypto/ecdsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io/ioutil" + "net/http" + "strconv" + "time" + "znc-server/app/main/api/internal/config" + + "github.com/golang-jwt/jwt/v4" +) + +// ApplePayService 是 Apple IAP 支付服务的结构体 +type ApplePayService struct { + config config.ApplepayConfig // 配置项 +} + +// NewApplePayService 是一个构造函数,用于初始化 ApplePayService +func NewApplePayService(c config.Config) *ApplePayService { + return &ApplePayService{ + config: c.Applepay, + } +} +func (a *ApplePayService) GetIappayAppID(productName string) string { + return fmt.Sprintf("%s.%s", a.config.BundleID, productName) +} + +// VerifyReceipt 验证苹果支付凭证 +func (a *ApplePayService) VerifyReceipt(ctx context.Context, receipt string) (*AppleVerifyResponse, error) { + var reqUrl string + if a.config.Sandbox { + reqUrl = a.config.SandboxVerifyURL + } else { + reqUrl = a.config.ProductionVerifyURL + } + + // 读取私钥 + privateKey, err := loadPrivateKey(a.config.LoadPrivateKeyPath) + if err != nil { + return nil, fmt.Errorf("加载私钥失败:%v", err) + } + + // 生成 JWT + token, err := generateJWT(privateKey, a.config.KeyID, a.config.IssuerID) + if err != nil { + return nil, fmt.Errorf("生成JWT失败:%v", err) + } + + // 构造查询参数 + queryParams := fmt.Sprintf("?receipt-data=%s", receipt) + fullUrl := reqUrl + queryParams + + // 构建 HTTP GET 请求 + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullUrl, nil) + if err != nil { + return nil, fmt.Errorf("创建 HTTP 请求失败:%v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + // 发送请求 + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("请求苹果验证接口失败:%v", err) + } + defer resp.Body.Close() + + // 解析响应 + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("读取响应体失败:%v", err) + } + + var verifyResponse AppleVerifyResponse + err = json.Unmarshal(body, &verifyResponse) + if err != nil { + return nil, fmt.Errorf("解析响应体失败:%v", err) + } + + // 根据实际响应处理逻辑 + if verifyResponse.Status != 0 { + return nil, fmt.Errorf("验证失败,状态码:%d", verifyResponse.Status) + } + + return &verifyResponse, nil +} + +func loadPrivateKey(path string) (*ecdsa.PrivateKey, error) { + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + block, _ := pem.Decode(data) + if block == nil || block.Type != "PRIVATE KEY" { + return nil, fmt.Errorf("无效的私钥数据") + } + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + ecdsaKey, ok := key.(*ecdsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("私钥类型错误") + } + return ecdsaKey, nil +} + +func generateJWT(privateKey *ecdsa.PrivateKey, keyID, issuerID string) (string, error) { + now := time.Now() + claims := jwt.RegisteredClaims{ + Issuer: issuerID, + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(1 * time.Hour)), + Audience: jwt.ClaimStrings{"appstoreconnect-v1"}, + } + token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) + token.Header["kid"] = keyID + tokenString, err := token.SignedString(privateKey) + if err != nil { + return "", err + } + return tokenString, nil +} + +// GenerateOutTradeNo 生成唯一订单号 +func (a *ApplePayService) GenerateOutTradeNo() string { + length := 16 + timestamp := time.Now().UnixNano() + timeStr := strconv.FormatInt(timestamp, 10) + randomPart := strconv.Itoa(int(timestamp % 1e6)) + combined := timeStr + randomPart + + if len(combined) >= length { + return combined[:length] + } + + for len(combined) < length { + combined += strconv.Itoa(int(timestamp % 10)) + } + + return combined +} + +// AppleVerifyResponse 定义苹果验证接口的响应结构 +type AppleVerifyResponse struct { + Status int `json:"status"` // 验证状态码:0 表示收据有效 + Receipt *Receipt `json:"receipt"` // 收据信息 +} + +// Receipt 定义收据的精简结构 +type Receipt struct { + BundleID string `json:"bundle_id"` // 应用的 Bundle ID + InApp []InAppItem `json:"in_app"` // 应用内购买记录 +} + +// InAppItem 定义单条交易记录 +type InAppItem struct { + ProductID string `json:"product_id"` // 商品 ID + TransactionID string `json:"transaction_id"` // 交易 ID + PurchaseDate string `json:"purchase_date"` // 购买日期 (ISO 8601) + OriginalTransID string `json:"original_transaction_id"` // 原始交易 ID +} diff --git a/app/main/api/internal/service/asynqService.go b/app/main/api/internal/service/asynqService.go new file mode 100644 index 0000000..6fab268 --- /dev/null +++ b/app/main/api/internal/service/asynqService.go @@ -0,0 +1,60 @@ +// asynq_service.go + +package service + +import ( + "encoding/json" + "znc-server/app/main/api/internal/config" + "znc-server/app/main/api/internal/types" + + "github.com/hibiken/asynq" + "github.com/zeromicro/go-zero/core/logx" +) + +type AsynqService struct { + client *asynq.Client + config config.Config +} + +// NewAsynqService 创建并初始化 Asynq 客户端 +func NewAsynqService(c config.Config) *AsynqService { + client := asynq.NewClient(asynq.RedisClientOpt{ + Addr: c.CacheRedis[0].Host, + Password: c.CacheRedis[0].Pass, + }) + + return &AsynqService{client: client, config: c} +} + +// Close 关闭 Asynq 客户端 +func (s *AsynqService) Close() error { + return s.client.Close() +} +func (s *AsynqService) SendQueryTask(orderID int64) error { + // 准备任务的 payload + payload := types.MsgPaySuccessQueryPayload{ + OrderID: orderID, + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + logx.Errorf("发送异步任务失败 (无法编码 payload): %v, 订单号: %d", err, orderID) + return err // 直接返回错误,避免继续执行 + } + + options := []asynq.Option{ + asynq.MaxRetry(5), // 设置最大重试次数 + } + // 创建任务 + task := asynq.NewTask(types.MsgPaySuccessQuery, payloadBytes, options...) + + // 将任务加入队列并获取任务信息 + info, err := s.client.Enqueue(task) + if err != nil { + logx.Errorf("发送异步任务失败 (加入队列失败): %+v, 订单号: %d", err, orderID) + return err + } + + // 记录成功日志,带上任务 ID 和队列信息 + logx.Infof("发送异步任务成功,任务ID: %s, 队列: %s, 订单号: %d", info.ID, info.Queue, orderID) + return nil +} diff --git a/app/main/api/internal/service/dictService.go b/app/main/api/internal/service/dictService.go new file mode 100644 index 0000000..85bb528 --- /dev/null +++ b/app/main/api/internal/service/dictService.go @@ -0,0 +1,47 @@ +package service + +import ( + "context" + "errors" + "znc-server/app/main/model" +) + +type DictService struct { + adminDictTypeModel model.AdminDictTypeModel + adminDictDataModel model.AdminDictDataModel +} + +func NewDictService(adminDictTypeModel model.AdminDictTypeModel, adminDictDataModel model.AdminDictDataModel) *DictService { + return &DictService{adminDictTypeModel: adminDictTypeModel, adminDictDataModel: adminDictDataModel} +} +func (s *DictService) GetDictLabel(ctx context.Context, dictType string, dictValue int64) (string, error) { + dictTypeModel, err := s.adminDictTypeModel.FindOneByDictType(ctx, dictType) + if err != nil { + return "", err + } + if dictTypeModel.Status != 1 { + return "", errors.New("字典类型未启用") + } + dictData, err := s.adminDictDataModel.FindOneByDictTypeDictValue(ctx, dictTypeModel.DictType, dictValue) + if err != nil { + return "", err + } + if dictData.Status != 1 { + return "", errors.New("字典数据未启用") + } + return dictData.DictLabel, nil +} +func (s *DictService) GetDictValue(ctx context.Context, dictType string, dictLabel string) (int64, error) { + dictTypeModel, err := s.adminDictTypeModel.FindOneByDictType(ctx, dictType) + if err != nil { + return 0, err + } + if dictTypeModel.Status != 1 { + return 0, errors.New("字典类型未启用") + } + dictData, err := s.adminDictDataModel.FindOneByDictTypeDictLabel(ctx, dictTypeModel.DictType, dictLabel) + if err != nil { + return 0, err + } + return dictData.DictValue, nil +} diff --git a/app/main/api/internal/service/imageService.go b/app/main/api/internal/service/imageService.go new file mode 100644 index 0000000..6c5ad79 --- /dev/null +++ b/app/main/api/internal/service/imageService.go @@ -0,0 +1,173 @@ +package service + +import ( + "bytes" + "fmt" + "image" + "image/jpeg" + "image/png" + "os" + "path/filepath" + + "github.com/fogleman/gg" + "github.com/skip2/go-qrcode" + "github.com/zeromicro/go-zero/core/logx" +) + +type ImageService struct { + baseImagePath string +} + +func NewImageService() *ImageService { + return &ImageService{ + baseImagePath: "static/images", // 原图存放目录 + } +} + +// ProcessImageWithQRCode 处理图片,在中间添加二维码 +func (s *ImageService) ProcessImageWithQRCode(qrcodeType, qrcodeUrl string) ([]byte, string, error) { + // 1. 根据qrcodeType确定使用哪张背景图 + var backgroundImageName string + switch qrcodeType { + case "promote": + backgroundImageName = "tg_qrcode_1.png" + case "invitation": + backgroundImageName = "yq_qrcode_1.png" + default: + backgroundImageName = "tg_qrcode_1.png" // 默认使用第一张图片 + } + + // 2. 读取原图 + originalImagePath := filepath.Join(s.baseImagePath, backgroundImageName) + originalImage, err := s.loadImage(originalImagePath) + if err != nil { + logx.Errorf("加载原图失败: %v, 图片路径: %s", err, originalImagePath) + return nil, "", fmt.Errorf("加载原图失败: %v", err) + } + + // 3. 获取原图尺寸 + bounds := originalImage.Bounds() + imgWidth := bounds.Dx() + imgHeight := bounds.Dy() + + // 4. 创建绘图上下文 + dc := gg.NewContext(imgWidth, imgHeight) + + // 5. 绘制原图作为背景 + dc.DrawImageAnchored(originalImage, imgWidth/2, imgHeight/2, 0.5, 0.5) + + // 6. 生成二维码(去掉白边) + qrCode, err := qrcode.New(qrcodeUrl, qrcode.Medium) + if err != nil { + logx.Errorf("生成二维码失败: %v, 二维码内容: %s", err, qrcodeUrl) + return nil, "", fmt.Errorf("生成二维码失败: %v", err) + } + // 禁用二维码边框,去掉白边 + qrCode.DisableBorder = true + + // 7. 根据二维码类型设置不同的尺寸和位置 + var qrSize int + var qrX, qrY int + + switch qrcodeType { + case "promote": + // promote类型:精确设置二维码尺寸 + qrSize = 280 // 固定尺寸280px + // 左下角位置:距左边和底边留一些边距 + qrX = 192 // 距左边180px + qrY = imgHeight - qrSize - 190 // 距底边100px + + case "invitation": + // invitation类型:精确设置二维码尺寸 + qrSize = 360 // 固定尺寸320px + // 中间偏上位置 + qrX = (imgWidth - qrSize) / 2 // 水平居中 + qrY = 555 // 垂直位置200px + + default: + // 默认(promote样式) + qrSize = 280 // 固定尺寸280px + qrX = 200 // 距左边180px + qrY = imgHeight - qrSize - 200 // 距底边100px + } + + // 8. 生成指定尺寸的二维码图片 + qrCodeImage := qrCode.Image(qrSize) + + // 9. 直接绘制二维码(不添加背景) + dc.DrawImageAnchored(qrCodeImage, qrX+qrSize/2, qrY+qrSize/2, 0.5, 0.5) + + // 11. 输出为字节数组 + var buf bytes.Buffer + err = png.Encode(&buf, dc.Image()) + if err != nil { + logx.Errorf("编码图片失败: %v", err) + return nil, "", fmt.Errorf("编码图片失败: %v", err) + } + + logx.Infof("成功生成带二维码的图片,类型: %s, 二维码内容: %s, 图片尺寸: %dx%d, 二维码尺寸: %dx%d, 位置: (%d,%d)", + qrcodeType, qrcodeUrl, imgWidth, imgHeight, qrSize, qrSize, qrX, qrY) + + return buf.Bytes(), "image/png", nil +} + +// loadImage 加载图片文件 +func (s *ImageService) loadImage(path string) (image.Image, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + // 尝试解码PNG + img, err := png.Decode(file) + if err != nil { + // 如果PNG解码失败,重新打开文件尝试JPEG + file.Close() + file, err = os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + img, err = jpeg.Decode(file) + if err != nil { + // 如果还是失败,使用通用解码器 + file.Close() + file, err = os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + img, _, err = image.Decode(file) + if err != nil { + return nil, err + } + } + } + + return img, nil +} + +// GetSupportedImageTypes 获取支持的图片类型列表 +func (s *ImageService) GetSupportedImageTypes() []string { + return []string{"promote", "invitation"} +} + +// CheckImageExists 检查指定类型的背景图是否存在 +func (s *ImageService) CheckImageExists(qrcodeType string) bool { + var backgroundImageName string + switch qrcodeType { + case "promote": + backgroundImageName = "tg_qrcode_1.png" + case "invitation": + backgroundImageName = "yq_qrcode_1.png" + default: + backgroundImageName = "tg_qrcode_1.png" + } + + imagePath := filepath.Join(s.baseImagePath, backgroundImageName) + _, err := os.Stat(imagePath) + return err == nil +} diff --git a/app/main/api/internal/service/tianyuanapi_sdk/client.go b/app/main/api/internal/service/tianyuanapi_sdk/client.go new file mode 100644 index 0000000..0f21ecb --- /dev/null +++ b/app/main/api/internal/service/tianyuanapi_sdk/client.go @@ -0,0 +1,416 @@ +package tianyuanapi + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" +) + +// API调用相关错误类型 +var ( + ErrQueryEmpty = errors.New("查询为空") + ErrSystem = errors.New("接口异常") + ErrDecryptFail = errors.New("解密失败") + ErrRequestParam = errors.New("请求参数结构不正确") + ErrInvalidParam = errors.New("参数校验不正确") + ErrInvalidIP = errors.New("未经授权的IP") + ErrMissingAccessId = errors.New("缺少Access-Id") + ErrInvalidAccessId = errors.New("未经授权的AccessId") + ErrFrozenAccount = errors.New("账户已冻结") + ErrArrears = errors.New("账户余额不足,无法请求") + ErrProductNotFound = errors.New("产品不存在") + ErrProductDisabled = errors.New("产品已停用") + ErrNotSubscribed = errors.New("未订阅此产品") + ErrBusiness = errors.New("业务失败") +) + +// 错误码映射 - 严格按照用户要求 +var ErrorCodeMap = map[error]int{ + ErrQueryEmpty: 1000, + ErrSystem: 1001, + ErrDecryptFail: 1002, + ErrRequestParam: 1003, + ErrInvalidParam: 1003, + ErrInvalidIP: 1004, + ErrMissingAccessId: 1005, + ErrInvalidAccessId: 1006, + ErrFrozenAccount: 1007, + ErrArrears: 1007, + ErrProductNotFound: 1008, + ErrProductDisabled: 1008, + ErrNotSubscribed: 1008, + ErrBusiness: 2001, +} + +// ApiCallOptions API调用选项 +type ApiCallOptions struct { + Json bool `json:"json,omitempty"` // 是否返回JSON格式 +} + +// Client 天元API客户端 +type Client struct { + accessID string + key string + baseURL string + timeout time.Duration + client *http.Client +} + +// Config 客户端配置 +type Config struct { + AccessID string // 访问ID + Key string // AES密钥(16进制) + BaseURL string // API基础URL + Timeout time.Duration // 超时时间 +} + +// Request 请求参数 +type Request struct { + InterfaceName string `json:"interfaceName"` // 接口名称 + Params map[string]interface{} `json:"params"` // 请求参数 + Timeout int `json:"timeout"` // 超时时间(毫秒) + Options *ApiCallOptions `json:"options"` // 调用选项 +} + +// ApiResponse HTTP API响应 +type ApiResponse struct { + Code int `json:"code"` + Message string `json:"message"` + TransactionID string `json:"transaction_id"` // 流水号 + Data string `json:"data"` // 加密的数据 +} + +// Response Call方法的响应 +type Response struct { + Code int `json:"code"` + Message string `json:"message"` + Success bool `json:"success"` + TransactionID string `json:"transaction_id"` // 流水号 + Data map[string]interface{} `json:"data"` // 解密后的数据 + Timeout int64 `json:"timeout"` // 请求耗时(毫秒) + Error string `json:"error,omitempty"` +} + +// NewClient 创建新的客户端实例 +func NewClient(config Config) (*Client, error) { + // 参数校验 + if config.AccessID == "" { + return nil, fmt.Errorf("accessID不能为空") + } + if config.Key == "" { + return nil, fmt.Errorf("key不能为空") + } + if config.BaseURL == "" { + config.BaseURL = "http://127.0.0.1:8080" + } + if config.Timeout == 0 { + config.Timeout = 60 * time.Second + } + + // 验证密钥格式 + if _, err := hex.DecodeString(config.Key); err != nil { + return nil, fmt.Errorf("无效的密钥格式,必须是16进制字符串: %v", err) + } + + return &Client{ + accessID: config.AccessID, + key: config.Key, + baseURL: config.BaseURL, + timeout: config.Timeout, + client: &http.Client{ + Timeout: config.Timeout, + }, + }, nil +} + +// Call 调用API接口 +func (c *Client) Call(req Request) (*Response, error) { + startTime := time.Now() + + // 参数校验 + if err := c.validateRequest(req); err != nil { + return nil, fmt.Errorf("请求参数校验失败: %v", err) + } + + // 加密参数 + jsonData, err := json.Marshal(req.Params) + if err != nil { + return nil, fmt.Errorf("参数序列化失败: %v", err) + } + + encryptedData, err := c.encrypt(string(jsonData)) + if err != nil { + return nil, fmt.Errorf("数据加密失败: %v", err) + } + + // 构建请求体 + requestBody := map[string]interface{}{ + "data": encryptedData, + } + + // 添加选项 + if req.Options != nil { + requestBody["options"] = req.Options + } else { + // 默认选项 + defaultOptions := &ApiCallOptions{ + Json: true, + } + requestBody["options"] = defaultOptions + } + + requestBodyBytes, err := json.Marshal(requestBody) + if err != nil { + return nil, fmt.Errorf("请求体序列化失败: %v", err) + } + + // 创建HTTP请求 + url := fmt.Sprintf("%s/api/v1/%s", c.baseURL, req.InterfaceName) + + httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBodyBytes)) + if err != nil { + return nil, fmt.Errorf("创建HTTP请求失败: %v", err) + } + + // 设置请求头 + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Access-Id", c.accessID) + httpReq.Header.Set("User-Agent", "TianyuanAPI-Go-SDK/1.0.0") + + // 发送请求 + resp, err := c.client.Do(httpReq) + if err != nil { + endTime := time.Now() + requestTime := endTime.Sub(startTime).Milliseconds() + return &Response{ + Success: false, + Message: "请求失败", + Error: err.Error(), + Timeout: requestTime, + }, nil + } + defer resp.Body.Close() + + // 读取响应 + body, err := io.ReadAll(resp.Body) + if err != nil { + endTime := time.Now() + requestTime := endTime.Sub(startTime).Milliseconds() + return &Response{ + Success: false, + Message: "读取响应失败", + Error: err.Error(), + Timeout: requestTime, + }, nil + } + + // 解析HTTP API响应 + var apiResp ApiResponse + if err := json.Unmarshal(body, &apiResp); err != nil { + endTime := time.Now() + requestTime := endTime.Sub(startTime).Milliseconds() + return &Response{ + Success: false, + Message: "响应解析失败", + Error: err.Error(), + Timeout: requestTime, + }, nil + } + + // 计算请求耗时 + endTime := time.Now() + requestTime := endTime.Sub(startTime).Milliseconds() + + // 构建Call方法的响应 + response := &Response{ + Code: apiResp.Code, + Message: apiResp.Message, + Success: apiResp.Code == 0, + TransactionID: apiResp.TransactionID, + Timeout: requestTime, + } + + // 如果有加密数据,尝试解密 + if apiResp.Data != "" { + decryptedData, err := c.decrypt(apiResp.Data) + if err == nil { + var decryptedMap map[string]interface{} + if json.Unmarshal([]byte(decryptedData), &decryptedMap) == nil { + response.Data = decryptedMap + } + } + } + + // 根据响应码返回对应的错误 + if apiResp.Code != 0 { + err := GetErrorByCode(apiResp.Code) + return nil, err + } + + return response, nil +} + +// CallInterface 简化接口调用方法 +func (c *Client) CallInterface(interfaceName string, params map[string]interface{}, options ...*ApiCallOptions) (*Response, error) { + var opts *ApiCallOptions + if len(options) > 0 { + opts = options[0] + } + + req := Request{ + InterfaceName: interfaceName, + Params: params, + Timeout: 60000, + Options: opts, + } + + return c.Call(req) +} + +// validateRequest 校验请求参数 +func (c *Client) validateRequest(req Request) error { + if req.InterfaceName == "" { + return fmt.Errorf("interfaceName不能为空") + } + if req.Params == nil { + return fmt.Errorf("params不能为空") + } + return nil +} + +// encrypt AES CBC加密 +func (c *Client) encrypt(plainText string) (string, error) { + keyBytes, err := hex.DecodeString(c.key) + if err != nil { + return "", err + } + + block, err := aes.NewCipher(keyBytes) + if err != nil { + return "", err + } + + // 生成随机IV + iv := make([]byte, aes.BlockSize) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return "", err + } + + // 填充数据 + paddedData := c.pkcs7Pad([]byte(plainText), aes.BlockSize) + + // 加密 + ciphertext := make([]byte, len(iv)+len(paddedData)) + copy(ciphertext, iv) + + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(ciphertext[len(iv):], paddedData) + + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// decrypt AES CBC解密 +func (c *Client) decrypt(encryptedText string) (string, error) { + keyBytes, err := hex.DecodeString(c.key) + if err != nil { + return "", err + } + + ciphertext, err := base64.StdEncoding.DecodeString(encryptedText) + if err != nil { + return "", err + } + + block, err := aes.NewCipher(keyBytes) + if err != nil { + return "", err + } + + if len(ciphertext) < aes.BlockSize { + return "", fmt.Errorf("密文太短") + } + + iv := ciphertext[:aes.BlockSize] + ciphertext = ciphertext[aes.BlockSize:] + + if len(ciphertext)%aes.BlockSize != 0 { + return "", fmt.Errorf("密文长度不是块大小的倍数") + } + + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(ciphertext, ciphertext) + + // 去除填充 + unpaddedData, err := c.pkcs7Unpad(ciphertext) + if err != nil { + return "", err + } + + return string(unpaddedData), nil +} + +// pkcs7Pad PKCS7填充 +func (c *Client) pkcs7Pad(data []byte, blockSize int) []byte { + padding := blockSize - len(data)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(data, padtext...) +} + +// pkcs7Unpad PKCS7去除填充 +func (c *Client) pkcs7Unpad(data []byte) ([]byte, error) { + length := len(data) + if length == 0 { + return nil, fmt.Errorf("数据为空") + } + unpadding := int(data[length-1]) + if unpadding > length { + return nil, fmt.Errorf("无效的填充") + } + return data[:length-unpadding], nil +} + +// GetErrorByCode 根据错误码获取错误 +func GetErrorByCode(code int) error { + // 对于有多个错误对应同一错误码的情况,返回第一个 + switch code { + case 1000: + return ErrQueryEmpty + case 1001: + return ErrSystem + case 1002: + return ErrDecryptFail + case 1003: + return ErrRequestParam + case 1004: + return ErrInvalidIP + case 1005: + return ErrMissingAccessId + case 1006: + return ErrInvalidAccessId + case 1007: + return ErrFrozenAccount + case 1008: + return ErrProductNotFound + case 2001: + return ErrBusiness + default: + return fmt.Errorf("未知错误码: %d", code) + } +} + +// GetCodeByError 根据错误获取错误码 +func GetCodeByError(err error) int { + if code, exists := ErrorCodeMap[err]; exists { + return code + } + return -1 +} diff --git a/app/main/api/internal/service/userService.go b/app/main/api/internal/service/userService.go new file mode 100644 index 0000000..1963dc3 --- /dev/null +++ b/app/main/api/internal/service/userService.go @@ -0,0 +1,296 @@ +package service + +import ( + "context" + "database/sql" + "znc-server/app/main/api/internal/config" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + jwtx "znc-server/common/jwt" + "znc-server/common/xerr" + + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type UserService struct { + Config *config.Config + userModel model.UserModel + userAuthModel model.UserAuthModel + userTempModel model.UserTempModel + agentModel model.AgentModel +} + +// NewUserService 创建UserService实例 +func NewUserService(config *config.Config, userModel model.UserModel, userAuthModel model.UserAuthModel, userTempModel model.UserTempModel, agentModel model.AgentModel) *UserService { + return &UserService{ + Config: config, + userModel: userModel, + userAuthModel: userAuthModel, + userTempModel: userTempModel, + agentModel: agentModel, + } +} + +// GenerateUUIDUserId 生成UUID用户ID +func (s *UserService) GenerateUUIDUserId(ctx context.Context) (string, error) { + id := uuid.NewString() + return id, nil +} + +// RegisterUUIDUser 注册UUID用户,返回用户ID +func (s *UserService) RegisterUUIDUser(ctx context.Context) (int64, error) { + // 生成UUID + uuidStr, err := s.GenerateUUIDUserId(ctx) + if err != nil { + return 0, err + } + + var userId int64 + err = s.userModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { + // 创建用户记录 + user := &model.User{} + result, err := s.userModel.Insert(ctx, session, user) + if err != nil { + return err + } + userId, err = result.LastInsertId() + if err != nil { + return err + } + + // 创建用户认证记录 + userAuth := &model.UserAuth{ + UserId: userId, + AuthType: model.UserAuthTypeUUID, + AuthKey: uuidStr, + } + _, err = s.userAuthModel.Insert(ctx, session, userAuth) + return err + }) + if err != nil { + return 0, err + } + + return userId, nil +} + +// generalUserToken 生成用户token +func (s *UserService) GeneralUserToken(ctx context.Context, userID int64, userType int64) (string, error) { + platform, err := ctxdata.GetPlatformFromCtx(ctx) + if err != nil { + return "", err + } + + var isAgent int64 + var agentID int64 + if userType == model.UserTypeNormal { + agent, err := s.agentModel.FindOneByUserId(ctx, userID) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return "", err + } + if agent != nil { + agentID = agent.Id + isAgent = model.AgentStatusYes + } + } else { + userTemp, err := s.userTempModel.FindOne(ctx, userID) + if err != nil { + return "", err + } + if userTemp != nil { + userID = userTemp.Id + } + } + token, generaErr := jwtx.GenerateJwtToken(jwtx.JwtClaims{ + UserId: userID, + AgentId: agentID, + Platform: platform, + UserType: userType, + IsAgent: isAgent, + }, s.Config.JwtAuth.AccessSecret, s.Config.JwtAuth.AccessExpire) + if generaErr != nil { + return "", errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "更新token, 生成token失败 : %d", userID) + } + return token, nil +} + +// RegisterUser 注册用户,返回用户ID +// 传入手机号,自动注册,如果ctx存在临时用户则临时用户转为正式用户 +func (s *UserService) RegisterUser(ctx context.Context, mobile string) (int64, error) { + claims, err := ctxdata.GetClaimsFromCtx(ctx) + if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) { + return 0, err + } + user, err := s.userModel.FindOneByMobile(ctx, sql.NullString{String: mobile, Valid: true}) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return 0, err + } + if user != nil { + return 0, errors.New("用户已注册") + } + // 普通注册 + if claims == nil { + var userId int64 + err = s.userModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { + user := &model.User{ + Mobile: sql.NullString{String: mobile, Valid: true}, + } + result, err := s.userModel.Insert(ctx, session, user) + if err != nil { + return err + } + userId, err = result.LastInsertId() + if err != nil { + return err + } + s.userAuthModel.Insert(ctx, session, &model.UserAuth{ + UserId: userId, + AuthType: model.UserAuthTypeMobile, + AuthKey: mobile, + }) + return nil + }) + if err != nil { + return 0, err + } + return userId, nil + } + + // 双重判断是否已经注册 + if claims.UserType == model.UserTypeNormal { + return 0, errors.New("用户已注册") + } + var userId int64 + // 临时转正式注册 + err = s.userModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { + user := &model.User{ + Mobile: sql.NullString{String: mobile, Valid: true}, + } + result, err := s.userModel.Insert(ctx, session, user) + if err != nil { + return err + } + userId, err = result.LastInsertId() + if err != nil { + return err + } + _, err = s.userAuthModel.Insert(ctx, session, &model.UserAuth{ + UserId: userId, + AuthType: model.UserAuthTypeMobile, + AuthKey: mobile, + }) + if err != nil { + return err + } + err = s.TempUserBindUser(ctx, session, userId) + if err != nil { + return err + } + return nil + }) + if err != nil { + return 0, err + } + return userId, nil +} + +// TempUserBindUser 临时用户绑定用户 +func (s *UserService) TempUserBindUser(ctx context.Context, session sqlx.Session, normalUserID int64) error { + claims, err := ctxdata.GetClaimsFromCtx(ctx) + if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) { + return err + } + + if claims == nil || claims.UserType != model.UserTypeTemp { + return errors.New("无临时用户") + } + + // 使用事务上下文查询临时用户 + userTemp, err := s.userTempModel.FindOne(ctx, claims.UserId) + if err != nil { + return err + } + + // 检查是否已经注册过 + userAuth, err := s.userAuthModel.FindOneByAuthTypeAuthKey(ctx, userTemp.AuthType, userTemp.AuthKey) + if err != nil && !errors.Is(err, model.ErrNotFound) { + return err + } + if userAuth != nil { + return errors.New("临时用户已注册") + } + + if session == nil { + err := s.userAuthModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { + _, err = s.userAuthModel.Insert(ctx, session, &model.UserAuth{ + UserId: normalUserID, + AuthType: userTemp.AuthType, + AuthKey: userTemp.AuthKey, + }) + if err != nil { + return err + } + + // 重新获取最新的userTemp数据,确保版本号是最新的 + latestUserTemp, err := s.userTempModel.FindOne(ctx, claims.UserId) + if err != nil { + return err + } + err = s.userTempModel.DeleteSoft(ctx, session, latestUserTemp) + if err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + return nil + } else { + _, err = s.userAuthModel.Insert(ctx, session, &model.UserAuth{ + UserId: normalUserID, + AuthType: userTemp.AuthType, + AuthKey: userTemp.AuthKey, + }) + if err != nil { + return err + } + + // 重新获取最新的userTemp数据,确保版本号是最新的 + latestUserTemp, err := s.userTempModel.FindOne(ctx, claims.UserId) + if err != nil { + return err + } + err = s.userTempModel.DeleteSoft(ctx, session, latestUserTemp) + if err != nil { + return err + } + return nil + } +} + +// _bak_RegisterUUIDUser 注册UUID用户,返回用户ID +func (s *UserService) _bak_RegisterUUIDUser(ctx context.Context) error { + // 生成UUID + uuidStr, err := s.GenerateUUIDUserId(ctx) + if err != nil { + return err + } + + err = s.userTempModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error { + // 创建用户临时记录 + userTemp := &model.UserTemp{ + AuthType: model.UserAuthTypeUUID, + AuthKey: uuidStr, + } + _, err := s.userTempModel.Insert(ctx, session, userTemp) + return err + }) + if err != nil { + return err + } + + return nil +} diff --git a/app/main/api/internal/service/verificationService.go b/app/main/api/internal/service/verificationService.go new file mode 100644 index 0000000..6c8686c --- /dev/null +++ b/app/main/api/internal/service/verificationService.go @@ -0,0 +1,217 @@ +package service + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "znc-server/app/main/api/internal/config" + tianyuanapi "znc-server/app/main/api/internal/service/tianyuanapi_sdk" + + "github.com/tidwall/gjson" +) +type VerificationService struct { + c config.Config + tianyuanapi *tianyuanapi.Client + apiRequestService *ApiRequestService +} + +func NewVerificationService(c config.Config, tianyuanapi *tianyuanapi.Client, apiRequestService *ApiRequestService) *VerificationService { + return &VerificationService{ + c: c, + tianyuanapi: tianyuanapi, + apiRequestService: apiRequestService, + } +} + +// 二要素 +type TwoFactorVerificationRequest struct { + Name string + IDCard string +} +type TwoFactorVerificationResp struct { + Msg string `json:"msg"` + Success bool `json:"success"` + Code int `json:"code"` + Data *TwoFactorVerificationData `json:"data"` // +} +type TwoFactorVerificationData struct { + Birthday string `json:"birthday"` + Result int `json:"result"` + Address string `json:"address"` + OrderNo string `json:"orderNo"` + Sex string `json:"sex"` + Desc string `json:"desc"` +} + +// 三要素 +type ThreeFactorVerificationRequest struct { + Name string + IDCard string + Mobile string +} + +// VerificationResult 定义校验结果结构体 +type VerificationResult struct { + Passed bool + Err error +} + +// ValidationError 定义校验错误类型 +type ValidationError struct { + Message string +} + +func (e *ValidationError) Error() string { + return e.Message +} + +func (r *VerificationService) TwoFactorVerification(request TwoFactorVerificationRequest) (*VerificationResult, error) { + appCode := r.c.Ali.Code + requestUrl := "https://kzidcardv1.market.alicloudapi.com/api-mall/api/id_card/check" + + // 构造查询参数 + data := url.Values{} + data.Add("name", request.Name) + data.Add("idcard", request.IDCard) + + req, err := http.NewRequest(http.MethodPost, requestUrl, strings.NewReader(data.Encode())) + if err != nil { + return nil, fmt.Errorf("创建请求失败: %v", err) + } + req.Header.Set("Authorization", "APPCODE "+appCode) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("请求失败: %v", err) + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("请求失败, 状态码: %d", resp.StatusCode) + } + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("响应体读取失败:%v", err) + } + var twoFactorVerificationResp TwoFactorVerificationResp + err = json.Unmarshal(respBody, &twoFactorVerificationResp) + if err != nil { + return nil, fmt.Errorf("二要素解析错误: %v", err) + } + + if !twoFactorVerificationResp.Success { + return &VerificationResult{ + Passed: false, + Err: &ValidationError{Message: "请输入有效的身份证号码"}, + }, nil + } + + if twoFactorVerificationResp.Code != 200 { + return &VerificationResult{ + Passed: false, + Err: &ValidationError{Message: twoFactorVerificationResp.Msg}, + }, nil + } + + if twoFactorVerificationResp.Data.Result == 1 { + return &VerificationResult{ + Passed: false, + Err: &ValidationError{Message: "姓名与身份证不一致"}, + }, nil + } + + return &VerificationResult{Passed: true, Err: nil}, nil +} + +func (r *VerificationService) TwoFactorVerificationWest(request TwoFactorVerificationRequest) (*VerificationResult, error) { + resp, err := r.tianyuanapi.CallInterface("YYSYBE08", map[string]interface{}{ + "name": request.Name, + "id_card": request.IDCard, + }) + if err != nil { + return nil, fmt.Errorf("请求失败: %v", err) + } + + respBytes, err := json.Marshal(resp.Data) + if err != nil { + return nil, fmt.Errorf("转换响应失败: %v", err) + } + + // 使用gjson获取resultCode + resultCode := gjson.GetBytes(respBytes, "ctidRequest.ctidAuth.resultCode") + if !resultCode.Exists() { + return &VerificationResult{ + Passed: false, + Err: &ValidationError{Message: "获取resultCode失败"}, + }, nil + } + + // 获取resultCode的第一个字符 + resultCodeStr := resultCode.String() + if len(resultCodeStr) == 0 { + return &VerificationResult{ + Passed: false, + Err: &ValidationError{Message: "resultCode为空"}, + }, nil + } + + firstChar := string(resultCodeStr[0]) + if firstChar != "0" && firstChar != "5" { + return &VerificationResult{ + Passed: false, + Err: &ValidationError{Message: "姓名与身份证不一致"}, + }, nil + } + + return &VerificationResult{Passed: true, Err: nil}, nil +} + +func (r *VerificationService) ThreeFactorVerification(request ThreeFactorVerificationRequest) (*VerificationResult, error) { + resp, err := r.tianyuanapi.CallInterface("YYSY09CD", map[string]interface{}{ + "name": request.Name, + "id_card": request.IDCard, + "mobile_no": request.Mobile, + }) + if err != nil { + return nil, fmt.Errorf("请求失败: %v", err) + } + + respBytes, err := json.Marshal(resp.Data) + if err != nil { + return nil, fmt.Errorf("转换响应失败: %v", err) + } + + // 使用gjson获取resultCode + resultCode := gjson.GetBytes(respBytes, "ctidRequest.ctidAuth.resultCode") + if !resultCode.Exists() { + return &VerificationResult{ + Passed: false, + Err: &ValidationError{Message: "身份信息异常"}, + }, nil + } + + // 获取resultCode的第一个字符 + resultCodeStr := resultCode.String() + if len(resultCodeStr) == 0 { + return &VerificationResult{ + Passed: false, + Err: &ValidationError{Message: "身份信息异常"}, + }, nil + } + + firstChar := string(resultCodeStr[0]) + if firstChar != "0" && firstChar != "5" { + return &VerificationResult{ + Passed: false, + Err: &ValidationError{Message: "姓名、证件号、手机号信息不一致"}, + }, nil + } + + return &VerificationResult{Passed: true, Err: nil}, nil +} diff --git a/app/main/api/internal/service/wechatpayService.go b/app/main/api/internal/service/wechatpayService.go new file mode 100644 index 0000000..03fb9d7 --- /dev/null +++ b/app/main/api/internal/service/wechatpayService.go @@ -0,0 +1,377 @@ +package service + +import ( + "context" + "fmt" + "net/http" + "strconv" + "time" + "znc-server/app/main/api/internal/config" + "znc-server/app/main/model" + "znc-server/common/ctxdata" + "znc-server/pkg/lzkit/lzUtils" + + "github.com/wechatpay-apiv3/wechatpay-go/core" + "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers" + "github.com/wechatpay-apiv3/wechatpay-go/core/downloader" + "github.com/wechatpay-apiv3/wechatpay-go/core/notify" + "github.com/wechatpay-apiv3/wechatpay-go/core/option" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/app" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi" + "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic" + "github.com/wechatpay-apiv3/wechatpay-go/utils" + "github.com/zeromicro/go-zero/core/logx" +) + +const ( + TradeStateSuccess = "SUCCESS" // 支付成功 + TradeStateRefund = "REFUND" // 转入退款 + TradeStateNotPay = "NOTPAY" // 未支付 + TradeStateClosed = "CLOSED" // 已关闭 + TradeStateRevoked = "REVOKED" // 已撤销(付款码支付) + TradeStateUserPaying = "USERPAYING" // 用户支付中(付款码支付) + TradeStatePayError = "PAYERROR" // 支付失败(其他原因,如银行返回失败) +) + +// InitType 初始化类型 +type InitType string + +const ( + InitTypePlatformCert InitType = "platform_cert" // 平台证书初始化 + InitTypeWxPayPubKey InitType = "wxpay_pubkey" // 微信支付公钥初始化 +) + +type WechatPayService struct { + config config.Config + wechatClient *core.Client + notifyHandler *notify.Handler + userAuthModel model.UserAuthModel +} + +// NewWechatPayService 创建微信支付服务实例 +func NewWechatPayService(c config.Config, userAuthModel model.UserAuthModel, initType InitType) *WechatPayService { + switch initType { + case InitTypePlatformCert: + return newWechatPayServiceWithPlatformCert(c, userAuthModel) + case InitTypeWxPayPubKey: + return newWechatPayServiceWithWxPayPubKey(c, userAuthModel) + default: + logx.Errorf("不支持的初始化类型: %s", initType) + panic(fmt.Sprintf("初始化失败,服务停止: %s", initType)) + } +} + +// newWechatPayServiceWithPlatformCert 使用平台证书初始化微信支付服务 +func newWechatPayServiceWithPlatformCert(c config.Config, userAuthModel model.UserAuthModel) *WechatPayService { + // 从配置中加载商户信息 + mchID := c.Wxpay.MchID + mchCertificateSerialNumber := c.Wxpay.MchCertificateSerialNumber + mchAPIv3Key := c.Wxpay.MchApiv3Key + + // 从文件中加载商户私钥 + mchPrivateKey, err := utils.LoadPrivateKeyWithPath(c.Wxpay.MchPrivateKeyPath) + if err != nil { + logx.Errorf("加载商户私钥失败: %v", err) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) // 记录错误并停止程序 + } + + // 使用商户私钥和其他参数初始化微信支付客户端 + opts := []core.ClientOption{ + option.WithWechatPayAutoAuthCipher(mchID, mchCertificateSerialNumber, mchPrivateKey, mchAPIv3Key), + } + client, err := core.NewClient(context.Background(), opts...) + if err != nil { + logx.Errorf("创建微信支付客户端失败: %v", err) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) // 记录错误并停止程序 + } + + // 在初始化时获取证书访问器并创建 notifyHandler + certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID) + notifyHandler, err := notify.NewRSANotifyHandler(mchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor)) + if err != nil { + logx.Errorf("获取证书访问器失败: %v", err) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) + } + + logx.Infof("微信支付客户端初始化成功(平台证书方式)") + return &WechatPayService{ + config: c, + wechatClient: client, + notifyHandler: notifyHandler, + userAuthModel: userAuthModel, + } +} + +// newWechatPayServiceWithWxPayPubKey 使用微信支付公钥初始化微信支付服务 +func newWechatPayServiceWithWxPayPubKey(c config.Config, userAuthModel model.UserAuthModel) *WechatPayService { + // 从配置中加载商户信息 + mchID := c.Wxpay.MchID + mchCertificateSerialNumber := c.Wxpay.MchCertificateSerialNumber + mchAPIv3Key := c.Wxpay.MchApiv3Key + mchPrivateKeyPath := c.Wxpay.MchPrivateKeyPath + mchPublicKeyID := c.Wxpay.MchPublicKeyID + mchPublicKeyPath := c.Wxpay.MchPublicKeyPath + // 从文件中加载商户私钥 + mchPrivateKey, err := utils.LoadPrivateKeyWithPath(mchPrivateKeyPath) + if err != nil { + logx.Errorf("加载商户私钥失败: %v", err) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) + } + + // 从文件中加载微信支付平台证书 + mchPublicKey, err := utils.LoadPublicKeyWithPath(mchPublicKeyPath) + if err != nil { + logx.Errorf("加载微信支付平台证书失败: %v", err) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) + } + + // 使用商户私钥和其他参数初始化微信支付客户端 + opts := []core.ClientOption{ + option.WithWechatPayPublicKeyAuthCipher(mchID, mchCertificateSerialNumber, mchPrivateKey, mchPublicKeyID, mchPublicKey), + } + client, err := core.NewClient(context.Background(), opts...) + if err != nil { + logx.Errorf("创建微信支付客户端失败: %v", err) + panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) + } + + // 初始化 notify.Handler + certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID) + notifyHandler := notify.NewNotifyHandler( + mchAPIv3Key, + verifiers.NewSHA256WithRSACombinedVerifier(certificateVisitor, mchPublicKeyID, *mchPublicKey)) + + logx.Infof("微信支付客户端初始化成功(微信支付公钥方式)") + return &WechatPayService{ + config: c, + wechatClient: client, + notifyHandler: notifyHandler, + userAuthModel: userAuthModel, + } +} + +// CreateWechatAppOrder 创建微信APP支付订单 +func (w *WechatPayService) CreateWechatAppOrder(ctx context.Context, amount float64, description string, outTradeNo string) (string, error) { + totalAmount := lzUtils.ToWechatAmount(amount) + + // 构建支付请求参数 + payRequest := app.PrepayRequest{ + Appid: core.String(w.config.Wxpay.AppID), + Mchid: core.String(w.config.Wxpay.MchID), + Description: core.String(description), + OutTradeNo: core.String(outTradeNo), + NotifyUrl: core.String(w.config.Wxpay.NotifyUrl), + Amount: &app.Amount{ + Total: core.Int64(totalAmount), + }, + } + + // 初始化 AppApiService + svc := app.AppApiService{Client: w.wechatClient} + + // 发起预支付请求 + resp, result, err := svc.Prepay(ctx, payRequest) + if err != nil { + return "", fmt.Errorf("微信支付订单创建失败: %v, 状态码: %d", err, result.Response.StatusCode) + } + + // 返回预支付交易会话标识 + return *resp.PrepayId, nil +} + +// CreateWechatMiniProgramOrder 创建微信小程序支付订单 +func (w *WechatPayService) CreateWechatMiniProgramOrder(ctx context.Context, amount float64, description string, outTradeNo string, openid string) (interface{}, error) { + totalAmount := lzUtils.ToWechatAmount(amount) + + // 构建支付请求参数 + payRequest := jsapi.PrepayRequest{ + Appid: core.String(w.config.WechatMini.AppID), + Mchid: core.String(w.config.Wxpay.MchID), + Description: core.String(description), + OutTradeNo: core.String(outTradeNo), + NotifyUrl: core.String(w.config.Wxpay.NotifyUrl), + Amount: &jsapi.Amount{ + Total: core.Int64(totalAmount), + }, + Payer: &jsapi.Payer{ + Openid: core.String(openid), // 用户的 OpenID,通过前端传入 + }} + + // 初始化 AppApiService + svc := jsapi.JsapiApiService{Client: w.wechatClient} + + // 发起预支付请求 + resp, result, err := svc.PrepayWithRequestPayment(ctx, payRequest) + if err != nil { + return "", fmt.Errorf("微信支付订单创建失败: %v, 状态码: %d", err, result.Response.StatusCode) + } + // 返回预支付交易会话标识 + return resp, nil +} + +// CreateWechatH5Order 创建微信H5支付订单 +func (w *WechatPayService) CreateWechatH5Order(ctx context.Context, amount float64, description string, outTradeNo string, openid string) (interface{}, error) { + totalAmount := lzUtils.ToWechatAmount(amount) + + // 构建支付请求参数 + payRequest := jsapi.PrepayRequest{ + Appid: core.String(w.config.WechatH5.AppID), + Mchid: core.String(w.config.Wxpay.MchID), + Description: core.String(description), + OutTradeNo: core.String(outTradeNo), + NotifyUrl: core.String(w.config.Wxpay.NotifyUrl), + Amount: &jsapi.Amount{ + Total: core.Int64(totalAmount), + }, + Payer: &jsapi.Payer{ + Openid: core.String(openid), // 用户的 OpenID,通过前端传入 + }} + + // 初始化 AppApiService + svc := jsapi.JsapiApiService{Client: w.wechatClient} + + // 发起预支付请求 + resp, result, err := svc.PrepayWithRequestPayment(ctx, payRequest) + logx.Infof("微信h5支付订单:resp: %+v, result: %+v, err: %+v", resp, result, err) + if err != nil { + return "", fmt.Errorf("微信支付订单创建失败: %v, 状态码: %d", err, result.Response.StatusCode) + } + // 返回预支付交易会话标识 + return resp, nil +} + +// CreateWechatOrder 创建微信支付订单(集成 APP、H5、小程序) +func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64, description string, outTradeNo string) (interface{}, error) { + // 根据 ctx 中的 platform 判断平台 + platform := ctx.Value("platform").(string) + + var prepayData interface{} + var err error + + switch platform { + case model.PlatformWxMini: + userID, getUidErr := ctxdata.GetUidFromCtx(ctx) + if getUidErr != nil { + return "", getUidErr + } + userAuthModel, findAuthModelErr := w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxMiniOpenID) + if findAuthModelErr != nil { + return "", findAuthModelErr + } + prepayData, err = w.CreateWechatMiniProgramOrder(ctx, amount, description, outTradeNo, userAuthModel.AuthKey) + if err != nil { + return "", err + } + case model.PlatformWxH5: + userID, getUidErr := ctxdata.GetUidFromCtx(ctx) + if getUidErr != nil { + return "", getUidErr + } + userAuthModel, findAuthModelErr := w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, model.UserAuthTypeWxh5OpenID) + if findAuthModelErr != nil { + return "", findAuthModelErr + } + prepayData, err = w.CreateWechatH5Order(ctx, amount, description, outTradeNo, userAuthModel.AuthKey) + if err != nil { + return "", err + } + case model.PlatformApp: + // 如果是 APP 平台,调用 APP 支付订单创建 + prepayData, err = w.CreateWechatAppOrder(ctx, amount, description, outTradeNo) + default: + return "", fmt.Errorf("不支持的支付平台: %s", platform) + } + + // 如果创建支付订单失败,返回错误 + if err != nil { + return "", fmt.Errorf("支付订单创建失败: %v", err) + } + + // 返回预支付ID + return prepayData, nil +} + +// HandleWechatPayNotification 处理微信支付回调 +func (w *WechatPayService) HandleWechatPayNotification(ctx context.Context, req *http.Request) (*payments.Transaction, error) { + transaction := new(payments.Transaction) + _, err := w.notifyHandler.ParseNotifyRequest(ctx, req, transaction) + if err != nil { + return nil, fmt.Errorf("微信支付通知处理失败: %v", err) + } + // 返回交易信息 + return transaction, nil +} + +// HandleRefundNotification 处理微信退款回调 +func (w *WechatPayService) HandleRefundNotification(ctx context.Context, req *http.Request) (*refunddomestic.Refund, error) { + refund := new(refunddomestic.Refund) + _, err := w.notifyHandler.ParseNotifyRequest(ctx, req, refund) + if err != nil { + return nil, fmt.Errorf("微信退款回调通知处理失败: %v", err) + } + return refund, nil +} + +// QueryOrderStatus 主动查询订单状态 +func (w *WechatPayService) QueryOrderStatus(ctx context.Context, transactionID string) (*payments.Transaction, error) { + svc := jsapi.JsapiApiService{Client: w.wechatClient} + + // 调用 QueryOrderById 方法查询订单状态 + resp, result, err := svc.QueryOrderById(ctx, jsapi.QueryOrderByIdRequest{ + TransactionId: core.String(transactionID), + Mchid: core.String(w.config.Wxpay.MchID), + }) + if err != nil { + return nil, fmt.Errorf("订单查询失败: %v, 状态码: %d", err, result.Response.StatusCode) + } + return resp, nil + +} + +// WeChatRefund 申请微信退款 +func (w *WechatPayService) WeChatRefund(ctx context.Context, outTradeNo string, refundAmount float64, totalAmount float64) error { + + // 生成唯一的退款单号 + outRefundNo := fmt.Sprintf("%s-refund", outTradeNo) + + // 初始化退款服务 + svc := refunddomestic.RefundsApiService{Client: w.wechatClient} + + // 创建退款请求 + resp, result, err := svc.Create(ctx, refunddomestic.CreateRequest{ + OutTradeNo: core.String(outTradeNo), + OutRefundNo: core.String(outRefundNo), + NotifyUrl: core.String(w.config.Wxpay.RefundNotifyUrl), + Amount: &refunddomestic.AmountReq{ + Currency: core.String("CNY"), + Refund: core.Int64(lzUtils.ToWechatAmount(refundAmount)), + Total: core.Int64(lzUtils.ToWechatAmount(totalAmount)), + }, + }) + if err != nil { + return fmt.Errorf("微信订单申请退款错误: %v", err) + } + // 打印退款结果 + logx.Infof("退款申请成功,状态码=%d,退款单号=%s,微信退款单号=%s", result.Response.StatusCode, *resp.OutRefundNo, *resp.RefundId) + return nil +} + +// GenerateOutTradeNo 生成唯一订单号 +func (w *WechatPayService) GenerateOutTradeNo() string { + length := 16 + timestamp := time.Now().UnixNano() + timeStr := strconv.FormatInt(timestamp, 10) + randomPart := strconv.Itoa(int(timestamp % 1e6)) + combined := timeStr + randomPart + + if len(combined) >= length { + return combined[:length] + } + + for len(combined) < length { + combined += strconv.Itoa(int(timestamp % 10)) + } + + return combined +} diff --git a/app/main/api/internal/svc/servicecontext.go b/app/main/api/internal/svc/servicecontext.go new file mode 100644 index 0000000..33b08f8 --- /dev/null +++ b/app/main/api/internal/svc/servicecontext.go @@ -0,0 +1,296 @@ +package svc + +import ( + "time" + "znc-server/app/main/api/internal/config" + "znc-server/app/main/api/internal/middleware" + "znc-server/app/main/api/internal/service" + tianyuanapi "znc-server/app/main/api/internal/service/tianyuanapi_sdk" + "znc-server/app/main/model" + + "github.com/hibiken/asynq" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/redis" + "github.com/zeromicro/go-zero/core/stores/sqlx" + "github.com/zeromicro/go-zero/rest" +) + +// ServiceContext 服务上下文 +type ServiceContext struct { + Config config.Config + Redis *redis.Redis + + // 中间件 + AuthInterceptor rest.Middleware + UserAuthInterceptor rest.Middleware + + // 用户相关模型 + UserModel model.UserModel + UserAuthModel model.UserAuthModel + UserTempModel model.UserTempModel + + // 产品相关模型 + ProductModel model.ProductModel + FeatureModel model.FeatureModel + ProductFeatureModel model.ProductFeatureModel + + // 订单相关模型 + OrderModel model.OrderModel + OrderRefundModel model.OrderRefundModel + QueryModel model.QueryModel + QueryCleanupLogModel model.QueryCleanupLogModel + QueryCleanupDetailModel model.QueryCleanupDetailModel + QueryCleanupConfigModel model.QueryCleanupConfigModel + + // 代理相关模型 + AgentModel model.AgentModel + AgentAuditModel model.AgentAuditModel + AgentClosureModel model.AgentClosureModel + AgentCommissionModel model.AgentCommissionModel + AgentCommissionDeductionModel model.AgentCommissionDeductionModel + AgentWalletModel model.AgentWalletModel + AgentLinkModel model.AgentLinkModel + AgentOrderModel model.AgentOrderModel + AgentRewardsModel model.AgentRewardsModel + AgentMembershipConfigModel model.AgentMembershipConfigModel + AgentMembershipRechargeOrderModel model.AgentMembershipRechargeOrderModel + AgentMembershipUserConfigModel model.AgentMembershipUserConfigModel + AgentProductConfigModel model.AgentProductConfigModel + AgentPlatformDeductionModel model.AgentPlatformDeductionModel + AgentActiveStatModel model.AgentActiveStatModel + AgentWithdrawalModel model.AgentWithdrawalModel + AgentRealNameModel model.AgentRealNameModel + AgentWithdrawalTaxModel model.AgentWithdrawalTaxModel + AgentWithdrawalTaxExemptionModel model.AgentWithdrawalTaxExemptionModel + + // 管理后台相关模型 + AdminApiModel model.AdminApiModel + AdminMenuModel model.AdminMenuModel + AdminRoleModel model.AdminRoleModel + AdminRoleApiModel model.AdminRoleApiModel + AdminRoleMenuModel model.AdminRoleMenuModel + AdminUserModel model.AdminUserModel + AdminUserRoleModel model.AdminUserRoleModel + AdminDictDataModel model.AdminDictDataModel + AdminDictTypeModel model.AdminDictTypeModel + AdminPromotionLinkModel model.AdminPromotionLinkModel + AdminPromotionLinkStatsTotalModel model.AdminPromotionLinkStatsTotalModel + AdminPromotionLinkStatsHistoryModel model.AdminPromotionLinkStatsHistoryModel + AdminPromotionOrderModel model.AdminPromotionOrderModel + + // 其他模型 + ExampleModel model.ExampleModel + GlobalNotificationsModel model.GlobalNotificationsModel + + // 服务 + AlipayService *service.AliPayService + WechatPayService *service.WechatPayService + ApplePayService *service.ApplePayService + ApiRequestService *service.ApiRequestService + AsynqServer *asynq.Server + AsynqService *service.AsynqService + VerificationService *service.VerificationService + AgentService *service.AgentService + UserService *service.UserService + DictService *service.DictService + AdminPromotionLinkStatsService *service.AdminPromotionLinkStatsService + ImageService *service.ImageService +} + +// NewServiceContext 创建服务上下文 +func NewServiceContext(c config.Config) *ServiceContext { + // ============================== 基础设施初始化 ============================== + db := sqlx.NewMysql(c.DataSource) + cacheConf := c.CacheRedis + + // 初始化Redis客户端 + redisConf := redis.RedisConf{ + Host: cacheConf[0].Host, + Pass: cacheConf[0].Pass, + Type: cacheConf[0].Type, + } + redisClient := redis.MustNewRedis(redisConf) + + // ============================== 用户相关模型 ============================== + userModel := model.NewUserModel(db, cacheConf) + userAuthModel := model.NewUserAuthModel(db, cacheConf) + userTempModel := model.NewUserTempModel(db, cacheConf) + + // ============================== 产品相关模型 ============================== + productModel := model.NewProductModel(db, cacheConf) + featureModel := model.NewFeatureModel(db, cacheConf) + productFeatureModel := model.NewProductFeatureModel(db, cacheConf) + + // ============================== 订单相关模型 ============================== + orderModel := model.NewOrderModel(db, cacheConf) + queryModel := model.NewQueryModel(db, cacheConf) + orderRefundModel := model.NewOrderRefundModel(db, cacheConf) + queryCleanupLogModel := model.NewQueryCleanupLogModel(db, cacheConf) + queryCleanupDetailModel := model.NewQueryCleanupDetailModel(db, cacheConf) + queryCleanupConfigModel := model.NewQueryCleanupConfigModel(db, cacheConf) + + // ============================== 代理相关模型 ============================== + agentModel := model.NewAgentModel(db, cacheConf) + agentAuditModel := model.NewAgentAuditModel(db, cacheConf) + agentClosureModel := model.NewAgentClosureModel(db, cacheConf) + agentCommissionModel := model.NewAgentCommissionModel(db, cacheConf) + agentCommissionDeductionModel := model.NewAgentCommissionDeductionModel(db, cacheConf) + agentWalletModel := model.NewAgentWalletModel(db, cacheConf) + agentLinkModel := model.NewAgentLinkModel(db, cacheConf) + agentOrderModel := model.NewAgentOrderModel(db, cacheConf) + agentRewardsModel := model.NewAgentRewardsModel(db, cacheConf) + agentMembershipConfigModel := model.NewAgentMembershipConfigModel(db, cacheConf) + agentMembershipRechargeOrderModel := model.NewAgentMembershipRechargeOrderModel(db, cacheConf) + agentMembershipUserConfigModel := model.NewAgentMembershipUserConfigModel(db, cacheConf) + agentProductConfigModel := model.NewAgentProductConfigModel(db, cacheConf) + agentPlatformDeductionModel := model.NewAgentPlatformDeductionModel(db, cacheConf) + agentActiveStatModel := model.NewAgentActiveStatModel(db, cacheConf) + agentWithdrawalModel := model.NewAgentWithdrawalModel(db, cacheConf) + agentRealNameModel := model.NewAgentRealNameModel(db, cacheConf) + agentWithdrawalTaxModel := model.NewAgentWithdrawalTaxModel(db, cacheConf) + agentWithdrawalTaxExemptionModel := model.NewAgentWithdrawalTaxExemptionModel(db, cacheConf) + // ============================== 管理后台相关模型 ============================== + adminApiModel := model.NewAdminApiModel(db, cacheConf) + adminMenuModel := model.NewAdminMenuModel(db, cacheConf) + adminRoleModel := model.NewAdminRoleModel(db, cacheConf) + adminRoleApiModel := model.NewAdminRoleApiModel(db, cacheConf) + adminRoleMenuModel := model.NewAdminRoleMenuModel(db, cacheConf) + adminUserModel := model.NewAdminUserModel(db, cacheConf) + adminUserRoleModel := model.NewAdminUserRoleModel(db, cacheConf) + adminDictDataModel := model.NewAdminDictDataModel(db, cacheConf) + adminDictTypeModel := model.NewAdminDictTypeModel(db, cacheConf) + adminPromotionLinkModel := model.NewAdminPromotionLinkModel(db, cacheConf) + adminPromotionLinkStatsTotalModel := model.NewAdminPromotionLinkStatsTotalModel(db, cacheConf) + adminPromotionLinkStatsHistoryModel := model.NewAdminPromotionLinkStatsHistoryModel(db, cacheConf) + adminPromotionOrderModel := model.NewAdminPromotionOrderModel(db, cacheConf) + + // ============================== 其他模型 ============================== + exampleModel := model.NewExampleModel(db, cacheConf) + globalNotificationsModel := model.NewGlobalNotificationsModel(db, cacheConf) + + // ============================== 第三方服务初始化 ============================== + tianyuanapi, err := tianyuanapi.NewClient(tianyuanapi.Config{ + AccessID: c.Tianyuanapi.AccessID, + Key: c.Tianyuanapi.Key, + BaseURL: c.Tianyuanapi.BaseURL, + Timeout: time.Duration(c.Tianyuanapi.Timeout) * time.Second, + }) + if err != nil { + logx.Errorf("初始化天远API失败: %+v", err) + } + + // ============================== 业务服务初始化 ============================== + alipayService := service.NewAliPayService(c) + wechatPayService := service.NewWechatPayService(c, userAuthModel, service.InitTypeWxPayPubKey) + applePayService := service.NewApplePayService(c) + apiRequestService := service.NewApiRequestService(c, featureModel, productFeatureModel, tianyuanapi) + verificationService := service.NewVerificationService(c, tianyuanapi, apiRequestService) + asynqService := service.NewAsynqService(c) + agentService := service.NewAgentService(c, agentModel, agentAuditModel, agentClosureModel, + agentCommissionModel, agentCommissionDeductionModel, agentWalletModel, agentLinkModel, + agentOrderModel, agentRewardsModel, agentMembershipConfigModel, agentMembershipRechargeOrderModel, + agentMembershipUserConfigModel, agentProductConfigModel, agentPlatformDeductionModel, + agentActiveStatModel, agentWithdrawalModel) + userService := service.NewUserService(&c, userModel, userAuthModel, userTempModel, agentModel) + dictService := service.NewDictService(adminDictTypeModel, adminDictDataModel) + adminPromotionLinkStatsService := service.NewAdminPromotionLinkStatsService(adminPromotionLinkModel, + adminPromotionLinkStatsTotalModel, adminPromotionLinkStatsHistoryModel) + imageService := service.NewImageService() + // ============================== 异步任务服务 ============================== + asynqServer := asynq.NewServer( + asynq.RedisClientOpt{Addr: c.CacheRedis[0].Host, Password: c.CacheRedis[0].Pass}, + asynq.Config{ + IsFailure: func(err error) bool { + logx.Errorf("异步任务失败: %+v \n", err) + return true + }, + Concurrency: 10, + }, + ) + + // ============================== 返回服务上下文 ============================== + return &ServiceContext{ + Config: c, + Redis: redisClient, + AuthInterceptor: middleware.NewAuthInterceptorMiddleware(c).Handle, + UserAuthInterceptor: middleware.NewUserAuthInterceptorMiddleware().Handle, + + // 用户相关模型 + UserModel: userModel, + UserAuthModel: userAuthModel, + UserTempModel: userTempModel, + + // 产品相关模型 + ProductModel: productModel, + FeatureModel: featureModel, + ProductFeatureModel: productFeatureModel, + + // 订单相关模型 + OrderModel: orderModel, + QueryModel: queryModel, + OrderRefundModel: orderRefundModel, + QueryCleanupLogModel: queryCleanupLogModel, + QueryCleanupDetailModel: queryCleanupDetailModel, + QueryCleanupConfigModel: queryCleanupConfigModel, + + // 代理相关模型 + AgentModel: agentModel, + AgentAuditModel: agentAuditModel, + AgentClosureModel: agentClosureModel, + AgentCommissionModel: agentCommissionModel, + AgentCommissionDeductionModel: agentCommissionDeductionModel, + AgentWalletModel: agentWalletModel, + AgentLinkModel: agentLinkModel, + AgentOrderModel: agentOrderModel, + AgentRewardsModel: agentRewardsModel, + AgentMembershipConfigModel: agentMembershipConfigModel, + AgentMembershipRechargeOrderModel: agentMembershipRechargeOrderModel, + AgentMembershipUserConfigModel: agentMembershipUserConfigModel, + AgentProductConfigModel: agentProductConfigModel, + AgentPlatformDeductionModel: agentPlatformDeductionModel, + AgentActiveStatModel: agentActiveStatModel, + AgentWithdrawalModel: agentWithdrawalModel, + AgentRealNameModel: agentRealNameModel, + AgentWithdrawalTaxModel: agentWithdrawalTaxModel, + AgentWithdrawalTaxExemptionModel: agentWithdrawalTaxExemptionModel, + + // 管理后台相关模型 + AdminApiModel: adminApiModel, + AdminMenuModel: adminMenuModel, + AdminRoleModel: adminRoleModel, + AdminRoleApiModel: adminRoleApiModel, + AdminRoleMenuModel: adminRoleMenuModel, + AdminUserModel: adminUserModel, + AdminUserRoleModel: adminUserRoleModel, + AdminDictDataModel: adminDictDataModel, + AdminDictTypeModel: adminDictTypeModel, + AdminPromotionLinkModel: adminPromotionLinkModel, + AdminPromotionLinkStatsTotalModel: adminPromotionLinkStatsTotalModel, + AdminPromotionLinkStatsHistoryModel: adminPromotionLinkStatsHistoryModel, + AdminPromotionOrderModel: adminPromotionOrderModel, + + // 其他模型 + ExampleModel: exampleModel, + GlobalNotificationsModel: globalNotificationsModel, + + // 服务 + AlipayService: alipayService, + WechatPayService: wechatPayService, + ApplePayService: applePayService, + ApiRequestService: apiRequestService, + AsynqServer: asynqServer, + AsynqService: asynqService, + VerificationService: verificationService, + AgentService: agentService, + UserService: userService, + DictService: dictService, + AdminPromotionLinkStatsService: adminPromotionLinkStatsService, + ImageService: imageService, + } +} + +func (s *ServiceContext) Close() { + if s.AsynqService != nil { + s.AsynqService.Close() + } +} diff --git a/app/main/api/internal/types/cache.go b/app/main/api/internal/types/cache.go new file mode 100644 index 0000000..ecd68cc --- /dev/null +++ b/app/main/api/internal/types/cache.go @@ -0,0 +1,20 @@ +package types + +const QueryCacheKey = "query:%d:%s" +const AgentVipCacheKey = "agentVip:%d:%s" + +type QueryCache struct { + Name string `json:"name"` + IDCard string `json:"id_card"` + Mobile string `json:"mobile"` + Product string `json:"product_id"` +} +type QueryCacheLoad struct { + Product string `json:"product_en"` + Params string `json:"params"` + AgentIdentifier string `json:"agent_dentifier"` +} + +type AgentVipCache struct { + Type string `json:"type"` +} diff --git a/app/main/api/internal/types/encrypPayload.go b/app/main/api/internal/types/encrypPayload.go new file mode 100644 index 0000000..3c6e385 --- /dev/null +++ b/app/main/api/internal/types/encrypPayload.go @@ -0,0 +1,6 @@ +package types + +type QueryShareLinkPayload struct { + OrderId int64 `json:"order_id"` + ExpireAt int64 `json:"expire_at"` +} diff --git a/app/main/api/internal/types/payload.go b/app/main/api/internal/types/payload.go new file mode 100644 index 0000000..672918f --- /dev/null +++ b/app/main/api/internal/types/payload.go @@ -0,0 +1,5 @@ +package types + +type MsgPaySuccessQueryPayload struct { + OrderID int64 `json:"order_id"` +} diff --git a/app/main/api/internal/types/query.go b/app/main/api/internal/types/query.go new file mode 100644 index 0000000..5687f34 --- /dev/null +++ b/app/main/api/internal/types/query.go @@ -0,0 +1,113 @@ +package types + +type MarriageReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} +type HomeServiceReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} + +// RiskAssessment 查询请求结构 +type RiskAssessmentReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} + +// CompanyInfo 查询请求结构 +type CompanyInfoReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} + +// RentalInfo 查询请求结构 +type RentalInfoReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} + +// PreLoanBackgroundCheck 查询请求结构 +type PreLoanBackgroundCheckReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} + +// BackgroundCheck 查询请求结构 +type BackgroundCheckReq struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} +type EntLawsuitReq struct { + EntName string `json:"ent_name" validate:"required,name"` + EntCode string `json:"ent_code" validate:"required,USCI"` + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} +type TocPhoneThreeElements struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` +} +type TocPhoneTwoElements struct { + Name string `json:"name" validate:"required,name"` + Mobile string `json:"mobile" validate:"required,mobile"` +} +type TocIDCardTwoElements struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` +} +type TocDualMarriage struct { + NameMan string `json:"name_man" validate:"required,name"` + IDCardMan string `json:"id_card_man" validate:"required,idCard"` + NameWoman string `json:"name_woman" validate:"required,name"` + IDCardWoman string `json:"id_card_woman" validate:"required,idCard"` +} +type TocPersonVehicleVerification struct { + Name string `json:"name" validate:"required,name"` + CarType string `json:"car_type" validate:"required"` + CarLicense string `json:"car_license" validate:"required"` +} + +// 银行卡黑名单 +type TocBankCardBlacklist struct { + Name string `json:"name" validate:"required,name"` + IDCard string `json:"id_card" validate:"required,idCard"` + Mobile string `json:"mobile" validate:"required,mobile"` + BankCard string `json:"bank_card" validate:"required"` +} + +// 手机号码风险 +type TocPhoneNumberRisk struct { + Mobile string `json:"mobile" validate:"required,mobile"` +} + +// 手机二次卡 +type TocPhoneSecondaryCard struct { + Mobile string `json:"mobile" validate:"required,mobile"` + StartDate string `json:"start_date" validate:"required"` +} + +type AgentQueryData struct { + Mobile string `json:"mobile"` + Code string `json:"code"` +} +type AgentIdentifier struct { + Product string `json:"product"` + AgentID int64 `json:"agent_id"` + Price string `json:"price"` +} diff --git a/app/main/api/internal/types/queryMap.go b/app/main/api/internal/types/queryMap.go new file mode 100644 index 0000000..13256a6 --- /dev/null +++ b/app/main/api/internal/types/queryMap.go @@ -0,0 +1,47 @@ +package types + +// 特殊名单 G26BJ05 +var G26BJ05FieldMapping = map[string]string{ + "IDCard": "id", + "Name": "name", + "Mobile": "cell", + "TimeRange": "time_range", +} + +// 个人不良 +var G34BJ03FieldMapping = map[string]string{ + "IDCard": "id_card", + "Name": "name", +} + +// 个人涉诉 G35SC01 +var G35SC01FieldMapping = map[string]string{ + "Name": "name", + "IDCard": "idcard", + "InquiredAuth": "inquired_auth", +} + +// 单人婚姻 G09SC02 +var G09SC02FieldMapping = map[string]string{ + "IDCard": "certNumMan", + "Name": "nameMan", +} + +// 借贷意向 G27BJ05 +var G27BJ05FieldMapping = map[string]string{ + "IDCard": "id", + "Name": "name", + "Mobile": "cell", +} + +// 借贷行为 G28BJ05 +var G28BJ05FieldMapping = map[string]string{ + "IDCard": "id", + "Name": "name", + "Mobile": "cell", +} + +// 股东人企关系精准版 G05HZ01 +var G05HZ01FieldMapping = map[string]string{ + "IDCard": "pid", +} diff --git a/app/main/api/internal/types/queryParams.go b/app/main/api/internal/types/queryParams.go new file mode 100644 index 0000000..1c243d5 --- /dev/null +++ b/app/main/api/internal/types/queryParams.go @@ -0,0 +1,73 @@ +package types + +type WestDexServiceRequestParams struct { + FieldMapping map[string]string + ApiID string +} + +var WestDexParams = map[string][]WestDexServiceRequestParams{ + "marriage": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + + }, + "backgroundcheck": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + }, + "companyinfo": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + }, + "homeservice": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + }, + "preloanbackgroundcheck": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + }, + "rentalinfo": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + }, + "riskassessment": { + {FieldMapping: G09SC02FieldMapping, ApiID: "G09SC02"}, // 单人婚姻 + {FieldMapping: G27BJ05FieldMapping, ApiID: "G27BJ05"}, // 借贷意向 + {FieldMapping: G28BJ05FieldMapping, ApiID: "G28BJ05"}, // 借贷行为 + {FieldMapping: G26BJ05FieldMapping, ApiID: "G26BJ05"}, // 特殊名单 + {FieldMapping: G05HZ01FieldMapping, ApiID: "G05HZ01"}, // 股东人企关系 + {FieldMapping: G34BJ03FieldMapping, ApiID: "G34BJ03"}, // 个人不良 + {FieldMapping: G35SC01FieldMapping, ApiID: "G35SC01"}, // 个人涉诉 + }, +} diff --git a/app/main/api/internal/types/taskname.go b/app/main/api/internal/types/taskname.go new file mode 100644 index 0000000..33329ee --- /dev/null +++ b/app/main/api/internal/types/taskname.go @@ -0,0 +1,4 @@ +package types + +const MsgPaySuccessQuery = "msg:pay_success:query" +const MsgCleanQueryData = "msg:clean_query_data" diff --git a/app/main/api/internal/types/types.go b/app/main/api/internal/types/types.go new file mode 100644 index 0000000..dd57ff0 --- /dev/null +++ b/app/main/api/internal/types/types.go @@ -0,0 +1,1746 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +type ActiveReward struct { + TotalReward float64 `json:"total_reward"` + Today ActiveRewardData `json:"today"` // 今日数据 + Last7D ActiveRewardData `json:"last7d"` // 近7天数据 + Last30D ActiveRewardData `json:"last30d"` // 近30天数据 +} + +type ActiveRewardData struct { + NewActiveReward float64 `json:"active_reward"` + SubPromoteReward float64 `json:"sub_promote_reward"` + SubUpgradeReward float64 `json:"sub_upgrade_reward"` + SubWithdrawReward float64 `json:"sub_withdraw_reward"` +} + +type AdminCreateFeatureReq struct { + ApiId string `json:"api_id"` // API标识 + Name string `json:"name"` // 描述 +} + +type AdminCreateFeatureResp struct { + Id int64 `json:"id"` // 功能ID +} + +type AdminCreateNotificationReq struct { + Title string `json:"title"` // 通知标题 + NotificationPage string `json:"notification_page"` // 通知页面 + Content string `json:"content"` // 通知内容 + StartDate string `json:"start_date"` // 生效开始日期(yyyy-MM-dd) + StartTime string `json:"start_time"` // 生效开始时间(HH:mm:ss) + EndDate string `json:"end_date"` // 生效结束日期(yyyy-MM-dd) + EndTime string `json:"end_time"` // 生效结束时间(HH:mm:ss) + Status int64 `json:"status"` // 状态:1-启用,0-禁用 +} + +type AdminCreateNotificationResp struct { + Id int64 `json:"id"` // 通知ID +} + +type AdminCreateOrderReq struct { + OrderNo string `json:"order_no"` // 商户订单号 + PlatformOrderId string `json:"platform_order_id"` // 支付订单号 + ProductName string `json:"product_name"` // 产品名称 + PaymentPlatform string `json:"payment_platform"` // 支付方式 + PaymentScene string `json:"payment_scene"` // 支付平台 + Amount float64 `json:"amount"` // 金额 + Status string `json:"status,default=pending"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 + IsPromotion int64 `json:"is_promotion,default=0"` // 是否推广订单:0-否,1-是 +} + +type AdminCreateOrderResp struct { + Id int64 `json:"id"` // 订单ID +} + +type AdminCreatePlatformUserReq struct { + Mobile string `json:"mobile"` // 手机号 + Password string `json:"password"` // 密码 + Nickname string `json:"nickname"` // 昵称 + Info string `json:"info"` // 备注信息 + Inside int64 `json:"inside"` // 是否内部用户 1-是 0-否 +} + +type AdminCreatePlatformUserResp struct { + Id int64 `json:"id"` // 用户ID +} + +type AdminCreateProductReq struct { + ProductName string `json:"product_name"` // 服务名 + ProductEn string `json:"product_en"` // 英文名 + Description string `json:"description"` // 描述 + Notes string `json:"notes,optional"` // 备注 + CostPrice float64 `json:"cost_price"` // 成本 + SellPrice float64 `json:"sell_price"` // 售价 +} + +type AdminCreateProductResp struct { + Id int64 `json:"id"` // 产品ID +} + +type AdminCreateUserReq struct { + Username string `json:"username"` // 用户名 + RealName string `json:"real_name"` // 真实姓名 + Status int64 `json:"status,default=1"` // 状态:0-禁用,1-启用 + RoleIds []int64 `json:"role_ids"` // 关联的角色ID列表 +} + +type AdminCreateUserResp struct { + Id int64 `json:"id"` // 用户ID +} + +type AdminDeleteFeatureReq struct { + Id int64 `path:"id"` // 功能ID +} + +type AdminDeleteFeatureResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminDeleteNotificationReq struct { + Id int64 `path:"id"` // 通知ID +} + +type AdminDeleteNotificationResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminDeleteOrderReq struct { + Id int64 `path:"id"` // 订单ID +} + +type AdminDeleteOrderResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminDeletePlatformUserReq struct { + Id int64 `path:"id"` // 用户ID +} + +type AdminDeletePlatformUserResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminDeleteProductReq struct { + Id int64 `path:"id"` // 产品ID +} + +type AdminDeleteProductResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminDeleteUserReq struct { + Id int64 `path:"id"` // 用户ID +} + +type AdminDeleteUserResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminGetAgentCommissionDeductionListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + ProductName *string `form:"product_name,optional"` // 产品名(可选) + Type *string `form:"type,optional"` // 类型(cost/pricing,可选) + Status *int64 `form:"status,optional"` // 状态(可选) +} + +type AdminGetAgentCommissionDeductionListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentCommissionDeductionListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentCommissionListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + ProductName *string `form:"product_name,optional"` // 产品名(可选) + Status *int64 `form:"status,optional"` // 状态(可选) +} + +type AdminGetAgentCommissionListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentCommissionListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentLinkListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + ProductName *string `form:"product_name,optional"` // 产品名(可选) + LinkIdentifier *string `form:"link_identifier,optional"` // 推广码(可选) +} + +type AdminGetAgentLinkListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentLinkListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + Mobile *string `form:"mobile,optional"` // 手机号(可选) + Region *string `form:"region,optional"` // 区域(可选) + ParentAgentId *int64 `form:"parent_agent_id,optional"` // 上级代理ID(可选) +} + +type AdminGetAgentListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentMembershipConfigListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + LevelName *string `form:"level_name,optional"` // 会员级别名称(可选) +} + +type AdminGetAgentMembershipConfigListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentMembershipConfigListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentMembershipRechargeOrderListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + UserId *int64 `form:"user_id,optional"` // 用户ID(可选) + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + OrderNo *string `form:"order_no,optional"` // 订单号(可选) + PlatformOrderId *string `form:"platform_order_id,optional"` // 平台订单号(可选) + Status *string `form:"status,optional"` // 状态(可选) + PaymentMethod *string `form:"payment_method,optional"` // 支付方式(可选) +} + +type AdminGetAgentMembershipRechargeOrderListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentMembershipRechargeOrderListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentPlatformDeductionListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + Type *string `form:"type,optional"` // 类型(cost/pricing,可选) + Status *int64 `form:"status,optional"` // 状态(可选) +} + +type AdminGetAgentPlatformDeductionListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentPlatformDeductionListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentProductionConfigListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + ProductName *string `form:"product_name,optional"` // 产品名(可选) + Id *int64 `form:"id,optional"` // 配置ID(可选) +} + +type AdminGetAgentProductionConfigListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentProductionConfigItem `json:"items"` // 列表数据 +} + +type AdminGetAgentRewardListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + RelationAgentId *int64 `form:"relation_agent_id,optional"` // 关联代理ID(可选) + Type *string `form:"type,optional"` // 奖励类型(可选) +} + +type AdminGetAgentRewardListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentRewardListItem `json:"items"` // 列表数据 +} + +type AdminGetAgentWithdrawalListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + AgentId *int64 `form:"agent_id,optional"` // 代理ID(可选) + Status *int64 `form:"status,optional"` // 状态(可选) + WithdrawNo *string `form:"withdraw_no,optional"` // 提现单号(可选) +} + +type AdminGetAgentWithdrawalListResp struct { + Total int64 `json:"total"` // 总数 + Items []AgentWithdrawalListItem `json:"items"` // 列表数据 +} + +type AdminGetFeatureDetailReq struct { + Id int64 `path:"id"` // 功能ID +} + +type AdminGetFeatureDetailResp struct { + Id int64 `json:"id"` // 功能ID + ApiId string `json:"api_id"` // API标识 + Name string `json:"name"` // 描述 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type AdminGetFeatureListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + ApiId *string `form:"api_id,optional"` // API标识 + Name *string `form:"name,optional"` // 描述 +} + +type AdminGetFeatureListResp struct { + Total int64 `json:"total"` // 总数 + Items []FeatureListItem `json:"items"` // 列表数据 +} + +type AdminGetNotificationDetailReq struct { + Id int64 `path:"id"` // 通知ID +} + +type AdminGetNotificationDetailResp struct { + Id int64 `json:"id"` // 通知ID + Title string `json:"title"` // 通知标题 + Content string `json:"content"` // 通知内容 + NotificationPage string `json:"notification_page"` // 通知页面 + StartDate string `json:"start_date"` // 生效开始日期 + StartTime string `json:"start_time"` // 生效开始时间 + EndDate string `json:"end_date"` // 生效结束日期 + EndTime string `json:"end_time"` // 生效结束时间 + Status int64 `json:"status"` // 状态 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type AdminGetNotificationListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + Title *string `form:"title,optional"` // 通知标题(可选) + NotificationPage *string `form:"notification_page,optional"` // 通知页面(可选) + Status *int64 `form:"status,optional"` // 状态(可选) + StartDate *string `form:"start_date,optional"` // 开始日期范围(可选) + EndDate *string `form:"end_date,optional"` // 结束日期范围(可选) +} + +type AdminGetNotificationListResp struct { + Total int64 `json:"total"` // 总数 + Items []NotificationListItem `json:"items"` // 列表数据 +} + +type AdminGetOrderDetailReq struct { + Id int64 `path:"id"` // 订单ID +} + +type AdminGetOrderDetailResp struct { + Id int64 `json:"id"` // 订单ID + OrderNo string `json:"order_no"` // 商户订单号 + PlatformOrderId string `json:"platform_order_id"` // 支付订单号 + ProductName string `json:"product_name"` // 产品名称 + PaymentPlatform string `json:"payment_platform"` // 支付方式 + PaymentScene string `json:"payment_scene"` // 支付平台 + Amount float64 `json:"amount"` // 金额 + Status string `json:"status"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 + QueryState string `json:"query_state"` // 查询状态:pending-待查询,success-查询成功,failed-查询失败 processing-查询中 + CreateTime string `json:"create_time"` // 创建时间 + PayTime string `json:"pay_time"` // 支付时间 + RefundTime string `json:"refund_time"` // 退款时间 + IsPromotion int64 `json:"is_promotion"` // 是否推广订单:0-否,1-是 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type AdminGetOrderListReq struct { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + OrderNo string `form:"order_no,optional"` // 商户订单号 + PlatformOrderId string `form:"platform_order_id,optional"` // 支付订单号 + ProductName string `form:"product_name,optional"` // 产品名称 + PaymentPlatform string `form:"payment_platform,optional"` // 支付方式 + PaymentScene string `form:"payment_scene,optional"` // 支付平台 + Amount float64 `form:"amount,optional"` // 金额 + Status string `form:"status,optional"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 + IsPromotion int64 `form:"is_promotion,optional,default=-1"` // 是否推广订单:0-否,1-是 + CreateTimeStart string `form:"create_time_start,optional"` // 创建时间开始 + CreateTimeEnd string `form:"create_time_end,optional"` // 创建时间结束 + PayTimeStart string `form:"pay_time_start,optional"` // 支付时间开始 + PayTimeEnd string `form:"pay_time_end,optional"` // 支付时间结束 + RefundTimeStart string `form:"refund_time_start,optional"` // 退款时间开始 + RefundTimeEnd string `form:"refund_time_end,optional"` // 退款时间结束 +} + +type AdminGetOrderListResp struct { + Total int64 `json:"total"` // 总数 + Items []OrderListItem `json:"items"` // 列表 +} + +type AdminGetPlatformUserDetailReq struct { + Id int64 `path:"id"` // 用户ID +} + +type AdminGetPlatformUserDetailResp struct { + Id int64 `json:"id"` // 用户ID + Mobile string `json:"mobile"` // 手机号 + Nickname string `json:"nickname"` // 昵称 + Info string `json:"info"` // 备注信息 + Inside int64 `json:"inside"` // 是否内部用户 1-是 0-否 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type AdminGetPlatformUserListReq struct { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + Mobile string `form:"mobile,optional"` // 手机号 + Nickname string `form:"nickname,optional"` // 昵称 + Inside int64 `form:"inside,optional"` // 是否内部用户 1-是 0-否 + CreateTimeStart string `form:"create_time_start,optional"` // 创建时间开始 + CreateTimeEnd string `form:"create_time_end,optional"` // 创建时间结束 + OrderBy string `form:"order_by,optional"` // 排序字段 + OrderType string `form:"order_type,optional"` // 排序类型 +} + +type AdminGetPlatformUserListResp struct { + Total int64 `json:"total"` // 总数 + Items []PlatformUserListItem `json:"items"` // 列表 +} + +type AdminGetProductDetailReq struct { + Id int64 `path:"id"` // 产品ID +} + +type AdminGetProductDetailResp struct { + Id int64 `json:"id"` // 产品ID + ProductName string `json:"product_name"` // 服务名 + ProductEn string `json:"product_en"` // 英文名 + Description string `json:"description"` // 描述 + Notes string `json:"notes"` // 备注 + CostPrice float64 `json:"cost_price"` // 成本 + SellPrice float64 `json:"sell_price"` // 售价 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type AdminGetProductFeatureListReq struct { + ProductId int64 `path:"product_id"` // 产品ID +} + +type AdminGetProductFeatureListResp struct { + Id int64 `json:"id"` // 关联ID + ProductId int64 `json:"product_id"` // 产品ID + FeatureId int64 `json:"feature_id"` // 功能ID + ApiId string `json:"api_id"` // API标识 + Name string `json:"name"` // 功能描述 + Sort int64 `json:"sort"` // 排序 + Enable int64 `json:"enable"` // 是否启用 + IsImportant int64 `json:"is_important"` // 是否重要 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type AdminGetProductListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"pageSize"` // 每页数量 + ProductName *string `form:"product_name,optional"` // 服务名 + ProductEn *string `form:"product_en,optional"` // 英文名 +} + +type AdminGetProductListResp struct { + Total int64 `json:"total"` // 总数 + Items []ProductListItem `json:"items"` // 列表数据 +} + +type AdminGetQueryCleanupConfigListReq struct { + Status int64 `form:"status,optional"` // 状态:1-启用,0-禁用 +} + +type AdminGetQueryCleanupConfigListResp struct { + Items []QueryCleanupConfigItem `json:"items"` // 配置列表 +} + +type AdminGetQueryCleanupDetailListReq struct { + LogId int64 `path:"log_id"` // 清理日志ID + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"page_size,default=20"` // 每页数量 +} + +type AdminGetQueryCleanupDetailListResp struct { + Total int64 `json:"total"` // 总数 + Items []QueryCleanupDetailItem `json:"items"` // 列表 +} + +type AdminGetQueryCleanupLogListReq struct { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"page_size,default=20"` // 每页数量 + Status int64 `form:"status,optional"` // 状态:1-成功,2-失败 + StartTime string `form:"start_time,optional"` // 开始时间 + EndTime string `form:"end_time,optional"` // 结束时间 +} + +type AdminGetQueryCleanupLogListResp struct { + Total int64 `json:"total"` // 总数 + Items []QueryCleanupLogItem `json:"items"` // 列表 +} + +type AdminGetQueryDetailByOrderIdReq struct { + OrderId int64 `path:"order_id"` +} + +type AdminGetQueryDetailByOrderIdResp struct { + Id int64 `json:"id"` // 主键ID + OrderId int64 `json:"order_id"` // 订单ID + UserId int64 `json:"user_id"` // 用户ID + ProductName string `json:"product_name"` // 产品ID + QueryParams map[string]interface{} `json:"query_params"` + QueryData []AdminQueryItem `json:"query_data"` + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + QueryState string `json:"query_state"` // 查询状态 +} + +type AdminGetUserDetailReq struct { + Id int64 `path:"id"` // 用户ID +} + +type AdminGetUserDetailResp struct { + Id int64 `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + RealName string `json:"real_name"` // 真实姓名 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + RoleIds []int64 `json:"role_ids"` // 关联的角色ID列表 +} + +type AdminGetUserListReq struct { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + Username string `form:"username,optional"` // 用户名 + RealName string `form:"real_name,optional"` // 真实姓名 + Status int64 `form:"status,optional,default=-1"` // 状态:0-禁用,1-启用 +} + +type AdminGetUserListResp struct { + Total int64 `json:"total"` // 总数 + Items []AdminUserListItem `json:"items"` // 列表 +} + +type AdminLoginReq struct { + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + Captcha bool `json:"captcha" validate:"required"` +} + +type AdminLoginResp struct { + AccessToken string `json:"access_token"` + AccessExpire int64 `json:"access_expire"` + RefreshAfter int64 `json:"refresh_after"` + Roles []string `json:"roles"` +} + +type AdminQueryItem struct { + Feature interface{} `json:"feature"` + Data interface{} `json:"data"` // 这里可以是 map 或 具体的 struct +} + +type AdminRefundOrderReq struct { + Id int64 `path:"id"` // 订单ID + RefundAmount float64 `json:"refund_amount"` // 退款金额 + RefundReason string `json:"refund_reason"` // 退款原因 +} + +type AdminRefundOrderResp struct { + Status string `json:"status"` // 退款状态 + RefundNo string `json:"refund_no"` // 退款单号 + Amount float64 `json:"amount"` // 退款金额 +} + +type AdminUpdateAgentMembershipConfigReq struct { + Id int64 `json:"id"` // 主键 + LevelName string `json:"level_name"` // 会员级别名称 + Price float64 `json:"price"` // 会员年费 + ReportCommission float64 `json:"report_commission"` // 直推报告收益 + LowerActivityReward *float64 `json:"lower_activity_reward,optional,omitempty"` // 下级活跃奖励金额 + NewActivityReward *float64 `json:"new_activity_reward,optional,omitempty"` // 新增活跃奖励金额 + LowerStandardCount *int64 `json:"lower_standard_count,optional,omitempty"` // 活跃下级达标个数 + NewLowerStandardCount *int64 `json:"new_lower_standard_count,optional,omitempty"` // 新增活跃下级达标个数 + LowerWithdrawRewardRatio *float64 `json:"lower_withdraw_reward_ratio,optional,omitempty"` // 下级提现奖励比例 + LowerConvertVipReward *float64 `json:"lower_convert_vip_reward,optional,omitempty"` // 下级转化VIP奖励 + LowerConvertSvipReward *float64 `json:"lower_convert_svip_reward,optional,omitempty"` // 下级转化SVIP奖励 + ExemptionAmount *float64 `json:"exemption_amount,optional,omitempty"` // 免责金额 + PriceIncreaseMax *float64 `json:"price_increase_max,optional,omitempty"` // 提价最高金额 + PriceRatio *float64 `json:"price_ratio,optional,omitempty"` // 提价区间收取比例 + PriceIncreaseAmount *float64 `json:"price_increase_amount,optional,omitempty"` // 在原本成本上加价的金额 +} + +type AdminUpdateAgentMembershipConfigResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminUpdateAgentProductionConfigReq struct { + Id int64 `json:"id"` // 主键 + CostPrice float64 `json:"cost_price"` // 成本 + PriceRangeMin float64 `json:"price_range_min"` // 最低定价 + PriceRangeMax float64 `json:"price_range_max"` // 最高定价 + PricingStandard float64 `json:"pricing_standard"` // 定价标准 + OverpricingRatio float64 `json:"overpricing_ratio"` // 超价比例 +} + +type AdminUpdateAgentProductionConfigResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminUpdateFeatureReq struct { + Id int64 `path:"id"` // 功能ID + ApiId *string `json:"api_id,optional"` // API标识 + Name *string `json:"name,optional"` // 描述 +} + +type AdminUpdateFeatureResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminUpdateNotificationReq struct { + Id int64 `path:"id"` // 通知ID + Title *string `json:"title,optional"` // 通知标题 + Content *string `json:"content,optional"` // 通知内容 + NotificationPage *string `json:"notification_page,optional"` // 通知页面 + StartDate *string `json:"start_date,optional"` // 生效开始日期 + StartTime *string `json:"start_time,optional"` // 生效开始时间 + EndDate *string `json:"end_date,optional"` // 生效结束日期 + EndTime *string `json:"end_time,optional"` // 生效结束时间 + Status *int64 `json:"status,optional"` // 状态 +} + +type AdminUpdateNotificationResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminUpdateOrderReq struct { + Id int64 `path:"id"` // 订单ID + OrderNo *string `json:"order_no,optional"` // 商户订单号 + PlatformOrderId *string `json:"platform_order_id,optional"` // 支付订单号 + ProductName *string `json:"product_name,optional"` // 产品名称 + PaymentPlatform *string `json:"payment_platform,optional"` // 支付方式 + PaymentScene *string `json:"payment_scene,optional"` // 支付平台 + Amount *float64 `json:"amount,optional"` // 金额 + Status *string `json:"status,optional"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 + PayTime *string `json:"pay_time,optional"` // 支付时间 + RefundTime *string `json:"refund_time,optional"` // 退款时间 + IsPromotion *int64 `json:"is_promotion,optional"` // 是否推广订单:0-否,1-是 +} + +type AdminUpdateOrderResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminUpdatePlatformUserReq struct { + Id int64 `path:"id"` // 用户ID + Mobile *string `json:"mobile,optional"` // 手机号 + Password *string `json:"password,optional"` // 密码 + Nickname *string `json:"nickname,optional"` // 昵称 + Info *string `json:"info,optional"` // 备注信息 + Inside *int64 `json:"inside,optional"` // 是否内部用户 1-是 0-否 +} + +type AdminUpdatePlatformUserResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminUpdateProductFeaturesReq struct { + ProductId int64 `path:"product_id"` // 产品ID + Features []ProductFeatureItem `json:"features"` // 功能列表 +} + +type AdminUpdateProductFeaturesResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminUpdateProductReq struct { + Id int64 `path:"id"` // 产品ID + ProductName *string `json:"product_name,optional"` // 服务名 + ProductEn *string `json:"product_en,optional"` // 英文名 + Description *string `json:"description,optional"` // 描述 + Notes *string `json:"notes,optional"` // 备注 + CostPrice *float64 `json:"cost_price,optional"` // 成本 + SellPrice *float64 `json:"sell_price,optional"` // 售价 +} + +type AdminUpdateProductResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminUpdateQueryCleanupConfigReq struct { + Id int64 `json:"id"` // 主键ID + ConfigValue string `json:"config_value"` // 配置值 + Status int64 `json:"status"` // 状态:1-启用,0-禁用 +} + +type AdminUpdateQueryCleanupConfigResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminUpdateUserReq struct { + Id int64 `path:"id"` // 用户ID + Username *string `json:"username,optional"` // 用户名 + RealName *string `json:"real_name,optional"` // 真实姓名 + Status *int64 `json:"status,optional"` // 状态:0-禁用,1-启用 + RoleIds []int64 `json:"role_ids,optional"` // 关联的角色ID列表 +} + +type AdminUpdateUserResp struct { + Success bool `json:"success"` // 是否成功 +} + +type AdminUserInfoReq struct { +} + +type AdminUserInfoResp struct { + Username string `json:"username"` // 用户名 + RealName string `json:"real_name"` // 真实姓名 + Roles []string `json:"roles"` // 角色编码列表 +} + +type AdminUserListItem struct { + Id int64 `json:"id"` // 用户ID + Username string `json:"username"` // 用户名 + RealName string `json:"real_name"` // 真实姓名 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + CreateTime string `json:"create_time"` // 创建时间 + RoleIds []int64 `json:"role_ids"` // 关联的角色ID列表 +} + +type AgentActivateMembershipReq struct { + Type string `json:"type,oneof=VIP SVIP"` // 会员类型:vip/svip +} + +type AgentActivateMembershipResp struct { + Id string `json:"id"` +} + +type AgentApplyReq struct { + Region string `json:"region"` + Mobile string `json:"mobile"` + Code string `json:"code"` + Ancestor string `json:"ancestor,optional"` +} + +type AgentApplyResp struct { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} + +type AgentAuditStatusResp struct { + Status int64 `json:"status"` // 0=待审核,1=审核通过,2=审核未通过 + AuditReason string `json:"audit_reason"` +} + +type AgentCommissionDeductionListItem struct { + Id int64 `json:"id"` // 主键 + AgentId int64 `json:"agent_id"` // 代理ID + DeductedAgentId int64 `json:"deducted_agent_id"` // 被扣代理ID + Amount float64 `json:"amount"` // 金额 + ProductName string `json:"product_name"` // 产品名 + Type string `json:"type"` // 类型(cost/pricing) + Status int64 `json:"status"` // 状态 + CreateTime string `json:"create_time"` // 创建时间 +} + +type AgentCommissionListItem struct { + Id int64 `json:"id"` // 主键 + AgentId int64 `json:"agent_id"` // 代理ID + OrderId int64 `json:"order_id"` // 订单ID + Amount float64 `json:"amount"` // 金额 + ProductName string `json:"product_name"` // 产品名 + Status int64 `json:"status"` // 状态 + CreateTime string `json:"create_time"` // 创建时间 +} + +type AgentGeneratingLinkReq struct { + Product string `json:"product"` + Price string `json:"price"` +} + +type AgentGeneratingLinkResp struct { + LinkIdentifier string `json:"link_identifier"` +} + +type AgentInfoResp struct { + Status int64 `json:"status"` // 0=待审核,1=审核通过,2=审核未通过,3=未申请 + IsAgent bool `json:"is_agent"` + AgentID int64 `json:"agent_id"` + Level string `json:"level"` + Region string `json:"region"` + Mobile string `json:"mobile"` + ExpiryTime string `json:"expiry_time"` + IsRealName bool `json:"is_real_name"` +} + +type AgentLinkListItem struct { + AgentId int64 `json:"agent_id"` // 代理ID + ProductName string `json:"product_name"` // 产品名 + Price float64 `json:"price"` // 价格 + LinkIdentifier string `json:"link_identifier"` // 推广码 + CreateTime string `json:"create_time"` // 创建时间 +} + +type AgentListItem struct { + Id int64 `json:"id"` // 主键 + UserId int64 `json:"user_id"` // 用户ID + ParentAgentId int64 `json:"parent_agent_id"` // 上级代理ID + LevelName string `json:"level_name"` // 等级名称 + Region string `json:"region"` // 区域 + Mobile string `json:"mobile"` // 手机号 + MembershipExpiryTime string `json:"membership_expiry_time"` // 会员到期时间 + Balance float64 `json:"balance"` // 钱包余额 + TotalEarnings float64 `json:"total_earnings"` // 累计收益 + FrozenBalance float64 `json:"frozen_balance"` // 冻结余额 + WithdrawnAmount float64 `json:"withdrawn_amount"` // 提现总额 + CreateTime string `json:"create_time"` // 创建时间 + IsRealNameVerified bool `json:"is_real_name_verified"` // 是否已实名认证 + RealName string `json:"real_name"` // 实名姓名 + IdCard string `json:"id_card"` // 身份证号 + RealNameStatus string `json:"real_name_status"` // 实名状态(pending/approved/rejected) +} + +type AgentMembershipConfigListItem struct { + Id int64 `json:"id"` // 主键 + LevelName string `json:"level_name"` // 会员级别名称 + Price *float64 `json:"price"` // 会员年费 + ReportCommission *float64 `json:"report_commission"` // 直推报告收益 + LowerActivityReward *float64 `json:"lower_activity_reward"` // 下级活跃奖励金额 + NewActivityReward *float64 `json:"new_activity_reward"` // 新增活跃奖励金额 + LowerStandardCount *int64 `json:"lower_standard_count"` // 活跃下级达标个数 + NewLowerStandardCount *int64 `json:"new_lower_standard_count"` // 新增活跃下级达标个数 + LowerWithdrawRewardRatio *float64 `json:"lower_withdraw_reward_ratio"` // 下级提现奖励比例 + LowerConvertVipReward *float64 `json:"lower_convert_vip_reward"` // 下级转化VIP奖励 + LowerConvertSvipReward *float64 `json:"lower_convert_svip_reward"` // 下级转化SVIP奖励 + ExemptionAmount *float64 `json:"exemption_amount"` // 免责金额 + PriceIncreaseMax *float64 `json:"price_increase_max"` // 提价最高金额 + PriceRatio *float64 `json:"price_ratio"` // 提价区间收取比例 + PriceIncreaseAmount *float64 `json:"price_increase_amount"` // 在原本成本上加价的金额 + CreateTime string `json:"create_time"` // 创建时间 +} + +type AgentMembershipProductConfigReq struct { + ProductID int64 `form:"product_id"` +} + +type AgentMembershipProductConfigResp struct { + AgentMembershipUserConfig AgentMembershipUserConfig `json:"agent_membership_user_config"` + ProductConfig ProductConfig `json:"product_config"` + PriceIncreaseMax float64 `json:"price_increase_max"` + PriceIncreaseAmount float64 `json:"price_increase_amount"` + PriceRatio float64 `json:"price_ratio"` +} + +type AgentMembershipRechargeOrderListItem struct { + Id int64 `json:"id"` // 主键 + UserId int64 `json:"user_id"` // 用户ID + AgentId int64 `json:"agent_id"` // 代理ID + LevelName string `json:"level_name"` // 等级名称 + Amount float64 `json:"amount"` // 金额 + PaymentMethod string `json:"payment_method"` // 支付方式 + OrderNo string `json:"order_no"` // 订单号 + PlatformOrderId string `json:"platform_order_id"` // 平台订单号 + Status string `json:"status"` // 状态 + CreateTime string `json:"create_time"` // 创建时间 +} + +type AgentMembershipUserConfig struct { + ProductID int64 `json:"product_id"` + PriceIncreaseAmount float64 `json:"price_increase_amount"` + PriceRangeFrom float64 `json:"price_range_from"` + PriceRangeTo float64 `json:"price_range_to"` + PriceRatio float64 `json:"price_ratio"` +} + +type AgentPlatformDeductionListItem struct { + Id int64 `json:"id"` // 主键 + AgentId int64 `json:"agent_id"` // 代理ID + Amount float64 `json:"amount"` // 金额 + Type string `json:"type"` // 类型(cost/pricing) + Status int64 `json:"status"` // 状态 + CreateTime string `json:"create_time"` // 创建时间 +} + +type AgentProductConfig struct { + ProductID int64 `json:"product_id"` + CostPrice float64 `json:"cost_price"` + PriceRangeMin float64 `json:"price_range_min"` + PriceRangeMax float64 `json:"price_range_max"` + PPricingStandard float64 `json:"p_pricing_standard"` + POverpricingRatio float64 `json:"p_overpricing_ratio"` + APricingStandard float64 `json:"a_pricing_standard"` + APricingEnd float64 `json:"a_pricing_end"` + AOverpricingRatio float64 `json:"a_overpricing_ratio"` +} + +type AgentProductConfigResp struct { + AgentProductConfig []AgentProductConfig +} + +type AgentProductionConfigItem struct { + Id int64 `json:"id"` // 主键 + ProductName string `json:"product_name"` // 产品名 + CostPrice float64 `json:"cost_price"` // 成本 + PriceRangeMin float64 `json:"price_range_min"` // 最低定价 + PriceRangeMax float64 `json:"price_range_max"` // 最高定价 + PricingStandard float64 `json:"pricing_standard"` // 定价标准 + OverpricingRatio float64 `json:"overpricing_ratio"` // 超价比例 + CreateTime string `json:"create_time"` // 创建时间 +} + +type AgentRealNameReq struct { + Name string `json:"name"` + IDCard string `json:"id_card"` + Mobile string `json:"mobile"` + Code string `json:"code"` +} + +type AgentRealNameResp struct { + Status string `json:"status"` +} + +type AgentRewardListItem struct { + Id int64 `json:"id"` // 主键 + AgentId int64 `json:"agent_id"` // 代理ID + RelationAgentId int64 `json:"relation_agent_id"` // 关联代理ID + Amount float64 `json:"amount"` // 金额 + Type string `json:"type"` // 奖励类型 + CreateTime string `json:"create_time"` // 创建时间 +} + +type AgentSubordinateContributionDetail struct { + ID int64 `json:"id"` + CreateTime string `json:"create_time"` + Amount float64 `json:"amount"` + Type string `json:"type"` +} + +type AgentSubordinateContributionStats struct { + CostCount int64 `json:"cost_count"` // 成本扣除次数 + CostAmount float64 `json:"cost_amount"` // 成本扣除总额 + PricingCount int64 `json:"pricing_count"` // 定价扣除次数 + PricingAmount float64 `json:"pricing_amount"` // 定价扣除总额 + DescendantPromotionCount int64 `json:"descendant_promotion_count"` // 下级推广次数 + DescendantPromotionAmount float64 `json:"descendant_promotion_amount"` // 下级推广总额 + DescendantUpgradeVipCount int64 `json:"descendant_upgrade_vip_count"` // 下级升级VIP次数 + DescendantUpgradeVipAmount float64 `json:"descendant_upgrade_vip_amount"` // 下级升级VIP总额 + DescendantUpgradeSvipCount int64 `json:"descendant_upgrade_svip_count"` // 下级升级SVIP次数 + DescendantUpgradeSvipAmount float64 `json:"descendant_upgrade_svip_amount"` // 下级升级SVIP总额 + DescendantStayActiveCount int64 `json:"descendant_stay_active_count"` // 下级保持活跃次数 + DescendantStayActiveAmount float64 `json:"descendant_stay_active_amount"` // 下级保持活跃总额 + DescendantNewActiveCount int64 `json:"descendant_new_active_count"` // 下级新增活跃次数 + DescendantNewActiveAmount float64 `json:"descendant_new_active_amount"` // 下级新增活跃总额 + DescendantWithdrawCount int64 `json:"descendant_withdraw_count"` // 下级提现次数 + DescendantWithdrawAmount float64 `json:"descendant_withdraw_amount"` // 下级提现总额 +} + +type AgentSubordinateList struct { + ID int64 `json:"id"` + Mobile string `json:"mobile"` + CreateTime string `json:"create_time"` + LevelName string `json:"level_name"` + TotalOrders int64 `json:"total_orders"` // 总单量 + TotalEarnings float64 `json:"total_earnings"` // 总金额 + TotalContribution float64 `json:"total_contribution"` // 总贡献 +} + +type AgentWithdrawalListItem struct { + Id int64 `json:"id"` // 主键 + AgentId int64 `json:"agent_id"` // 代理ID + WithdrawNo string `json:"withdraw_no"` // 提现单号 + Amount float64 `json:"amount"` // 金额 + Status int64 `json:"status"` // 状态 + PayeeAccount string `json:"payee_account"` // 收款账户 + Remark string `json:"remark"` // 备注 + CreateTime string `json:"create_time"` // 创建时间 +} + +type BindMobileReq struct { + Mobile string `json:"mobile" validate:"required,mobile"` + Code string `json:"code" validate:"required"` +} + +type BindMobileResp struct { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} + +type Commission struct { + ProductName string `json:"product_name"` + Amount float64 `json:"amount"` + CreateTime string `json:"create_time"` +} + +type CreateMenuReq struct { + Pid int64 `json:"pid,optional"` // 父菜单ID + Name string `json:"name"` // 路由名称 + Path string `json:"path,optional"` // 路由路径 + Component string `json:"component,optional"` // 组件路径 + Redirect string `json:"redirect,optional"` // 重定向路径 + Meta map[string]interface{} `json:"meta"` // 路由元数据 + Status int64 `json:"status,optional,default=1"` // 状态:0-禁用,1-启用 + Type string `json:"type"` // 类型 + Sort int64 `json:"sort,optional"` // 排序 +} + +type CreateMenuResp struct { + Id int64 `json:"id"` // 菜单ID +} + +type CreatePromotionLinkReq struct { + Name string `json:"name"` // 链接名称 +} + +type CreatePromotionLinkResp struct { + Id int64 `json:"id"` // 链接ID + Url string `json:"url"` // 生成的推广链接URL +} + +type CreateRoleReq struct { + RoleName string `json:"role_name"` // 角色名称 + RoleCode string `json:"role_code"` // 角色编码 + Description string `json:"description"` // 角色描述 + Status int64 `json:"status,default=1"` // 状态:0-禁用,1-启用 + Sort int64 `json:"sort,default=0"` // 排序 + MenuIds []int64 `json:"menu_ids"` // 关联的菜单ID列表 +} + +type CreateRoleResp struct { + Id int64 `json:"id"` // 角色ID +} + +type DeleteMenuReq struct { + Id int64 `path:"id"` // 菜单ID +} + +type DeleteMenuResp struct { + Success bool `json:"success"` // 是否成功 +} + +type DeletePromotionLinkReq struct { + Id int64 `path:"id"` // 链接ID +} + +type DeletePromotionLinkResp struct { + Success bool `json:"success"` // 是否成功 +} + +type DeleteRoleReq struct { + Id int64 `path:"id"` // 角色ID +} + +type DeleteRoleResp struct { + Success bool `json:"success"` // 是否成功 +} + +type DirectPushReport struct { + TotalCommission float64 `json:"total_commission"` + TotalReport int `json:"total_report"` + Today TimeRangeReport `json:"today"` // 近24小时数据 + Last7D TimeRangeReport `json:"last7d"` // 近7天数据 + Last30D TimeRangeReport `json:"last30d"` // 近30天数据 +} + +type Feature struct { + ID int64 `json:"id"` // 功能ID + ApiID string `json:"api_id"` // API标识 + Name string `json:"name"` // 功能描述 +} + +type FeatureListItem struct { + Id int64 `json:"id"` // 功能ID + ApiId string `json:"api_id"` // API标识 + Name string `json:"name"` // 描述 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type GetAgentPromotionQrcodeReq struct { + QrcodeType string `form:"qrcode_type"` + QrcodeUrl string `form:"qrcode_url"` +} + +type GetAgentRevenueInfoReq struct { +} + +type GetAgentRevenueInfoResp struct { + Balance float64 `json:"balance"` + FrozenBalance float64 `json:"frozen_balance"` + TotalEarnings float64 `json:"total_earnings"` + DirectPush DirectPushReport `json:"direct_push"` // 直推报告数据 + ActiveReward ActiveReward `json:"active_reward"` // 活跃下级奖励数据 +} + +type GetAgentSubordinateContributionDetailReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 + SubordinateID int64 `form:"subordinate_id"` // 下级ID +} + +type GetAgentSubordinateContributionDetailResp struct { + Mobile string `json:"mobile"` + Total int64 `json:"total"` // 总记录数 + CreateTime string `json:"create_time"` + TotalEarnings float64 `json:"total_earnings"` // 总金额 + TotalContribution float64 `json:"total_contribution"` // 总贡献 + TotalOrders int64 `json:"total_orders"` // 总单量 + LevelName string `json:"level_name"` // 等级名称 + List []AgentSubordinateContributionDetail `json:"list"` // 查询列表 + Stats AgentSubordinateContributionStats `json:"stats"` // 统计数据 +} + +type GetAgentSubordinateListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 +} + +type GetAgentSubordinateListResp struct { + Total int64 `json:"total"` // 总记录数 + List []AgentSubordinateList `json:"list"` // 查询列表 +} + +type GetCommissionReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 +} + +type GetCommissionResp struct { + Total int64 `json:"total"` // 总记录数 + List []Commission `json:"list"` // 查询列表 +} + +type GetLinkDataReq struct { + LinkIdentifier string `form:"link_identifier"` +} + +type GetLinkDataResp struct { + Product +} + +type GetMenuAllReq struct { +} + +type GetMenuAllResp struct { + Name string `json:"name"` + Path string `json:"path"` + Redirect string `json:"redirect,omitempty"` + Component string `json:"component,omitempty"` + Sort int64 `json:"sort"` + Meta map[string]interface{} `json:"meta"` + Children []GetMenuAllResp `json:"children"` +} + +type GetMenuDetailReq struct { + Id int64 `path:"id"` // 菜单ID +} + +type GetMenuDetailResp struct { + Id int64 `json:"id"` // 菜单ID + Pid int64 `json:"pid"` // 父菜单ID + Name string `json:"name"` // 路由名称 + Path string `json:"path"` // 路由路径 + Component string `json:"component"` // 组件路径 + Redirect string `json:"redirect"` // 重定向路径 + Meta map[string]interface{} `json:"meta"` // 路由元数据 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + Type string `json:"type"` // 类型 + Sort int64 `json:"sort"` // 排序 + CreateTime string `json:"createTime"` // 创建时间 + UpdateTime string `json:"updateTime"` // 更新时间 +} + +type GetMenuListReq struct { + Name string `form:"name,optional"` // 菜单名称 + Path string `form:"path,optional"` // 路由路径 + Status int64 `form:"status,optional,default=-1"` // 状态:0-禁用,1-启用 + Type string `form:"type,optional"` // 类型 +} + +type GetNotificationsResp struct { + Notifications []Notification `json:"notifications"` // 通知列表 + Total int64 `json:"total"` // 总记录数 +} + +type GetProductByEnRequest struct { + ProductEn string `path:"product_en"` +} + +type GetProductByIDRequest struct { + Id int64 `path:"id"` +} + +type GetPromotionLinkDetailReq struct { + Id int64 `path:"id"` // 链接ID +} + +type GetPromotionLinkDetailResp struct { + Name string `json:"name"` // 链接名称 + Url string `json:"url"` // 推广链接URL + ClickCount int64 `json:"click_count"` // 点击数 + PayCount int64 `json:"pay_count"` // 付费次数 + PayAmount string `json:"pay_amount"` // 付费金额 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + LastClickTime string `json:"last_click_time,optional"` // 最后点击时间 + LastPayTime string `json:"last_pay_time,optional"` // 最后付费时间 +} + +type GetPromotionLinkListReq struct { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + Name string `form:"name,optional"` // 链接名称 + Url string `form:"url,optional"` // 推广链接URL +} + +type GetPromotionLinkListResp struct { + Total int64 `json:"total"` // 总数 + Items []PromotionLinkItem `json:"items"` // 列表 +} + +type GetPromotionStatsHistoryReq struct { + StartDate string `form:"start_date"` // 开始日期,格式:YYYY-MM-DD + EndDate string `form:"end_date"` // 结束日期,格式:YYYY-MM-DD +} + +type GetPromotionStatsTotalReq struct { +} + +type GetPromotionStatsTotalResp struct { + TodayPayAmount float64 `json:"today_pay_amount"` // 今日金额 + TodayClickCount int64 `json:"today_click_count"` // 今日点击数 + TodayPayCount int64 `json:"today_pay_count"` // 今日付费次数 + TotalPayAmount float64 `json:"total_pay_amount"` // 总金额 + TotalClickCount int64 `json:"total_click_count"` // 总点击数 + TotalPayCount int64 `json:"total_pay_count"` // 总付费次数 +} + +type GetRewardsReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 +} + +type GetRewardsResp struct { + Total int64 `json:"total"` // 总记录数 + List []Rewards `json:"list"` // 查询列表 +} + +type GetRoleDetailReq struct { + Id int64 `path:"id"` // 角色ID +} + +type GetRoleDetailResp struct { + Id int64 `json:"id"` // 角色ID + RoleName string `json:"role_name"` // 角色名称 + RoleCode string `json:"role_code"` // 角色编码 + Description string `json:"description"` // 角色描述 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + Sort int64 `json:"sort"` // 排序 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + MenuIds []int64 `json:"menu_ids"` // 关联的菜单ID列表 +} + +type GetRoleListReq struct { + Page int64 `form:"page,default=1"` // 页码 + PageSize int64 `form:"pageSize,default=20"` // 每页数量 + Name string `form:"name,optional"` // 角色名称 + Code string `form:"code,optional"` // 角色编码 + Status int64 `form:"status,optional,default=-1"` // 状态:0-禁用,1-启用 +} + +type GetRoleListResp struct { + Total int64 `json:"total"` // 总数 + Items []RoleListItem `json:"items"` // 列表 +} + +type GetWithdrawalReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 +} + +type GetWithdrawalResp struct { + Total int64 `json:"total"` // 总记录数 + List []Withdrawal `json:"list"` // 查询列表 +} + +type GetWithdrawalTaxExemptionReq struct { +} + +type GetWithdrawalTaxExemptionResp struct { + TotalExemptionAmount float64 `json:"total_exemption_amount"` + UsedExemptionAmount float64 `json:"used_exemption_amount"` + RemainingExemptionAmount float64 `json:"remaining_exemption_amount"` + TaxRate float64 `json:"tax_rate"` +} + +type HealthCheckResp struct { + Status string `json:"status"` // 服务状态 + Message string `json:"message"` // 状态信息 +} + +type IapCallbackReq struct { + OrderID int64 `json:"order_id" validate:"required"` + TransactionReceipt string `json:"transaction_receipt" validate:"required"` +} + +type MenuListItem struct { + Id int64 `json:"id"` // 菜单ID + Pid int64 `json:"pid"` // 父菜单ID + Name string `json:"name"` // 路由名称 + Path string `json:"path"` // 路由路径 + Component string `json:"component"` // 组件路径 + Redirect string `json:"redirect"` // 重定向路径 + Meta map[string]interface{} `json:"meta"` // 路由元数据 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + Type string `json:"type"` // 类型 + Sort int64 `json:"sort"` // 排序 + CreateTime string `json:"createTime"` // 创建时间 + Children []MenuListItem `json:"children"` // 子菜单 +} + +type MobileCodeLoginReq struct { + Mobile string `json:"mobile"` + Code string `json:"code" validate:"required"` +} + +type MobileCodeLoginResp struct { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} + +type Notification struct { + Title string `json:"title"` // 通知标题 + Content string `json:"content"` // 通知内容 (富文本) + NotificationPage string `json:"notificationPage"` // 通知页面 + StartDate string `json:"startDate"` // 通知开始日期,格式 "YYYY-MM-DD" + EndDate string `json:"endDate"` // 通知结束日期,格式 "YYYY-MM-DD" + StartTime string `json:"startTime"` // 每天通知开始时间,格式 "HH:MM:SS" + EndTime string `json:"endTime"` // 每天通知结束时间,格式 "HH:MM:SS" +} + +type NotificationListItem struct { + Id int64 `json:"id"` // 通知ID + Title string `json:"title"` // 通知标题 + NotificationPage string `json:"notification_page"` // 通知页面 + Content string `json:"content"` // 通知内容 + StartDate string `json:"start_date"` // 生效开始日期 + StartTime string `json:"start_time"` // 生效开始时间 + EndDate string `json:"end_date"` // 生效结束日期 + EndTime string `json:"end_time"` // 生效结束时间 + Status int64 `json:"status"` // 状态 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type OrderListItem struct { + Id int64 `json:"id"` // 订单ID + OrderNo string `json:"order_no"` // 商户订单号 + PlatformOrderId string `json:"platform_order_id"` // 支付订单号 + ProductName string `json:"product_name"` // 产品名称 + PaymentPlatform string `json:"payment_platform"` // 支付方式 + PaymentScene string `json:"payment_scene"` // 支付平台 + Amount float64 `json:"amount"` // 金额 + Status string `json:"status"` // 支付状态:pending-待支付,paid-已支付,refunded-已退款,closed-已关闭,failed-支付失败 + QueryState string `json:"query_state"` // 查询状态:pending-待查询,success-查询成功,failed-查询失败 processing-查询中 + CreateTime string `json:"create_time"` // 创建时间 + PayTime string `json:"pay_time"` // 支付时间 + RefundTime string `json:"refund_time"` // 退款时间 + IsPromotion int64 `json:"is_promotion"` // 是否推广订单:0-否,1-是 +} + +type PaymentCheckReq struct { + OrderNo string `json:"order_no" validate:"required"` +} + +type PaymentCheckResp struct { + Type string `json:"type"` + Status string `json:"status"` +} + +type PaymentReq struct { + Id string `json:"id"` + PayMethod string `json:"pay_method"` + PayType string `json:"pay_type" validate:"required,oneof=query agent_vip"` +} + +type PaymentResp struct { + PrepayData interface{} `json:"prepay_data"` + PrepayId string `json:"prepay_id"` + OrderNo string `json:"order_no"` +} + +type PlatformUserListItem struct { + Id int64 `json:"id"` // 用户ID + Mobile string `json:"mobile"` // 手机号 + Nickname string `json:"nickname"` // 昵称 + Info string `json:"info"` // 备注信息 + Inside int64 `json:"inside"` // 是否内部用户 1-是 0-否 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type Product struct { + ProductName string `json:"product_name"` + ProductEn string `json:"product_en"` + Description string `json:"description"` + Notes string `json:"notes,optional"` + SellPrice float64 `json:"sell_price"` + Features []Feature `json:"features"` // 关联功能列表 +} + +type ProductConfig struct { + ProductID int64 `json:"product_id"` + CostPrice float64 `json:"cost_price"` + PriceRangeMin float64 `json:"price_range_min"` + PriceRangeMax float64 `json:"price_range_max"` +} + +type ProductFeatureItem struct { + FeatureId int64 `json:"feature_id"` // 功能ID + Sort int64 `json:"sort"` // 排序 + Enable int64 `json:"enable"` // 是否启用 + IsImportant int64 `json:"is_important"` // 是否重要 +} + +type ProductListItem struct { + Id int64 `json:"id"` // 产品ID + ProductName string `json:"product_name"` // 服务名 + ProductEn string `json:"product_en"` // 英文名 + Description string `json:"description"` // 描述 + Notes string `json:"notes"` // 备注 + CostPrice float64 `json:"cost_price"` // 成本 + SellPrice float64 `json:"sell_price"` // 售价 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type ProductResponse struct { + Product +} + +type PromotionLinkItem struct { + Id int64 `json:"id"` // 链接ID + Name string `json:"name"` // 链接名称 + Url string `json:"url"` // 推广链接URL + ClickCount int64 `json:"click_count"` // 点击数 + PayCount int64 `json:"pay_count"` // 付费次数 + PayAmount string `json:"pay_amount"` // 付费金额 + CreateTime string `json:"create_time"` // 创建时间 + LastClickTime string `json:"last_click_time,optional"` // 最后点击时间 + LastPayTime string `json:"last_pay_time,optional"` // 最后付费时间 +} + +type PromotionStatsHistoryItem struct { + Id int64 `json:"id"` // 记录ID + LinkId int64 `json:"link_id"` // 链接ID + PayAmount float64 `json:"pay_amount"` // 金额 + ClickCount int64 `json:"click_count"` // 点击数 + PayCount int64 `json:"pay_count"` // 付费次数 + StatsDate string `json:"stats_date"` // 统计日期 +} + +type Query struct { + Id int64 `json:"id"` // 主键ID + OrderId int64 `json:"order_id"` // 订单ID + UserId int64 `json:"user_id"` // 用户ID + ProductName string `json:"product_name"` // 产品ID + QueryParams map[string]interface{} `json:"query_params"` + QueryData []QueryItem `json:"query_data"` + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 + QueryState string `json:"query_state"` // 查询状态 +} + +type QueryCleanupConfigItem struct { + Id int64 `json:"id"` // 主键ID + ConfigKey string `json:"config_key"` // 配置键 + ConfigValue string `json:"config_value"` // 配置值 + ConfigDesc string `json:"config_desc"` // 配置描述 + Status int64 `json:"status"` // 状态:1-启用,0-禁用 + CreateTime string `json:"create_time"` // 创建时间 + UpdateTime string `json:"update_time"` // 更新时间 +} + +type QueryCleanupDetailItem struct { + Id int64 `json:"id"` // 主键ID + CleanupLogId int64 `json:"cleanup_log_id"` // 清理日志ID + QueryId int64 `json:"query_id"` // 查询ID + OrderId int64 `json:"order_id"` // 订单ID + UserId int64 `json:"user_id"` // 用户ID + ProductName string `json:"product_name"` // 产品名称 + QueryState string `json:"query_state"` // 查询状态 + CreateTimeOld string `json:"create_time_old"` // 原创建时间 + CreateTime string `json:"create_time"` // 创建时间 +} + +type QueryCleanupLogItem struct { + Id int64 `json:"id"` // 主键ID + CleanupTime string `json:"cleanup_time"` // 清理时间 + CleanupBefore string `json:"cleanup_before"` // 清理截止时间 + Status int64 `json:"status"` // 状态:1-成功,2-失败 + AffectedRows int64 `json:"affected_rows"` // 影响行数 + ErrorMsg string `json:"error_msg"` // 错误信息 + Remark string `json:"remark"` // 备注 + CreateTime string `json:"create_time"` // 创建时间 +} + +type QueryDetailByOrderIdReq struct { + OrderId int64 `path:"order_id"` +} + +type QueryDetailByOrderIdResp struct { + Query +} + +type QueryDetailByOrderNoReq struct { + OrderNo string `path:"order_no"` +} + +type QueryDetailByOrderNoResp struct { + Query +} + +type QueryExampleReq struct { + Feature string `form:"feature"` +} + +type QueryExampleResp struct { + Query +} + +type QueryGenerateShareLinkReq struct { + OrderId *int64 `json:"order_id,optional"` + OrderNo *string `json:"order_no,optional"` +} + +type QueryGenerateShareLinkResp struct { + ShareLink string `json:"share_link"` +} + +type QueryItem struct { + Feature interface{} `json:"feature"` + Data interface{} `json:"data"` // 这里可以是 map 或 具体的 struct +} + +type QueryListReq struct { + Page int64 `form:"page"` // 页码 + PageSize int64 `form:"page_size"` // 每页数据量 +} + +type QueryListResp struct { + Total int64 `json:"total"` // 总记录数 + List []Query `json:"list"` // 查询列表 +} + +type QueryProvisionalOrderReq struct { + Id string `path:"id"` +} + +type QueryProvisionalOrderResp struct { + Name string `json:"name"` + IdCard string `json:"id_card"` + Mobile string `json:"mobile"` + Product Product `json:"product"` +} + +type QueryReq struct { + Data string `json:"data" validate:"required"` +} + +type QueryResp struct { + Id string `json:"id"` +} + +type QueryRetryReq struct { + Id int64 `path:"id"` +} + +type QueryRetryResp struct { + Query +} + +type QueryServiceReq struct { + Product string `path:"product"` + Data string `json:"data" validate:"required"` + AgentIdentifier string `json:"agent_identifier,optional"` + App bool `json:"app,optional"` +} + +type QueryServiceResp struct { + Id string `json:"id"` + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} + +type QueryShareDetailReq struct { + Id string `path:"id"` +} + +type QueryShareDetailResp struct { + Status string `json:"status"` + Query +} + +type QuerySingleTestReq struct { + Params map[string]interface{} `json:"params"` + Api string `json:"api"` +} + +type QuerySingleTestResp struct { + Data interface{} `json:"data"` + Api string `json:"api"` +} + +type RecordLinkClickReq struct { + Path string `path:"path"` // 链接路径 +} + +type RecordLinkClickResp struct { + Success bool `json:"success"` // 是否成功 +} + +type Rewards struct { + Type string `json:"type"` + Amount float64 `json:"amount"` + CreateTime string `json:"create_time"` +} + +type RoleListItem struct { + Id int64 `json:"id"` // 角色ID + RoleName string `json:"role_name"` // 角色名称 + RoleCode string `json:"role_code"` // 角色编码 + Description string `json:"description"` // 角色描述 + Status int64 `json:"status"` // 状态:0-禁用,1-启用 + Sort int64 `json:"sort"` // 排序 + CreateTime string `json:"create_time"` // 创建时间 + MenuIds []int64 `json:"menu_ids"` // 关联的菜单ID列表 +} + +type SaveAgentMembershipUserConfigReq struct { + ProductID int64 `json:"product_id"` + PriceIncreaseAmount float64 `json:"price_increase_amount"` + PriceRangeFrom float64 `json:"price_range_from"` + PriceRangeTo float64 `json:"price_range_to"` + PriceRatio float64 `json:"price_ratio"` +} + +type TimeRangeReport struct { + Commission float64 `json:"commission"` // 佣金 + Report int `json:"report"` // 报告量 +} + +type UpdateMenuReq struct { + Id int64 `path:"id"` // 菜单ID + Pid int64 `json:"pid,optional"` // 父菜单ID + Name string `json:"name"` // 路由名称 + Path string `json:"path,optional"` // 路由路径 + Component string `json:"component,optional"` // 组件路径 + Redirect string `json:"redirect,optional"` // 重定向路径 + Meta map[string]interface{} `json:"meta"` // 路由元数据 + Status int64 `json:"status,optional"` // 状态:0-禁用,1-启用 + Type string `json:"type"` // 类型 + Sort int64 `json:"sort,optional"` // 排序 +} + +type UpdateMenuResp struct { + Success bool `json:"success"` // 是否成功 +} + +type UpdatePromotionLinkReq struct { + Id int64 `path:"id"` // 链接ID + Name *string `json:"name,optional"` // 链接名称 +} + +type UpdatePromotionLinkResp struct { + Success bool `json:"success"` // 是否成功 +} + +type UpdateQueryDataReq struct { + Id int64 `json:"id"` // 查询ID + QueryData string `json:"query_data"` // 查询数据(未加密的JSON) +} + +type UpdateQueryDataResp struct { + Id int64 `json:"id"` + UpdatedAt string `json:"updated_at"` // 更新时间 +} + +type UpdateRoleReq struct { + Id int64 `path:"id"` // 角色ID + RoleName *string `json:"role_name,optional"` // 角色名称 + RoleCode *string `json:"role_code,optional"` // 角色编码 + Description *string `json:"description,optional"` // 角色描述 + Status *int64 `json:"status,optional"` // 状态:0-禁用,1-启用 + Sort *int64 `json:"sort,optional"` // 排序 + MenuIds []int64 `json:"menu_ids,optional"` // 关联的菜单ID列表 +} + +type UpdateRoleResp struct { + Success bool `json:"success"` // 是否成功 +} + +type User struct { + Id int64 `json:"id"` + Mobile string `json:"mobile"` + NickName string `json:"nickName"` + UserType int64 `json:"userType"` +} + +type UserInfoResp struct { + UserInfo User `json:"userInfo"` +} + +type WXH5AuthReq struct { + Code string `json:"code"` +} + +type WXH5AuthResp struct { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} + +type WXMiniAuthReq struct { + Code string `json:"code"` + Platform string `json:"platform,optional,default=tyc"` +} + +type WXMiniAuthResp struct { + AccessToken string `json:"accessToken"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} + +type Withdrawal struct { + Status int64 `json:"status"` + Amount float64 `json:"amount"` + WithdrawalNo string `json:"withdrawal_no"` + Remark string `json:"remark"` + PayeeAccount string `json:"payee_account"` + CreateTime string `json:"create_time"` +} + +type WithdrawalReq struct { + Amount float64 `json:"amount"` // 提现金额 + PayeeAccount string `json:"payee_account"` + PayeeName string `json:"payee_name"` +} + +type WithdrawalResp struct { + Status int64 `json:"status"` // 1申请中 2成功 3失败 + FailMsg string `json:"fail_msg"` +} + +type GetAppVersionResp struct { + Version string `json:"version"` + WgtUrl string `json:"wgtUrl"` +} + +type SendSmsReq struct { + Mobile string `json:"mobile" validate:"required,mobile"` + ActionType string `json:"actionType" validate:"required,oneof=login register query agentApply realName bindMobile"` +} diff --git a/app/main/api/main.go b/app/main/api/main.go new file mode 100644 index 0000000..c44b0db --- /dev/null +++ b/app/main/api/main.go @@ -0,0 +1,66 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "znc-server/app/main/api/internal/config" + "znc-server/app/main/api/internal/handler" + "znc-server/app/main/api/internal/middleware" + "znc-server/app/main/api/internal/queue" + "znc-server/app/main/api/internal/svc" + + "github.com/zeromicro/go-zero/core/logx" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/rest" +) + +func main() { + // 读取环境变量 ENV,默认为 "prod" + env := os.Getenv("ENV") + if env == "" { + env = "production" + } + + // 根据 ENV 加载不同的配置文件 + var defaultConfigFile string + if env == "development" { + defaultConfigFile = "app/main/api/etc/main.dev.yaml" + } else { + defaultConfigFile = "etc/main.yaml" + } + configFile := flag.String("f", defaultConfigFile, "the config file") + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + + svcContext := svc.NewServiceContext(c) + defer svcContext.Close() + + // 启动 asynq 消费者 + go func() { + ctx := context.Background() + // 初始化 cron job 或异步任务队列 + asynq := queue.NewCronJob(ctx, svcContext) + mux := asynq.Register() + + // 启动 asynq 消费者 + if err := svcContext.AsynqServer.Run(mux); err != nil { + logx.WithContext(ctx).Errorf("异步任务启动失败: %v", err) + os.Exit(1) + } + fmt.Println("异步任务启动!!!") + }() + + server := rest.MustNewServer(c.RestConf) + server.Use(middleware.GlobalSourceInterceptor) + defer server.Stop() + + handler.RegisterHandlers(server, svcContext) + + fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) + server.Start() +} diff --git a/app/main/api/static/images/tg_qrcode_1.png b/app/main/api/static/images/tg_qrcode_1.png new file mode 100644 index 0000000000000000000000000000000000000000..1157bb4009680ed582a7c18e4fb6f689446769d5 GIT binary patch literal 866564 zcmX_H1yqx5`(FSfCn+_gr39p8fYROFUDA#b5`v?9ba!`3D>%BPBn25M5-LcE_22uw z-}gWFfx{W+Y|r!TzOU<7SDdc43K=m2F#rG{Q&Uyc2LSNk000Ck z_+_|P&8Xg@kPYYorlX#I+WiSKPBj#KVmbki^Woyji z;eF*y+TZ%1o;fSr)xIuPsJ`vKoqfR0F?qA-Gg=a*vM(zm^bf|)3Jk=$g`-zgHEn8z z?=C{BJdMw0jAUXOR!{Xk1&GX@eE`3MgVBs@t=Umo3FI$dSZUBJW{zy}(37|lHa`wt zjyV!k?E8Bi#I&=obO;T8)0Fl3tjj&6F1~(D^mnE_X+|akZO-H+P=;hiJ;GRZWKc)9CC4RuZY?S0Kcp z>y`{arEb#_fNt^O3gG)=fsbLm0Cemb9P|NTUpz|zFxL-GKzP6lJ^&QANpCG}lY6_3K8rU)+M*P6GXcnM0CX+!X5h+o56FDP0YFlt%C87XFcjuu_*g

316#zxV~h~L6=|C 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�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 z%0A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%0DgH2rSOrz}A;DX+-(Ha)9DUiqsml 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