From 37983f3b60d136a91659e615db0cdf3d91f02918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=91=E8=90=8C=E8=8A=BD?= <3205788256@qq.com> Date: Wed, 13 May 2026 12:11:46 +0800 Subject: [PATCH] Update mengyastore --- mengyastore-backend-go/.gitignore | 3 +- mengyastore-backend-go/Makefile | 3 + mengyastore-backend-go/cmd/migrate/main.go | 20 +- mengyastore-backend-go/docker-compose.yml | 6 +- mengyastore-backend-go/docs/docs.go | 2193 +++++++++++++++++ mengyastore-backend-go/docs/swagger.json | 2174 ++++++++++++++++ mengyastore-backend-go/docs/swagger.yaml | 1390 +++++++++++ mengyastore-backend-go/go.mod | 29 +- mengyastore-backend-go/go.sum | 104 +- .../internal/config/config.go | 80 +- .../internal/database/db.go | 15 +- .../internal/database/models.go | 26 +- .../internal/handlers/admin_chat.go | 44 +- .../internal/handlers/admin_orders.go | 20 + .../internal/handlers/admin_product.go | 129 +- .../internal/handlers/admin_site.go | 37 +- .../internal/handlers/admin_status.go | 13 +- .../internal/handlers/chat.go | 25 +- .../internal/handlers/order.go | 956 ++++--- .../internal/handlers/public.go | 15 + .../internal/handlers/stats.go | 21 + .../internal/handlers/swagger_models.go | 162 ++ .../internal/handlers/wishlist.go | 37 +- .../internal/models/order.go | 3 + .../internal/models/product.go | 2 + mengyastore-backend-go/internal/mq/client.go | 16 +- .../internal/mq/consumer.go | 2 +- mengyastore-backend-go/internal/mq/payload.go | 2 +- .../internal/mq/topology.go | 6 +- .../internal/payment/mengya.go | 143 ++ .../internal/payment/types.go | 3 + .../internal/storage/orderstore.go | 165 +- .../internal/storage/productstore.go | 125 +- .../internal/storage/sitestore.go | 2 +- mengyastore-backend-go/main.go | 170 +- mengyastore-backend-go/后端文档.md | 44 +- mengyastore-backend-java/.gitattributes | 2 - mengyastore-backend-java/.gitignore | 33 - .../.mvn/wrapper/maven-wrapper.properties | 3 - mengyastore-backend-java/compose.yaml | 1 - mengyastore-backend-java/mvnw | 295 --- mengyastore-backend-java/mvnw.cmd | 189 -- mengyastore-backend-java/pom.xml | 121 - .../MengyastoreBackendJavaApplication.java | 17 - .../config/AppProperties.java | 33 - .../config/CorsConfig.java | 44 - .../config/RabbitMQConfig.java | 63 - .../config/RedisConfig.java | 15 - .../config/RestTemplateConfig.java | 20 - .../config/WebMvcConfig.java | 31 - .../controller/ChatController.java | 38 - .../controller/HealthController.java | 55 - .../controller/PublicOrderController.java | 67 - .../controller/PublicProductController.java | 57 - .../controller/StatsController.java | 42 - .../controller/UserOrderController.java | 27 - .../controller/WishlistController.java | 45 - .../controller/admin/AdminChatController.java | 46 - .../admin/AdminOrderController.java | 29 - .../admin/AdminProductController.java | 52 - .../controller/admin/AdminSiteController.java | 38 - .../admin/AdminStatusController.java | 25 - .../admin/AdminVerifyController.java | 30 - .../dto/request/AdminChatMessageRequest.java | 11 - .../dto/request/AdminProductRequest.java | 46 - .../dto/request/AdminVerifyRequest.java | 8 - .../dto/request/ChatMessageRequest.java | 11 - .../dto/request/CheckoutRequest.java | 21 - .../dto/request/MaintenanceRequest.java | 9 - .../dto/request/ProductStatusRequest.java | 8 - .../dto/request/SmtpConfigRequest.java | 13 - .../dto/response/ApiResponse.java | 25 - .../dto/response/ChatMessageResponse.java | 18 - .../dto/response/HealthResponse.java | 11 - .../dto/response/MaintenanceResponse.java | 11 - .../dto/response/OrderResponse.java | 27 - .../dto/response/ProductResponse.java | 37 - .../dto/response/SmtpConfigResponse.java | 15 - .../dto/response/StatsResponse.java | 11 - .../dto/response/WishlistResponse.java | 12 - .../entity/ChatMessage.java | 33 - .../mengyastorebackendjava/entity/Order.java | 61 - .../entity/Product.java | 80 - .../entity/ProductCode.java | 29 - .../entity/SiteSetting.java | 26 - .../entity/Wishlist.java | 30 - .../entity/converter/StringListConverter.java | 39 - .../exception/BusinessException.java | 21 - .../exception/GlobalExceptionHandler.java | 35 - .../mq/OrderEmailConsumer.java | 64 - .../mq/OrderEmailPayload.java | 21 - .../mq/OrderEmailProducer.java | 36 - .../mq/RabbitMQTopology.java | 16 - .../repository/ChatMessageRepository.java | 19 - .../repository/OrderRepository.java | 20 - .../repository/ProductCodeRepository.java | 19 - .../repository/ProductRepository.java | 29 - .../repository/SiteSettingRepository.java | 9 - .../repository/WishlistRepository.java | 22 - .../security/AdminTokenInterceptor.java | 46 - .../security/BearerTokenInterceptor.java | 49 - .../security/CurrentUserHolder.java | 20 - .../security/SproutGateUser.java | 13 - .../security/SproutGateVerifyResult.java | 11 - .../service/ChatService.java | 86 - .../service/EmailService.java | 105 - .../service/OrderService.java | 183 -- .../service/ProductService.java | 246 -- .../service/SiteService.java | 119 - .../service/SproutGateAuthService.java | 37 - .../service/SystemStatusService.java | 296 --- .../service/WishlistService.java | 61 - .../src/main/resources/application.yaml | 85 - ...engyastoreBackendJavaApplicationTests.java | 13 - mengyastore-backend-java/后端文档.md | 112 - mengyastore-frontend/package-lock.json | 49 - mengyastore-frontend/package.json | 1 - mengyastore-frontend/src/App.vue | 385 +-- .../src/assets/payment/微信个人收款码.png | Bin 138711 -> 0 bytes .../src/assets/payment/微信赞赏码.png | Bin 160292 -> 0 bytes .../src/assets/payment/支付宝个人收款码.png | Bin 243398 -> 0 bytes mengyastore-frontend/src/assets/styles.css | 770 +++--- mengyastore-frontend/src/main.js | 2 - .../src/modules/admin/AdminPage.vue | 4 +- .../admin/components/AdminChatPanel.vue | 4 +- .../admin/components/AdminMaintenanceRow.vue | 2 +- .../admin/components/AdminProductModal.vue | 41 +- .../admin/components/AdminProductTable.vue | 4 +- .../modules/admin/components/AdminSMTPRow.vue | 2 +- .../admin/components/AdminTokenRow.vue | 2 +- .../src/modules/auth/AuthCallback.vue | 2 +- .../src/modules/chat/ChatWidget.vue | 4 +- .../src/modules/shared/SplashScreen.vue | 22 +- .../src/modules/shared/api.js | 121 +- .../src/modules/shell/AppAdminUnlockModal.vue | 64 + .../src/modules/shell/AppSiteFooter.vue | 45 + .../src/modules/shell/AppTopNav.vue | 271 ++ .../src/modules/store/CheckoutPage.vue | 463 ++-- .../src/modules/store/ProductDetail.vue | 473 ++-- .../src/modules/store/StorePage.vue | 18 +- .../CheckoutAwaitingFulfillment.vue | 181 ++ .../store/components/CheckoutOrderDone.vue | 26 + .../components/CheckoutOrderFormPanel.vue | 50 + .../store/components/CheckoutOrderHeader.vue | 27 + .../components/CheckoutProductSummary.vue | 45 + .../modules/store/components/ProductCard.vue | 226 +- .../store/composables/useClipboardFeedback.js | 44 + .../composables/useMengyaPaymentPolling.js | 67 + .../store/composables/usePaymentCountdown.js | 94 + .../src/modules/user/MyOrdersPage.vue | 12 +- .../src/modules/wishlist/WishlistPage.vue | 4 +- mengyastore-frontend/vite.config.js | 2 +- test/server.py | 111 + 萌芽小店项目经历-golang.md | 5 - 萌芽小店项目经历-java.md | 5 - 面试相关/字节后端面试模拟-mengyastore.md | 109 + 156 files changed, 10064 insertions(+), 5681 deletions(-) create mode 100644 mengyastore-backend-go/Makefile create mode 100644 mengyastore-backend-go/docs/docs.go create mode 100644 mengyastore-backend-go/docs/swagger.json create mode 100644 mengyastore-backend-go/docs/swagger.yaml create mode 100644 mengyastore-backend-go/internal/handlers/swagger_models.go create mode 100644 mengyastore-backend-go/internal/payment/mengya.go create mode 100644 mengyastore-backend-go/internal/payment/types.go delete mode 100644 mengyastore-backend-java/.gitattributes delete mode 100644 mengyastore-backend-java/.gitignore delete mode 100644 mengyastore-backend-java/.mvn/wrapper/maven-wrapper.properties delete mode 100644 mengyastore-backend-java/compose.yaml delete mode 100644 mengyastore-backend-java/mvnw delete mode 100644 mengyastore-backend-java/mvnw.cmd delete mode 100644 mengyastore-backend-java/pom.xml delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/MengyastoreBackendJavaApplication.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/AppProperties.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/CorsConfig.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/RabbitMQConfig.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/RedisConfig.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/RestTemplateConfig.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/WebMvcConfig.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/ChatController.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/HealthController.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/PublicOrderController.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/PublicProductController.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/StatsController.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/UserOrderController.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/WishlistController.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminChatController.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminOrderController.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminProductController.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminSiteController.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminStatusController.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminVerifyController.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/AdminChatMessageRequest.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/AdminProductRequest.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/AdminVerifyRequest.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/ChatMessageRequest.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/CheckoutRequest.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/MaintenanceRequest.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/ProductStatusRequest.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/SmtpConfigRequest.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/ApiResponse.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/ChatMessageResponse.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/HealthResponse.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/MaintenanceResponse.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/OrderResponse.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/ProductResponse.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/SmtpConfigResponse.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/StatsResponse.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/WishlistResponse.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/ChatMessage.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/Order.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/Product.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/ProductCode.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/SiteSetting.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/Wishlist.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/converter/StringListConverter.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/exception/BusinessException.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/exception/GlobalExceptionHandler.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/OrderEmailConsumer.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/OrderEmailPayload.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/OrderEmailProducer.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/RabbitMQTopology.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/ChatMessageRepository.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/OrderRepository.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/ProductCodeRepository.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/ProductRepository.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/SiteSettingRepository.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/WishlistRepository.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/AdminTokenInterceptor.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/BearerTokenInterceptor.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/CurrentUserHolder.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/SproutGateUser.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/SproutGateVerifyResult.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/ChatService.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/EmailService.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/OrderService.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/ProductService.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/SiteService.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/SproutGateAuthService.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/SystemStatusService.java delete mode 100644 mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/WishlistService.java delete mode 100644 mengyastore-backend-java/src/main/resources/application.yaml delete mode 100644 mengyastore-backend-java/src/test/java/com/smyhub/store/mengyastorebackendjava/MengyastoreBackendJavaApplicationTests.java delete mode 100644 mengyastore-backend-java/后端文档.md delete mode 100644 mengyastore-frontend/src/assets/payment/微信个人收款码.png delete mode 100644 mengyastore-frontend/src/assets/payment/微信赞赏码.png delete mode 100644 mengyastore-frontend/src/assets/payment/支付宝个人收款码.png create mode 100644 mengyastore-frontend/src/modules/shell/AppAdminUnlockModal.vue create mode 100644 mengyastore-frontend/src/modules/shell/AppSiteFooter.vue create mode 100644 mengyastore-frontend/src/modules/shell/AppTopNav.vue create mode 100644 mengyastore-frontend/src/modules/store/components/CheckoutAwaitingFulfillment.vue create mode 100644 mengyastore-frontend/src/modules/store/components/CheckoutOrderDone.vue create mode 100644 mengyastore-frontend/src/modules/store/components/CheckoutOrderFormPanel.vue create mode 100644 mengyastore-frontend/src/modules/store/components/CheckoutOrderHeader.vue create mode 100644 mengyastore-frontend/src/modules/store/components/CheckoutProductSummary.vue create mode 100644 mengyastore-frontend/src/modules/store/composables/useClipboardFeedback.js create mode 100644 mengyastore-frontend/src/modules/store/composables/useMengyaPaymentPolling.js create mode 100644 mengyastore-frontend/src/modules/store/composables/usePaymentCountdown.js create mode 100644 test/server.py delete mode 100644 萌芽小店项目经历-golang.md delete mode 100644 萌芽小店项目经历-java.md create mode 100644 面试相关/字节后端面试模拟-mengyastore.md diff --git a/mengyastore-backend-go/.gitignore b/mengyastore-backend-go/.gitignore index 91fcb8f..5cf9eea 100644 --- a/mengyastore-backend-go/.gitignore +++ b/mengyastore-backend-go/.gitignore @@ -1,5 +1,6 @@ -# Local secrets — use .env.example / .env.production.example as templates +# Local secrets — use .env.development / .env.production as templates .env +.env.development .env.local .env.production *.exe diff --git a/mengyastore-backend-go/Makefile b/mengyastore-backend-go/Makefile new file mode 100644 index 0000000..3d5bc23 --- /dev/null +++ b/mengyastore-backend-go/Makefile @@ -0,0 +1,3 @@ +.PHONY: swagger +swagger: + swag init -g main.go -o docs --parseDependency --parseInternal diff --git a/mengyastore-backend-go/cmd/migrate/main.go b/mengyastore-backend-go/cmd/migrate/main.go index 64e0907..a50d32f 100644 --- a/mengyastore-backend-go/cmd/migrate/main.go +++ b/mengyastore-backend-go/cmd/migrate/main.go @@ -1,5 +1,5 @@ -// migrate imports existing JSON data files into the MySQL database. -// Run once after switching to DB storage: +// migrate 将历史上导出的 JSON 数据文件导入 MySQL。 +// 在切到数据库存储方式后执行一次即可: // // go run ./cmd/migrate/main.go package main @@ -21,7 +21,7 @@ import ( ) func main() { - // Load from env / .env (run from repo root: go run ./cmd/migrate, or set ENV_FILE). + // 从环境变量 / .env.development 加载(在仓库根目录执行 go run ./cmd/migrate,或设置 ENV_FILE)。 cfg, err := config.Load() if err != nil { log.Fatalf("load config: %v", err) @@ -34,7 +34,7 @@ func main() { log.Fatalf("open db: %v", err) } - // Ensure tables exist + // 确保表结构存在 if err := db.AutoMigrate( &database.ProductRow{}, &database.ProductCodeRow{}, @@ -55,7 +55,7 @@ func main() { log.Println("✅ 数据导入完成!") } -// ─── Products ───────────────────────────────────────────────────────────────── +// ─── 商品 ───────────────────────────────────────────────────────────────────── type jsonProduct struct { ID string `json:"id"` @@ -123,7 +123,7 @@ func migrateProducts(db *gorm.DB) { log.Printf("[products] 导入 %s 失败: %v", p.ID, result.Error) continue } - // Codes → product_codes + // 卡密写入 product_codes 表 for _, code := range p.Codes { if code == "" { continue @@ -137,7 +137,7 @@ func migrateProducts(db *gorm.DB) { log.Printf("[products] 导入 %d 条商品", len(products)) } -// ─── Orders ─────────────────────────────────────────────────────────────────── +// ─── 订单 ───────────────────────────────────────────────────────────────────── type jsonOrder struct { ID string `json:"id"` @@ -201,7 +201,7 @@ func migrateOrders(db *gorm.DB) { log.Printf("[orders] 导入 %d 条订单", len(orders)) } -// ─── Wishlists ──────────────────────────────────────────────────────────────── +// ─── 收藏夹 ─────────────────────────────────────────────────────────────────── func migrateWishlists(db *gorm.DB) { data, err := os.ReadFile("data/json/wishlists.json") @@ -227,7 +227,7 @@ func migrateWishlists(db *gorm.DB) { log.Printf("[wishlists] 导入 %d 条收藏记录", count) } -// ─── Chats ──────────────────────────────────────────────────────────────────── +// ─── 聊天 ───────────────────────────────────────────────────────────────────── type jsonChatMsg struct { ID string `json:"id"` @@ -269,7 +269,7 @@ func migrateChats(db *gorm.DB) { log.Printf("[chats] 导入 %d 条聊天消息", count) } -// ─── Site settings ──────────────────────────────────────────────────────────── +// ─── 站点设置 ───────────────────────────────────────────────────────────────── type jsonSite struct { TotalVisits int `json:"totalVisits"` diff --git a/mengyastore-backend-go/docker-compose.yml b/mengyastore-backend-go/docker-compose.yml index 050fa5f..90586f1 100644 --- a/mengyastore-backend-go/docker-compose.yml +++ b/mengyastore-backend-go/docker-compose.yml @@ -6,7 +6,7 @@ services: ports: - "28081:8080" environment: - # 生产环境(本地开发请使用 APP_ENV=development + 根目录 .env) + # 生产环境(本地开发请使用 APP_ENV=development + 根目录 .env.development,或 ENV_FILE) APP_ENV: production GIN_MODE: release TZ: Asia/Shanghai @@ -14,11 +14,11 @@ services: DATABASE_DSN: ${DATABASE_DSN:-} ADMIN_TOKEN: ${ADMIN_TOKEN:-changeme} AUTH_API_URL: ${AUTH_API_URL:-} - REDIS_ENABLED: "true" + REDIS_ENABLED: "false" REDIS_ADDR: ${REDIS_ADDR:-127.0.0.1:6379} REDIS_PASSWORD: ${REDIS_PASSWORD:-} REDIS_DB: ${REDIS_DB:-1} - RABBITMQ_ENABLED: "true" + RABBITMQ_ENABLED: "false" RABBITMQ_ENV: prod RABBITMQ_URL: ${RABBITMQ_URL} restart: unless-stopped diff --git a/mengyastore-backend-go/docs/docs.go b/mengyastore-backend-go/docs/docs.go new file mode 100644 index 0000000..e6c1863 --- /dev/null +++ b/mengyastore-backend-go/docs/docs.go @@ -0,0 +1,2193 @@ +// Code generated by swaggo/swag. DO NOT EDIT. + +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/": { + "get": { + "description": "返回服务描述、主要路径指引与版本(非 OpenAPI 详尽列表,完整契约见 /swagger)。", + "produces": [ + "application/json" + ], + "tags": [ + "元信息" + ], + "summary": "API 根信息与端点索引", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/admin/chat": { + "get": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-聊天" + ], + "summary": "全部会话列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/chat/{account}": { + "get": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-聊天" + ], + "summary": "指定用户聊天记录", + "parameters": [ + { + "type": "string", + "description": "用户账号", + "name": "account", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerMessagesWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "post": { + "security": [ + { + "AdminToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-聊天" + ], + "summary": "管理员回复", + "parameters": [ + { + "type": "string", + "description": "用户账号", + "name": "account", + "in": "path", + "required": true + }, + { + "description": "回复内容", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.AdminChatPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerOneChatMsgWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "delete": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-聊天" + ], + "summary": "清空会话", + "parameters": [ + { + "type": "string", + "description": "用户账号", + "name": "account", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerBoolOKWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/orders": { + "get": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-订单" + ], + "summary": "全部订单(管理)", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerOrdersBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/orders/{id}": { + "delete": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-订单" + ], + "summary": "删除订单", + "parameters": [ + { + "type": "string", + "description": "订单 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerBoolOKWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/products": { + "get": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-商品" + ], + "summary": "全部商品(管理)", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerProductListBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "post": { + "security": [ + { + "AdminToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-商品" + ], + "summary": "创建商品", + "parameters": [ + { + "description": "商品字段", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.ProductPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerProductOneBody" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/products/{id}": { + "put": { + "security": [ + { + "AdminToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-商品" + ], + "summary": "更新商品", + "parameters": [ + { + "type": "string", + "description": "商品 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "商品字段", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.ProductPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerProductOneBody" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "delete": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-商品" + ], + "summary": "删除商品", + "parameters": [ + { + "type": "string", + "description": "商品 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerBoolOKWrap" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/products/{id}/status": { + "patch": { + "security": [ + { + "AdminToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-商品" + ], + "summary": "切换上架状态", + "parameters": [ + { + "type": "string", + "description": "商品 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "{active}", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.TogglePayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerProductOneBody" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/site/maintenance": { + "post": { + "security": [ + { + "AdminToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-站点" + ], + "summary": "设置维护模式", + "parameters": [ + { + "description": "维护开关与原因", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.MaintenancePayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerMaintenanceWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/site/smtp": { + "get": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-站点" + ], + "summary": "获取 SMTP 配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerSMTPWrap" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "post": { + "security": [ + { + "AdminToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-站点" + ], + "summary": "保存 SMTP 配置", + "parameters": [ + { + "description": "SMTP 字段", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/storage.SMTPConfig" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerStringOKBody" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/system-status": { + "get": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-系统" + ], + "summary": "系统运行状态", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/verify": { + "post": { + "description": "响应为 {\"valid\": true/false},不泄露真实口令。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-认证" + ], + "summary": "校验管理员令牌", + "parameters": [ + { + "description": "待校验 token", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.AdminVerifyTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerValidBody" + } + } + } + } + }, + "/api/chat/messages": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "聊天(用户)" + ], + "summary": "我的聊天消息", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerMessagesWrap" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "聊天(用户)" + ], + "summary": "发送用户消息", + "parameters": [ + { + "description": "消息正文", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.ChatMessagePayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerOneChatMsgWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/checkout": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "订单" + ], + "summary": "结账创建订单", + "parameters": [ + { + "type": "string", + "description": "Bearer 用户 token(部分商品必填)", + "name": "Authorization", + "in": "header" + }, + { + "description": "结账参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CheckoutPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerCheckoutWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "409": { + "description": "库存锁定冲突或同用户待支付", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/health": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "健康检查" + ], + "summary": "健康检查", + "responses": { + "200": { + "description": "status ok;rabbitmq 为 disabled|ok|error:...", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/orders": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "订单" + ], + "summary": "我的订单", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerOrdersBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/orders/{id}/cancel": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "订单" + ], + "summary": "取消订单", + "parameters": [ + { + "type": "string", + "description": "订单 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerCancelWrap" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/orders/{id}/confirm": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "订单" + ], + "summary": "确认订单", + "parameters": [ + { + "type": "string", + "description": "订单 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerConfirmWrap" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "409": { + "description": "待支付未到账等", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "410": { + "description": "已取消或超时", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/orders/{id}/payment-status": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "订单" + ], + "summary": "订单支付状态", + "parameters": [ + { + "type": "string", + "description": "订单 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerPaymentStatusWrap" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/products": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "公开" + ], + "summary": "上架商品列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerProductListBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/products/{id}/view": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "公开" + ], + "summary": "记录商品浏览", + "parameters": [ + { + "type": "string", + "description": "商品 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerProductViewWrap" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/site/maintenance": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "公开" + ], + "summary": "维护模式状态", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerMaintenanceWrap" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/site/visit": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "公开" + ], + "summary": "记录站点访问", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerVisitWrap" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/stats": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "公开" + ], + "summary": "站点统计", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerStatsWrap" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/webhooks/mengya-pay": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Webhook" + ], + "summary": "萌芽支付 Webhook", + "parameters": [ + { + "type": "string", + "description": "与 WEBHOOK_MENGYA_SECRET 一致", + "name": "X-Webhook-Secret", + "in": "header" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerWebhookMengyaResp" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/wishlist": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "收藏" + ], + "summary": "收藏列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerWishlistWrap" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "收藏" + ], + "summary": "添加收藏", + "parameters": [ + { + "description": "商品 ID", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.WishlistItemPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerWishlistWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/wishlist/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "收藏" + ], + "summary": "移除收藏", + "parameters": [ + { + "type": "string", + "description": "商品 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerWishlistWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + } + }, + "definitions": { + "handlers.AdminChatPayload": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + } + }, + "handlers.AdminVerifyTokenRequest": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "handlers.ChatMessagePayload": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + } + }, + "handlers.CheckoutPayload": { + "type": "object", + "properties": { + "contactEmail": { + "type": "string" + }, + "contactPhone": { + "type": "string" + }, + "note": { + "type": "string" + }, + "notifyEmail": { + "type": "string" + }, + "paymentMethod": { + "type": "string" + }, + "productId": { + "type": "string" + }, + "quantity": { + "type": "integer" + } + } + }, + "handlers.MaintenancePayload": { + "type": "object", + "properties": { + "maintenance": { + "type": "boolean" + }, + "reason": { + "type": "string" + } + } + }, + "handlers.ProductPayload": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "coverUrl": { + "type": "string" + }, + "deliveryMode": { + "type": "string" + }, + "description": { + "type": "string" + }, + "discountPrice": { + "type": "number" + }, + "fixedContent": { + "type": "string" + }, + "fulfillmentType": { + "type": "string" + }, + "maxPerAccount": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "paymentQrUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "price": { + "type": "number" + }, + "requireLogin": { + "type": "boolean" + }, + "screenshotUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "showContact": { + "type": "boolean" + }, + "showNote": { + "type": "boolean" + }, + "tags": { + "type": "string" + } + } + }, + "handlers.SwaggerBoolOK": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + } + } + }, + "handlers.SwaggerBoolOKWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerBoolOK" + } + } + }, + "handlers.SwaggerCancelMsg": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "ok": { + "type": "boolean" + } + } + }, + "handlers.SwaggerCancelWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerCancelMsg" + } + } + }, + "handlers.SwaggerCheckoutData": { + "type": "object", + "properties": { + "orderId": { + "type": "string" + }, + "paymentExpectedTotal": { + "type": "number" + }, + "paymentExpiresAt": { + "type": "string" + }, + "paymentMethod": { + "type": "string" + }, + "paymentQrUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "productId": { + "type": "string" + }, + "productQty": { + "type": "integer" + }, + "qrCodeUrl": { + "type": "string" + }, + "status": { + "type": "string" + }, + "viewCount": { + "type": "integer" + } + } + }, + "handlers.SwaggerCheckoutWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerCheckoutData" + } + } + }, + "handlers.SwaggerConfirmData": { + "type": "object", + "properties": { + "deliveredCodes": { + "type": "array", + "items": { + "type": "string" + } + }, + "deliveryMode": { + "type": "string" + }, + "isManual": { + "type": "boolean" + }, + "orderId": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "handlers.SwaggerConfirmWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerConfirmData" + } + } + }, + "handlers.SwaggerErrorBody": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "handlers.SwaggerMaintenanceData": { + "type": "object", + "properties": { + "maintenance": { + "type": "boolean" + }, + "reason": { + "type": "string" + } + } + }, + "handlers.SwaggerMaintenanceWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerMaintenanceData" + } + } + }, + "handlers.SwaggerMessagesInner": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ChatMessage" + } + } + } + }, + "handlers.SwaggerMessagesWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerMessagesInner" + } + } + }, + "handlers.SwaggerOneChatMsgWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerSingleChatWrap" + } + } + }, + "handlers.SwaggerOrdersBody": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Order" + } + } + } + }, + "handlers.SwaggerPaymentStatusData": { + "type": "object", + "properties": { + "expectedTotal": { + "type": "number" + }, + "paymentExpiresAt": { + "type": "string" + }, + "paymentMethod": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "handlers.SwaggerPaymentStatusWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerPaymentStatusData" + } + } + }, + "handlers.SwaggerProductListBody": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Product" + } + } + } + }, + "handlers.SwaggerProductOneBody": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.Product" + } + } + }, + "handlers.SwaggerProductViewData": { + "type": "object", + "properties": { + "counted": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "viewCount": { + "type": "integer" + } + } + }, + "handlers.SwaggerProductViewWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerProductViewData" + } + } + }, + "handlers.SwaggerSMTPWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/storage.SMTPConfig" + } + } + }, + "handlers.SwaggerSingleChatWrap": { + "type": "object", + "properties": { + "message": { + "$ref": "#/definitions/models.ChatMessage" + } + } + }, + "handlers.SwaggerStatsData": { + "type": "object", + "properties": { + "totalOrders": { + "type": "integer" + }, + "totalVisits": { + "type": "integer" + } + } + }, + "handlers.SwaggerStatsWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerStatsData" + } + } + }, + "handlers.SwaggerStringOKBody": { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + }, + "handlers.SwaggerValidBody": { + "type": "object", + "properties": { + "valid": { + "type": "boolean" + } + } + }, + "handlers.SwaggerVisitData": { + "type": "object", + "properties": { + "counted": { + "type": "boolean" + }, + "totalVisits": { + "type": "integer" + } + } + }, + "handlers.SwaggerVisitWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerVisitData" + } + } + }, + "handlers.SwaggerWebhookMengyaResp": { + "type": "object", + "properties": { + "matched": { + "type": "boolean" + }, + "matched_order_id": { + "type": "string" + }, + "ok": { + "type": "boolean" + } + } + }, + "handlers.SwaggerWishlistItems": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handlers.SwaggerWishlistWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerWishlistItems" + } + } + }, + "handlers.TogglePayload": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + } + } + }, + "handlers.WishlistItemPayload": { + "type": "object", + "properties": { + "productId": { + "type": "string" + } + } + }, + "models.ChatMessage": { + "type": "object", + "properties": { + "accountId": { + "type": "string" + }, + "accountName": { + "type": "string" + }, + "content": { + "type": "string" + }, + "fromAdmin": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "sentAt": { + "type": "string" + } + } + }, + "models.Order": { + "type": "object", + "properties": { + "contactEmail": { + "type": "string" + }, + "contactPhone": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "deliveredCodes": { + "type": "array", + "items": { + "type": "string" + } + }, + "deliveryMode": { + "type": "string" + }, + "id": { + "type": "string" + }, + "note": { + "type": "string" + }, + "notifyEmail": { + "type": "string" + }, + "paymentExpectedTotal": { + "type": "number" + }, + "paymentExpiresAt": { + "type": "string" + }, + "paymentMethod": { + "type": "string" + }, + "productId": { + "type": "string" + }, + "productName": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "userAccount": { + "type": "string" + }, + "userName": { + "type": "string" + } + } + }, + "models.Product": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "coverUrl": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "deliveryMode": { + "type": "string" + }, + "description": { + "type": "string" + }, + "discountPrice": { + "type": "number" + }, + "fixedContent": { + "type": "string" + }, + "fulfillmentType": { + "type": "string" + }, + "id": { + "type": "string" + }, + "maxPerAccount": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "paymentQrUrls": { + "description": "PaymentQrURLs 萌芽支付收款码图片链接(可多条)。", + "type": "array", + "items": { + "type": "string" + } + }, + "price": { + "type": "number" + }, + "quantity": { + "type": "integer" + }, + "requireLogin": { + "type": "boolean" + }, + "screenshotUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "showContact": { + "type": "boolean" + }, + "showNote": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "totalSold": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + }, + "verificationUrl": { + "type": "string" + }, + "viewCount": { + "type": "integer" + } + } + }, + "storage.SMTPConfig": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "fromName": { + "type": "string" + }, + "host": { + "type": "string" + }, + "password": { + "type": "string" + }, + "port": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "AdminToken": { + "description": "管理端令牌(也可使用 Authorization 头或 query token,见各接口说明)", + "type": "apiKey", + "name": "X-Admin-Token", + "in": "header" + }, + "BearerAuth": { + "description": "用户访问令牌,格式: Bearer 空格 + token", + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "WebhookSecret": { + "description": "与服务端 WEBHOOK_MENGYA_SECRET 一致时校验萌芽支付 Webhook", + "type": "apiKey", + "name": "X-Webhook-Secret", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0.0-go", + Host: "localhost:8080", + BasePath: "/", + Schemes: []string{"http", "https"}, + Title: "萌芽小店 API", + Description: "商品、下单、站点统计、收藏与客服聊天;用户登录由萌芽账户认证中心(SproutGate)校验。监听地址以 HTTP_LISTEN_ADDR 为准。", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/mengyastore-backend-go/docs/swagger.json b/mengyastore-backend-go/docs/swagger.json new file mode 100644 index 0000000..a0cf097 --- /dev/null +++ b/mengyastore-backend-go/docs/swagger.json @@ -0,0 +1,2174 @@ +{ + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "商品、下单、站点统计、收藏与客服聊天;用户登录由萌芽账户认证中心(SproutGate)校验。监听地址以 HTTP_LISTEN_ADDR 为准。", + "title": "萌芽小店 API", + "contact": {}, + "version": "1.0.0-go" + }, + "host": "localhost:8080", + "basePath": "/", + "paths": { + "/": { + "get": { + "description": "返回服务描述、主要路径指引与版本(非 OpenAPI 详尽列表,完整契约见 /swagger)。", + "produces": [ + "application/json" + ], + "tags": [ + "元信息" + ], + "summary": "API 根信息与端点索引", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/admin/chat": { + "get": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-聊天" + ], + "summary": "全部会话列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/chat/{account}": { + "get": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-聊天" + ], + "summary": "指定用户聊天记录", + "parameters": [ + { + "type": "string", + "description": "用户账号", + "name": "account", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerMessagesWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "post": { + "security": [ + { + "AdminToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-聊天" + ], + "summary": "管理员回复", + "parameters": [ + { + "type": "string", + "description": "用户账号", + "name": "account", + "in": "path", + "required": true + }, + { + "description": "回复内容", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.AdminChatPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerOneChatMsgWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "delete": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-聊天" + ], + "summary": "清空会话", + "parameters": [ + { + "type": "string", + "description": "用户账号", + "name": "account", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerBoolOKWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/orders": { + "get": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-订单" + ], + "summary": "全部订单(管理)", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerOrdersBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/orders/{id}": { + "delete": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-订单" + ], + "summary": "删除订单", + "parameters": [ + { + "type": "string", + "description": "订单 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerBoolOKWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/products": { + "get": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-商品" + ], + "summary": "全部商品(管理)", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerProductListBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "post": { + "security": [ + { + "AdminToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-商品" + ], + "summary": "创建商品", + "parameters": [ + { + "description": "商品字段", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.ProductPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerProductOneBody" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/products/{id}": { + "put": { + "security": [ + { + "AdminToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-商品" + ], + "summary": "更新商品", + "parameters": [ + { + "type": "string", + "description": "商品 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "商品字段", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.ProductPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerProductOneBody" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "delete": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-商品" + ], + "summary": "删除商品", + "parameters": [ + { + "type": "string", + "description": "商品 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerBoolOKWrap" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/products/{id}/status": { + "patch": { + "security": [ + { + "AdminToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-商品" + ], + "summary": "切换上架状态", + "parameters": [ + { + "type": "string", + "description": "商品 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "{active}", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.TogglePayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerProductOneBody" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/site/maintenance": { + "post": { + "security": [ + { + "AdminToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-站点" + ], + "summary": "设置维护模式", + "parameters": [ + { + "description": "维护开关与原因", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.MaintenancePayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerMaintenanceWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/site/smtp": { + "get": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-站点" + ], + "summary": "获取 SMTP 配置", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerSMTPWrap" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "post": { + "security": [ + { + "AdminToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-站点" + ], + "summary": "保存 SMTP 配置", + "parameters": [ + { + "description": "SMTP 字段", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/storage.SMTPConfig" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerStringOKBody" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/system-status": { + "get": { + "security": [ + { + "AdminToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-系统" + ], + "summary": "系统运行状态", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/admin/verify": { + "post": { + "description": "响应为 {\"valid\": true/false},不泄露真实口令。", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "管理端-认证" + ], + "summary": "校验管理员令牌", + "parameters": [ + { + "description": "待校验 token", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.AdminVerifyTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerValidBody" + } + } + } + } + }, + "/api/chat/messages": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "聊天(用户)" + ], + "summary": "我的聊天消息", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerMessagesWrap" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "聊天(用户)" + ], + "summary": "发送用户消息", + "parameters": [ + { + "description": "消息正文", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.ChatMessagePayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerOneChatMsgWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "429": { + "description": "Too Many Requests", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/checkout": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "订单" + ], + "summary": "结账创建订单", + "parameters": [ + { + "type": "string", + "description": "Bearer 用户 token(部分商品必填)", + "name": "Authorization", + "in": "header" + }, + { + "description": "结账参数", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CheckoutPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerCheckoutWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "409": { + "description": "库存锁定冲突或同用户待支付", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/health": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "健康检查" + ], + "summary": "健康检查", + "responses": { + "200": { + "description": "status ok;rabbitmq 为 disabled|ok|error:...", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/orders": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "订单" + ], + "summary": "我的订单", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerOrdersBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/orders/{id}/cancel": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "订单" + ], + "summary": "取消订单", + "parameters": [ + { + "type": "string", + "description": "订单 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerCancelWrap" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/orders/{id}/confirm": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "订单" + ], + "summary": "确认订单", + "parameters": [ + { + "type": "string", + "description": "订单 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerConfirmWrap" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "409": { + "description": "待支付未到账等", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "410": { + "description": "已取消或超时", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/orders/{id}/payment-status": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "订单" + ], + "summary": "订单支付状态", + "parameters": [ + { + "type": "string", + "description": "订单 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerPaymentStatusWrap" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/products": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "公开" + ], + "summary": "上架商品列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerProductListBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/products/{id}/view": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "公开" + ], + "summary": "记录商品浏览", + "parameters": [ + { + "type": "string", + "description": "商品 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerProductViewWrap" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/site/maintenance": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "公开" + ], + "summary": "维护模式状态", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerMaintenanceWrap" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/site/visit": { + "post": { + "produces": [ + "application/json" + ], + "tags": [ + "公开" + ], + "summary": "记录站点访问", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerVisitWrap" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/stats": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "公开" + ], + "summary": "站点统计", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerStatsWrap" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/webhooks/mengya-pay": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Webhook" + ], + "summary": "萌芽支付 Webhook", + "parameters": [ + { + "type": "string", + "description": "与 WEBHOOK_MENGYA_SECRET 一致", + "name": "X-Webhook-Secret", + "in": "header" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerWebhookMengyaResp" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/wishlist": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "收藏" + ], + "summary": "收藏列表", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerWishlistWrap" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "收藏" + ], + "summary": "添加收藏", + "parameters": [ + { + "description": "商品 ID", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.WishlistItemPayload" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerWishlistWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + }, + "/api/wishlist/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "收藏" + ], + "summary": "移除收藏", + "parameters": [ + { + "type": "string", + "description": "商品 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.SwaggerWishlistWrap" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handlers.SwaggerErrorBody" + } + } + } + } + } + }, + "definitions": { + "handlers.AdminChatPayload": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + } + }, + "handlers.AdminVerifyTokenRequest": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "handlers.ChatMessagePayload": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + } + }, + "handlers.CheckoutPayload": { + "type": "object", + "properties": { + "contactEmail": { + "type": "string" + }, + "contactPhone": { + "type": "string" + }, + "note": { + "type": "string" + }, + "notifyEmail": { + "type": "string" + }, + "paymentMethod": { + "type": "string" + }, + "productId": { + "type": "string" + }, + "quantity": { + "type": "integer" + } + } + }, + "handlers.MaintenancePayload": { + "type": "object", + "properties": { + "maintenance": { + "type": "boolean" + }, + "reason": { + "type": "string" + } + } + }, + "handlers.ProductPayload": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "coverUrl": { + "type": "string" + }, + "deliveryMode": { + "type": "string" + }, + "description": { + "type": "string" + }, + "discountPrice": { + "type": "number" + }, + "fixedContent": { + "type": "string" + }, + "fulfillmentType": { + "type": "string" + }, + "maxPerAccount": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "paymentQrUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "price": { + "type": "number" + }, + "requireLogin": { + "type": "boolean" + }, + "screenshotUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "showContact": { + "type": "boolean" + }, + "showNote": { + "type": "boolean" + }, + "tags": { + "type": "string" + } + } + }, + "handlers.SwaggerBoolOK": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + } + } + }, + "handlers.SwaggerBoolOKWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerBoolOK" + } + } + }, + "handlers.SwaggerCancelMsg": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "ok": { + "type": "boolean" + } + } + }, + "handlers.SwaggerCancelWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerCancelMsg" + } + } + }, + "handlers.SwaggerCheckoutData": { + "type": "object", + "properties": { + "orderId": { + "type": "string" + }, + "paymentExpectedTotal": { + "type": "number" + }, + "paymentExpiresAt": { + "type": "string" + }, + "paymentMethod": { + "type": "string" + }, + "paymentQrUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "productId": { + "type": "string" + }, + "productQty": { + "type": "integer" + }, + "qrCodeUrl": { + "type": "string" + }, + "status": { + "type": "string" + }, + "viewCount": { + "type": "integer" + } + } + }, + "handlers.SwaggerCheckoutWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerCheckoutData" + } + } + }, + "handlers.SwaggerConfirmData": { + "type": "object", + "properties": { + "deliveredCodes": { + "type": "array", + "items": { + "type": "string" + } + }, + "deliveryMode": { + "type": "string" + }, + "isManual": { + "type": "boolean" + }, + "orderId": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "handlers.SwaggerConfirmWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerConfirmData" + } + } + }, + "handlers.SwaggerErrorBody": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "handlers.SwaggerMaintenanceData": { + "type": "object", + "properties": { + "maintenance": { + "type": "boolean" + }, + "reason": { + "type": "string" + } + } + }, + "handlers.SwaggerMaintenanceWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerMaintenanceData" + } + } + }, + "handlers.SwaggerMessagesInner": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ChatMessage" + } + } + } + }, + "handlers.SwaggerMessagesWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerMessagesInner" + } + } + }, + "handlers.SwaggerOneChatMsgWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerSingleChatWrap" + } + } + }, + "handlers.SwaggerOrdersBody": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Order" + } + } + } + }, + "handlers.SwaggerPaymentStatusData": { + "type": "object", + "properties": { + "expectedTotal": { + "type": "number" + }, + "paymentExpiresAt": { + "type": "string" + }, + "paymentMethod": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "handlers.SwaggerPaymentStatusWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerPaymentStatusData" + } + } + }, + "handlers.SwaggerProductListBody": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Product" + } + } + } + }, + "handlers.SwaggerProductOneBody": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.Product" + } + } + }, + "handlers.SwaggerProductViewData": { + "type": "object", + "properties": { + "counted": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "viewCount": { + "type": "integer" + } + } + }, + "handlers.SwaggerProductViewWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerProductViewData" + } + } + }, + "handlers.SwaggerSMTPWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/storage.SMTPConfig" + } + } + }, + "handlers.SwaggerSingleChatWrap": { + "type": "object", + "properties": { + "message": { + "$ref": "#/definitions/models.ChatMessage" + } + } + }, + "handlers.SwaggerStatsData": { + "type": "object", + "properties": { + "totalOrders": { + "type": "integer" + }, + "totalVisits": { + "type": "integer" + } + } + }, + "handlers.SwaggerStatsWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerStatsData" + } + } + }, + "handlers.SwaggerStringOKBody": { + "type": "object", + "properties": { + "data": { + "type": "string" + } + } + }, + "handlers.SwaggerValidBody": { + "type": "object", + "properties": { + "valid": { + "type": "boolean" + } + } + }, + "handlers.SwaggerVisitData": { + "type": "object", + "properties": { + "counted": { + "type": "boolean" + }, + "totalVisits": { + "type": "integer" + } + } + }, + "handlers.SwaggerVisitWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerVisitData" + } + } + }, + "handlers.SwaggerWebhookMengyaResp": { + "type": "object", + "properties": { + "matched": { + "type": "boolean" + }, + "matched_order_id": { + "type": "string" + }, + "ok": { + "type": "boolean" + } + } + }, + "handlers.SwaggerWishlistItems": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handlers.SwaggerWishlistWrap": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.SwaggerWishlistItems" + } + } + }, + "handlers.TogglePayload": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + } + } + }, + "handlers.WishlistItemPayload": { + "type": "object", + "properties": { + "productId": { + "type": "string" + } + } + }, + "models.ChatMessage": { + "type": "object", + "properties": { + "accountId": { + "type": "string" + }, + "accountName": { + "type": "string" + }, + "content": { + "type": "string" + }, + "fromAdmin": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "sentAt": { + "type": "string" + } + } + }, + "models.Order": { + "type": "object", + "properties": { + "contactEmail": { + "type": "string" + }, + "contactPhone": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "deliveredCodes": { + "type": "array", + "items": { + "type": "string" + } + }, + "deliveryMode": { + "type": "string" + }, + "id": { + "type": "string" + }, + "note": { + "type": "string" + }, + "notifyEmail": { + "type": "string" + }, + "paymentExpectedTotal": { + "type": "number" + }, + "paymentExpiresAt": { + "type": "string" + }, + "paymentMethod": { + "type": "string" + }, + "productId": { + "type": "string" + }, + "productName": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "userAccount": { + "type": "string" + }, + "userName": { + "type": "string" + } + } + }, + "models.Product": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "coverUrl": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "deliveryMode": { + "type": "string" + }, + "description": { + "type": "string" + }, + "discountPrice": { + "type": "number" + }, + "fixedContent": { + "type": "string" + }, + "fulfillmentType": { + "type": "string" + }, + "id": { + "type": "string" + }, + "maxPerAccount": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "paymentQrUrls": { + "description": "PaymentQrURLs 萌芽支付收款码图片链接(可多条)。", + "type": "array", + "items": { + "type": "string" + } + }, + "price": { + "type": "number" + }, + "quantity": { + "type": "integer" + }, + "requireLogin": { + "type": "boolean" + }, + "screenshotUrls": { + "type": "array", + "items": { + "type": "string" + } + }, + "showContact": { + "type": "boolean" + }, + "showNote": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "totalSold": { + "type": "integer" + }, + "updatedAt": { + "type": "string" + }, + "verificationUrl": { + "type": "string" + }, + "viewCount": { + "type": "integer" + } + } + }, + "storage.SMTPConfig": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "fromName": { + "type": "string" + }, + "host": { + "type": "string" + }, + "password": { + "type": "string" + }, + "port": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "AdminToken": { + "description": "管理端令牌(也可使用 Authorization 头或 query token,见各接口说明)", + "type": "apiKey", + "name": "X-Admin-Token", + "in": "header" + }, + "BearerAuth": { + "description": "用户访问令牌,格式: Bearer 空格 + token", + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "WebhookSecret": { + "description": "与服务端 WEBHOOK_MENGYA_SECRET 一致时校验萌芽支付 Webhook", + "type": "apiKey", + "name": "X-Webhook-Secret", + "in": "header" + } + } +} \ No newline at end of file diff --git a/mengyastore-backend-go/docs/swagger.yaml b/mengyastore-backend-go/docs/swagger.yaml new file mode 100644 index 0000000..32dd346 --- /dev/null +++ b/mengyastore-backend-go/docs/swagger.yaml @@ -0,0 +1,1390 @@ +basePath: / +definitions: + handlers.AdminChatPayload: + properties: + content: + type: string + type: object + handlers.AdminVerifyTokenRequest: + properties: + token: + type: string + type: object + handlers.ChatMessagePayload: + properties: + content: + type: string + type: object + handlers.CheckoutPayload: + properties: + contactEmail: + type: string + contactPhone: + type: string + note: + type: string + notifyEmail: + type: string + paymentMethod: + type: string + productId: + type: string + quantity: + type: integer + type: object + handlers.MaintenancePayload: + properties: + maintenance: + type: boolean + reason: + type: string + type: object + handlers.ProductPayload: + properties: + active: + type: boolean + codes: + items: + type: string + type: array + coverUrl: + type: string + deliveryMode: + type: string + description: + type: string + discountPrice: + type: number + fixedContent: + type: string + fulfillmentType: + type: string + maxPerAccount: + type: integer + name: + type: string + paymentQrUrls: + items: + type: string + type: array + price: + type: number + requireLogin: + type: boolean + screenshotUrls: + items: + type: string + type: array + showContact: + type: boolean + showNote: + type: boolean + tags: + type: string + type: object + handlers.SwaggerBoolOK: + properties: + ok: + type: boolean + type: object + handlers.SwaggerBoolOKWrap: + properties: + data: + $ref: '#/definitions/handlers.SwaggerBoolOK' + type: object + handlers.SwaggerCancelMsg: + properties: + message: + type: string + ok: + type: boolean + type: object + handlers.SwaggerCancelWrap: + properties: + data: + $ref: '#/definitions/handlers.SwaggerCancelMsg' + type: object + handlers.SwaggerCheckoutData: + properties: + orderId: + type: string + paymentExpectedTotal: + type: number + paymentExpiresAt: + type: string + paymentMethod: + type: string + paymentQrUrls: + items: + type: string + type: array + productId: + type: string + productQty: + type: integer + qrCodeUrl: + type: string + status: + type: string + viewCount: + type: integer + type: object + handlers.SwaggerCheckoutWrap: + properties: + data: + $ref: '#/definitions/handlers.SwaggerCheckoutData' + type: object + handlers.SwaggerConfirmData: + properties: + deliveredCodes: + items: + type: string + type: array + deliveryMode: + type: string + isManual: + type: boolean + orderId: + type: string + status: + type: string + type: object + handlers.SwaggerConfirmWrap: + properties: + data: + $ref: '#/definitions/handlers.SwaggerConfirmData' + type: object + handlers.SwaggerErrorBody: + properties: + error: + type: string + type: object + handlers.SwaggerMaintenanceData: + properties: + maintenance: + type: boolean + reason: + type: string + type: object + handlers.SwaggerMaintenanceWrap: + properties: + data: + $ref: '#/definitions/handlers.SwaggerMaintenanceData' + type: object + handlers.SwaggerMessagesInner: + properties: + messages: + items: + $ref: '#/definitions/models.ChatMessage' + type: array + type: object + handlers.SwaggerMessagesWrap: + properties: + data: + $ref: '#/definitions/handlers.SwaggerMessagesInner' + type: object + handlers.SwaggerOneChatMsgWrap: + properties: + data: + $ref: '#/definitions/handlers.SwaggerSingleChatWrap' + type: object + handlers.SwaggerOrdersBody: + properties: + data: + items: + $ref: '#/definitions/models.Order' + type: array + type: object + handlers.SwaggerPaymentStatusData: + properties: + expectedTotal: + type: number + paymentExpiresAt: + type: string + paymentMethod: + type: string + status: + type: string + type: object + handlers.SwaggerPaymentStatusWrap: + properties: + data: + $ref: '#/definitions/handlers.SwaggerPaymentStatusData' + type: object + handlers.SwaggerProductListBody: + properties: + data: + items: + $ref: '#/definitions/models.Product' + type: array + type: object + handlers.SwaggerProductOneBody: + properties: + data: + $ref: '#/definitions/models.Product' + type: object + handlers.SwaggerProductViewData: + properties: + counted: + type: boolean + id: + type: string + viewCount: + type: integer + type: object + handlers.SwaggerProductViewWrap: + properties: + data: + $ref: '#/definitions/handlers.SwaggerProductViewData' + type: object + handlers.SwaggerSMTPWrap: + properties: + data: + $ref: '#/definitions/storage.SMTPConfig' + type: object + handlers.SwaggerSingleChatWrap: + properties: + message: + $ref: '#/definitions/models.ChatMessage' + type: object + handlers.SwaggerStatsData: + properties: + totalOrders: + type: integer + totalVisits: + type: integer + type: object + handlers.SwaggerStatsWrap: + properties: + data: + $ref: '#/definitions/handlers.SwaggerStatsData' + type: object + handlers.SwaggerStringOKBody: + properties: + data: + type: string + type: object + handlers.SwaggerValidBody: + properties: + valid: + type: boolean + type: object + handlers.SwaggerVisitData: + properties: + counted: + type: boolean + totalVisits: + type: integer + type: object + handlers.SwaggerVisitWrap: + properties: + data: + $ref: '#/definitions/handlers.SwaggerVisitData' + type: object + handlers.SwaggerWebhookMengyaResp: + properties: + matched: + type: boolean + matched_order_id: + type: string + ok: + type: boolean + type: object + handlers.SwaggerWishlistItems: + properties: + items: + items: + type: string + type: array + type: object + handlers.SwaggerWishlistWrap: + properties: + data: + $ref: '#/definitions/handlers.SwaggerWishlistItems' + type: object + handlers.TogglePayload: + properties: + active: + type: boolean + type: object + handlers.WishlistItemPayload: + properties: + productId: + type: string + type: object + models.ChatMessage: + properties: + accountId: + type: string + accountName: + type: string + content: + type: string + fromAdmin: + type: boolean + id: + type: string + sentAt: + type: string + type: object + models.Order: + properties: + contactEmail: + type: string + contactPhone: + type: string + createdAt: + type: string + deliveredCodes: + items: + type: string + type: array + deliveryMode: + type: string + id: + type: string + note: + type: string + notifyEmail: + type: string + paymentExpectedTotal: + type: number + paymentExpiresAt: + type: string + paymentMethod: + type: string + productId: + type: string + productName: + type: string + quantity: + type: integer + status: + type: string + userAccount: + type: string + userName: + type: string + type: object + models.Product: + properties: + active: + type: boolean + codes: + items: + type: string + type: array + coverUrl: + type: string + createdAt: + type: string + deliveryMode: + type: string + description: + type: string + discountPrice: + type: number + fixedContent: + type: string + fulfillmentType: + type: string + id: + type: string + maxPerAccount: + type: integer + name: + type: string + paymentQrUrls: + description: PaymentQrURLs 萌芽支付收款码图片链接(可多条)。 + items: + type: string + type: array + price: + type: number + quantity: + type: integer + requireLogin: + type: boolean + screenshotUrls: + items: + type: string + type: array + showContact: + type: boolean + showNote: + type: boolean + tags: + items: + type: string + type: array + totalSold: + type: integer + updatedAt: + type: string + verificationUrl: + type: string + viewCount: + type: integer + type: object + storage.SMTPConfig: + properties: + email: + type: string + enabled: + type: boolean + fromName: + type: string + host: + type: string + password: + type: string + port: + type: string + type: object +host: localhost:8080 +info: + contact: {} + description: 商品、下单、站点统计、收藏与客服聊天;用户登录由萌芽账户认证中心(SproutGate)校验。监听地址以 HTTP_LISTEN_ADDR + 为准。 + title: 萌芽小店 API + version: 1.0.0-go +paths: + /: + get: + description: 返回服务描述、主要路径指引与版本(非 OpenAPI 详尽列表,完整契约见 /swagger)。 + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + summary: API 根信息与端点索引 + tags: + - 元信息 + /api/admin/chat: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 全部会话列表 + tags: + - 管理端-聊天 + /api/admin/chat/{account}: + delete: + parameters: + - description: 用户账号 + in: path + name: account + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerBoolOKWrap' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 清空会话 + tags: + - 管理端-聊天 + get: + parameters: + - description: 用户账号 + in: path + name: account + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerMessagesWrap' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 指定用户聊天记录 + tags: + - 管理端-聊天 + post: + consumes: + - application/json + parameters: + - description: 用户账号 + in: path + name: account + required: true + type: string + - description: 回复内容 + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.AdminChatPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerOneChatMsgWrap' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 管理员回复 + tags: + - 管理端-聊天 + /api/admin/orders: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerOrdersBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 全部订单(管理) + tags: + - 管理端-订单 + /api/admin/orders/{id}: + delete: + parameters: + - description: 订单 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerBoolOKWrap' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 删除订单 + tags: + - 管理端-订单 + /api/admin/products: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerProductListBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 全部商品(管理) + tags: + - 管理端-商品 + post: + consumes: + - application/json + parameters: + - description: 商品字段 + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.ProductPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerProductOneBody' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 创建商品 + tags: + - 管理端-商品 + /api/admin/products/{id}: + delete: + parameters: + - description: 商品 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerBoolOKWrap' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 删除商品 + tags: + - 管理端-商品 + put: + consumes: + - application/json + parameters: + - description: 商品 ID + in: path + name: id + required: true + type: string + - description: 商品字段 + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.ProductPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerProductOneBody' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "404": + description: Not Found + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 更新商品 + tags: + - 管理端-商品 + /api/admin/products/{id}/status: + patch: + consumes: + - application/json + parameters: + - description: 商品 ID + in: path + name: id + required: true + type: string + - description: '{active}' + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.TogglePayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerProductOneBody' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "404": + description: Not Found + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 切换上架状态 + tags: + - 管理端-商品 + /api/admin/site/maintenance: + post: + consumes: + - application/json + parameters: + - description: 维护开关与原因 + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.MaintenancePayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerMaintenanceWrap' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 设置维护模式 + tags: + - 管理端-站点 + /api/admin/site/smtp: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerSMTPWrap' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 获取 SMTP 配置 + tags: + - 管理端-站点 + post: + consumes: + - application/json + parameters: + - description: SMTP 字段 + in: body + name: body + required: true + schema: + $ref: '#/definitions/storage.SMTPConfig' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerStringOKBody' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 保存 SMTP 配置 + tags: + - 管理端-站点 + /api/admin/system-status: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - AdminToken: [] + summary: 系统运行状态 + tags: + - 管理端-系统 + /api/admin/verify: + post: + consumes: + - application/json + description: '响应为 {"valid": true/false},不泄露真实口令。' + parameters: + - description: 待校验 token + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.AdminVerifyTokenRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerValidBody' + summary: 校验管理员令牌 + tags: + - 管理端-认证 + /api/chat/messages: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerMessagesWrap' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - BearerAuth: [] + summary: 我的聊天消息 + tags: + - 聊天(用户) + post: + consumes: + - application/json + parameters: + - description: 消息正文 + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.ChatMessagePayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerOneChatMsgWrap' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "429": + description: Too Many Requests + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - BearerAuth: [] + summary: 发送用户消息 + tags: + - 聊天(用户) + /api/checkout: + post: + consumes: + - application/json + parameters: + - description: Bearer 用户 token(部分商品必填) + in: header + name: Authorization + type: string + - description: 结账参数 + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.CheckoutPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerCheckoutWrap' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "404": + description: Not Found + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "409": + description: 库存锁定冲突或同用户待支付 + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + summary: 结账创建订单 + tags: + - 订单 + /api/health: + get: + produces: + - application/json + responses: + "200": + description: status ok;rabbitmq 为 disabled|ok|error:... + schema: + additionalProperties: true + type: object + summary: 健康检查 + tags: + - 健康检查 + /api/orders: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerOrdersBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - BearerAuth: [] + summary: 我的订单 + tags: + - 订单 + /api/orders/{id}/cancel: + post: + parameters: + - description: 订单 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerCancelWrap' + "404": + description: Not Found + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "409": + description: Conflict + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + summary: 取消订单 + tags: + - 订单 + /api/orders/{id}/confirm: + post: + parameters: + - description: 订单 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerConfirmWrap' + "404": + description: Not Found + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "409": + description: 待支付未到账等 + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "410": + description: 已取消或超时 + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + summary: 确认订单 + tags: + - 订单 + /api/orders/{id}/payment-status: + get: + parameters: + - description: 订单 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerPaymentStatusWrap' + "404": + description: Not Found + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + summary: 订单支付状态 + tags: + - 订单 + /api/products: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerProductListBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + summary: 上架商品列表 + tags: + - 公开 + /api/products/{id}/view: + post: + parameters: + - description: 商品 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerProductViewWrap' + "404": + description: Not Found + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + summary: 记录商品浏览 + tags: + - 公开 + /api/site/maintenance: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerMaintenanceWrap' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + summary: 维护模式状态 + tags: + - 公开 + /api/site/visit: + post: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerVisitWrap' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + summary: 记录站点访问 + tags: + - 公开 + /api/stats: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerStatsWrap' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + summary: 站点统计 + tags: + - 公开 + /api/webhooks/mengya-pay: + post: + consumes: + - application/json + parameters: + - description: 与 WEBHOOK_MENGYA_SECRET 一致 + in: header + name: X-Webhook-Secret + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerWebhookMengyaResp' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "403": + description: Forbidden + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + summary: 萌芽支付 Webhook + tags: + - Webhook + /api/wishlist: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerWishlistWrap' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - BearerAuth: [] + summary: 收藏列表 + tags: + - 收藏 + post: + consumes: + - application/json + parameters: + - description: 商品 ID + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.WishlistItemPayload' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerWishlistWrap' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - BearerAuth: [] + summary: 添加收藏 + tags: + - 收藏 + /api/wishlist/{id}: + delete: + parameters: + - description: 商品 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handlers.SwaggerWishlistWrap' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handlers.SwaggerErrorBody' + security: + - BearerAuth: [] + summary: 移除收藏 + tags: + - 收藏 +schemes: +- http +- https +securityDefinitions: + AdminToken: + description: 管理端令牌(也可使用 Authorization 头或 query token,见各接口说明) + in: header + name: X-Admin-Token + type: apiKey + BearerAuth: + description: '用户访问令牌,格式: Bearer 空格 + token' + in: header + name: Authorization + type: apiKey + WebhookSecret: + description: 与服务端 WEBHOOK_MENGYA_SECRET 一致时校验萌芽支付 Webhook + in: header + name: X-Webhook-Secret + type: apiKey +swagger: "2.0" diff --git a/mengyastore-backend-go/go.mod b/mengyastore-backend-go/go.mod index dff371b..acd32e3 100644 --- a/mengyastore-backend-go/go.mod +++ b/mengyastore-backend-go/go.mod @@ -1,19 +1,26 @@ module mengyastore-backend -go 1.21.0 +go 1.23.0 require ( github.com/gin-contrib/cors v1.7.2 github.com/gin-gonic/gin v1.9.1 + github.com/go-sql-driver/mysql v1.9.3 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/rabbitmq/amqp091-go v1.10.0 + github.com/redis/go-redis/v9 v9.18.0 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.1 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.31.1 ) require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -22,30 +29,36 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect - github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.1 // indirect - github.com/redis/go-redis/v9 v9.18.0 // indirect + github.com/swaggo/swag v1.8.12 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/arch v0.7.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/protobuf v1.34.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/mengyastore-backend-go/go.sum b/mengyastore-backend-go/go.sum index f652402..4f5195d 100644 --- a/mengyastore-backend-go/go.sum +++ b/mengyastore-backend-go/go.sum @@ -1,5 +1,15 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -20,10 +30,22 @@ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uq github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -36,8 +58,8 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1 github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -47,18 +69,27 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -66,6 +97,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -81,6 +113,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -88,10 +121,19 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= +github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -99,24 +141,62 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= diff --git a/mengyastore-backend-go/internal/config/config.go b/mengyastore-backend-go/internal/config/config.go index 11cbf1e..301e957 100644 --- a/mengyastore-backend-go/internal/config/config.go +++ b/mengyastore-backend-go/internal/config/config.go @@ -10,7 +10,7 @@ import ( "github.com/joho/godotenv" ) -// Config is populated entirely from environment variables (optionally set via .env — see Load). +// Config 全部由环境变量填充(可通过工作目录下 .env.development 或 ENV_FILE 设置,见 Load)。 type Config struct { AppEnv string AdminToken string @@ -31,17 +31,26 @@ type Config struct { RabbitMQEnabled bool RabbitMQURL string RabbitMQEnv string + + WebhookMengyaSecret string + PaymentPendingTTLSecs int + + // GinDebug:为 true 时启用 Gin Debug(打印路由表等);默认 false,日志更干净。 + GinDebug bool + + // EnableSwagger:为 true 时注册 /swagger UI;默认在 GIN_DEBUG 或 ENABLE_SWAGGER 为真时为 true。 + EnableSwagger bool } -// App environment: affects defaults when DATABASE_DSN / RABBITMQ_ENV are omitted. -// APP_ENV=production → prod DB default, RABBITMQ_ENV default prod -// Otherwise → development defaults (test DB, rabbit dev). +// 应用环境:在未设置 DATABASE_DSN / RABBITMQ_ENV 时决定默认值。 +// APP_ENV=production → 生产库 DSN、RABBITMQ_ENV 默认 prod。 +// 否则 → 开发默认(测试库、RabbitMQ 开发环境)。 const ( EnvDevelopment = "development" EnvProduction = "production" ) -// Built-in DSN fallbacks when DATABASE_DSN is empty. +// 内建 DSN:当 DATABASE_DSN 为空时使用。 const ( TestDSN = "mengyastore-test:mengyastore-test@tcp(10.1.1.100:3306)/mengyastore-test?charset=utf8mb4&parseTime=True&loc=Local" ProdDSN = "mengyastore:mengyastore@tcp(192.168.1.100:3306)/mengyastore?charset=utf8mb4&parseTime=True&loc=Local" @@ -60,26 +69,33 @@ const ( DefaultRedisPassword = "tyh@19900420" ) -// Load reads optional .env file(s) then builds Config from the process environment. +// Load 读取可选的 env 文件,再从进程环境组装 Config。 // -// Dotenv resolution order: -// 1. File named by ENV_FILE (if set) -// 2. .env in current working directory +// 解析 env 文件的顺序: +// 1. 环境变量 ENV_FILE 指定的文件(若设置了) +// 2. 当前工作目录下的 .env.development(不存在则忽略) // -// Variables (all optional unless noted): +// 环境变量说明(除非注明否则均可选): // -// APP_ENV — development | production (default: development) -// ADMIN_TOKEN — admin API token (default: changeme) -// AUTH_API_URL — SproutGate base URL -// DATABASE_DSN — MySQL DSN; if empty, uses TestDSN or ProdDSN from APP_ENV +// APP_ENV — development | production(默认 development) +// ADMIN_TOKEN — 管理端 API 令牌(默认 changeme) +// AUTH_API_URL — 萌芽认证中心 SproutGate 根 URL +// DATABASE_DSN — MySQL DSN;为空则按 APP_ENV 选用 TestDSN / ProdDSN // RABBITMQ_ENABLED — true/false -// RABBITMQ_URL — full amqp URL -// RABBITMQ_ENV — dev | prod (default from APP_ENV) -// RABBITMQ_PASSWORD — if RABBITMQ_URL empty but RABBITMQ_ENABLED, builds URL with host/vhost defaults +// RABBITMQ_URL — 完整 amqp URL +// RABBITMQ_ENV — dev | prod(默认随 APP_ENV) +// RABBITMQ_PASSWORD — 若已启用 MQ 但未设 RABBITMQ_URL,则用默认主机/vhost 拼 URL 时需要口令 // // HTTP_LISTEN_ADDR — 进程监听,默认 :8080 // PUBLIC_API_BASE_URL — 对外 API 基地址(反向代理场景) // +// WEBHOOK_MENGYA_SECRET — 若设置,到账 Webhook 须带相同请求头 X-Webhook-Secret +// PAYMENT_PENDING_SECONDS — 萌芽支付待支付超时(秒,默认 60) +// +// GIN_DEBUG — 1/true/yes/on:Gin Debug;默认关闭;为真时同时开启 Swagger UI(/swagger) +// ENABLE_SWAGGER — 1/true/yes/on:启用 Swagger UI;与 GIN_DEBUG 任一为真即注册 /swagger +// WEBHOOK_LOG_VERBOSE — 1/true:萌芽 Webhook 日志附带 notice/body 摘要(排查渠道用) +// // REDIS_ENABLED — 默认 true;显式 false/0/off 则关闭 // REDIS_ADDR — 默认 development→TestRedisAddr,production→ProdRedisAddr // REDIS_PASSWORD — 默认 DefaultRedisPassword(建议生产用环境变量覆盖) @@ -168,10 +184,34 @@ func Load() (*Config, error) { } } + cfg.WebhookMengyaSecret = strings.TrimSpace(os.Getenv("WEBHOOK_MENGYA_SECRET")) + if sec := strings.TrimSpace(os.Getenv("PAYMENT_PENDING_SECONDS")); sec != "" { + if n, err := strconv.Atoi(sec); err == nil && n > 0 { + cfg.PaymentPendingTTLSecs = n + } + } + if cfg.PaymentPendingTTLSecs <= 0 { + cfg.PaymentPendingTTLSecs = 60 + } + + cfg.GinDebug = envTruthy(os.Getenv("GIN_DEBUG")) + + cfg.EnableSwagger = cfg.GinDebug || envTruthy(os.Getenv("ENABLE_SWAGGER")) + return cfg, nil } -// redisEnabledFromEnv defaults to true unless explicitly turned off. +// envTruthy:将 1、true、yes、on 视为真(不区分大小写)。 +func envTruthy(s string) bool { + switch strings.ToLower(strings.TrimSpace(s)) { + case "1", "true", "yes", "on": + return true + default: + return false + } +} + +// redisEnabledFromEnv:除非显式关闭,否则默认启用 Redis。 func redisEnabledFromEnv(v string) bool { s := strings.ToLower(strings.TrimSpace(v)) if s == "false" || s == "0" || s == "no" || s == "off" { @@ -188,8 +228,8 @@ func loadDotenv() { _ = godotenv.Load(f) return } - // Standard local file; ignore missing. - _ = godotenv.Load(filepath.Clean(".env")) + // 本地开发默认文件名;不存在则忽略。 + _ = godotenv.Load(filepath.Clean(".env.development")) } func normalizeAppEnv(s string) string { diff --git a/mengyastore-backend-go/internal/database/db.go b/mengyastore-backend-go/internal/database/db.go index 2d5f102..566830f 100644 --- a/mengyastore-backend-go/internal/database/db.go +++ b/mengyastore-backend-go/internal/database/db.go @@ -2,6 +2,8 @@ package database import ( "log" + "log/slog" + "os" "time" "gorm.io/driver/mysql" @@ -11,8 +13,17 @@ import ( // Open 初始化 GORM 数据库连接并自动同步所有表结构。 func Open(dsn string) (*gorm.DB, error) { + gormLogger := logger.New( + log.New(os.Stdout, "", log.LstdFlags|log.Lmicroseconds), + logger.Config{ + SlowThreshold: 500 * time.Millisecond, + LogLevel: logger.Warn, + IgnoreRecordNotFoundError: true, + Colorful: false, + }, + ) db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Warn), + Logger: gormLogger, }) if err != nil { return nil, err @@ -29,7 +40,7 @@ func Open(dsn string) (*gorm.DB, error) { if err := autoMigrate(db); err != nil { return nil, err } - log.Println("[DB] 数据库连接成功,表结构已同步") + slog.Info("db", "event", "ready", "migrate", "ok") return db, nil } diff --git a/mengyastore-backend-go/internal/database/models.go b/mengyastore-backend-go/internal/database/models.go index ce1e283..996a300 100644 --- a/mengyastore-backend-go/internal/database/models.go +++ b/mengyastore-backend-go/internal/database/models.go @@ -7,7 +7,7 @@ import ( "time" ) -// StringSlice is a JSON-serialized string slice stored as a MySQL TEXT/JSON column. +// StringSlice 表示以 JSON 序列化形式存入 MySQL TEXT/JSON 列的字符串切片。 type StringSlice []string func (s StringSlice) Value() (driver.Value, error) { @@ -31,9 +31,9 @@ func (s *StringSlice) Scan(src any) error { return json.Unmarshal(raw, s) } -// ─── Products ──────────────────────────────────────────────────────────────── +// ─── 商品 ───────────────────────────────────────────────────────────────────── -// ProductRow is the GORM model for the `products` table. +// ProductRow 为 `products` 表的 GORM 模型。 type ProductRow struct { ID string `gorm:"primaryKey;size:36"` Name string `gorm:"size:255;not null"` @@ -55,12 +55,14 @@ type ProductRow struct { FixedContent string `gorm:"type:text"` ShowNote bool `gorm:"default:true"` ShowContact bool `gorm:"default:true"` + // PaymentQrURLs JSON:萌芽支付等多张收款码图片链接。 + PaymentQrURLs StringSlice `gorm:"column:payment_qr_urls;type:json"` CreatedAt time.Time `gorm:"index"` } func (ProductRow) TableName() string { return "products" } -// ProductCodeRow stores individual codes for a product (one row per code). +// ProductCodeRow 单条卡密一行,按商品维度存储。 type ProductCodeRow struct { ID uint `gorm:"primaryKey;autoIncrement"` ProductID string `gorm:"size:36;not null;index"` @@ -69,7 +71,7 @@ type ProductCodeRow struct { func (ProductCodeRow) TableName() string { return "product_codes" } -// ─── Orders ────────────────────────────────────────────────────────────────── +// ─── 订单 ───────────────────────────────────────────────────────────────────── type OrderRow struct { ID string `gorm:"primaryKey;size:36"` @@ -85,14 +87,20 @@ type OrderRow struct { ContactPhone string `gorm:"size:50"` ContactEmail string `gorm:"size:255"` NotifyEmail string `gorm:"size:255"` + // PaymentMethod 如 mengya;历史订单或免费单可能为空。 + PaymentMethod string `gorm:"size:32;default:'';index"` + // PaymentExpectedTotal 待支付时应付快照(元),用于 Webhook 金额核对。 + PaymentExpectedTotal float64 `gorm:"default:0"` + // PaymentExpiresAt 待支付截止时间;非待支付订单为 NULL。 + PaymentExpiresAt *time.Time `gorm:"index"` CreatedAt time.Time } func (OrderRow) TableName() string { return "orders" } -// ─── Site settings ─────────────────────────────────────────────────────────── +// ─── 站点设置 ───────────────────────────────────────────────────────────────── -// SiteSettingRow stores arbitrary key-value pairs for site-wide settings. +// SiteSettingRow 站点级键值配置。 type SiteSettingRow struct { Key string `gorm:"primaryKey;size:64"` Value string `gorm:"type:text"` @@ -100,7 +108,7 @@ type SiteSettingRow struct { func (SiteSettingRow) TableName() string { return "site_settings" } -// ─── Wishlists ─────────────────────────────────────────────────────────────── +// ─── 收藏夹 ─────────────────────────────────────────────────────────────────── type WishlistRow struct { ID uint `gorm:"primaryKey;autoIncrement"` @@ -110,7 +118,7 @@ type WishlistRow struct { func (WishlistRow) TableName() string { return "wishlists" } -// ─── Chat messages ─────────────────────────────────────────────────────────── +// ─── 聊天消息 ───────────────────────────────────────────────────────────────── type ChatMessageRow struct { ID string `gorm:"primaryKey;size:36"` diff --git a/mengyastore-backend-go/internal/handlers/admin_chat.go b/mengyastore-backend-go/internal/handlers/admin_chat.go index a354fcc..e23a2e0 100644 --- a/mengyastore-backend-go/internal/handlers/admin_chat.go +++ b/mengyastore-backend-go/internal/handlers/admin_chat.go @@ -8,6 +8,14 @@ import ( ) // GetAllConversations 返回所有用户会话列表。 +// @Summary 全部会话列表 +// @Tags 管理端-聊天 +// @Produce json +// @Security AdminToken +// @Success 200 {object} map[string]interface{} +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/admin/chat [get] func (h *AdminHandler) GetAllConversations(c *gin.Context) { if !h.requireAdmin(c) { return @@ -21,6 +29,16 @@ func (h *AdminHandler) GetAllConversations(c *gin.Context) { } // GetConversation 返回指定账号的全部消息记录。 +// @Summary 指定用户聊天记录 +// @Tags 管理端-聊天 +// @Produce json +// @Security AdminToken +// @Param account path string true "用户账号" +// @Success 200 {object} SwaggerMessagesWrap +// @Failure 400 {object} SwaggerErrorBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/admin/chat/{account} [get] func (h *AdminHandler) GetConversation(c *gin.Context) { if !h.requireAdmin(c) { return @@ -38,11 +56,23 @@ func (h *AdminHandler) GetConversation(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": gin.H{"messages": msgs}}) } -type adminChatPayload struct { +type AdminChatPayload struct { Content string `json:"content"` } // AdminReply 向指定用户发送管理员回复。 +// @Summary 管理员回复 +// @Tags 管理端-聊天 +// @Accept json +// @Produce json +// @Security AdminToken +// @Param account path string true "用户账号" +// @Param body body AdminChatPayload true "回复内容" +// @Success 200 {object} SwaggerOneChatMsgWrap +// @Failure 400 {object} SwaggerErrorBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/admin/chat/{account} [post] func (h *AdminHandler) AdminReply(c *gin.Context) { if !h.requireAdmin(c) { return @@ -52,7 +82,7 @@ func (h *AdminHandler) AdminReply(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "缺少账号参数"}) return } - var payload adminChatPayload + var payload AdminChatPayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数有误"}) return @@ -71,6 +101,16 @@ func (h *AdminHandler) AdminReply(c *gin.Context) { } // ClearConversation 清除与指定用户的全部消息记录。 +// @Summary 清空会话 +// @Tags 管理端-聊天 +// @Produce json +// @Security AdminToken +// @Param account path string true "用户账号" +// @Success 200 {object} SwaggerBoolOKWrap +// @Failure 400 {object} SwaggerErrorBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/admin/chat/{account} [delete] func (h *AdminHandler) ClearConversation(c *gin.Context) { if !h.requireAdmin(c) { return diff --git a/mengyastore-backend-go/internal/handlers/admin_orders.go b/mengyastore-backend-go/internal/handlers/admin_orders.go index 7fd120e..ce56f53 100644 --- a/mengyastore-backend-go/internal/handlers/admin_orders.go +++ b/mengyastore-backend-go/internal/handlers/admin_orders.go @@ -6,6 +6,15 @@ import ( "github.com/gin-gonic/gin" ) +// ListAllOrders 管理端全部订单。 +// @Summary 全部订单(管理) +// @Tags 管理端-订单 +// @Produce json +// @Security AdminToken +// @Success 200 {object} SwaggerOrdersBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/admin/orders [get] func (h *AdminHandler) ListAllOrders(c *gin.Context) { if !h.requireAdmin(c) { return @@ -18,6 +27,17 @@ func (h *AdminHandler) ListAllOrders(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": orders}) } +// DeleteOrder 管理端删除订单。 +// @Summary 删除订单 +// @Tags 管理端-订单 +// @Produce json +// @Security AdminToken +// @Param id path string true "订单 ID" +// @Success 200 {object} SwaggerBoolOKWrap +// @Failure 400 {object} SwaggerErrorBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/admin/orders/{id} [delete] func (h *AdminHandler) DeleteOrder(c *gin.Context) { if !h.requireAdmin(c) { return diff --git a/mengyastore-backend-go/internal/handlers/admin_product.go b/mengyastore-backend-go/internal/handlers/admin_product.go index dcb7aeb..454e665 100644 --- a/mengyastore-backend-go/internal/handlers/admin_product.go +++ b/mengyastore-backend-go/internal/handlers/admin_product.go @@ -9,7 +9,7 @@ import ( "mengyastore-backend/internal/models" ) -type productPayload struct { +type ProductPayload struct { Name string `json:"name"` Price float64 `json:"price"` DiscountPrice float64 `json:"discountPrice"` @@ -17,6 +17,7 @@ type productPayload struct { CoverURL string `json:"coverUrl"` Codes []string `json:"codes"` ScreenshotURLs []string `json:"screenshotUrls"` + PaymentQrURLs []string `json:"paymentQrUrls"` Description string `json:"description"` Active *bool `json:"active"` RequireLogin bool `json:"requireLogin"` @@ -28,7 +29,7 @@ type productPayload struct { ShowContact bool `json:"showContact"` } -func normalizeFulfillmentPayload(payload *productPayload) string { +func normalizeFulfillmentPayload(payload *ProductPayload) string { ft := strings.TrimSpace(strings.ToLower(payload.FulfillmentType)) if ft == "fixed" { return "fixed" @@ -36,16 +37,26 @@ func normalizeFulfillmentPayload(payload *productPayload) string { return "card" } -type togglePayload struct { +type TogglePayload struct { Active bool `json:"active"` } -// VerifyAdminToken checks whether the supplied token is correct. -// Returns {"valid": true/false} without leaking the real token value. +// AdminVerifyTokenRequest 管理端校验令牌(Body,非 Header)。 +type AdminVerifyTokenRequest struct { + Token string `json:"token"` +} + +// VerifyAdminToken 校验请求中的令牌是否正确。 +// @Summary 校验管理员令牌 +// @Description 响应为 {"valid": true/false},不泄露真实口令。 +// @Tags 管理端-认证 +// @Accept json +// @Produce json +// @Param body body AdminVerifyTokenRequest true "待校验 token" +// @Success 200 {object} SwaggerValidBody +// @Router /api/admin/verify [post] func (h *AdminHandler) VerifyAdminToken(c *gin.Context) { - var payload struct { - Token string `json:"token"` - } + var payload AdminVerifyTokenRequest if err := c.ShouldBindJSON(&payload); err != nil || payload.Token == "" { c.JSON(http.StatusOK, gin.H{"valid": false}) return @@ -53,6 +64,15 @@ func (h *AdminHandler) VerifyAdminToken(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"valid": payload.Token == h.cfg.AdminToken}) } +// ListAllProducts 管理端商品全量列表(含下架与卡密等敏感字段)。 +// @Summary 全部商品(管理) +// @Tags 管理端-商品 +// @Produce json +// @Security AdminToken +// @Success 200 {object} SwaggerProductListBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/admin/products [get] func (h *AdminHandler) ListAllProducts(c *gin.Context) { if !h.requireAdmin(c) { return @@ -65,18 +85,35 @@ func (h *AdminHandler) ListAllProducts(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": items}) } +// CreateProduct 创建商品。 +// @Summary 创建商品 +// @Tags 管理端-商品 +// @Accept json +// @Produce json +// @Security AdminToken +// @Param body body ProductPayload true "商品字段" +// @Success 200 {object} SwaggerProductOneBody +// @Failure 400 {object} SwaggerErrorBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/admin/products [post] func (h *AdminHandler) CreateProduct(c *gin.Context) { if !h.requireAdmin(c) { return } - var payload productPayload + var payload ProductPayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数有误"}) return } screenshotURLs, valid := normalizeScreenshotURLs(payload.ScreenshotURLs) if !valid { - c.JSON(http.StatusBadRequest, gin.H{"error": "截图链接最多 5 条"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "截图链接最多 6 条"}) + return + } + paymentQrURLs, valid := normalizePaymentQrURLs(payload.PaymentQrURLs) + if !valid { + c.JSON(http.StatusBadRequest, gin.H{"error": "萌芽收款渠道链接最多 6 条"}) return } active := true @@ -100,6 +137,7 @@ func (h *AdminHandler) CreateProduct(c *gin.Context) { CoverURL: strings.TrimSpace(payload.CoverURL), Codes: payload.Codes, ScreenshotURLs: screenshotURLs, + PaymentQrURLs: paymentQrURLs, Description: payload.Description, Active: active, RequireLogin: payload.RequireLogin, @@ -118,19 +156,38 @@ func (h *AdminHandler) CreateProduct(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": created}) } +// UpdateProduct 更新商品。 +// @Summary 更新商品 +// @Tags 管理端-商品 +// @Accept json +// @Produce json +// @Security AdminToken +// @Param id path string true "商品 ID" +// @Param body body ProductPayload true "商品字段" +// @Success 200 {object} SwaggerProductOneBody +// @Failure 400 {object} SwaggerErrorBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 404 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/admin/products/{id} [put] func (h *AdminHandler) UpdateProduct(c *gin.Context) { if !h.requireAdmin(c) { return } id := c.Param("id") - var payload productPayload + var payload ProductPayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数有误"}) return } screenshotURLs, valid := normalizeScreenshotURLs(payload.ScreenshotURLs) if !valid { - c.JSON(http.StatusBadRequest, gin.H{"error": "截图链接最多 5 条"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "截图链接最多 6 条"}) + return + } + paymentQrURLs, valid := normalizePaymentQrURLs(payload.PaymentQrURLs) + if !valid { + c.JSON(http.StatusBadRequest, gin.H{"error": "萌芽收款渠道链接最多 6 条"}) return } active := false @@ -154,6 +211,7 @@ func (h *AdminHandler) UpdateProduct(c *gin.Context) { CoverURL: strings.TrimSpace(payload.CoverURL), Codes: payload.Codes, ScreenshotURLs: screenshotURLs, + PaymentQrURLs: paymentQrURLs, Description: payload.Description, Active: active, RequireLogin: payload.RequireLogin, @@ -172,12 +230,25 @@ func (h *AdminHandler) UpdateProduct(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": updated}) } +// ToggleProduct 上架/下架切换。 +// @Summary 切换上架状态 +// @Tags 管理端-商品 +// @Accept json +// @Produce json +// @Security AdminToken +// @Param id path string true "商品 ID" +// @Param body body TogglePayload true "{active}" +// @Success 200 {object} SwaggerProductOneBody +// @Failure 400 {object} SwaggerErrorBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 404 {object} SwaggerErrorBody +// @Router /api/admin/products/{id}/status [patch] func (h *AdminHandler) ToggleProduct(c *gin.Context) { if !h.requireAdmin(c) { return } id := c.Param("id") - var payload togglePayload + var payload TogglePayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数有误"}) return @@ -190,6 +261,16 @@ func (h *AdminHandler) ToggleProduct(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": updated}) } +// DeleteProduct 删除商品。 +// @Summary 删除商品 +// @Tags 管理端-商品 +// @Produce json +// @Security AdminToken +// @Param id path string true "商品 ID" +// @Success 200 {object} SwaggerBoolOKWrap +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/admin/products/{id} [delete] func (h *AdminHandler) DeleteProduct(c *gin.Context) { if !h.requireAdmin(c) { return @@ -202,6 +283,26 @@ func (h *AdminHandler) DeleteProduct(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true}}) } +const maxScreenshotURLsAdmin = 6 +const maxPaymentQrURLsAdmin = 6 + +func normalizePaymentQrURLs(urls []string) ([]string, bool) { + cleaned := make([]string, 0, len(urls)) + seen := map[string]bool{} + for _, u := range urls { + trimmed := strings.TrimSpace(u) + if trimmed == "" || seen[trimmed] { + continue + } + seen[trimmed] = true + cleaned = append(cleaned, trimmed) + if len(cleaned) > maxPaymentQrURLsAdmin { + return nil, false + } + } + return cleaned, true +} + func normalizeScreenshotURLs(urls []string) ([]string, bool) { cleaned := make([]string, 0, len(urls)) for _, url := range urls { @@ -210,7 +311,7 @@ func normalizeScreenshotURLs(urls []string) ([]string, bool) { continue } cleaned = append(cleaned, trimmed) - if len(cleaned) > 5 { + if len(cleaned) > maxScreenshotURLsAdmin { return nil, false } } diff --git a/mengyastore-backend-go/internal/handlers/admin_site.go b/mengyastore-backend-go/internal/handlers/admin_site.go index 5521d1c..5670eae 100644 --- a/mengyastore-backend-go/internal/handlers/admin_site.go +++ b/mengyastore-backend-go/internal/handlers/admin_site.go @@ -8,16 +8,28 @@ import ( "mengyastore-backend/internal/storage" ) -type maintenancePayload struct { +type MaintenancePayload struct { Maintenance bool `json:"maintenance"` Reason string `json:"reason"` } +// SetMaintenance 设置站点维护模式。 +// @Summary 设置维护模式 +// @Tags 管理端-站点 +// @Accept json +// @Produce json +// @Security AdminToken +// @Param body body MaintenancePayload true "维护开关与原因" +// @Success 200 {object} SwaggerMaintenanceWrap +// @Failure 400 {object} SwaggerErrorBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/admin/site/maintenance [post] func (h *AdminHandler) SetMaintenance(c *gin.Context) { if !h.requireAdmin(c) { return } - var payload maintenancePayload + var payload MaintenancePayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数有误"}) return @@ -34,6 +46,15 @@ func (h *AdminHandler) SetMaintenance(c *gin.Context) { }) } +// GetSMTPConfig 获取 SMTP 配置(密码脱敏)。 +// @Summary 获取 SMTP 配置 +// @Tags 管理端-站点 +// @Produce json +// @Security AdminToken +// @Success 200 {object} SwaggerSMTPWrap +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/admin/site/smtp [get] func (h *AdminHandler) GetSMTPConfig(c *gin.Context) { if !h.requireAdmin(c) { return @@ -51,6 +72,18 @@ func (h *AdminHandler) GetSMTPConfig(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": masked}) } +// SetSMTPConfig 保存 SMTP 配置。 +// @Summary 保存 SMTP 配置 +// @Tags 管理端-站点 +// @Accept json +// @Produce json +// @Security AdminToken +// @Param body body storage.SMTPConfig true "SMTP 字段" +// @Success 200 {object} SwaggerStringOKBody +// @Failure 400 {object} SwaggerErrorBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/admin/site/smtp [post] func (h *AdminHandler) SetSMTPConfig(c *gin.Context) { if !h.requireAdmin(c) { return diff --git a/mengyastore-backend-go/internal/handlers/admin_status.go b/mengyastore-backend-go/internal/handlers/admin_status.go index da7b3cd..e5414da 100644 --- a/mengyastore-backend-go/internal/handlers/admin_status.go +++ b/mengyastore-backend-go/internal/handlers/admin_status.go @@ -17,7 +17,7 @@ import ( "mengyastore-backend/internal/mq" ) -// SystemStatusHandler exposes admin-only operational status. +// SystemStatusHandler 向管理端暴露运维状态(仅供管理员)。 type SystemStatusHandler struct { cfg *config.Config db *gormDB.DB @@ -29,7 +29,14 @@ func NewSystemStatusHandler(cfg *config.Config, db *gormDB.DB, mqClient *mq.Clie return &SystemStatusHandler{cfg: cfg, db: db, mq: mqClient, start: startedAt} } -// GetSystemStatus returns JSON for the admin dashboard. Requires admin token. +// GetSystemStatus 返回管理后台用 JSON,需通过管理员令牌。 +// @Summary 系统运行状态 +// @Tags 管理端-系统 +// @Produce json +// @Security AdminToken +// @Success 200 {object} map[string]interface{} +// @Failure 401 {object} SwaggerErrorBody +// @Router /api/admin/system-status [get] func (h *SystemStatusHandler) GetSystemStatus(c *gin.Context) { if !h.adminTokenOK(c) { return @@ -278,7 +285,7 @@ func (h *SystemStatusHandler) rabbitmqInfo() gin.H { return out } -// parseAMQPBroker returns host, port, vhost, user without password (for display only). +// parseAMQPBroker 从 amqp URL 解析主机、端口、vhost、用户名(不含口令,仅展示用)。 func parseAMQPBroker(raw string) (host, port, vhost, user string) { if raw == "" { return "", "", "", "" diff --git a/mengyastore-backend-go/internal/handlers/chat.go b/mengyastore-backend-go/internal/handlers/chat.go index ddaaa80..ff2d76d 100644 --- a/mengyastore-backend-go/internal/handlers/chat.go +++ b/mengyastore-backend-go/internal/handlers/chat.go @@ -35,6 +35,14 @@ func (h *ChatHandler) requireChatUser(c *gin.Context) (account, name string, ok } // GetMyMessages 返回当前登录用户的全部聊天消息。 +// @Summary 我的聊天消息 +// @Tags 聊天(用户) +// @Produce json +// @Security BearerAuth +// @Success 200 {object} SwaggerMessagesWrap +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/chat/messages [get] func (h *ChatHandler) GetMyMessages(c *gin.Context) { account, _, ok := h.requireChatUser(c) if !ok { @@ -48,17 +56,30 @@ func (h *ChatHandler) GetMyMessages(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": gin.H{"messages": msgs}}) } -type chatMessagePayload struct { +// ChatMessagePayload 用户发送消息请求体。 +type ChatMessagePayload struct { Content string `json:"content"` } // SendMyMessage 向管理员发送一条用户消息。 +// @Summary 发送用户消息 +// @Tags 聊天(用户) +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param body body ChatMessagePayload true "消息正文" +// @Success 200 {object} SwaggerOneChatMsgWrap +// @Failure 400 {object} SwaggerErrorBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 429 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/chat/messages [post] func (h *ChatHandler) SendMyMessage(c *gin.Context) { account, name, ok := h.requireChatUser(c) if !ok { return } - var payload chatMessagePayload + var payload ChatMessagePayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数有误"}) return diff --git a/mengyastore-backend-go/internal/handlers/order.go b/mengyastore-backend-go/internal/handlers/order.go index d05e538..93cf428 100644 --- a/mengyastore-backend-go/internal/handlers/order.go +++ b/mengyastore-backend-go/internal/handlers/order.go @@ -1,49 +1,84 @@ package handlers import ( + "bytes" "context" + "crypto/subtle" + "encoding/json" + "errors" "fmt" - "log" + "io" + "log/slog" + "math" "net/http" "net/url" + "os" "strings" "time" "github.com/gin-gonic/gin" + "gorm.io/gorm" "mengyastore-backend/internal/auth" + "mengyastore-backend/internal/database" "mengyastore-backend/internal/email" "mengyastore-backend/internal/models" "mengyastore-backend/internal/mq" + "mengyastore-backend/internal/payment" "mengyastore-backend/internal/storage" ) const qrSize = "320x320" +var errCheckoutProductInactive = errors.New("product inactive") + +type checkoutBusyError struct{ wait int } + +func (e *checkoutBusyError) Error() string { + return fmt.Sprintf("正在有人购买中...请等待%d秒", e.wait) +} + +type sameUserPendingError struct{ wait int } + +func (e *sameUserPendingError) Error() string { + return fmt.Sprintf("您有待支付的订单,请完成支付或等待%d秒后重试", e.wait) +} + type OrderHandler struct { - productStore *storage.ProductStore - orderStore *storage.OrderStore - siteStore *storage.SiteStore - authClient *auth.SproutGateClient - mqClient *mq.Client // optional; nil disables RabbitMQ path + productStore *storage.ProductStore + orderStore *storage.OrderStore + siteStore *storage.SiteStore + authClient *auth.SproutGateClient + mqClient *mq.Client + paymentPendingTTL time.Duration + webhookMengyaSecret string } -type checkoutPayload struct { - ProductID string `json:"productId"` - - Quantity int `json:"quantity"` - - Note string `json:"note"` - - ContactPhone string `json:"contactPhone"` - - ContactEmail string `json:"contactEmail"` - - NotifyEmail string `json:"notifyEmail"` +// CheckoutPayload 结账请求体(POST /api/checkout)。 +type CheckoutPayload struct { + ProductID string `json:"productId"` + Quantity int `json:"quantity"` + Note string `json:"note"` + ContactPhone string `json:"contactPhone"` + ContactEmail string `json:"contactEmail"` + NotifyEmail string `json:"notifyEmail"` + PaymentMethod string `json:"paymentMethod"` } -func NewOrderHandler(productStore *storage.ProductStore, orderStore *storage.OrderStore, siteStore *storage.SiteStore, authClient *auth.SproutGateClient, mqClient *mq.Client) *OrderHandler { - return &OrderHandler{productStore: productStore, orderStore: orderStore, siteStore: siteStore, authClient: authClient, mqClient: mqClient} +func NewOrderHandler(productStore *storage.ProductStore, orderStore *storage.OrderStore, siteStore *storage.SiteStore, authClient *auth.SproutGateClient, mqClient *mq.Client, pendingPaymentSecs int, webhookMengyaSecret string) *OrderHandler { + secs := pendingPaymentSecs + if secs <= 0 { + secs = 60 + } + return &OrderHandler{ + productStore: productStore, + orderStore: orderStore, + siteStore: siteStore, + authClient: authClient, + mqClient: mqClient, + paymentPendingTTL: time.Duration(secs) * time.Second, + webhookMengyaSecret: webhookMengyaSecret, + } } func (h *OrderHandler) sendOrderNotify(toEmail, toName, productName, orderID string, qty int, codes []string, isManual bool) { @@ -68,10 +103,10 @@ func (h *OrderHandler) sendOrderNotify(toEmail, toName, productName, orderID str }) cancel() if err == nil { - log.Printf("[MQ] queued order email order=%s to=%s", orderID, toEmail) + slog.Info("mq", "event", "order_email_queued", "order_id", orderID, "to_email", toEmail) return } - log.Printf("[MQ] publish failed, fallback email: %v", err) + slog.Warn("mq", "event", "publish_failed", "err", err) } go func() { @@ -91,392 +126,737 @@ func (h *OrderHandler) sendOrderNotify(toEmail, toName, productName, orderID str Codes: codes, IsManual: isManual, }); err != nil { - log.Printf("[Email] 发送通知失败 order=%s to=%s: %v", orderID, toEmail, err) + slog.Warn("email", "event", "order_notify_failed", "order_id", orderID, "to", toEmail, "err", err) } else { - log.Printf("[Email] 发送通知成功 order=%s to=%s", orderID, toEmail) + slog.Info("email", "event", "order_notify_sent", "order_id", orderID, "to", toEmail) } }() } func (h *OrderHandler) tryExtractUserWithEmail(c *gin.Context) (account, username, userEmail string) { - authHeader := c.GetHeader("Authorization") - if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { - return "", "", "" - } - userToken := strings.TrimPrefix(authHeader, "Bearer ") - result, err := h.authClient.VerifyToken(userToken) - if err != nil || !result.Valid || result.User == nil { - return "", "", "" - } - return result.User.Account, result.User.Username, result.User.Email - } -func (h *OrderHandler) CreateOrder(c *gin.Context) { +func effectiveUnitPrice(p models.Product) float64 { + if p.Price == 0 { + return 0 + } + if p.DiscountPrice > 0 && p.DiscountPrice < p.Price { + return p.DiscountPrice + } + return p.Price +} +func snapMoney(v float64) float64 { + return math.Round(v*100) / 100 +} + +func (h *OrderHandler) allocateInventory(product *models.Product, qty int) (delivered []string, updated models.Product, err error) { + isFixed := strings.EqualFold(strings.TrimSpace(product.FulfillmentType), "fixed") + if isFixed { + content := strings.TrimSpace(product.FixedContent) + if content == "" { + return nil, models.Product{}, fmt.Errorf("fixed content missing") + } + delivered = make([]string, qty) + for i := 0; i < qty; i++ { + delivered[i] = content + } + up, e := h.productStore.GetByID(product.ID) + return delivered, up, e + } + if product.Quantity < qty { + return nil, models.Product{}, fmt.Errorf("out of stock") + } + deliveredCodes, ok := extractCodes(product, qty) + if !ok { + return nil, models.Product{}, fmt.Errorf("codes insufficient") + } + product.Quantity = len(product.Codes) + up, e := h.productStore.Update(product.ID, *product) + if e != nil { + return nil, models.Product{}, e + } + return deliveredCodes, up, nil +} + +func qrFromOrder(orderID, productID string) string { + qrPayload := fmt.Sprintf("order:%s:%s", orderID, productID) + return fmt.Sprintf("https://api.qrserver.com/v1/create-qr-code/?size=%s&data=%s", qrSize, url.QueryEscape(qrPayload)) +} + +// CreateOrder 创建订单(结账)。需登录与否取决于商品 requireLogin;Bearer 可选传入以关联账户与限购。 +// @Summary 结账创建订单 +// @Tags 订单 +// @Accept json +// @Produce json +// @Param Authorization header string false "Bearer 用户 token(部分商品必填)" +// @Param body body CheckoutPayload true "结账参数" +// @Success 200 {object} SwaggerCheckoutWrap +// @Failure 400 {object} SwaggerErrorBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 404 {object} SwaggerErrorBody +// @Failure 409 {object} SwaggerErrorBody "库存锁定冲突或同用户待支付" +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/checkout [post] +func (h *OrderHandler) CreateOrder(c *gin.Context) { userAccount, userName, userEmail := h.tryExtractUserWithEmail(c) - var payload checkoutPayload - + var payload CheckoutPayload if err := c.ShouldBindJSON(&payload); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数有误"}) - return - } payload.ProductID = strings.TrimSpace(payload.ProductID) - if payload.ProductID == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "缺少必填字段"}) - return - } - if payload.Quantity <= 0 { - payload.Quantity = 1 - } product, err := h.productStore.GetByID(payload.ProductID) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) - return - } - if !product.Active { - c.JSON(http.StatusBadRequest, gin.H{"error": "商品暂时无法购买"}) - return - } - if product.RequireLogin && userAccount == "" { - c.JSON(http.StatusUnauthorized, gin.H{"error": "该商品需要登录后才能购买"}) - return - } if product.MaxPerAccount > 0 && userAccount != "" { - purchased, err := h.orderStore.CountPurchasedByAccount(userAccount, product.ID) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - if purchased+payload.Quantity > product.MaxPerAccount { - remain := product.MaxPerAccount - purchased - if remain <= 0 { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("每个账户最多购买 %d 个,您已达上限", product.MaxPerAccount)}) - } else { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("每个账户最多购买 %d 个,您还可购买 %d 个", product.MaxPerAccount, remain)}) - } - return - } - } - var deliveredCodes []string + total := snapMoney(effectiveUnitPrice(product) * float64(payload.Quantity)) - var updatedProduct models.Product - - isFixed := strings.EqualFold(strings.TrimSpace(product.FulfillmentType), "fixed") - - if isFixed { - - content := strings.TrimSpace(product.FixedContent) - - if content == "" { - - c.JSON(http.StatusBadRequest, gin.H{"error": "该商品为固定内容发货,但未配置发货内容,请联系管理员"}) - - return - - } - - deliveredCodes = make([]string, payload.Quantity) - - for i := 0; i < payload.Quantity; i++ { - - deliveredCodes[i] = content - - } - - var err error - - updatedProduct, err = h.productStore.GetByID(product.ID) - - if err != nil { - - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - - return - - } - - } else { - - if product.Quantity < payload.Quantity { - - c.JSON(http.StatusBadRequest, gin.H{"error": "库存不足"}) - - return - - } - - var ok bool - - deliveredCodes, ok = extractCodes(&product, payload.Quantity) - - if !ok { - - c.JSON(http.StatusBadRequest, gin.H{"error": "卡密不足"}) - - return - - } - - product.Quantity = len(product.Codes) - - var err error - - updatedProduct, err = h.productStore.Update(product.ID, product) - - if err != nil { - - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - - return - - } + rawPM := strings.ToLower(strings.TrimSpace(payload.PaymentMethod)) + if total > 0 && (rawPM == "legacy_manual" || rawPM == "legacy" || rawPM == "manual") { + c.JSON(http.StatusBadRequest, gin.H{"error": "该支付方式已停用,请使用萌芽支付"}) + return + } + notifyEmail := strings.TrimSpace(userEmail) + if notifyEmail == "" { + notifyEmail = strings.TrimSpace(payload.NotifyEmail) + } + if notifyEmail == "" { + notifyEmail = strings.TrimSpace(payload.ContactEmail) } deliveryMode := "auto" - notifyEmail := strings.TrimSpace(userEmail) - - if notifyEmail == "" { - - notifyEmail = strings.TrimSpace(payload.NotifyEmail) - + if total > 0 { + hasQR := false + for _, u := range product.PaymentQrURLs { + if strings.TrimSpace(u) != "" { + hasQR = true + break + } + } + if !hasQR { + c.JSON(http.StatusBadRequest, gin.H{"error": "该商品尚未配置萌芽支付收款渠道,请在管理后台上传收款码图片链接"}) + return + } } - if notifyEmail == "" { - - notifyEmail = strings.TrimSpace(payload.ContactEmail) - - } - - order := models.Order{ - - ProductID: updatedProduct.ID, - - ProductName: updatedProduct.Name, - - UserAccount: userAccount, - - UserName: userName, - - Quantity: payload.Quantity, - - DeliveredCodes: deliveredCodes, - - Status: "completed", - - DeliveryMode: deliveryMode, - - Note: strings.TrimSpace(payload.Note), - - ContactPhone: strings.TrimSpace(payload.ContactPhone), - - ContactEmail: strings.TrimSpace(payload.ContactEmail), - - NotifyEmail: notifyEmail, - } - - created, err := h.orderStore.Create(order) - - if err != nil { - - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - - return - - } - - if err := h.productStore.IncrementSold(updatedProduct.ID, payload.Quantity); err != nil { - - log.Printf("[Order] 更新销量失败 (非致命): %v", err) - - } - - h.sendOrderNotify(notifyEmail, userName, updatedProduct.Name, created.ID, payload.Quantity, deliveredCodes, false) - - qrPayload := fmt.Sprintf("order:%s:%s", created.ID, created.ProductID) - - qrURL := fmt.Sprintf("https://api.qrserver.com/v1/create-qr-code/?size=%s&data=%s", qrSize, url.QueryEscape(qrPayload)) - - c.JSON(http.StatusOK, gin.H{ - - "data": gin.H{ - - "orderId": created.ID, - - "qrCodeUrl": qrURL, - - "productId": created.ProductID, - - "productQty": created.Quantity, - - "viewCount": updatedProduct.ViewCount, - - "status": created.Status, - }, - }) - -} - -func (h *OrderHandler) ConfirmOrder(c *gin.Context) { - - orderID := c.Param("id") - - order, err := h.orderStore.Confirm(orderID) - - if err != nil { - - c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) - - return - - } - - isManual := order.DeliveryMode == "manual" - - if isManual { - - confirmNotifyEmail := order.NotifyEmail - - if confirmNotifyEmail == "" { - - confirmNotifyEmail = order.ContactEmail - + if total <= 0 { + deliveredCodes, updatedProduct, err := h.allocateInventory(&product, payload.Quantity) + if err != nil { + msg := err.Error() + switch msg { + case "fixed content missing": + c.JSON(http.StatusBadRequest, gin.H{"error": "该商品为固定内容发货,但未配置发货内容,请联系管理员"}) + case "out of stock": + c.JSON(http.StatusBadRequest, gin.H{"error": "库存不足"}) + case "codes insufficient": + c.JSON(http.StatusBadRequest, gin.H{"error": "卡密不足"}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": msg}) + } + return } - h.sendOrderNotify(confirmNotifyEmail, order.UserName, order.ProductName, order.ID, order.Quantity, order.DeliveredCodes, false) + baseOrder := models.Order{ + ProductID: updatedProduct.ID, + ProductName: updatedProduct.Name, + UserAccount: userAccount, + UserName: userName, + Quantity: payload.Quantity, + DeliveredCodes: deliveredCodes, + DeliveryMode: deliveryMode, + Note: strings.TrimSpace(payload.Note), + ContactPhone: strings.TrimSpace(payload.ContactPhone), + ContactEmail: strings.TrimSpace(payload.ContactEmail), + NotifyEmail: notifyEmail, + } + baseOrder.Status = "completed" + baseOrder.PaymentMethod = payment.MethodMengya + created, err := h.orderStore.Create(baseOrder) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if err := h.productStore.IncrementSold(updatedProduct.ID, payload.Quantity); err != nil { + slog.Warn("order", "event", "increment_sold_failed", "err", err) + } + h.sendOrderNotify(notifyEmail, userName, updatedProduct.Name, created.ID, payload.Quantity, deliveredCodes, false) + resp := gin.H{ + "orderId": created.ID, + "qrCodeUrl": qrFromOrder(created.ID, created.ProductID), + "productId": created.ProductID, + "productQty": created.Quantity, + "viewCount": updatedProduct.ViewCount, + "status": created.Status, + "paymentMethod": payment.MethodMengya, + } + c.JSON(http.StatusOK, gin.H{"data": resp}) + return } - c.JSON(http.StatusOK, gin.H{ + var ( + created models.Order + updatedProduct models.Product + ) - "data": gin.H{ + err = h.productStore.Transaction(func(tx *gorm.DB) error { + now := time.Now().UTC() + plock, err := h.productStore.GetByIDForUpdate(tx, payload.ProductID) + if err != nil { + return err + } + if !plock.Active { + return errCheckoutProductInactive + } - "orderId": order.ID, + var block database.OrderRow + err = tx.Where("product_id = ? AND status = ? AND payment_expires_at IS NOT NULL AND payment_expires_at > ?", + payload.ProductID, "pending_payment", now). + Order("created_at ASC").First(&block).Error + if err == nil { + w := int(math.Ceil(block.PaymentExpiresAt.Sub(now).Seconds())) + if w < 1 { + w = 1 + } + req := strings.TrimSpace(userAccount) + holder := strings.TrimSpace(block.UserAccount) + if req != "" && holder == req { + return &sameUserPendingError{wait: w} + } + return &checkoutBusyError{wait: w} + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } - "status": order.Status, + deliveredCodes, updated, err := h.allocateInventoryTx(tx, &plock, payload.Quantity) + if err != nil { + return err + } + updatedProduct = updated - "deliveryMode": order.DeliveryMode, - - "deliveredCodes": order.DeliveredCodes, - - "isManual": isManual, - }, + exp := time.Now().UTC().Add(h.paymentPendingTTL) + pending := models.Order{ + ProductID: updated.ID, + ProductName: updated.Name, + UserAccount: userAccount, + UserName: userName, + Quantity: payload.Quantity, + DeliveredCodes: deliveredCodes, + DeliveryMode: deliveryMode, + Note: strings.TrimSpace(payload.Note), + ContactPhone: strings.TrimSpace(payload.ContactPhone), + ContactEmail: strings.TrimSpace(payload.ContactEmail), + NotifyEmail: notifyEmail, + Status: "pending_payment", + PaymentMethod: payment.MethodMengya, + PaymentExpectedTotal: total, + PaymentExpiresAt: &exp, + } + cr, err := h.orderStore.CreateTx(tx, pending) + if err != nil { + return err + } + created = cr + return nil }) + if err != nil { + var busy *checkoutBusyError + if errors.As(err, &busy) { + c.JSON(http.StatusConflict, gin.H{"error": busy.Error()}) + return + } + var dup *sameUserPendingError + if errors.As(err, &dup) { + c.JSON(http.StatusConflict, gin.H{"error": dup.Error()}) + return + } + if errors.Is(err, errCheckoutProductInactive) { + c.JSON(http.StatusBadRequest, gin.H{"error": "商品暂时无法购买"}) + return + } + msg := err.Error() + switch msg { + case "fixed content missing": + c.JSON(http.StatusBadRequest, gin.H{"error": "该商品为固定内容发货,但未配置发货内容,请联系管理员"}) + case "out of stock": + c.JSON(http.StatusBadRequest, gin.H{"error": "库存不足"}) + case "codes insufficient": + c.JSON(http.StatusBadRequest, gin.H{"error": "卡密不足"}) + case "product not found": + c.JSON(http.StatusNotFound, gin.H{"error": "商品不存在"}) + default: + c.JSON(http.StatusInternalServerError, gin.H{"error": msg}) + } + return + } + + resp := gin.H{ + "orderId": created.ID, + "qrCodeUrl": qrFromOrder(created.ID, created.ProductID), + "productId": created.ProductID, + "productQty": created.Quantity, + "viewCount": updatedProduct.ViewCount, + "status": created.Status, + "paymentMethod": payment.MethodMengya, + "paymentExpectedTotal": created.PaymentExpectedTotal, + "paymentQrUrls": updatedProduct.PaymentQrURLs, + "paymentExpiresAt": created.PaymentExpiresAt, + } + c.JSON(http.StatusOK, gin.H{"data": resp}) } -func (h *OrderHandler) ListMyOrders(c *gin.Context) { +func confirmResponse(order models.Order) gin.H { + isManual := order.DeliveryMode == "manual" + return gin.H{ + "orderId": order.ID, + "status": order.Status, + "deliveryMode": order.DeliveryMode, + "deliveredCodes": order.DeliveredCodes, + "isManual": isManual, + } +} - authHeader := c.GetHeader("Authorization") - - if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { - - c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"}) +// ConfirmOrder 用户确认订单状态(手动核销、待支付轮询后取货等)。 +// @Summary 确认订单 +// @Tags 订单 +// @Produce json +// @Param id path string true "订单 ID" +// @Success 200 {object} SwaggerConfirmWrap +// @Failure 404 {object} SwaggerErrorBody +// @Failure 409 {object} SwaggerErrorBody "待支付未到账等" +// @Failure 410 {object} SwaggerErrorBody "已取消或超时" +// @Router /api/orders/{id}/confirm [post] +func (h *OrderHandler) ConfirmOrder(c *gin.Context) { + orderID := c.Param("id") + order, err := h.orderStore.GetByID(orderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + switch order.Status { + case "completed": + isManual := order.DeliveryMode == "manual" + if isManual { + confirmNotifyEmail := order.NotifyEmail + if confirmNotifyEmail == "" { + confirmNotifyEmail = order.ContactEmail + } + h.sendOrderNotify(confirmNotifyEmail, order.UserName, order.ProductName, order.ID, order.Quantity, order.DeliveredCodes, false) + } + c.JSON(http.StatusOK, gin.H{"data": confirmResponse(order)}) return + case "cancelled": + c.JSON(http.StatusGone, gin.H{"error": "订单已取消"}) + return + + case "pending_payment": + h.syncExpiredPending(order) + order, err = h.orderStore.GetByID(orderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + if order.Status == "cancelled" { + c.JSON(http.StatusGone, gin.H{"error": "订单已超时未支付"}) + return + } + if order.Status == "completed" { + h.sendOrderNotify(order.NotifyEmail, order.UserName, order.ProductName, order.ID, order.Quantity, order.DeliveredCodes, false) + c.JSON(http.StatusOK, gin.H{"data": confirmResponse(order)}) + return + } + c.JSON(http.StatusConflict, gin.H{"error": "等待到账确认,到账后系统将自动核销;您也可稍后再次点击"}) + return + + default: + // 手动待发货等,状态仍为 pending + order2, err := h.orderStore.Confirm(orderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + isManual := order2.DeliveryMode == "manual" + if isManual { + confirmNotifyEmail := order2.NotifyEmail + if confirmNotifyEmail == "" { + confirmNotifyEmail = order2.ContactEmail + } + h.sendOrderNotify(confirmNotifyEmail, order2.UserName, order2.ProductName, order2.ID, order2.Quantity, order2.DeliveredCodes, false) + } + c.JSON(http.StatusOK, gin.H{"data": confirmResponse(order2)}) + return + } +} + +// GetOrderPaymentStatus 前端轮询订单支付状态(不返回卡密)。 +// @Summary 订单支付状态 +// @Tags 订单 +// @Produce json +// @Param id path string true "订单 ID" +// @Success 200 {object} SwaggerPaymentStatusWrap +// @Failure 404 {object} SwaggerErrorBody +// @Router /api/orders/{id}/payment-status [get] +func (h *OrderHandler) GetOrderPaymentStatus(c *gin.Context) { + orderID := c.Param("id") + order, err := h.orderStore.GetByID(orderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + if order.Status == "pending_payment" { + h.syncExpiredPending(order) + order, _ = h.orderStore.GetByID(orderID) + } + + out := gin.H{ + "status": order.Status, + "expectedTotal": order.PaymentExpectedTotal, + "paymentExpiresAt": order.PaymentExpiresAt, + "paymentMethod": order.PaymentMethod, + } + c.JSON(http.StatusOK, gin.H{"data": out}) +} + +func (h *OrderHandler) syncExpiredPending(order models.Order) { + if order.Status != "pending_payment" || order.PaymentExpiresAt == nil { + return + } + if time.Now().UTC().Before(*order.PaymentExpiresAt) { + return + } + if err := h.releasePendingReserved(order); err != nil { + slog.Warn("payment", "event", "expire_release_failed", "order_id", order.ID, "err", err) + } +} + +func (h *OrderHandler) releasePendingReserved(o models.Order) error { + changed, err := h.orderStore.CancelPendingStaleIfEligible(o.ID) + if err != nil { + return err + } + if !changed { + return nil + } + up, err := h.productStore.GetByID(o.ProductID) + if err != nil { + return err + } + fixed := strings.EqualFold(strings.TrimSpace(up.FulfillmentType), "fixed") + if !fixed && len(o.DeliveredCodes) > 0 { + if err := h.productStore.PrependReservedCodes(o.ProductID, o.DeliveredCodes); err != nil { + return err + } + } + return nil +} +func (h *OrderHandler) releaseAllExpired(now time.Time) { + list, err := h.orderStore.ListExpiredPendingPayments(now) + if err != nil { + slog.Warn("payment", "event", "list_expired_failed", "err", err) + return + } + for _, o := range list { + if err := h.releasePendingReserved(o); err != nil { + slog.Warn("payment", "event", "expire_sweep_failed", "order_id", o.ID, "err", err) + } + } +} + +// RunExpiredPaymentSweep 后台定期释放超时未支付的预留库存。 +func (h *OrderHandler) RunExpiredPaymentSweep(ctx context.Context, every time.Duration) { + t := time.NewTicker(every) + defer t.Stop() + for { + select { + case <-ctx.Done(): + return + case <-t.C: + h.releaseAllExpired(time.Now().UTC()) + } + } +} + +func webhookSecretMatches(expected string, hdr string) bool { + if expected == "" { + return true + } + got := strings.TrimSpace(hdr) + if len(got) != len(expected) { + return false + } + return subtle.ConstantTimeCompare([]byte(expected), []byte(got)) == 1 +} + +func webhookLogVerbose() bool { + switch strings.ToLower(strings.TrimSpace(os.Getenv("WEBHOOK_LOG_VERBOSE"))) { + case "1", "true", "yes", "on": + return true + default: + return false + } +} + +func clampOneLine(s string, maxRunes int) string { + if maxRunes <= 0 { + maxRunes = 200 + } + s = strings.ReplaceAll(strings.ReplaceAll(s, "\r\n", " "), "\n", " ") + s = strings.TrimSpace(s) + r := []rune(s) + if len(r) > maxRunes { + return string(r[:maxRunes]) + "…" + } + return s +} + +// MengyaPaymentWebhook 萌芽支付到账通知(Body 为渠道 JSON)。若配置 WEBHOOK_MENGYA_SECRET 则必须带 X-Webhook-Secret。 +// @Summary 萌芽支付 Webhook +// @Tags Webhook +// @Accept json +// @Produce json +// @Param X-Webhook-Secret header string false "与 WEBHOOK_MENGYA_SECRET 一致" +// @Success 200 {object} SwaggerWebhookMengyaResp +// @Failure 400 {object} SwaggerErrorBody +// @Failure 403 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/webhooks/mengya-pay [post] +func (h *OrderHandler) MengyaPaymentWebhook(c *gin.Context) { + verbose := webhookLogVerbose() + if !webhookSecretMatches(h.webhookMengyaSecret, c.GetHeader("X-Webhook-Secret")) { + slog.Warn("mengya webhook", "result", "forbidden", "remote", c.ClientIP()) + c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) + return + } + + raw, err := io.ReadAll(c.Request.Body) + if err != nil { + slog.Warn("mengya webhook", "result", "read_body", "err", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "read body"}) + return + } + + noticeHint := payment.NoticeFieldsForLog(raw) + amount, parseOK := payment.ParseWebhookJSON(raw) + + base := []any{ + "remote", c.ClientIP(), + "bytes", len(raw), + "parse_ok", parseOK, + "amount", amount, + } + if verbose { + base = append(base, + "notice", clampOneLine(noticeHint, 280), + "body", clampOneLine(string(raw), 600), + ) + } + + if !parseOK || amount <= 0 { + if json.Valid(bytes.TrimSpace(raw)) && noticeHint == "" { + base = append(base, "hint", "json_without_notice_keys") + } + slog.Warn("mengya webhook", append([]any{"result", "parse_amount_failed"}, base...)...) + c.JSON(http.StatusBadRequest, gin.H{"error": "无法解析到账金额"}) + return + } + + o, matched, err := h.orderStore.TryFulfillMengyaPayment(amount, time.Now().UTC()) + if err != nil { + slog.Error("mengya webhook", append([]any{"result", "fulfill_error", "err", err}, base...)...) + c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) + return + } + + orderID := "-" + if matched { + orderID = o.ID + } + result := "no_order_match" + if matched { + result = "ok" + } + slog.Info("mengya webhook", append([]any{"result", result, "matched", matched, "order_id", orderID}, base...)...) + + out := gin.H{"ok": true, "matched": matched} + if matched { + fulfilled, err := h.orderStore.GetByID(o.ID) + if err == nil { + h.sendOrderNotify(fulfilled.NotifyEmail, fulfilled.UserName, fulfilled.ProductName, fulfilled.ID, fulfilled.Quantity, fulfilled.DeliveredCodes, false) + out["matched_order_id"] = fulfilled.ID + } + } + c.JSON(http.StatusOK, out) +} + +// CancelOrder 用户取消待支付订单并释放预留库存(当前实现不校验 Bearer,凭订单 ID 操作)。 +// @Summary 取消订单 +// @Tags 订单 +// @Produce json +// @Param id path string true "订单 ID" +// @Success 200 {object} SwaggerCancelWrap +// @Failure 404 {object} SwaggerErrorBody +// @Failure 409 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/orders/{id}/cancel [post] +func (h *OrderHandler) CancelOrder(c *gin.Context) { + orderID := c.Param("id") + order, err := h.orderStore.GetByID(orderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "订单不存在"}) + return + } + switch order.Status { + case "cancelled": + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "message": "订单已取消"}}) + return + case "completed": + c.JSON(http.StatusConflict, gin.H{"error": "已完成的订单无法取消"}) + return + case "pending_payment": + if err := h.releasePendingReserved(order); err != nil { + slog.Warn("order", "event", "cancel_release_failed", "order_id", order.ID, "err", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "取消失败,请稍候再试"}) + return + } + c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "message": "订单已取消"}}) + return + default: + c.JSON(http.StatusConflict, gin.H{"error": "当前订单状态不支持取消"}) + return + } +} + +// ListMyOrders 当前登录用户的订单列表(待支付条目不返回卡密)。 +// @Summary 我的订单 +// @Tags 订单 +// @Produce json +// @Security BearerAuth +// @Success 200 {object} SwaggerOrdersBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/orders [get] +func (h *OrderHandler) ListMyOrders(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { + c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"}) + return } userToken := strings.TrimPrefix(authHeader, "Bearer ") - result, err := h.authClient.VerifyToken(userToken) - if err != nil || !result.Valid || result.User == nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"}) - return - } orders, err := h.orderStore.ListByAccount(result.User.Account) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + } + for i := range orders { + if orders[i].Status == "pending_payment" { + orders[i].DeliveredCodes = nil + } } c.JSON(http.StatusOK, gin.H{"data": orders}) +} +func (h *OrderHandler) allocateInventoryTx(tx *gorm.DB, product *models.Product, qty int) (delivered []string, updated models.Product, err error) { + isFixed := strings.EqualFold(strings.TrimSpace(product.FulfillmentType), "fixed") + if isFixed { + content := strings.TrimSpace(product.FixedContent) + if content == "" { + return nil, models.Product{}, fmt.Errorf("fixed content missing") + } + delivered = make([]string, qty) + for i := 0; i < qty; i++ { + delivered[i] = content + } + up, e := h.productStore.GetByIDForUpdate(tx, product.ID) + return delivered, up, e + } + if product.Quantity < qty { + return nil, models.Product{}, fmt.Errorf("out of stock") + } + deliveredCodes, ok := extractCodes(product, qty) + if !ok { + return nil, models.Product{}, fmt.Errorf("codes insufficient") + } + product.Quantity = len(product.Codes) + up, e := h.productStore.UpdateTx(tx, product.ID, *product) + if e != nil { + return nil, models.Product{}, e + } + return deliveredCodes, up, nil } func extractCodes(product *models.Product, count int) ([]string, bool) { - if count <= 0 { - return nil, false - } - if len(product.Codes) < count { - return nil, false - } - delivered := make([]string, count) - copy(delivered, product.Codes[:count]) - product.Codes = product.Codes[count:] - return delivered, true - } diff --git a/mengyastore-backend-go/internal/handlers/public.go b/mengyastore-backend-go/internal/handlers/public.go index da8b453..c512bb4 100644 --- a/mengyastore-backend-go/internal/handlers/public.go +++ b/mengyastore-backend-go/internal/handlers/public.go @@ -18,6 +18,13 @@ func NewPublicHandler(store *storage.ProductStore) *PublicHandler { return &PublicHandler{store: store} } +// ListProducts 上架商品列表(公开字段,不含卡密与管理信息)。 +// @Summary 上架商品列表 +// @Tags 公开 +// @Produce json +// @Success 200 {object} SwaggerProductListBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/products [get] func (h *PublicHandler) ListProducts(c *gin.Context) { items, err := h.store.ListActive() if err != nil { @@ -27,6 +34,14 @@ func (h *PublicHandler) ListProducts(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": sanitizeForPublic(items)}) } +// RecordProductView 记录商品浏览(去重策略由服务端指纹决定)。 +// @Summary 记录商品浏览 +// @Tags 公开 +// @Produce json +// @Param id path string true "商品 ID" +// @Success 200 {object} SwaggerProductViewWrap +// @Failure 404 {object} SwaggerErrorBody +// @Router /api/products/{id}/view [post] func (h *PublicHandler) RecordProductView(c *gin.Context) { id := c.Param("id") fingerprint := buildViewerFingerprint(c) diff --git a/mengyastore-backend-go/internal/handlers/stats.go b/mengyastore-backend-go/internal/handlers/stats.go index 9c9ed79..2a6316b 100644 --- a/mengyastore-backend-go/internal/handlers/stats.go +++ b/mengyastore-backend-go/internal/handlers/stats.go @@ -17,6 +17,13 @@ func NewStatsHandler(orderStore *storage.OrderStore, siteStore *storage.SiteStor return &StatsHandler{orderStore: orderStore, siteStore: siteStore} } +// GetStats 订单总数与站点访问总数。 +// @Summary 站点统计 +// @Tags 公开 +// @Produce json +// @Success 200 {object} SwaggerStatsWrap +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/stats [get] func (h *StatsHandler) GetStats(c *gin.Context) { totalOrders, err := h.orderStore.Count() if err != nil { @@ -36,6 +43,13 @@ func (h *StatsHandler) GetStats(c *gin.Context) { }) } +// RecordVisit 记录一次站点访问并返回累计访问量。 +// @Summary 记录站点访问 +// @Tags 公开 +// @Produce json +// @Success 200 {object} SwaggerVisitWrap +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/site/visit [post] func (h *StatsHandler) RecordVisit(c *gin.Context) { fingerprint := buildViewerFingerprint(c) totalVisits, counted, err := h.siteStore.RecordVisit(fingerprint) @@ -51,6 +65,13 @@ func (h *StatsHandler) RecordVisit(c *gin.Context) { }) } +// GetMaintenance 当前维护模式与原因。 +// @Summary 维护模式状态 +// @Tags 公开 +// @Produce json +// @Success 200 {object} SwaggerMaintenanceWrap +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/site/maintenance [get] func (h *StatsHandler) GetMaintenance(c *gin.Context) { enabled, reason, err := h.siteStore.GetMaintenance() if err != nil { diff --git a/mengyastore-backend-go/internal/handlers/swagger_models.go b/mengyastore-backend-go/internal/handlers/swagger_models.go new file mode 100644 index 0000000..24ce495 --- /dev/null +++ b/mengyastore-backend-go/internal/handlers/swagger_models.go @@ -0,0 +1,162 @@ +package handlers + +import ( + "time" + + "mengyastore-backend/internal/models" + "mengyastore-backend/internal/storage" +) + +// 以下类型仅用于 swag/OpenAPI 描述,与实际 handler 返回值形状对齐。 + +type SwaggerErrorBody struct { + Error string `json:"error"` +} + +type SwaggerValidBody struct { + Valid bool `json:"valid"` +} + +type SwaggerProductListBody struct { + Data []models.Product `json:"data"` +} + +type SwaggerProductOneBody struct { + Data models.Product `json:"data"` +} + +type SwaggerOrdersBody struct { + Data []models.Order `json:"data"` +} + +type SwaggerSMTPWrap struct { + Data storage.SMTPConfig `json:"data"` +} + +type SwaggerStringOKBody struct { + Data string `json:"data"` +} + +type SwaggerBoolOKWrap struct { + Data SwaggerBoolOK `json:"data"` +} + +type SwaggerBoolOK struct { + OK bool `json:"ok"` +} + +type SwaggerProductViewWrap struct { + Data SwaggerProductViewData `json:"data"` +} + +type SwaggerProductViewData struct { + ID string `json:"id"` + ViewCount int `json:"viewCount"` + Counted bool `json:"counted"` +} + +type SwaggerStatsWrap struct { + Data SwaggerStatsData `json:"data"` +} + +type SwaggerStatsData struct { + TotalOrders int `json:"totalOrders"` + TotalVisits int `json:"totalVisits"` +} + +type SwaggerVisitWrap struct { + Data SwaggerVisitData `json:"data"` +} + +type SwaggerVisitData struct { + TotalVisits int `json:"totalVisits"` + Counted bool `json:"counted"` +} + +type SwaggerMaintenanceWrap struct { + Data SwaggerMaintenanceData `json:"data"` +} + +type SwaggerMaintenanceData struct { + Maintenance bool `json:"maintenance"` + Reason string `json:"reason"` +} + +type SwaggerCheckoutWrap struct { + Data SwaggerCheckoutData `json:"data"` +} + +type SwaggerCheckoutData struct { + OrderID string `json:"orderId"` + QrCodeURL string `json:"qrCodeUrl"` + ProductID string `json:"productId"` + ProductQty int `json:"productQty"` + ViewCount int `json:"viewCount"` + Status string `json:"status"` + PaymentMethod string `json:"paymentMethod"` + PaymentExpectedTotal float64 `json:"paymentExpectedTotal,omitempty"` + PaymentQrURLs []string `json:"paymentQrUrls,omitempty"` + PaymentExpiresAt *time.Time `json:"paymentExpiresAt,omitempty"` +} + +type SwaggerConfirmWrap struct { + Data SwaggerConfirmData `json:"data"` +} + +type SwaggerConfirmData struct { + OrderID string `json:"orderId"` + Status string `json:"status"` + DeliveryMode string `json:"deliveryMode"` + DeliveredCodes []string `json:"deliveredCodes"` + IsManual bool `json:"isManual"` +} + +type SwaggerPaymentStatusWrap struct { + Data SwaggerPaymentStatusData `json:"data"` +} + +type SwaggerPaymentStatusData struct { + Status string `json:"status"` + ExpectedTotal float64 `json:"expectedTotal"` + PaymentExpiresAt *time.Time `json:"paymentExpiresAt"` + PaymentMethod string `json:"paymentMethod"` +} + +type SwaggerWebhookMengyaResp struct { + OK bool `json:"ok"` + Matched bool `json:"matched"` + MatchedOrderID string `json:"matched_order_id,omitempty"` +} + +type SwaggerCancelWrap struct { + Data SwaggerCancelMsg `json:"data"` +} + +type SwaggerCancelMsg struct { + OK bool `json:"ok"` + Message string `json:"message,omitempty"` +} + +type SwaggerWishlistWrap struct { + Data SwaggerWishlistItems `json:"data"` +} + +type SwaggerWishlistItems struct { + Items []string `json:"items"` +} + +type SwaggerMessagesWrap struct { + Data SwaggerMessagesInner `json:"data"` +} + +type SwaggerMessagesInner struct { + Messages []models.ChatMessage `json:"messages"` +} + +type SwaggerOneChatMsgWrap struct { + Data SwaggerSingleChatWrap `json:"data"` +} + +type SwaggerSingleChatWrap struct { + Message models.ChatMessage `json:"message"` +} diff --git a/mengyastore-backend-go/internal/handlers/wishlist.go b/mengyastore-backend-go/internal/handlers/wishlist.go index 388418f..af8ce3f 100644 --- a/mengyastore-backend-go/internal/handlers/wishlist.go +++ b/mengyastore-backend-go/internal/handlers/wishlist.go @@ -34,6 +34,15 @@ func (h *WishlistHandler) requireUser(c *gin.Context) (string, bool) { return result.User.Account, true } +// GetWishlist 当前用户收藏的商品 ID 列表。 +// @Summary 收藏列表 +// @Tags 收藏 +// @Produce json +// @Security BearerAuth +// @Success 200 {object} SwaggerWishlistWrap +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/wishlist [get] func (h *WishlistHandler) GetWishlist(c *gin.Context) { account, ok := h.requireUser(c) if !ok { @@ -47,16 +56,29 @@ func (h *WishlistHandler) GetWishlist(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": gin.H{"items": ids}}) } -type wishlistItemPayload struct { +// WishlistItemPayload 添加收藏请求体。 +type WishlistItemPayload struct { ProductID string `json:"productId"` } +// AddToWishlist 添加收藏。 +// @Summary 添加收藏 +// @Tags 收藏 +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param body body WishlistItemPayload true "商品 ID" +// @Success 200 {object} SwaggerWishlistWrap +// @Failure 400 {object} SwaggerErrorBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/wishlist [post] func (h *WishlistHandler) AddToWishlist(c *gin.Context) { account, ok := h.requireUser(c) if !ok { return } - var payload wishlistItemPayload + var payload WishlistItemPayload if err := c.ShouldBindJSON(&payload); err != nil || payload.ProductID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数有误"}) return @@ -69,6 +91,17 @@ func (h *WishlistHandler) AddToWishlist(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": gin.H{"items": ids}}) } +// RemoveFromWishlist 按商品 ID 移除收藏。 +// @Summary 移除收藏 +// @Tags 收藏 +// @Produce json +// @Security BearerAuth +// @Param id path string true "商品 ID" +// @Success 200 {object} SwaggerWishlistWrap +// @Failure 400 {object} SwaggerErrorBody +// @Failure 401 {object} SwaggerErrorBody +// @Failure 500 {object} SwaggerErrorBody +// @Router /api/wishlist/{id} [delete] func (h *WishlistHandler) RemoveFromWishlist(c *gin.Context) { account, ok := h.requireUser(c) if !ok { diff --git a/mengyastore-backend-go/internal/models/order.go b/mengyastore-backend-go/internal/models/order.go index 373708e..bd24fc9 100644 --- a/mengyastore-backend-go/internal/models/order.go +++ b/mengyastore-backend-go/internal/models/order.go @@ -16,5 +16,8 @@ type Order struct { ContactPhone string `json:"contactPhone"` ContactEmail string `json:"contactEmail"` NotifyEmail string `json:"notifyEmail"` + PaymentMethod string `json:"paymentMethod,omitempty"` + PaymentExpectedTotal float64 `json:"paymentExpectedTotal,omitempty"` + PaymentExpiresAt *time.Time `json:"paymentExpiresAt,omitempty"` CreatedAt time.Time `json:"createdAt"` } diff --git a/mengyastore-backend-go/internal/models/product.go b/mengyastore-backend-go/internal/models/product.go index eb88503..ad99d70 100644 --- a/mengyastore-backend-go/internal/models/product.go +++ b/mengyastore-backend-go/internal/models/product.go @@ -11,6 +11,8 @@ type Product struct { Quantity int `json:"quantity"` CoverURL string `json:"coverUrl"` ScreenshotURLs []string `json:"screenshotUrls"` + // PaymentQrURLs 萌芽支付收款码图片链接(可多条)。 + PaymentQrURLs []string `json:"paymentQrUrls"` VerificationURL string `json:"verificationUrl"` Codes []string `json:"codes"` ViewCount int `json:"viewCount"` diff --git a/mengyastore-backend-go/internal/mq/client.go b/mengyastore-backend-go/internal/mq/client.go index 7755a22..99d548c 100644 --- a/mengyastore-backend-go/internal/mq/client.go +++ b/mengyastore-backend-go/internal/mq/client.go @@ -15,7 +15,7 @@ import ( "mengyastore-backend/internal/storage" ) -// Client manages a single AMQP connection, a publish channel, and naming for one env. +// Client 封装单条 AMQP 连接、发布用 Channel,以及单环境的命名。 type Client struct { conn *amqp.Connection pubCh *amqp.Channel @@ -32,7 +32,7 @@ type Client struct { consumerSite *storage.SiteStore } -// New connects to RabbitMQ and declares exchange + queue + binding (idempotent). +// New 连接 RabbitMQ 并声明交换机 + 队列 + 绑定(幂等)。 func New(amqpURL, env string) (*Client, error) { if amqpURL == "" { return nil, fmt.Errorf("empty amqp url") @@ -141,7 +141,7 @@ func (c *Client) passiveQueueLocked() error { return err } -// PublishOrderEmail publishes a persistent JSON message to the order-email routing key. +// PublishOrderEmail 向 order-email 路由键发布一条持久化 JSON 消息。 func (c *Client) PublishOrderEmail(ctx context.Context, p OrderEmailPayload) error { body, err := json.Marshal(p) if err != nil { @@ -190,7 +190,7 @@ func (c *Client) PublishOrderEmail(ctx context.Context, p OrderEmailPayload) err return nil } -// QueueInspectInfo returns current queue depth and consumer count (passive declare). +// QueueInspectInfo 返回当前队列深度与消费者数(被动声明查询)。 func (c *Client) QueueInspectInfo() (messages int, consumers int, err error) { c.mu.Lock() defer c.mu.Unlock() @@ -216,7 +216,7 @@ func (c *Client) QueueInspectInfo() (messages int, consumers int, err error) { return msgs, cons, err } -// Ping checks that the publish channel can query the declared queue (liveness for /api/health). +// Ping 检查发布 Channel 能否对声明的队列做被动查询(供 /api/health 探活)。 func (c *Client) Ping() error { c.mu.Lock() defer c.mu.Unlock() @@ -231,10 +231,10 @@ func (c *Client) Ping() error { return err } -// Env returns the sanitized environment suffix used in exchange/queue names. +// Env 返回交换机/队列名使用的环境后缀(已规范化)。 func (c *Client) Env() string { return c.env } -// StartConsumer runs the order-email consumer until ctx is cancelled (run in a goroutine). +// StartConsumer 在 ctx 取消前运行订单邮件消费者(通常在 goroutine 中调用)。 func (c *Client) StartConsumer(ctx context.Context, site *storage.SiteStore) { c.mu.Lock() c.consumerCtx = ctx @@ -248,7 +248,7 @@ func (c *Client) StartConsumer(ctx context.Context, site *storage.SiteStore) { RunOrderEmailConsumer(ctx, conn, env, site) } -// Close releases the publish channel and connection. +// Close 关闭发布 Channel 与连接。 func (c *Client) Close() { c.closing.Do(func() { c.mu.Lock() diff --git a/mengyastore-backend-go/internal/mq/consumer.go b/mengyastore-backend-go/internal/mq/consumer.go index 64a058d..2a37664 100644 --- a/mengyastore-backend-go/internal/mq/consumer.go +++ b/mengyastore-backend-go/internal/mq/consumer.go @@ -11,7 +11,7 @@ import ( "mengyastore-backend/internal/storage" ) -// RunOrderEmailConsumer runs until ctx is done. Must be started in its own goroutine. +// RunOrderEmailConsumer 运行至 ctx 结束,请在独立 goroutine 中启动。 func RunOrderEmailConsumer(ctx context.Context, conn *amqp.Connection, env string, site *storage.SiteStore) { if conn == nil || site == nil { return diff --git a/mengyastore-backend-go/internal/mq/payload.go b/mengyastore-backend-go/internal/mq/payload.go index b1499ba..e42c8c6 100644 --- a/mengyastore-backend-go/internal/mq/payload.go +++ b/mengyastore-backend-go/internal/mq/payload.go @@ -2,7 +2,7 @@ package mq import "mengyastore-backend/internal/email" -// OrderEmailPayload is the JSON body published to RabbitMQ (no SMTP secrets). +// OrderEmailPayload 发布到 RabbitMQ 的订单邮件 JSON 负载(不含 SMTP 口令)。 type OrderEmailPayload struct { ToEmail string `json:"toEmail"` ToName string `json:"toName"` diff --git a/mengyastore-backend-go/internal/mq/topology.go b/mengyastore-backend-go/internal/mq/topology.go index 44534a8..d084567 100644 --- a/mengyastore-backend-go/internal/mq/topology.go +++ b/mengyastore-backend-go/internal/mq/topology.go @@ -9,15 +9,15 @@ import ( const routingKeyOrderEmail = "order.email.notify" -// OrderEmailRoutingKey is the binding / publish key for order notification messages. +// OrderEmailRoutingKey 订单通知消息的绑定键 / 发布 routing key。 func OrderEmailRoutingKey() string { return routingKeyOrderEmail } -// ExchangeName returns the durable topic exchange for this app + env (isolation from other apps on same broker). +// ExchangeName 返回本应用 + 环境对应的持久化 topic 交换机名(与 Broker 上其它应用隔离)。 func ExchangeName(env string) string { return fmt.Sprintf("ex.mengyastore.%s.events", sanitizeEnv(env)) } -// QueueName returns the durable order-email queue name for this env. +// QueueName 返回本环境下持久化的订单邮件队列名。 func QueueName(env string) string { return fmt.Sprintf("q.mengyastore.%s.order_email", sanitizeEnv(env)) } diff --git a/mengyastore-backend-go/internal/payment/mengya.go b/mengyastore-backend-go/internal/payment/mengya.go new file mode 100644 index 0000000..e050037 --- /dev/null +++ b/mengyastore-backend-go/internal/payment/mengya.go @@ -0,0 +1,143 @@ +package payment + +import ( + "bytes" + "encoding/json" + "fmt" + "math" + "regexp" + "strconv" + "strings" +) + +// 依次尝试:微信等常带 ¥;支付宝等到账文案多为「收款x.xx元」无货币符号。 +var mengyaNoticeAmountPatterns = []*regexp.Regexp{ + regexp.MustCompile(`到账[¥¥]\s*([\d]+(?:\.[\d]+)?)`), + regexp.MustCompile(`到账\s*([\d]+(?:\.[\d]+)?)\s*元`), + regexp.MustCompile(`收款[¥¥]\s*([\d]+(?:\.[\d]+)?)(?:\s*元)?`), + regexp.MustCompile(`收款\s*([\d]+(?:\.[\d]+)?)\s*元`), +} + +// AmountEpsilon:Webhook 到账金额与订单快照金额对比时的容差。 +const AmountEpsilon = 0.005 + +func AmountAlmostEqual(expected, actual float64) bool { + return math.Abs(expected-actual) < AmountEpsilon +} + +// ParseWebhookJSON 从转发通知体中解析支付金额: +// 优先 body.notice,其次顶层 notice,否则在序列化后的 JSON 字符串中扫描。 +func ParseWebhookJSON(raw []byte) (float64, bool) { + if len(bytes.TrimSpace(raw)) == 0 { + return 0, false + } + var top map[string]json.RawMessage + if err := json.Unmarshal(raw, &top); err != nil { + return scanAmount(string(raw)) + } + if v, ok := top["notice"]; ok { + if s := jsonString(v); s != "" { + return scanAmount(s) + } + } + if inner, ok := top["body"]; ok { + var nested map[string]json.RawMessage + if json.Unmarshal(inner, &nested) == nil { + if v, ok := nested["notice"]; ok { + if s := jsonString(v); s != "" { + return scanAmount(s) + } + } + if amt, ok := scanAmount(jsonString(inner)); ok { + return amt, ok + } + return scanAmount(string(inner)) + } + if amt, ok := scanAmount(strings.Trim(strings.Trim(string(inner), "\""), `"`)); ok { + return amt, ok + } + return scanAmount(string(inner)) + } + buf, _ := json.Marshal(top) + return scanAmount(string(buf)) +} + +func jsonString(raw json.RawMessage) string { + var s string + if json.Unmarshal(raw, &s) == nil { + return s + } + return "" +} + +func scanAmount(text string) (float64, bool) { + if amt, ok := parseAmountRegex(text); ok { + return amt, true + } + return parseAmountRegex(strings.ReplaceAll(text, `\n`, "\n")) +} + +func parseAmountRegex(text string) (float64, bool) { + for _, re := range mengyaNoticeAmountPatterns { + match := re.FindStringSubmatch(text) + if len(match) < 2 { + continue + } + s := strings.ReplaceAll(match[1], ",", "") + yuan, err := strconv.ParseFloat(s, 64) + if err != nil { + continue + } + return yuan, true + } + return 0, false +} + +// NoticeFieldsForLog 从常见萌芽/转发 JSON 里抽出 notice 等字段,仅供后台日志排查(如微信/支付宝文案差异)。 +func NoticeFieldsForLog(raw []byte) string { + if len(bytes.TrimSpace(raw)) == 0 { + return "" + } + var top map[string]json.RawMessage + if err := json.Unmarshal(raw, &top); err != nil { + return "" + } + var parts []string + add := func(k, v string) { + if v == "" { + return + } + if len(v) > 600 { + v = v[:600] + "…" + } + parts = append(parts, fmt.Sprintf("%s=%s", k, strconv.Quote(v))) + } + if v, ok := top["notice"]; ok { + add("notice", jsonString(v)) + } + if inner, ok := top["body"]; ok { + var nested map[string]json.RawMessage + if json.Unmarshal(inner, &nested) == nil { + if v, ok := nested["notice"]; ok { + add("body.notice", jsonString(v)) + } + if v, ok := nested["content"]; ok { + add("body.content", jsonString(v)) + } + if v, ok := nested["text"]; ok { + add("body.text", jsonString(v)) + } + } else if s := jsonString(inner); s != "" { + add("body(string)", s) + } + } + for _, k := range []string{"title", "msg", "message", "desc", "description"} { + if v, ok := top[k]; ok { + add(k, jsonString(v)) + } + } + if len(parts) == 0 { + return "" + } + return strings.Join(parts, "; ") +} diff --git a/mengyastore-backend-go/internal/payment/types.go b/mengyastore-backend-go/internal/payment/types.go new file mode 100644 index 0000000..8b1fb30 --- /dev/null +++ b/mengyastore-backend-go/internal/payment/types.go @@ -0,0 +1,3 @@ +package payment + +const MethodMengya = "mengya" diff --git a/mengyastore-backend-go/internal/storage/orderstore.go b/mengyastore-backend-go/internal/storage/orderstore.go index 496a24e..6854e16 100644 --- a/mengyastore-backend-go/internal/storage/orderstore.go +++ b/mengyastore-backend-go/internal/storage/orderstore.go @@ -1,15 +1,20 @@ package storage import ( + "errors" "fmt" + "time" "github.com/google/uuid" "gorm.io/gorm" + "gorm.io/gorm/clause" "mengyastore-backend/internal/database" "mengyastore-backend/internal/models" ) +const amountMatchEpsilon = 0.009 + type OrderStore struct { db *gorm.DB } @@ -20,20 +25,23 @@ func NewOrderStore(db *gorm.DB) (*OrderStore, error) { func orderRowToModel(row database.OrderRow) models.Order { return models.Order{ - ID: row.ID, - ProductID: row.ProductID, - ProductName: row.ProductName, - UserAccount: row.UserAccount, - UserName: row.UserName, - Quantity: row.Quantity, - DeliveredCodes: row.DeliveredCodes, - Status: row.Status, - DeliveryMode: row.DeliveryMode, - Note: row.Note, - ContactPhone: row.ContactPhone, - ContactEmail: row.ContactEmail, - NotifyEmail: row.NotifyEmail, - CreatedAt: row.CreatedAt, + ID: row.ID, + ProductID: row.ProductID, + ProductName: row.ProductName, + UserAccount: row.UserAccount, + UserName: row.UserName, + Quantity: row.Quantity, + DeliveredCodes: row.DeliveredCodes, + Status: row.Status, + DeliveryMode: row.DeliveryMode, + Note: row.Note, + ContactPhone: row.ContactPhone, + ContactEmail: row.ContactEmail, + NotifyEmail: row.NotifyEmail, + PaymentMethod: row.PaymentMethod, + PaymentExpectedTotal: row.PaymentExpectedTotal, + PaymentExpiresAt: row.PaymentExpiresAt, + CreatedAt: row.CreatedAt, } } @@ -45,24 +53,62 @@ func (s *OrderStore) Create(order models.Order) (models.Order, error) { order.DeliveredCodes = []string{} } row := database.OrderRow{ - ID: order.ID, - ProductID: order.ProductID, - ProductName: order.ProductName, - UserAccount: order.UserAccount, - UserName: order.UserName, - Quantity: order.Quantity, - DeliveredCodes: database.StringSlice(order.DeliveredCodes), - Status: order.Status, - DeliveryMode: order.DeliveryMode, - Note: order.Note, - ContactPhone: order.ContactPhone, - ContactEmail: order.ContactEmail, - NotifyEmail: order.NotifyEmail, + ID: order.ID, + ProductID: order.ProductID, + ProductName: order.ProductName, + UserAccount: order.UserAccount, + UserName: order.UserName, + Quantity: order.Quantity, + DeliveredCodes: database.StringSlice(order.DeliveredCodes), + Status: order.Status, + DeliveryMode: order.DeliveryMode, + Note: order.Note, + ContactPhone: order.ContactPhone, + ContactEmail: order.ContactEmail, + NotifyEmail: order.NotifyEmail, + PaymentMethod: order.PaymentMethod, + PaymentExpectedTotal: order.PaymentExpectedTotal, + PaymentExpiresAt: order.PaymentExpiresAt, } if err := s.db.Create(&row).Error; err != nil { return models.Order{}, err } order.CreatedAt = row.CreatedAt + order.PaymentExpiresAt = row.PaymentExpiresAt + return order, nil +} + +// CreateTx 在已有事务内创建订单。 +func (s *OrderStore) CreateTx(tx *gorm.DB, order models.Order) (models.Order, error) { + if order.ID == "" { + order.ID = uuid.NewString() + } + if len(order.DeliveredCodes) == 0 { + order.DeliveredCodes = []string{} + } + row := database.OrderRow{ + ID: order.ID, + ProductID: order.ProductID, + ProductName: order.ProductName, + UserAccount: order.UserAccount, + UserName: order.UserName, + Quantity: order.Quantity, + DeliveredCodes: database.StringSlice(order.DeliveredCodes), + Status: order.Status, + DeliveryMode: order.DeliveryMode, + Note: order.Note, + ContactPhone: order.ContactPhone, + ContactEmail: order.ContactEmail, + NotifyEmail: order.NotifyEmail, + PaymentMethod: order.PaymentMethod, + PaymentExpectedTotal: order.PaymentExpectedTotal, + PaymentExpiresAt: order.PaymentExpiresAt, + } + if err := tx.Create(&row).Error; err != nil { + return models.Order{}, err + } + order.CreatedAt = row.CreatedAt + order.PaymentExpiresAt = row.PaymentExpiresAt return order, nil } @@ -112,14 +158,13 @@ func (s *OrderStore) ListAll() ([]models.Order, error) { func (s *OrderStore) CountPurchasedByAccount(account, productID string) (int, error) { var total int64 - // 统计 pending(手动待发货)和 completed 两种状态,防止用户快速下单绕过购买数量限制。 + // pending:手动待发货;pending_payment:萌芽支付待到账(会计入限购)。 err := s.db.Model(&database.OrderRow{}). - Where("user_account = ? AND product_id = ? AND status IN ?", account, productID, []string{"pending", "completed"}). + Where("user_account = ? AND product_id = ? AND status IN ?", account, productID, []string{"pending", "pending_payment", "completed"}). Select("COALESCE(SUM(quantity), 0)").Scan(&total).Error return int(total), err } -// Count 返回所有订单的总数量。 func (s *OrderStore) Count() (int, error) { var count int64 if err := s.db.Model(&database.OrderRow{}).Count(&count).Error; err != nil { @@ -128,13 +173,69 @@ func (s *OrderStore) Count() (int, error) { return int(count), nil } -// Delete 根据 ID 删除单条订单。 func (s *OrderStore) Delete(id string) error { return s.db.Delete(&database.OrderRow{}, "id = ?", id).Error } -// UpdateCodes 更新订单的已发货卡密列表。 func (s *OrderStore) UpdateCodes(id string, codes []string) error { return s.db.Model(&database.OrderRow{}).Where("id = ?", id). Update("delivered_codes", database.StringSlice(codes)).Error } + +// TryFulfillMengyaPayment 在事务内按 FIFO 匹配一笔待支付订单并置为 completed、增加销量。无匹配时 ok=false。 +func (s *OrderStore) TryFulfillMengyaPayment(amount float64, now time.Time) (order models.Order, ok bool, err error) { + err = s.db.Transaction(func(tx *gorm.DB) error { + var row database.OrderRow + res := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("status = ? AND payment_expires_at IS NOT NULL AND payment_expires_at > ?", "pending_payment", now). + Where("ABS(payment_expected_total - ?) < ?", amount, amountMatchEpsilon). + Order("created_at ASC"). + First(&row) + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return nil + } + return res.Error + } + if err := tx.Model(&database.OrderRow{}).Where("id = ?", row.ID).Update("status", "completed").Error; err != nil { + return err + } + if err := tx.Model(&database.ProductRow{}).Where("id = ?", row.ProductID). + UpdateColumn("total_sold", gorm.Expr("total_sold + ?", row.Quantity)).Error; err != nil { + return err + } + row.Status = "completed" + order = orderRowToModel(row) + ok = true + return nil + }) + if err != nil { + return models.Order{}, false, err + } + return order, ok, nil +} + +// ListExpiredPendingPayments 返回已过期仍未支付的订单(用于释放预留库存)。 +func (s *OrderStore) ListExpiredPendingPayments(now time.Time) ([]models.Order, error) { + var rows []database.OrderRow + if err := s.db.Where("status = ? AND payment_expires_at IS NOT NULL AND payment_expires_at <= ?", "pending_payment", now). + Find(&rows).Error; err != nil { + return nil, err + } + out := make([]models.Order, len(rows)) + for i, r := range rows { + out[i] = orderRowToModel(r) + } + return out, nil +} + +// CancelPendingStaleIfEligible 仅在仍为 pending_payment 时将订单作废并清空预留内容;返回值 ok 表示成功取消(可安全退回库存)。 +func (s *OrderStore) CancelPendingStaleIfEligible(id string) (ok bool, err error) { + res := s.db.Model(&database.OrderRow{}). + Where("id = ? AND status = ?", id, "pending_payment"). + Updates(map[string]interface{}{ + "status": "cancelled", + "delivered_codes": database.StringSlice([]string{}), + }) + return res.RowsAffected > 0, res.Error +} diff --git a/mengyastore-backend-go/internal/storage/productstore.go b/mengyastore-backend-go/internal/storage/productstore.go index 0ef4f28..5799e52 100644 --- a/mengyastore-backend-go/internal/storage/productstore.go +++ b/mengyastore-backend-go/internal/storage/productstore.go @@ -9,6 +9,7 @@ import ( "github.com/google/uuid" "gorm.io/gorm" + "gorm.io/gorm/clause" "mengyastore-backend/internal/database" "mengyastore-backend/internal/models" @@ -16,7 +17,8 @@ import ( const defaultCoverURL = "https://img.shumengya.top/i/2026/01/04/695a55058c37f.png" const viewCooldown = 6 * time.Hour -const maxScreenshotURLs = 5 +const maxScreenshotURLs = 6 +const maxPaymentQrURLs = 6 type ProductStore struct { db *gorm.DB @@ -55,6 +57,7 @@ func rowToModel(row database.ProductRow, codes []string) models.Product { Tags: row.Tags, CoverURL: row.CoverURL, ScreenshotURLs: row.ScreenshotURLs, + PaymentQrURLs: []string(row.PaymentQrURLs), VerificationURL: row.VerificationURL, Description: row.Description, Active: row.Active, @@ -74,8 +77,12 @@ func rowToModel(row database.ProductRow, codes []string) models.Product { } func (s *ProductStore) loadCodes(productID string) ([]string, error) { + return s.loadCodesTx(s.db, productID) +} + +func (s *ProductStore) loadCodesTx(tx *gorm.DB, productID string) ([]string, error) { var rows []database.ProductCodeRow - if err := s.db.Where("product_id = ?", productID).Find(&rows).Error; err != nil { + if err := tx.Where("product_id = ?", productID).Find(&rows).Error; err != nil { return nil, err } codes := make([]string, len(rows)) @@ -85,8 +92,46 @@ func (s *ProductStore) loadCodes(productID string) ([]string, error) { return codes, nil } +// Transaction 在同一数据库连接上开启事务并执行 fn。 +func (s *ProductStore) Transaction(fn func(tx *gorm.DB) error) error { + return s.db.Transaction(fn) +} + +// GetByIDForUpdate 在事务内以 FOR UPDATE 锁定商品行(须在事务中调用)。 +func (s *ProductStore) GetByIDForUpdate(tx *gorm.DB, id string) (models.Product, error) { + var row database.ProductRow + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&row, "id = ?", id).Error; err != nil { + return models.Product{}, fmt.Errorf("product not found") + } + codes, err := s.loadCodesTx(tx, id) + if err != nil { + return models.Product{}, err + } + return rowToModel(row, codes), nil +} + +// PrependReservedCodes 把曾预留在订单里的卡密按原顺序放回该商品可用库存队列前端(FIFO 出库顺序)。 +func (s *ProductStore) PrependReservedCodes(productID string, codes []string) error { + codes = sanitizeCodes(codes) + if len(codes) == 0 { + return nil + } + existing, err := s.loadCodes(productID) + if err != nil { + return err + } + merged := make([]string, 0, len(codes)+len(existing)) + merged = append(merged, codes...) + merged = append(merged, existing...) + return s.replaceCodes(productID, merged) +} + func (s *ProductStore) replaceCodes(productID string, codes []string) error { - if err := s.db.Where("product_id = ?", productID).Delete(&database.ProductCodeRow{}).Error; err != nil { + return s.replaceCodesTx(s.db, productID, codes) +} + +func (s *ProductStore) replaceCodesTx(tx *gorm.DB, productID string, codes []string) error { + if err := tx.Where("product_id = ?", productID).Delete(&database.ProductCodeRow{}).Error; err != nil { return err } if len(codes) == 0 { @@ -96,7 +141,7 @@ func (s *ProductStore) replaceCodes(productID string, codes []string) error { for _, code := range codes { rows = append(rows, database.ProductCodeRow{ProductID: productID, Code: code}) } - return s.db.CreateInBatches(rows, 100).Error + return tx.CreateInBatches(rows, 100).Error } func (s *ProductStore) ListAll() ([]models.Product, error) { @@ -158,6 +203,7 @@ func (s *ProductStore) Create(p models.Product) (models.Product, error) { Tags: database.StringSlice(p.Tags), CoverURL: p.CoverURL, ScreenshotURLs: database.StringSlice(p.ScreenshotURLs), + PaymentQrURLs: database.StringSlice(p.PaymentQrURLs), VerificationURL: p.VerificationURL, Description: p.Description, Active: p.Active, @@ -198,6 +244,7 @@ func (s *ProductStore) Update(id string, patch models.Product) (models.Product, "tags": database.StringSlice(normalized.Tags), "cover_url": normalized.CoverURL, "screenshot_urls": database.StringSlice(normalized.ScreenshotURLs), + "payment_qr_urls": database.StringSlice(normalized.PaymentQrURLs), "verification_url": normalized.VerificationURL, "description": normalized.Description, "active": normalized.Active, @@ -221,6 +268,45 @@ func (s *ProductStore) Update(id string, patch models.Product) (models.Product, return rowToModel(updated, codes), nil } +// UpdateTx 在事务内更新商品及卡密列表(与 Update 行为一致)。 +func (s *ProductStore) UpdateTx(tx *gorm.DB, id string, patch models.Product) (models.Product, error) { + var row database.ProductRow + if err := tx.First(&row, "id = ?", id).Error; err != nil { + return models.Product{}, fmt.Errorf("product not found") + } + normalized := normalizeProduct(patch) + + if err := tx.Model(&row).Updates(map[string]interface{}{ + "name": normalized.Name, + "price": normalized.Price, + "discount_price": normalized.DiscountPrice, + "tags": database.StringSlice(normalized.Tags), + "cover_url": normalized.CoverURL, + "screenshot_urls": database.StringSlice(normalized.ScreenshotURLs), + "payment_qr_urls": database.StringSlice(normalized.PaymentQrURLs), + "verification_url": normalized.VerificationURL, + "description": normalized.Description, + "active": normalized.Active, + "require_login": normalized.RequireLogin, + "max_per_account": normalized.MaxPerAccount, + "delivery_mode": normalized.DeliveryMode, + "fulfillment_type": normalized.FulfillmentType, + "fixed_content": normalized.FixedContent, + "show_note": normalized.ShowNote, + "show_contact": normalized.ShowContact, + }).Error; err != nil { + return models.Product{}, err + } + if err := s.replaceCodesTx(tx, id, normalized.Codes); err != nil { + return models.Product{}, err + } + + var updated database.ProductRow + tx.First(&updated, "id = ?", id) + codes, _ := s.loadCodesTx(tx, id) + return rowToModel(updated, codes), nil +} + func (s *ProductStore) Toggle(id string, active bool) (models.Product, error) { if err := s.db.Model(&database.ProductRow{}).Where("id = ?", id).Update("active", active).Error; err != nil { return models.Product{}, err @@ -266,10 +352,8 @@ func (s *ProductStore) IncrementView(id, fingerprint string) (models.Product, bo return rowToModel(row, nil), true, nil } -// IncrementViewCount atomically increments view_count by 1 and returns the -// updated product (without codes). It is called by the handler when Redis-based -// deduplication has already confirmed this is a new view, so no in-memory lock -// is needed here. +// IncrementViewCount 原子地将 view_count +1,并返回更新后的商品(不含卡密)。 +// 在 Handler 已用 Redis 去重确认本次为新浏览时调用,故此处无需进程内锁。 func (s *ProductStore) IncrementViewCount(id string) (models.Product, error) { if err := s.db.Model(&database.ProductRow{}).Where("id = ?", id). UpdateColumn("view_count", gorm.Expr("view_count + 1")).Error; err != nil { @@ -282,8 +366,8 @@ func (s *ProductStore) IncrementViewCount(id string) (models.Product, error) { return rowToModel(row, nil), nil } -// GetViewState returns the current product state without modifying any counter. -// Used by the handler when a view is detected as duplicate via Redis. +// GetViewState 只读取当前商品状态,不修改任何计数。 +// 在 Handler 已通过 Redis 判定为重复访问时使用。 func (s *ProductStore) GetViewState(id string) (models.Product, error) { var row database.ProductRow if err := s.db.First(&row, "id = ?", id).Error; err != nil { @@ -309,12 +393,16 @@ func normalizeProduct(item models.Product) models.Product { item.Tags = []string{} } item.Tags = sanitizeTags(item.Tags) + if item.PaymentQrURLs == nil { + item.PaymentQrURLs = []string{} + } if item.ScreenshotURLs == nil { item.ScreenshotURLs = []string{} } if len(item.ScreenshotURLs) > maxScreenshotURLs { item.ScreenshotURLs = item.ScreenshotURLs[:maxScreenshotURLs] } + item.PaymentQrURLs = sanitizePaymentQrURLs(item.PaymentQrURLs) if item.Codes == nil { item.Codes = []string{} } @@ -340,6 +428,23 @@ func normalizeProduct(item models.Product) models.Product { return item } +func sanitizePaymentQrURLs(urls []string) []string { + clean := make([]string, 0, len(urls)) + seen := map[string]bool{} + for _, u := range urls { + t := strings.TrimSpace(u) + if t == "" || seen[t] { + continue + } + seen[t] = true + clean = append(clean, t) + if len(clean) >= maxPaymentQrURLs { + break + } + } + return clean +} + func sanitizeCodes(codes []string) []string { clean := make([]string, 0, len(codes)) seen := map[string]bool{} diff --git a/mengyastore-backend-go/internal/storage/sitestore.go b/mengyastore-backend-go/internal/storage/sitestore.go index 247eed6..c16f0e7 100644 --- a/mengyastore-backend-go/internal/storage/sitestore.go +++ b/mengyastore-backend-go/internal/storage/sitestore.go @@ -20,7 +20,7 @@ func NewSiteStore(db *gorm.DB) (*SiteStore, error) { func (s *SiteStore) get(key string) (string, error) { var row database.SiteSettingRow // `key` 是 MySQL 保留字,需用反引号转义以避免 SQL 语法错误。 - // 使用 Find+Limit 而非 First:缺键时不产生 ErrRecordNotFound,避免 GORM 默认 logger 刷「record not found」。 + // 使用 Find+Limit 而不是 First:缺键时不产生 ErrRecordNotFound,避免 GORM 默认 logger 刷「record not found」。 if err := s.db.Where("`key` = ?", key).Limit(1).Find(&row).Error; err != nil { return "", err } diff --git a/mengyastore-backend-go/main.go b/mengyastore-backend-go/main.go index 4cac064..b14a08c 100644 --- a/mengyastore-backend-go/main.go +++ b/mengyastore-backend-go/main.go @@ -1,13 +1,40 @@ +// @title 萌芽小店 API +// @version 1.0.0-go +// @description 商品、下单、站点统计、收藏与客服聊天;用户登录由萌芽账户认证中心(SproutGate)校验。监听地址以 HTTP_LISTEN_ADDR 为准。 +// @host localhost:8080 +// @BasePath / +// @schemes http https + +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization +// @description 用户访问令牌,格式: Bearer 空格 + token + +// @securityDefinitions.apikey AdminToken +// @in header +// @name X-Admin-Token +// @description 管理端令牌(也可使用 Authorization 头或 query token,见各接口说明) + +// @securityDefinitions.apikey WebhookSecret +// @in header +// @name X-Webhook-Secret +// @description 与服务端 WEBHOOK_MENGYA_SECRET 一致时校验萌芽支付 Webhook package main import ( "context" "log" + "log/slog" "net/http" + "os" "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + + _ "mengyastore-backend/docs" "mengyastore-backend/internal/auth" "mengyastore-backend/internal/config" @@ -17,7 +44,98 @@ import ( "mengyastore-backend/internal/storage" ) +const apiVersion = "1.0.0-go" + +func initLogging() { + h := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key != slog.TimeKey { + return a + } + t := a.Value.Time() + if t.IsZero() { + return a + } + return slog.String("time", t.Format("2006-01-02 15:04:05")) + }, + }) + slog.SetDefault(slog.New(h)) +} + +func accessLog() gin.HandlerFunc { + return func(c *gin.Context) { + t0 := time.Now() + path := c.Request.URL.Path + q := c.Request.URL.RawQuery + c.Next() + p := path + if q != "" { + p = path + "?" + q + } + slog.Info("http", + "method", c.Request.Method, + "path", p, + "status", c.Writer.Status(), + "ms", time.Since(t0).Milliseconds(), + "ip", c.ClientIP(), + ) + } +} + +// rootAPIInfo 浏览器或客户端访问服务根路径时返回 API 说明(JSON)。 +// @Summary API 根信息与端点索引 +// @Description 返回服务描述、主要路径指引与版本(非 OpenAPI 详尽列表,完整契约见 /swagger)。 +// @Tags 元信息 +// @Produce json +// @Success 200 {object} map[string]interface{} +// @Router / [get] +func rootAPIInfo(c *gin.Context) { + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + loc = time.FixedZone("CST", 8*3600) + } + c.JSON(http.StatusOK, gin.H{ + "description": "萌芽小店电商后端:商品、下单、站点统计、收藏与客服聊天;用户登录认证由萌芽账户认证中心(SproutGate)校验。", + "endpoints": gin.H{ + "health": "/api/health", + "public": "/api/products, /api/checkout, /api/stats, /api/site/*, POST /api/products/:id/view", + "orders": "/api/orders (Bearer), GET /api/orders/:id/payment-status, POST /api/orders/:id/confirm", + "webhooks": "POST /api/webhooks/mengya-pay (萌芽支付到账,可选 X-Webhook-Secret)", + "wishlist": "/api/wishlist (Bearer)", + "chat_user": "/api/chat/messages (Bearer)", + "chat_admin": "/api/admin/chat/* (X-Admin-Token)", + "admin": "/api/admin/* 含 verify、products、site、smtp、orders、system-status (X-Admin-Token)", + "auth": "用户认证 via 萌芽认证中心 (Authorization: Bearer)", + }, + "message": "萌芽小店 后端 API 服务运行中", + "timestamp": time.Now().In(loc).Format(time.RFC3339), + "version": apiVersion, + }) +} + +// HealthCheck 返回进程与可选 RabbitMQ 探活结果。 +// @Summary 健康检查 +// @Tags 健康检查 +// @Produce json +// @Success 200 {object} map[string]interface{} "status ok;rabbitmq 为 disabled|ok|error:..." +// @Router /api/health [get] +func HealthCheck(mqClient *mq.Client) gin.HandlerFunc { + return func(c *gin.Context) { + resp := gin.H{"status": "ok", "rabbitmq": "disabled"} + if mqClient != nil { + if err := mqClient.Ping(); err != nil { + resp["rabbitmq"] = "error: " + err.Error() + } else { + resp["rabbitmq"] = "ok" + } + } + c.JSON(http.StatusOK, resp) + } +} + func main() { + initLogging() cfg, err := config.Load() if err != nil { log.Fatalf("加载配置失败: %v", err) @@ -57,47 +175,55 @@ func main() { var mqClient *mq.Client if cfg.RabbitMQEnabled { if cfg.RabbitMQURL == "" { - log.Println("[MQ] 已启用但未配置连接串:请设置 RABBITMQ_URL,或设置 RABBITMQ_PASSWORD(及可选 RABBITMQ_HOST/RABBITMQ_VHOST)") + slog.Warn("mq", "event", "enabled_no_url", "hint", "set RABBITMQ_URL or RABBITMQ_PASSWORD") } else { c, err := mq.New(cfg.RabbitMQURL, cfg.RabbitMQEnv) if err != nil { - log.Printf("[MQ] 连接失败,将仅用直发邮件降级: %v", err) + slog.Warn("mq", "event", "connect_failed", "err", err) } else { mqClient = c defer mqClient.Close() go mqClient.StartConsumer(ctx, siteStore) - log.Printf("[MQ] 已连接 env=%s exchange=%s", cfg.RabbitMQEnv, mq.ExchangeName(cfg.RabbitMQEnv)) + slog.Info("mq", "event", "connected", "env", cfg.RabbitMQEnv, "exchange", mq.ExchangeName(cfg.RabbitMQEnv)) } } } - r := gin.Default() + if cfg.GinDebug { + gin.SetMode(gin.DebugMode) + } else { + gin.SetMode(gin.ReleaseMode) + } + r := gin.New() + if err := r.SetTrustedProxies(nil); err != nil { + slog.Warn("server", "event", "trusted_proxies", "err", err) + } + r.Use(gin.Recovery()) + r.Use(accessLog()) r.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, - AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Admin-Token"}, + AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Admin-Token", "X-Webhook-Secret"}, ExposeHeaders: []string{"Content-Length"}, AllowCredentials: false, MaxAge: 12 * time.Hour, })) - r.GET("/api/health", func(c *gin.Context) { - resp := gin.H{"status": "ok", "rabbitmq": "disabled"} - if mqClient != nil { - if err := mqClient.Ping(); err != nil { - resp["rabbitmq"] = "error: " + err.Error() - } else { - resp["rabbitmq"] = "ok" - } - } - c.JSON(http.StatusOK, resp) - }) + r.GET("/", rootAPIInfo) + + r.GET("/api/health", HealthCheck(mqClient)) + + if cfg.EnableSwagger { + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + slog.Info("server", "event", "swagger", "path", "/swagger/index.html") + } authClient := auth.NewSproutGateClient(cfg.AuthAPIURL) publicHandler := handlers.NewPublicHandler(store) adminHandler := handlers.NewAdminHandler(store, cfg, siteStore, orderStore, chatStore) - orderHandler := handlers.NewOrderHandler(store, orderStore, siteStore, authClient, mqClient) + orderHandler := handlers.NewOrderHandler(store, orderStore, siteStore, authClient, mqClient, cfg.PaymentPendingTTLSecs, cfg.WebhookMengyaSecret) + go orderHandler.RunExpiredPaymentSweep(ctx, 25*time.Second) statsHandler := handlers.NewStatsHandler(orderStore, siteStore) wishlistHandler := handlers.NewWishlistHandler(wishlistStore, authClient) chatHandler := handlers.NewChatHandler(chatStore, authClient) @@ -105,12 +231,15 @@ func main() { r.GET("/api/products", publicHandler.ListProducts) r.POST("/api/checkout", orderHandler.CreateOrder) + r.GET("/api/orders/:id/payment-status", orderHandler.GetOrderPaymentStatus) + r.POST("/api/webhooks/mengya-pay", orderHandler.MengyaPaymentWebhook) r.POST("/api/products/:id/view", publicHandler.RecordProductView) r.GET("/api/stats", statsHandler.GetStats) r.POST("/api/site/visit", statsHandler.RecordVisit) r.GET("/api/site/maintenance", statsHandler.GetMaintenance) r.GET("/api/orders", orderHandler.ListMyOrders) r.POST("/api/orders/:id/confirm", orderHandler.ConfirmOrder) + r.POST("/api/orders/:id/cancel", orderHandler.CancelOrder) r.POST("/api/admin/verify", adminHandler.VerifyAdminToken) r.GET("/api/admin/products", adminHandler.ListAllProducts) @@ -139,8 +268,9 @@ func main() { r.POST("/api/admin/chat/:account", adminHandler.AdminReply) r.DELETE("/api/admin/chat/:account", adminHandler.ClearConversation) - log.Println("萌芽小店后端启动于 http://localhost:8080") - if err := r.Run(":8080"); err != nil { - log.Fatalf("服务器启动失败: %v", err) + slog.Info("server", "event", "listen", "addr", cfg.HTTPListenAddr) + if err := r.Run(cfg.HTTPListenAddr); err != nil { + slog.Error("server", "event", "exit", "err", err) + os.Exit(1) } } diff --git a/mengyastore-backend-go/后端文档.md b/mengyastore-backend-go/后端文档.md index a625c30..7296964 100644 --- a/mengyastore-backend-go/后端文档.md +++ b/mengyastore-backend-go/后端文档.md @@ -12,7 +12,7 @@ | google/uuid | 订单 ID | | github.com/rabbitmq/amqp091-go | RabbitMQ(可选) | | github.com/redis/go-redis/v9 | Redis 探测(可选) | -| github.com/joho/godotenv | `.env` 加载 | +| github.com/joho/godotenv | `.env.development`(或 `ENV_FILE`)加载 | ## 目录结构(与仓库一致) @@ -58,7 +58,9 @@ mengyastore-backend-go/ | GET | `/api/stats` | 订单数、访问量等 | | POST | `/api/site/visit` | 站点访问计数 | | GET | `/api/site/maintenance` | 维护状态 | -| POST | `/api/checkout` | 创建订单(卡密扣库或固定内容填充 `delivered_codes`) | +| POST | `/api/checkout` | 创建订单:`paymentMethod` 默认 `mengya`,见下文「萌芽支付」 | +| GET | `/api/orders/:id/payment-status` | 结账轮询:`status`、`paymentExpiresAt`(不回卡密) | +| POST | `/api/webhooks/mengya-pay` | 到账通知 Webhook:`X-Webhook-Secret` 与配置一致时可省略 | | GET | `/api/orders` | 当前用户订单(Bearer) | | POST | `/api/orders/:id/confirm` | 确认订单(返回发货内容等) | @@ -97,6 +99,7 @@ mengyastore-backend-go/ |------|------| | fulfillment_type | `card` 卡密逐条;`fixed` 固定内容(不限库存) | | fixed_content | 固定发货正文(网盘链接、说明等);公开接口不返回 | +| payment_qr_urls | JSON 数组:`paymentQrUrls`,萌芽支付收款码图片直链(最多 10),管理端填写 | | delivery_mode | 订单维度习惯字段:`auto` / `manual`(与 fulfillment 独立) | | 其余 | name, price, discount_price, tags, cover_url, screenshot_urls, description, active, require_login, max_per_account, total_sold, view_count, show_note, show_contact … | @@ -106,7 +109,7 @@ mengyastore-backend-go/ ### orders -含 `delivered_codes`(JSON)、`notify_email`、`delivery_mode`、`status` 等。 +含 `delivered_codes`(JSON)、`notify_email`、`delivery_mode`、支付方式字段 `payment_method`、`payment_expected_total`、`payment_expires_at`。**`status`** 含:`pending`、`pending_payment`(萌芽待到账)、`completed`、`cancelled`(超时未付释放库存)等。 ### site_settings @@ -114,27 +117,48 @@ KV:如 `totalVisits`、`maintenance`、`smtpHost`、`smtpPassword` 等;缺 ## 配置说明 -配置由 **`config.Load()`** 从环境变量读取,可选 **`./.env`**(或 `ENV_FILE` 指定)。 +配置由 **`config.Load()`** 从**进程环境变量**读取。未设置 **`ENV_FILE`** 时,会在当前工作目录尝试加载 **`./.env.development`**(文件不存在则跳过,不报错)。生产或其它环境请用 **`ENV_FILE=/path/to/.env.production`** 等方式显式指定文件。 要点: - `DATABASE_DSN` 为空时按 `APP_ENV` 使用内建测试/生产默认 DSN(仅开发便利)。 -- RabbitMQ、Redis 开关与地址见 `config.go` 注释。 +- RabbitMQ、Redis 开关与地址见 `internal/config/config.go` 注释。 - `HTTP_LISTEN_ADDR`、`PUBLIC_API_BASE_URL` 供系统状态 JSON 展示。 +- `WEBHOOK_MENGYA_SECRET`:若设置,到账 Webhook 须在请求头带相同 `X-Webhook-Secret`。 +- `PAYMENT_PENDING_SECONDS`:萌芽支付待支付窗口(秒,默认 60)。 + +### API 文档(Swagger UI)开关 + +OpenAPI 规范由 **`swag`** 从代码注释生成(`docs/swagger.json`;重生成见仓库根目录 **`Makefile`** 的 `swagger` 目标)。运行期是否挂载 UI 由配置决定: + +| 变量 | 说明 | +|------|------| +| **`ENABLE_SWAGGER`** | 为 `1` / `true` / `yes` / `on`(不区分大小写)时,注册 **`GET /swagger/*`**,浏览器打开 **`/swagger/index.html`**。 | +| **`GIN_DEBUG`** | 为真时除 Gin Debug 模式外,也会**开启** Swagger UI(与 `ENABLE_SWAGGER` 二选一满足即可)。 | + +生产环境若未设置上述变量,默认**不**暴露 Swagger,减少接口探测面。 + +## 萌芽支付(Webhook 核销) + +- 结账 `paymentMethod: mengya`(默认)且**非免费**:预留卡密或固定交付内容,`status=pending_payment`,写入 `payment_expected_total`、`payment_expires_at`(UTC);**此时不计销量**,到账后再 `completed` 并 `IncrementSold` + 邮件。 +- **`POST /api/webhooks/mengya-pay`**:JSON 体与 `test/server.py` 打印结构兼容,从 `body.notice` / `notice` 等字段抽取 `到账¥x.xx` 金额,与待支付订单快照匹配(FIFO,`ABS` 容差)。 +- **等额归因**:多笔同额待支付可能被核销到最早一单,需业务规避或后续增强。 +- 超时:后台定时扫描将订单 `cancelled` 并退回卡密库存(固定内容类仅清空订单预留)。 +- 旧版 `paymentMethod=legacy_manual` 等请求已停用(返回 400)。 ## 发货与订单逻辑 ### fulfillment_type = `card`(默认) -1. `POST /api/checkout` 校验库存 ≥ 购买数量。 +1. `POST /api/checkout` 校验库存 ≥ 购买数量(**萌芽支付**下为预留待付,见上文)。 2. 从 `product_codes` 取出对应条数写入订单 `delivered_codes`,并 `Update` 商品去掉已发码。 -3. 销量 `IncrementSold`;邮件/MQ 通知。 +3. 销量 `IncrementSold`(**免费单**在结账时计入;**付费萌芽支付**在 Webhook 核销后 `IncrementSold`);邮件/MQ 通知。 ### fulfillment_type = `fixed` 1. 不校验卡密条数;不修改 `product_codes`。 2. 将 `fixed_content` 复制 `quantity` 次填入 `delivered_codes`(与多件购买展示一致)。 -3. 仍 `IncrementSold`;邮件/MQ 同自动发货路径。 +3. 仍 `IncrementSold`(规则同 card);邮件/MQ 同自动发货路径。 ### delivery_mode(订单) @@ -147,10 +171,12 @@ KV:如 `totalVisits`、`maintenance`、`smtpHost`、`smtpPassword` 等;缺 ## 本地开发 +在 **`mengyastore-backend-go`** 目录放置 **`.env.development`**(可被 git 忽略),或导出环境变量后再启动。 + ```bash go run . go build -o mengyastore-backend.exe . -go run ./cmd/migrate/main.go # 按需迁移旧 JSON +go run ./cmd/migrate/main.go # 按需迁移旧 JSON;同样读取 .env.development 或 ENV_FILE ``` --- diff --git a/mengyastore-backend-java/.gitattributes b/mengyastore-backend-java/.gitattributes deleted file mode 100644 index 3b41682..0000000 --- a/mengyastore-backend-java/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -/mvnw text eol=lf -*.cmd text eol=crlf diff --git a/mengyastore-backend-java/.gitignore b/mengyastore-backend-java/.gitignore deleted file mode 100644 index 667aaef..0000000 --- a/mengyastore-backend-java/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -HELP.md -target/ -.mvn/wrapper/maven-wrapper.jar -!**/src/main/**/target/ -!**/src/test/**/target/ - -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache - -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ -build/ -!**/src/main/**/build/ -!**/src/test/**/build/ - -### VS Code ### -.vscode/ diff --git a/mengyastore-backend-java/.mvn/wrapper/maven-wrapper.properties b/mengyastore-backend-java/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index c595b00..0000000 --- a/mengyastore-backend-java/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,3 +0,0 @@ -wrapperVersion=3.3.4 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip diff --git a/mengyastore-backend-java/compose.yaml b/mengyastore-backend-java/compose.yaml deleted file mode 100644 index 4c71203..0000000 --- a/mengyastore-backend-java/compose.yaml +++ /dev/null @@ -1 +0,0 @@ -services: { } diff --git a/mengyastore-backend-java/mvnw b/mengyastore-backend-java/mvnw deleted file mode 100644 index bd8896b..0000000 --- a/mengyastore-backend-java/mvnw +++ /dev/null @@ -1,295 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.4 -# -# Optional ENV vars -# ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output -# ---------------------------------------------------------------------------- - -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x - -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac - -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" - - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 - fi - fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi - fi -} - -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} - -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} - -scriptDir="$(dirname "$0")" -scriptName="$(basename "$0")" - -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} - -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" -fi - -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac - -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" -fi - -mkdir -p -- "${MAVEN_HOME%/*}" - -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" -fi - -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v - -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac - -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then - distributionSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 - fi -fi - -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" -else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" -fi - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -actualDistributionDir="" - -# First try the expected directory name (for regular distributions) -if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then - if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then - actualDistributionDir="$distributionUrlNameMain" - fi -fi - -# If not found, search for any directory with the Maven executable (for snapshots) -if [ -z "$actualDistributionDir" ]; then - # enable globbing to iterate over items - set +f - for dir in "$TMP_DOWNLOAD_DIR"/*; do - if [ -d "$dir" ]; then - if [ -f "$dir/bin/$MVN_CMD" ]; then - actualDistributionDir="$(basename "$dir")" - break - fi - fi - done - set -f -fi - -if [ -z "$actualDistributionDir" ]; then - verbose "Contents of $TMP_DOWNLOAD_DIR:" - verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" - die "Could not find Maven distribution directory in extracted archive" -fi - -verbose "Found extracted Maven distribution directory: $actualDistributionDir" -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/mengyastore-backend-java/mvnw.cmd b/mengyastore-backend-java/mvnw.cmd deleted file mode 100644 index 92450f9..0000000 --- a/mengyastore-backend-java/mvnw.cmd +++ /dev/null @@ -1,189 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' - -$MAVEN_M2_PATH = "$HOME/.m2" -if ($env:MAVEN_USER_HOME) { - $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" -} - -if (-not (Test-Path -Path $MAVEN_M2_PATH)) { - New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null -} - -$MAVEN_WRAPPER_DISTS = $null -if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { - $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" -} else { - $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" -} - -$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -$actualDistributionDir = "" - -# First try the expected directory name (for regular distributions) -$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" -$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" -if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { - $actualDistributionDir = $distributionUrlNameMain -} - -# If not found, search for any directory with the Maven executable (for snapshots) -if (!$actualDistributionDir) { - Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { - $testPath = Join-Path $_.FullName "bin/$MVN_CMD" - if (Test-Path -Path $testPath -PathType Leaf) { - $actualDistributionDir = $_.Name - } - } -} - -if (!$actualDistributionDir) { - Write-Error "Could not find Maven distribution directory in extracted archive" -} - -Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/mengyastore-backend-java/pom.xml b/mengyastore-backend-java/pom.xml deleted file mode 100644 index 503ea79..0000000 --- a/mengyastore-backend-java/pom.xml +++ /dev/null @@ -1,121 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.5.12 - - - com.smyhub.store - mengyastore-backend-java - 0.0.1-SNAPSHOT - mengyastore-backend-java - 萌芽小店 Java Spring Boot 后端 - - 17 - - - - - org.springframework.boot - spring-boot-starter-web - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - - - com.mysql - mysql-connector-j - runtime - - - - - org.springframework.boot - spring-boot-starter-amqp - - - - - org.springframework.boot - spring-boot-starter-data-redis - - - - - org.springframework.boot - spring-boot-starter-validation - - - - - org.springframework.boot - spring-boot-starter-mail - - - - - org.springframework.boot - spring-boot-starter-actuator - - - - - org.projectlombok - lombok - true - - - - - org.springframework.boot - spring-boot-devtools - runtime - true - - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - - - diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/MengyastoreBackendJavaApplication.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/MengyastoreBackendJavaApplication.java deleted file mode 100644 index c247a17..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/MengyastoreBackendJavaApplication.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.scheduling.annotation.EnableScheduling; -import org.springframework.transaction.annotation.EnableTransactionManagement; - -@SpringBootApplication -@EnableTransactionManagement -@EnableScheduling -public class MengyastoreBackendJavaApplication { - - public static void main(String[] args) { - SpringApplication.run(MengyastoreBackendJavaApplication.class, args); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/AppProperties.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/AppProperties.java deleted file mode 100644 index 733c7f7..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/AppProperties.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -@Data -@Component -@ConfigurationProperties(prefix = "app") -public class AppProperties { - - private String adminToken = "changeme"; - - private String authApiUrl = "https://auth.api.shumengya.top"; - - /** Mirrors Go APP_ENV: {@code development} | {@code production}; used by system-status panel. */ - private String appEnv = "development"; - - private boolean rabbitmqEnabled = false; - - /** - * Mirrors Go REDIS_ENABLED (default on unless explicitly disabled). - */ - private boolean redisEnabled = true; - - private String rabbitmqEnv = "dev"; - - private String redisEnv = "dev"; - - private String publicApiBaseUrl = ""; - - private String corsAllowedOrigins = "http://localhost:5173,http://localhost:3000"; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/CorsConfig.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/CorsConfig.java deleted file mode 100644 index 5932584..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/CorsConfig.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.config; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.util.StringUtils; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.filter.CorsFilter; - -import java.util.Arrays; -import java.util.List; - -@Configuration -@RequiredArgsConstructor -public class CorsConfig { - - private final AppProperties appProperties; - - @Bean - public CorsFilter corsFilter() { - CorsConfiguration config = new CorsConfiguration(); - - String originsRaw = appProperties.getCorsAllowedOrigins(); - if (StringUtils.hasText(originsRaw)) { - List origins = Arrays.asList(originsRaw.split(",")); - config.setAllowedOrigins(origins); - } else { - config.addAllowedOriginPattern("*"); - } - - config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); - config.setAllowedHeaders(List.of( - "Content-Type", "Authorization", "X-Admin-Token", - "X-Requested-With", "Accept", "Accept-Language" - )); - config.setAllowCredentials(true); - config.setMaxAge(86400L); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/api/**", config); - return new CorsFilter(source); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/RabbitMQConfig.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/RabbitMQConfig.java deleted file mode 100644 index 03b46bd..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/RabbitMQConfig.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.config; - -import com.smyhub.store.mengyastorebackendjava.mq.RabbitMQTopology; -import lombok.RequiredArgsConstructor; -import org.springframework.amqp.core.*; -import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.amqp.core.AcknowledgeMode; -import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@RequiredArgsConstructor -@ConditionalOnProperty(name = "app.rabbitmq-enabled", havingValue = "true") -public class RabbitMQConfig { - - private final AppProperties appProperties; - - @Bean - public Jackson2JsonMessageConverter jsonMessageConverter() { - return new Jackson2JsonMessageConverter(); - } - - @Bean - public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { - RabbitTemplate template = new RabbitTemplate(connectionFactory); - template.setMessageConverter(jsonMessageConverter()); - return template; - } - - @Bean - public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) { - SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); - factory.setConnectionFactory(connectionFactory); - factory.setMessageConverter(jsonMessageConverter()); - factory.setPrefetchCount(1); - factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); - return factory; - } - - @Bean - public TopicExchange orderEmailExchange() { - String name = RabbitMQTopology.exchangeName(appProperties.getRabbitmqEnv()); - return ExchangeBuilder.topicExchange(name).durable(true).build(); - } - - @Bean - public Queue orderEmailQueue() { - String name = RabbitMQTopology.queueName(appProperties.getRabbitmqEnv()); - return QueueBuilder.durable(name).build(); - } - - @Bean - public Binding orderEmailBinding() { - return BindingBuilder - .bind(orderEmailQueue()) - .to(orderEmailExchange()) - .with(RabbitMQTopology.ORDER_EMAIL_ROUTING_KEY); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/RedisConfig.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/RedisConfig.java deleted file mode 100644 index 2f65a2e..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/RedisConfig.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.StringRedisTemplate; - -@Configuration -public class RedisConfig { - - @Bean - public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) { - return new StringRedisTemplate(connectionFactory); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/RestTemplateConfig.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/RestTemplateConfig.java deleted file mode 100644 index c2470bb..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/RestTemplateConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.config; - -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestTemplate; - -import java.time.Duration; - -@Configuration -public class RestTemplateConfig { - - @Bean - public RestTemplate restTemplate(RestTemplateBuilder builder) { - return builder - .connectTimeout(Duration.ofSeconds(5)) - .readTimeout(Duration.ofSeconds(8)) - .build(); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/WebMvcConfig.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/WebMvcConfig.java deleted file mode 100644 index def68e4..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/config/WebMvcConfig.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.config; - -import com.smyhub.store.mengyastorebackendjava.security.AdminTokenInterceptor; -import com.smyhub.store.mengyastorebackendjava.security.BearerTokenInterceptor; -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -@RequiredArgsConstructor -public class WebMvcConfig implements WebMvcConfigurer { - - private final AdminTokenInterceptor adminTokenInterceptor; - private final BearerTokenInterceptor bearerTokenInterceptor; - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(adminTokenInterceptor) - .addPathPatterns("/api/admin/**") - .excludePathPatterns("/api/admin/verify"); - - registry.addInterceptor(bearerTokenInterceptor) - .addPathPatterns( - "/api/orders", - "/api/wishlist", - "/api/wishlist/**", - "/api/chat/messages" - ); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/ChatController.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/ChatController.java deleted file mode 100644 index c8d7935..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/ChatController.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.controller; - -import com.smyhub.store.mengyastorebackendjava.dto.request.ChatMessageRequest; -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import com.smyhub.store.mengyastorebackendjava.dto.response.ChatMessageResponse; -import com.smyhub.store.mengyastorebackendjava.security.CurrentUserHolder; -import com.smyhub.store.mengyastorebackendjava.security.SproutGateUser; -import com.smyhub.store.mengyastorebackendjava.service.ChatService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/chat") -@RequiredArgsConstructor -public class ChatController { - - private final ChatService chatService; - - @GetMapping("/messages") - public ResponseEntity>> getMessages() { - String account = CurrentUserHolder.get().getAccount(); - return ResponseEntity.ok(ApiResponse.ok(chatService.getUserMessages(account))); - } - - @PostMapping("/messages") - public ResponseEntity> sendMessage( - @Valid @RequestBody ChatMessageRequest req) { - SproutGateUser user = CurrentUserHolder.get(); - ChatMessageResponse msg = chatService.sendUserMessage( - user.getAccount(), user.getUsername(), req.getContent() - ); - return ResponseEntity.ok(ApiResponse.ok(msg)); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/HealthController.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/HealthController.java deleted file mode 100644 index 920e33d..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/HealthController.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.controller; - -import com.smyhub.store.mengyastorebackendjava.config.AppProperties; -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import com.smyhub.store.mengyastorebackendjava.dto.response.HealthResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Slf4j -@RestController -@RequestMapping("/api") -public class HealthController { - - private final AppProperties appProperties; - - @Autowired(required = false) - private RabbitTemplate rabbitTemplate; - - public HealthController(AppProperties appProperties) { - this.appProperties = appProperties; - } - - @GetMapping("/health") - public ResponseEntity> health() { - HealthResponse body = HealthResponse.builder() - .status("ok") - .rabbitmq(checkRabbitMQ()) - .build(); - return ResponseEntity.ok(ApiResponse.ok(body)); - } - - private String checkRabbitMQ() { - if (!appProperties.isRabbitmqEnabled()) { - return "disabled"; - } - if (rabbitTemplate == null) { - return "disabled"; - } - try { - rabbitTemplate.execute(channel -> { - channel.basicQos(1); - return null; - }); - return "ok"; - } catch (Exception e) { - log.debug("[Health] RabbitMQ ping failed: {}", e.getMessage()); - return "error"; - } - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/PublicOrderController.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/PublicOrderController.java deleted file mode 100644 index 18bee7f..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/PublicOrderController.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.controller; - -import com.smyhub.store.mengyastorebackendjava.dto.request.CheckoutRequest; -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import com.smyhub.store.mengyastorebackendjava.security.SproutGateUser; -import com.smyhub.store.mengyastorebackendjava.security.SproutGateVerifyResult; -import com.smyhub.store.mengyastorebackendjava.service.OrderService; -import com.smyhub.store.mengyastorebackendjava.service.SproutGateAuthService; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.*; - -import java.util.Map; - -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class PublicOrderController { - - private final OrderService orderService; - private final SproutGateAuthService authService; - - @PostMapping("/checkout") - public ResponseEntity>> checkout( - @Valid @RequestBody CheckoutRequest req, - HttpServletRequest request) { - - SproutGateUser user = tryResolveUser(request); - OrderService.CheckoutResult result = orderService.checkout(req, user); - - String qrPayload = "order:" + result.orderId() + ":" + result.productId(); - String qrUrl = "https://api.qrserver.com/v1/create-qr-code/?size=320x320&data=" - + java.net.URLEncoder.encode(qrPayload, java.nio.charset.StandardCharsets.UTF_8); - - return ResponseEntity.ok(ApiResponse.ok(Map.of( - "orderId", result.orderId(), - "qrCodeUrl", qrUrl, - "productId", result.productId(), - "productQty", result.qty(), - "viewCount", result.viewCount(), - "status", result.status() - ))); - } - - @PostMapping("/orders/{id}/confirm") - public ResponseEntity>> confirmOrder(@PathVariable String id) { - var order = orderService.confirmOrder(id); - return ResponseEntity.ok(ApiResponse.ok(Map.of( - "orderId", order.getId(), - "status", order.getStatus(), - "deliveryMode", order.getDeliveryMode(), - "deliveredCodes", order.getDeliveredCodes(), - "isManual", "manual".equals(order.getDeliveryMode()) - ))); - } - - private SproutGateUser tryResolveUser(HttpServletRequest request) { - String header = request.getHeader("Authorization"); - if (!StringUtils.hasText(header) || !header.startsWith("Bearer ")) return null; - String token = header.substring(7); - SproutGateVerifyResult result = authService.verifyToken(token); - return (result.isValid() && result.getUser() != null) ? result.getUser() : null; - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/PublicProductController.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/PublicProductController.java deleted file mode 100644 index b0b53b2..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/PublicProductController.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.controller; - -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import com.smyhub.store.mengyastorebackendjava.dto.response.ProductResponse; -import com.smyhub.store.mengyastorebackendjava.service.ProductService; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class PublicProductController { - - private final ProductService productService; - - @GetMapping("/products") - public ResponseEntity>> listProducts() { - return ResponseEntity.ok(ApiResponse.ok(productService.listPublicProducts())); - } - - @PostMapping("/products/{id}/view") - public ResponseEntity>> recordView( - @PathVariable String id, - HttpServletRequest request) { - String fingerprint = buildFingerprint(request); - ProductResponse p = productService.recordView(id, fingerprint); - return ResponseEntity.ok(ApiResponse.ok(Map.of( - "id", p.getId(), - "viewCount", p.getViewCount(), - "counted", true - ))); - } - - private String buildFingerprint(HttpServletRequest request) { - String ip = getClientIp(request); - String ua = nvl(request.getHeader("User-Agent")); - String lang = nvl(request.getHeader("Accept-Language")); - return ip + "|" + ua + "|" + lang; - } - - private String getClientIp(HttpServletRequest request) { - String forwarded = request.getHeader("X-Forwarded-For"); - if (forwarded != null && !forwarded.isBlank()) { - return forwarded.split(",")[0].strip(); - } - return request.getRemoteAddr(); - } - - private String nvl(String s) { - return s != null ? s.strip() : ""; - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/StatsController.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/StatsController.java deleted file mode 100644 index 9b5ad4b..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/StatsController.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.controller; - -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import com.smyhub.store.mengyastorebackendjava.dto.response.MaintenanceResponse; -import com.smyhub.store.mengyastorebackendjava.dto.response.StatsResponse; -import com.smyhub.store.mengyastorebackendjava.repository.OrderRepository; -import com.smyhub.store.mengyastorebackendjava.service.SiteService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.Map; - -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class StatsController { - - private final OrderRepository orderRepository; - private final SiteService siteService; - - @GetMapping("/stats") - public ResponseEntity> getStats() { - long total = orderRepository.count(); - int visits = siteService.getTotalVisits(); - return ResponseEntity.ok(ApiResponse.ok(StatsResponse.builder() - .totalOrders(total) - .totalVisits(visits) - .build())); - } - - @PostMapping("/site/visit") - public ResponseEntity>> recordVisit() { - int total = siteService.incrementVisits(); - return ResponseEntity.ok(ApiResponse.ok(Map.of("total", total, "counted", true))); - } - - @GetMapping("/site/maintenance") - public ResponseEntity> getMaintenance() { - return ResponseEntity.ok(ApiResponse.ok(siteService.getMaintenance())); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/UserOrderController.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/UserOrderController.java deleted file mode 100644 index e94a208..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/UserOrderController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.controller; - -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import com.smyhub.store.mengyastorebackendjava.dto.response.OrderResponse; -import com.smyhub.store.mengyastorebackendjava.security.CurrentUserHolder; -import com.smyhub.store.mengyastorebackendjava.service.OrderService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -@RestController -@RequestMapping("/api") -@RequiredArgsConstructor -public class UserOrderController { - - private final OrderService orderService; - - @GetMapping("/orders") - public ResponseEntity>> myOrders() { - String account = CurrentUserHolder.get().getAccount(); - return ResponseEntity.ok(ApiResponse.ok(orderService.getMyOrders(account))); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/WishlistController.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/WishlistController.java deleted file mode 100644 index c28cc1f..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/WishlistController.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.controller; - -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import com.smyhub.store.mengyastorebackendjava.security.CurrentUserHolder; -import com.smyhub.store.mengyastorebackendjava.service.WishlistService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/wishlist") -@RequiredArgsConstructor -public class WishlistController { - - private final WishlistService wishlistService; - - @GetMapping - public ResponseEntity>>> getWishlist() { - String account = CurrentUserHolder.get().getAccount(); - List items = wishlistService.getWishlistIds(account); - return ResponseEntity.ok(ApiResponse.ok(Map.of("items", items))); - } - - @PostMapping - public ResponseEntity>>> addToWishlist( - @RequestBody Map body) { - String productId = body.get("productId"); - String account = CurrentUserHolder.get().getAccount(); - wishlistService.addToWishlist(account, productId); - List items = wishlistService.getWishlistIds(account); - return ResponseEntity.ok(ApiResponse.ok(Map.of("items", items))); - } - - @DeleteMapping("/{productId}") - public ResponseEntity>>> removeFromWishlist( - @PathVariable String productId) { - String account = CurrentUserHolder.get().getAccount(); - wishlistService.removeFromWishlist(account, productId); - List items = wishlistService.getWishlistIds(account); - return ResponseEntity.ok(ApiResponse.ok(Map.of("items", items))); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminChatController.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminChatController.java deleted file mode 100644 index 5f29d53..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminChatController.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.controller.admin; - -import com.smyhub.store.mengyastorebackendjava.dto.request.AdminChatMessageRequest; -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import com.smyhub.store.mengyastorebackendjava.dto.response.ChatMessageResponse; -import com.smyhub.store.mengyastorebackendjava.service.ChatService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/admin/chat") -@RequiredArgsConstructor -public class AdminChatController { - - private final ChatService chatService; - - @GetMapping - public ResponseEntity>>> listConversations() { - return ResponseEntity.ok(ApiResponse.ok(chatService.listAllConversations())); - } - - @GetMapping("/{account}") - public ResponseEntity>> getConversation( - @PathVariable String account) { - return ResponseEntity.ok(ApiResponse.ok(chatService.getConversation(account))); - } - - @PostMapping("/{account}") - public ResponseEntity> replyToUser( - @PathVariable String account, - @Valid @RequestBody AdminChatMessageRequest req) { - ChatMessageResponse msg = chatService.sendAdminMessage(account, req.getContent()); - return ResponseEntity.ok(ApiResponse.ok(msg)); - } - - @DeleteMapping("/{account}") - public ResponseEntity> clearConversation(@PathVariable String account) { - chatService.clearConversation(account); - return ResponseEntity.ok(ApiResponse.ok(null)); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminOrderController.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminOrderController.java deleted file mode 100644 index b77ce5e..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminOrderController.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.controller.admin; - -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import com.smyhub.store.mengyastorebackendjava.dto.response.OrderResponse; -import com.smyhub.store.mengyastorebackendjava.service.OrderService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/admin/orders") -@RequiredArgsConstructor -public class AdminOrderController { - - private final OrderService orderService; - - @GetMapping - public ResponseEntity>> listOrders() { - return ResponseEntity.ok(ApiResponse.ok(orderService.listAllOrders())); - } - - @DeleteMapping("/{id}") - public ResponseEntity> deleteOrder(@PathVariable String id) { - orderService.deleteOrder(id); - return ResponseEntity.ok(ApiResponse.ok(null)); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminProductController.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminProductController.java deleted file mode 100644 index f8c9ceb..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminProductController.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.controller.admin; - -import com.smyhub.store.mengyastorebackendjava.dto.request.AdminProductRequest; -import com.smyhub.store.mengyastorebackendjava.dto.request.ProductStatusRequest; -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import com.smyhub.store.mengyastorebackendjava.dto.response.ProductResponse; -import com.smyhub.store.mengyastorebackendjava.service.ProductService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/api/admin/products") -@RequiredArgsConstructor -public class AdminProductController { - - private final ProductService productService; - - @GetMapping - public ResponseEntity>> listProducts() { - return ResponseEntity.ok(ApiResponse.ok(productService.listAllProducts())); - } - - @PostMapping - public ResponseEntity> createProduct( - @Valid @RequestBody AdminProductRequest req) { - return ResponseEntity.ok(ApiResponse.ok(productService.createProduct(req))); - } - - @PutMapping("/{id}") - public ResponseEntity> updateProduct( - @PathVariable String id, - @Valid @RequestBody AdminProductRequest req) { - return ResponseEntity.ok(ApiResponse.ok(productService.updateProduct(id, req))); - } - - @PatchMapping("/{id}/status") - public ResponseEntity> toggleStatus( - @PathVariable String id, - @RequestBody ProductStatusRequest req) { - return ResponseEntity.ok(ApiResponse.ok(productService.toggleStatus(id, req.isActive()))); - } - - @DeleteMapping("/{id}") - public ResponseEntity> deleteProduct(@PathVariable String id) { - productService.deleteProduct(id); - return ResponseEntity.ok(ApiResponse.ok(null)); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminSiteController.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminSiteController.java deleted file mode 100644 index aa194b9..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminSiteController.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.controller.admin; - -import com.smyhub.store.mengyastorebackendjava.dto.request.MaintenanceRequest; -import com.smyhub.store.mengyastorebackendjava.dto.request.SmtpConfigRequest; -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import com.smyhub.store.mengyastorebackendjava.dto.response.MaintenanceResponse; -import com.smyhub.store.mengyastorebackendjava.dto.response.SmtpConfigResponse; -import com.smyhub.store.mengyastorebackendjava.service.SiteService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/admin/site") -@RequiredArgsConstructor -public class AdminSiteController { - - private final SiteService siteService; - - @PostMapping("/maintenance") - public ResponseEntity> setMaintenance( - @RequestBody MaintenanceRequest req) { - siteService.setMaintenance(req.isEnabled(), req.getReason()); - return ResponseEntity.ok(ApiResponse.ok(siteService.getMaintenance())); - } - - @GetMapping("/smtp") - public ResponseEntity> getSmtp() { - return ResponseEntity.ok(ApiResponse.ok(siteService.getSmtpConfigResponse(true))); - } - - @PostMapping("/smtp") - public ResponseEntity> setSmtp( - @RequestBody SmtpConfigRequest req) { - siteService.setSmtpConfig(req); - return ResponseEntity.ok(ApiResponse.ok(siteService.getSmtpConfigResponse(true))); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminStatusController.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminStatusController.java deleted file mode 100644 index 8c12b34..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminStatusController.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.controller.admin; - -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import com.smyhub.store.mengyastorebackendjava.service.SystemStatusService; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Map; - -@RestController -@RequestMapping("/api/admin") -@RequiredArgsConstructor -public class AdminStatusController { - - private final SystemStatusService systemStatusService; - - @GetMapping("/system-status") - public ResponseEntity>> getSystemStatus(HttpServletRequest request) { - return ResponseEntity.ok(ApiResponse.ok(systemStatusService.buildStatus(request))); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminVerifyController.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminVerifyController.java deleted file mode 100644 index 0f3eb69..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/controller/admin/AdminVerifyController.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.controller.admin; - -import com.smyhub.store.mengyastorebackendjava.config.AppProperties; -import com.smyhub.store.mengyastorebackendjava.dto.request.AdminVerifyRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.*; - -import java.util.Map; - -@RestController -@RequestMapping("/api/admin") -@RequiredArgsConstructor -public class AdminVerifyController { - - private final AppProperties appProperties; - - /** - * Matches Go backend: top-level {@code {"valid": true/false}} (frontend reads {@code response.data.valid} from axios). - */ - @PostMapping("/verify") - public ResponseEntity> verify(@RequestBody(required = false) AdminVerifyRequest req) { - if (req == null || !StringUtils.hasText(req.getToken())) { - return ResponseEntity.ok(Map.of("valid", false)); - } - boolean valid = req.getToken().equals(appProperties.getAdminToken()); - return ResponseEntity.ok(Map.of("valid", valid)); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/AdminChatMessageRequest.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/AdminChatMessageRequest.java deleted file mode 100644 index 61fbc0c..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/AdminChatMessageRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.request; - -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -@Data -public class AdminChatMessageRequest { - - @NotBlank(message = "消息内容不能为空") - private String content; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/AdminProductRequest.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/AdminProductRequest.java deleted file mode 100644 index a802d04..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/AdminProductRequest.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.request; - -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -import java.util.ArrayList; -import java.util.List; - -@Data -public class AdminProductRequest { - - @NotBlank(message = "商品名称不能为空") - private String name; - - private double price; - - private double discountPrice; - - private List tags = new ArrayList<>(); - - private String coverUrl; - - private List screenshotUrls = new ArrayList<>(); - - private String verificationUrl; - - private String description; - - private boolean active = true; - - private boolean requireLogin; - - private int maxPerAccount; - - private String deliveryMode = "auto"; - - private String fulfillmentType = "card"; - - private String fixedContent; - - private boolean showNote = true; - - private boolean showContact = true; - - private List codes = new ArrayList<>(); -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/AdminVerifyRequest.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/AdminVerifyRequest.java deleted file mode 100644 index 93bfaf2..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/AdminVerifyRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.request; - -import lombok.Data; - -@Data -public class AdminVerifyRequest { - private String token; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/ChatMessageRequest.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/ChatMessageRequest.java deleted file mode 100644 index f175ab0..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/ChatMessageRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.request; - -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -@Data -public class ChatMessageRequest { - - @NotBlank(message = "消息内容不能为空") - private String content; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/CheckoutRequest.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/CheckoutRequest.java deleted file mode 100644 index b2dc47d..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/CheckoutRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.request; - -import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -@Data -public class CheckoutRequest { - - @NotBlank(message = "productId 不能为空") - private String productId; - - private int quantity = 1; - - private String note; - - private String contactPhone; - - private String contactEmail; - - private String notifyEmail; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/MaintenanceRequest.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/MaintenanceRequest.java deleted file mode 100644 index 7a79769..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/MaintenanceRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.request; - -import lombok.Data; - -@Data -public class MaintenanceRequest { - private boolean enabled; - private String reason = ""; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/ProductStatusRequest.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/ProductStatusRequest.java deleted file mode 100644 index d2867cf..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/ProductStatusRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.request; - -import lombok.Data; - -@Data -public class ProductStatusRequest { - private boolean active; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/SmtpConfigRequest.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/SmtpConfigRequest.java deleted file mode 100644 index 74fc31f..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/request/SmtpConfigRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.request; - -import lombok.Data; - -@Data -public class SmtpConfigRequest { - private boolean enabled = true; - private String email; - private String password; - private String fromName; - private String host = "smtp.qq.com"; - private String port = "465"; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/ApiResponse.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/ApiResponse.java deleted file mode 100644 index 8dc3ff2..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/ApiResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.response; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Getter; - -@Getter -@JsonInclude(JsonInclude.Include.NON_NULL) -public class ApiResponse { - - private final T data; - private final String error; - - private ApiResponse(T data, String error) { - this.data = data; - this.error = error; - } - - public static ApiResponse ok(T data) { - return new ApiResponse<>(data, null); - } - - public static ApiResponse error(String message) { - return new ApiResponse<>(null, message); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/ChatMessageResponse.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/ChatMessageResponse.java deleted file mode 100644 index 4357e51..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/ChatMessageResponse.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.response; - -import lombok.Builder; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@Builder -public class ChatMessageResponse { - - private String id; - private String accountId; - private String accountName; - private String content; - private LocalDateTime sentAt; - private boolean fromAdmin; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/HealthResponse.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/HealthResponse.java deleted file mode 100644 index 968bb44..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/HealthResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.response; - -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class HealthResponse { - private String status; - private String rabbitmq; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/MaintenanceResponse.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/MaintenanceResponse.java deleted file mode 100644 index 81f7d79..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/MaintenanceResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.response; - -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class MaintenanceResponse { - private boolean maintenance; - private String reason; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/OrderResponse.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/OrderResponse.java deleted file mode 100644 index 8c45825..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/OrderResponse.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.response; - -import lombok.Builder; -import lombok.Data; - -import java.time.LocalDateTime; -import java.util.List; - -@Data -@Builder -public class OrderResponse { - - private String id; - private String productId; - private String productName; - private String userAccount; - private String userName; - private int quantity; - private List deliveredCodes; - private String status; - private String deliveryMode; - private String note; - private String contactPhone; - private String contactEmail; - private String notifyEmail; - private LocalDateTime createdAt; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/ProductResponse.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/ProductResponse.java deleted file mode 100644 index 0081ee0..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/ProductResponse.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.response; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Builder; -import lombok.Data; - -import java.time.LocalDateTime; -import java.util.List; - -@Data -@Builder -@JsonInclude(JsonInclude.Include.NON_NULL) -public class ProductResponse { - - private String id; - private String name; - private double price; - private double discountPrice; - private List tags; - private int quantity; - private String coverUrl; - private List screenshotUrls; - private String verificationUrl; - private List codes; - private int viewCount; - private String description; - private boolean active; - private boolean requireLogin; - private int maxPerAccount; - private int totalSold; - private String deliveryMode; - private String fulfillmentType; - private String fixedContent; - private boolean showNote; - private boolean showContact; - private LocalDateTime createdAt; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/SmtpConfigResponse.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/SmtpConfigResponse.java deleted file mode 100644 index a3e2ed7..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/SmtpConfigResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.response; - -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class SmtpConfigResponse { - private boolean enabled; - private String email; - private String password; - private String fromName; - private String host; - private String port; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/StatsResponse.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/StatsResponse.java deleted file mode 100644 index 61c4d4d..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/StatsResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.response; - -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class StatsResponse { - private long totalOrders; - private int totalVisits; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/WishlistResponse.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/WishlistResponse.java deleted file mode 100644 index 6f7dd60..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/dto/response/WishlistResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.dto.response; - -import lombok.Builder; -import lombok.Data; - -@Data -@Builder -public class WishlistResponse { - private Long id; - private String accountId; - private String productId; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/ChatMessage.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/ChatMessage.java deleted file mode 100644 index 9c838a9..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/ChatMessage.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "chat_messages") -@Getter -@Setter -public class ChatMessage { - - @Id - @Column(length = 36) - private String id; - - @Column(name = "account_id", length = 255, nullable = false) - private String accountId; - - @Column(name = "account_name", length = 255) - private String accountName; - - @Column(columnDefinition = "text", nullable = false) - private String content; - - @Column(name = "sent_at", nullable = false) - private LocalDateTime sentAt; - - @Column(name = "from_admin", columnDefinition = "tinyint(1) default 0") - private boolean fromAdmin; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/Order.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/Order.java deleted file mode 100644 index 37f5aae..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/Order.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.entity; - -import com.smyhub.store.mengyastorebackendjava.entity.converter.StringListConverter; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Table(name = "orders") -@Getter -@Setter -public class Order { - - @Id - @Column(length = 36) - private String id; - - @Column(name = "product_id", length = 36, nullable = false) - private String productId; - - @Column(name = "product_name", length = 255, nullable = false) - private String productName; - - @Column(name = "user_account", length = 255) - private String userAccount; - - @Column(name = "user_name", length = 255) - private String userName; - - @Column(nullable = false, columnDefinition = "int default 1") - private int quantity; - - @Convert(converter = StringListConverter.class) - @Column(name = "delivered_codes", columnDefinition = "json") - private List deliveredCodes = new ArrayList<>(); - - @Column(length = 20, nullable = false, columnDefinition = "varchar(20) default 'pending'") - private String status = "pending"; - - @Column(name = "delivery_mode", length = 20, columnDefinition = "varchar(20) default 'auto'") - private String deliveryMode = "auto"; - - @Column(columnDefinition = "text") - private String note; - - @Column(name = "contact_phone", length = 50) - private String contactPhone; - - @Column(name = "contact_email", length = 255) - private String contactEmail; - - @Column(name = "notify_email", length = 255) - private String notifyEmail; - - @Column(name = "created_at") - private LocalDateTime createdAt; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/Product.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/Product.java deleted file mode 100644 index b53834b..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/Product.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.entity; - -import com.smyhub.store.mengyastorebackendjava.entity.converter.StringListConverter; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Table(name = "products") -@Getter -@Setter -public class Product { - - @Id - @Column(length = 36) - private String id; - - @Column(length = 255, nullable = false) - private String name; - - @Column(nullable = false, columnDefinition = "double default 0") - private double price; - - @Column(columnDefinition = "double default 0") - private double discountPrice; - - @Convert(converter = StringListConverter.class) - @Column(columnDefinition = "json") - private List tags = new ArrayList<>(); - - @Column(length = 500) - private String coverUrl; - - @Convert(converter = StringListConverter.class) - @Column(columnDefinition = "json") - private List screenshotUrls = new ArrayList<>(); - - @Column(length = 500, columnDefinition = "varchar(500) default ''") - private String verificationUrl; - - @Column(columnDefinition = "text") - private String description; - - @Column(columnDefinition = "tinyint(1) default 1") - private boolean active = true; - - @Column(columnDefinition = "tinyint(1) default 0") - private boolean requireLogin; - - @Column(columnDefinition = "int default 0") - private int maxPerAccount; - - @Column(columnDefinition = "int default 0") - private int totalSold; - - @Column(columnDefinition = "int default 0") - private int viewCount; - - @Column(length = 20, columnDefinition = "varchar(20) default 'auto'") - private String deliveryMode = "auto"; - - @Column(length = 16, columnDefinition = "varchar(16) default 'card'") - private String fulfillmentType = "card"; - - @Column(columnDefinition = "text") - private String fixedContent; - - @Column(columnDefinition = "tinyint(1) default 1") - private boolean showNote = true; - - @Column(columnDefinition = "tinyint(1) default 1") - private boolean showContact = true; - - @Column - private LocalDateTime createdAt; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/ProductCode.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/ProductCode.java deleted file mode 100644 index 52373df..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/ProductCode.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Entity -@Table(name = "product_codes") -@Getter -@Setter -@NoArgsConstructor -public class ProductCode { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "product_id", length = 36, nullable = false) - private String productId; - - @Column(columnDefinition = "text", nullable = false) - private String code; - - public ProductCode(String productId, String code) { - this.productId = productId; - this.code = code; - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/SiteSetting.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/SiteSetting.java deleted file mode 100644 index 9862ec3..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/SiteSetting.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Entity -@Table(name = "site_settings") -@Getter -@Setter -@NoArgsConstructor -public class SiteSetting { - - @Id - @Column(name = "`key`", length = 64) - private String key; - - @Column(columnDefinition = "text") - private String value; - - public SiteSetting(String key, String value) { - this.key = key; - this.value = value; - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/Wishlist.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/Wishlist.java deleted file mode 100644 index 241730f..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/Wishlist.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.entity; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Entity -@Table(name = "wishlists", - uniqueConstraints = @UniqueConstraint(name = "idx_wishlist", columnNames = {"account_id", "product_id"})) -@Getter -@Setter -@NoArgsConstructor -public class Wishlist { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "account_id", length = 255, nullable = false) - private String accountId; - - @Column(name = "product_id", length = 36, nullable = false) - private String productId; - - public Wishlist(String accountId, String productId) { - this.accountId = accountId; - this.productId = productId; - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/converter/StringListConverter.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/converter/StringListConverter.java deleted file mode 100644 index 4e41cb5..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/entity/converter/StringListConverter.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.entity.converter; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.persistence.AttributeConverter; -import jakarta.persistence.Converter; - -import java.util.ArrayList; -import java.util.List; - -@Converter -public class StringListConverter implements AttributeConverter, String> { - - private static final ObjectMapper MAPPER = new ObjectMapper(); - - @Override - public String convertToDatabaseColumn(List attribute) { - if (attribute == null) { - return "[]"; - } - try { - return MAPPER.writeValueAsString(attribute); - } catch (Exception e) { - return "[]"; - } - } - - @Override - public List convertToEntityAttribute(String dbData) { - if (dbData == null || dbData.isBlank()) { - return new ArrayList<>(); - } - try { - return MAPPER.readValue(dbData, new TypeReference<>() {}); - } catch (Exception e) { - return new ArrayList<>(); - } - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/exception/BusinessException.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/exception/BusinessException.java deleted file mode 100644 index 0820ba6..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/exception/BusinessException.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.exception; - -import org.springframework.http.HttpStatus; - -public class BusinessException extends RuntimeException { - - private final HttpStatus status; - - public BusinessException(String message) { - this(message, HttpStatus.BAD_REQUEST); - } - - public BusinessException(String message, HttpStatus status) { - super(message); - this.status = status; - } - - public HttpStatus getStatus() { - return status; - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/exception/GlobalExceptionHandler.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/exception/GlobalExceptionHandler.java deleted file mode 100644 index 8909453..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.exception; - -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -@Slf4j -@RestControllerAdvice -public class GlobalExceptionHandler { - - @ExceptionHandler(BusinessException.class) - public ResponseEntity> handleBusiness(BusinessException ex) { - return ResponseEntity.status(ex.getStatus()).body(ApiResponse.error(ex.getMessage())); - } - - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleValidation(MethodArgumentNotValidException ex) { - String message = ex.getBindingResult().getAllErrors().stream() - .findFirst() - .map(e -> e.getDefaultMessage()) - .orElse("请求参数有误"); - return ResponseEntity.badRequest().body(ApiResponse.error(message)); - } - - @ExceptionHandler(Exception.class) - public ResponseEntity> handleGeneric(Exception ex) { - log.error("Unhandled exception", ex); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error("服务器内部错误")); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/OrderEmailConsumer.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/OrderEmailConsumer.java deleted file mode 100644 index 5ac3096..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/OrderEmailConsumer.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.mq; - -import com.rabbitmq.client.Channel; -import com.smyhub.store.mengyastorebackendjava.service.EmailService; -import com.smyhub.store.mengyastorebackendjava.service.SiteService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.amqp.core.Message; -import org.springframework.amqp.rabbit.annotation.RabbitListener; -import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener; -import org.springframework.amqp.support.AmqpHeaders; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.messaging.handler.annotation.Header; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@RequiredArgsConstructor -@ConditionalOnProperty(name = "app.rabbitmq-enabled", havingValue = "true") -public class OrderEmailConsumer { - - private final SiteService siteService; - private final EmailService emailService; - - @RabbitListener(queues = "#{T(com.smyhub.store.mengyastorebackendjava.mq.RabbitMQTopology).queueName(@appProperties.rabbitmqEnv)}", ackMode = "MANUAL") - public void onMessage(OrderEmailPayload payload, Message message, Channel channel, - @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception { - if (payload == null || payload.getToEmail() == null || payload.getToEmail().isBlank()) { - channel.basicAck(deliveryTag, false); - return; - } - - SiteService.SmtpConfig cfg = siteService.getSmtpConfig(); - if (!cfg.isConfigured()) { - log.info("[MQ] skip email order={}: smtp not configured", payload.getOrderId()); - channel.basicAck(deliveryTag, false); - return; - } - - EmailService.OrderNotifyData data = new EmailService.OrderNotifyData(); - data.toEmail = payload.getToEmail(); - data.toName = payload.getToName(); - data.productName = payload.getProductName(); - data.orderId = payload.getOrderId(); - data.quantity = payload.getQuantity(); - data.codes = payload.getCodes(); - data.isManual = payload.isManual(); - - try { - emailService.sendOrderNotify(cfg, data); - channel.basicAck(deliveryTag, false); - log.info("[MQ] email ok order={} to={}", payload.getOrderId(), payload.getToEmail()); - } catch (Exception e) { - log.warn("[MQ] send email fail order={}: {}", payload.getOrderId(), e.getMessage()); - boolean redelivered = message.getMessageProperties().isRedelivered(); - if (redelivered) { - log.warn("[MQ] drop order={} after failed redelivery", payload.getOrderId()); - channel.basicAck(deliveryTag, false); - } else { - channel.basicNack(deliveryTag, false, true); - } - } - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/OrderEmailPayload.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/OrderEmailPayload.java deleted file mode 100644 index 1fc88fc..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/OrderEmailPayload.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.mq; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class OrderEmailPayload { - - private String toEmail; - private String toName; - private String productName; - private String orderId; - private int quantity; - private List codes; - private boolean isManual; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/OrderEmailProducer.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/OrderEmailProducer.java deleted file mode 100644 index dd0689c..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/OrderEmailProducer.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.mq; - -import com.smyhub.store.mengyastorebackendjava.config.AppProperties; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.amqp.AmqpException; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@RequiredArgsConstructor -public class OrderEmailProducer { - - private final RabbitTemplate rabbitTemplate; - private final AppProperties appProperties; - - /** - * Publishes an order email payload to RabbitMQ. - * Returns true on success, false on failure (caller should fall back to direct email). - */ - public boolean publish(OrderEmailPayload payload) { - if (!appProperties.isRabbitmqEnabled()) { - return false; - } - String exchange = RabbitMQTopology.exchangeName(appProperties.getRabbitmqEnv()); - try { - rabbitTemplate.convertAndSend(exchange, RabbitMQTopology.ORDER_EMAIL_ROUTING_KEY, payload); - log.info("[MQ] queued order email order={} to={}", payload.getOrderId(), payload.getToEmail()); - return true; - } catch (AmqpException e) { - log.warn("[MQ] publish failed, will fallback to direct email: {}", e.getMessage()); - return false; - } - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/RabbitMQTopology.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/RabbitMQTopology.java deleted file mode 100644 index 7db7820..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/mq/RabbitMQTopology.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.mq; - -public final class RabbitMQTopology { - - private RabbitMQTopology() {} - - public static final String ORDER_EMAIL_ROUTING_KEY = "order.email.notify"; - - public static String exchangeName(String env) { - return "ex.mengyastore." + env + ".events"; - } - - public static String queueName(String env) { - return "q.mengyastore." + env + ".order_email"; - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/ChatMessageRepository.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/ChatMessageRepository.java deleted file mode 100644 index 76c2bcf..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/ChatMessageRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.repository; - -import com.smyhub.store.mengyastorebackendjava.entity.ChatMessage; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Repository -public interface ChatMessageRepository extends JpaRepository { - - List findByAccountIdOrderBySentAtAsc(String accountId); - - List findAllByOrderByAccountIdAscSentAtAsc(); - - @Transactional - void deleteByAccountId(String accountId); -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/OrderRepository.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/OrderRepository.java deleted file mode 100644 index 871c421..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/OrderRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.repository; - -import com.smyhub.store.mengyastorebackendjava.entity.Order; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface OrderRepository extends JpaRepository { - - List findByUserAccountOrderByCreatedAtDesc(String userAccount); - - List findAllByOrderByCreatedAtDesc(); - - @Query("SELECT COALESCE(SUM(o.quantity), 0) FROM Order o WHERE o.userAccount = :account AND o.productId = :productId AND o.status <> 'cancelled'") - int sumQuantityByUserAccountAndProductId(@Param("account") String account, @Param("productId") String productId); -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/ProductCodeRepository.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/ProductCodeRepository.java deleted file mode 100644 index 44181b7..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/ProductCodeRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.repository; - -import com.smyhub.store.mengyastorebackendjava.entity.ProductCode; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Repository -public interface ProductCodeRepository extends JpaRepository { - - List findByProductId(String productId); - - long countByProductId(String productId); - - @Transactional - void deleteByProductId(String productId); -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/ProductRepository.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/ProductRepository.java deleted file mode 100644 index 79a3065..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/ProductRepository.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.repository; - -import com.smyhub.store.mengyastorebackendjava.entity.Product; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Repository -public interface ProductRepository extends JpaRepository { - - List findByActiveTrueOrderByCreatedAtDesc(); - - List findAllByOrderByCreatedAtDesc(); - - @Transactional - @Modifying - @Query("UPDATE Product p SET p.totalSold = p.totalSold + :count WHERE p.id = :id") - void incrementTotalSold(@Param("id") String id, @Param("count") int count); - - @Transactional - @Modifying - @Query("UPDATE Product p SET p.viewCount = p.viewCount + 1 WHERE p.id = :id") - void incrementViewCount(@Param("id") String id); -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/SiteSettingRepository.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/SiteSettingRepository.java deleted file mode 100644 index 4412918..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/SiteSettingRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.repository; - -import com.smyhub.store.mengyastorebackendjava.entity.SiteSetting; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface SiteSettingRepository extends JpaRepository { -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/WishlistRepository.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/WishlistRepository.java deleted file mode 100644 index 0dce62b..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/repository/WishlistRepository.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.repository; - -import com.smyhub.store.mengyastorebackendjava.entity.Wishlist; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface WishlistRepository extends JpaRepository { - - List findByAccountId(String accountId); - - Optional findByAccountIdAndProductId(String accountId, String productId); - - boolean existsByAccountIdAndProductId(String accountId, String productId); - - @Transactional - void deleteByAccountIdAndProductId(String accountId, String productId); -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/AdminTokenInterceptor.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/AdminTokenInterceptor.java deleted file mode 100644 index 4d3de58..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/AdminTokenInterceptor.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.security; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.smyhub.store.mengyastorebackendjava.config.AppProperties; -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; -import org.springframework.web.servlet.HandlerInterceptor; - -@Component -@RequiredArgsConstructor -public class AdminTokenInterceptor implements HandlerInterceptor { - - private final AppProperties appProperties; - private final ObjectMapper objectMapper; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - String token = resolveToken(request); - if (StringUtils.hasText(token) && token.equals(appProperties.getAdminToken())) { - return true; - } - response.setStatus(HttpStatus.UNAUTHORIZED.value()); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setCharacterEncoding("UTF-8"); - objectMapper.writeValue(response.getWriter(), ApiResponse.error("unauthorized")); - return false; - } - - private String resolveToken(HttpServletRequest request) { - String token = request.getHeader("X-Admin-Token"); - if (StringUtils.hasText(token)) { - return token; - } - token = request.getHeader("Authorization"); - if (StringUtils.hasText(token)) { - return token; - } - return request.getParameter("token"); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/BearerTokenInterceptor.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/BearerTokenInterceptor.java deleted file mode 100644 index bd26c46..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/BearerTokenInterceptor.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.security; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.smyhub.store.mengyastorebackendjava.dto.response.ApiResponse; -import com.smyhub.store.mengyastorebackendjava.service.SproutGateAuthService; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; -import org.springframework.web.servlet.HandlerInterceptor; - -@Component -@RequiredArgsConstructor -public class BearerTokenInterceptor implements HandlerInterceptor { - - private final SproutGateAuthService authService; - private final ObjectMapper objectMapper; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - String header = request.getHeader("Authorization"); - if (!StringUtils.hasText(header) || !header.startsWith("Bearer ")) { - return sendUnauthorized(response, "请先登录"); - } - String token = header.substring(7); - SproutGateVerifyResult result = authService.verifyToken(token); - if (!result.isValid() || result.getUser() == null) { - return sendUnauthorized(response, "登录已过期,请重新登录"); - } - CurrentUserHolder.set(result.getUser()); - return true; - } - - @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { - CurrentUserHolder.clear(); - } - - private boolean sendUnauthorized(HttpServletResponse response, String message) throws Exception { - response.setStatus(HttpStatus.UNAUTHORIZED.value()); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setCharacterEncoding("UTF-8"); - objectMapper.writeValue(response.getWriter(), ApiResponse.error(message)); - return false; - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/CurrentUserHolder.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/CurrentUserHolder.java deleted file mode 100644 index 84171cb..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/CurrentUserHolder.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.security; - -public final class CurrentUserHolder { - - private static final ThreadLocal HOLDER = new ThreadLocal<>(); - - private CurrentUserHolder() {} - - public static void set(SproutGateUser user) { - HOLDER.set(user); - } - - public static SproutGateUser get() { - return HOLDER.get(); - } - - public static void clear() { - HOLDER.remove(); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/SproutGateUser.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/SproutGateUser.java deleted file mode 100644 index 982b541..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/SproutGateUser.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.security; - -import lombok.Data; - -@Data -public class SproutGateUser { - - private String account; - - private String username; - - private String email; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/SproutGateVerifyResult.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/SproutGateVerifyResult.java deleted file mode 100644 index 47efdc5..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/security/SproutGateVerifyResult.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.security; - -import lombok.Data; - -@Data -public class SproutGateVerifyResult { - - private boolean valid; - - private SproutGateUser user; -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/ChatService.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/ChatService.java deleted file mode 100644 index a2022bd..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/ChatService.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.service; - -import com.smyhub.store.mengyastorebackendjava.dto.response.ChatMessageResponse; -import com.smyhub.store.mengyastorebackendjava.entity.ChatMessage; -import com.smyhub.store.mengyastorebackendjava.exception.BusinessException; -import com.smyhub.store.mengyastorebackendjava.repository.ChatMessageRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -@Service -@RequiredArgsConstructor -public class ChatService { - - private final ChatMessageRepository chatMessageRepository; - private final Map lastSentMap = new ConcurrentHashMap<>(); - - public List getUserMessages(String accountId) { - return chatMessageRepository.findByAccountIdOrderBySentAtAsc(accountId) - .stream().map(this::toResponse).toList(); - } - - @Transactional - public ChatMessageResponse sendUserMessage(String accountId, String accountName, String content) { - LocalDateTime last = lastSentMap.get(accountId); - if (last != null && last.plusSeconds(1).isAfter(LocalDateTime.now())) { - throw new BusinessException("发送太频繁,请稍后再试", HttpStatus.TOO_MANY_REQUESTS); - } - lastSentMap.put(accountId, LocalDateTime.now()); - - ChatMessage msg = new ChatMessage(); - msg.setId(UUID.randomUUID().toString()); - msg.setAccountId(accountId); - msg.setAccountName(accountName); - msg.setContent(content); - msg.setSentAt(LocalDateTime.now()); - msg.setFromAdmin(false); - return toResponse(chatMessageRepository.save(msg)); - } - - public Map> listAllConversations() { - Map> result = new LinkedHashMap<>(); - chatMessageRepository.findAllByOrderByAccountIdAscSentAtAsc().forEach(row -> { - result.computeIfAbsent(row.getAccountId(), k -> new ArrayList<>()).add(toResponse(row)); - }); - return result; - } - - public List getConversation(String accountId) { - return chatMessageRepository.findByAccountIdOrderBySentAtAsc(accountId) - .stream().map(this::toResponse).toList(); - } - - @Transactional - public ChatMessageResponse sendAdminMessage(String accountId, String content) { - ChatMessage msg = new ChatMessage(); - msg.setId(UUID.randomUUID().toString()); - msg.setAccountId(accountId); - msg.setAccountName("管理员"); - msg.setContent(content); - msg.setSentAt(LocalDateTime.now()); - msg.setFromAdmin(true); - return toResponse(chatMessageRepository.save(msg)); - } - - @Transactional - public void clearConversation(String accountId) { - chatMessageRepository.deleteByAccountId(accountId); - } - - private ChatMessageResponse toResponse(ChatMessage m) { - return ChatMessageResponse.builder() - .id(m.getId()) - .accountId(m.getAccountId()) - .accountName(m.getAccountName()) - .content(m.getContent()) - .sentAt(m.getSentAt()) - .fromAdmin(m.isFromAdmin()) - .build(); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/EmailService.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/EmailService.java deleted file mode 100644 index 882172d..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/EmailService.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.service; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - -import javax.net.ssl.SSLSocketFactory; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Properties; -import jakarta.mail.*; -import jakarta.mail.internet.*; - -@Slf4j -@Service -public class EmailService { - - private static final DateTimeFormatter FORMATTER = - DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss"); - - public void sendOrderNotify(SiteService.SmtpConfig cfg, OrderNotifyData data) { - if (!cfg.isConfigured() || data.toEmail == null || data.toEmail.isBlank()) { - return; - } - try { - Properties props = buildMailProperties(cfg); - Session session = Session.getInstance(props, new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(cfg.email, cfg.password); - } - }); - Message message = new MimeMessage(session); - message.setFrom(new InternetAddress(cfg.email, cfg.fromName, "UTF-8")); - message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(data.toEmail)); - message.setSubject(data.isManual ? "【萌芽小店】您的订单正在处理中" : "【萌芽小店】您的订单已发货"); - message.setContent(buildBody(data), "text/plain; charset=UTF-8"); - Transport.send(message); - log.info("[Email] 发送成功 order={} to={}", data.orderId, data.toEmail); - } catch (Exception e) { - log.error("[Email] 发送失败 order={} to={}: {}", data.orderId, data.toEmail, e.getMessage()); - } - } - - private Properties buildMailProperties(SiteService.SmtpConfig cfg) { - Properties props = new Properties(); - props.put("mail.smtp.host", cfg.host); - props.put("mail.smtp.port", cfg.port); - props.put("mail.smtp.auth", "true"); - if ("465".equals(cfg.port)) { - props.put("mail.smtp.ssl.enable", "true"); - props.put("mail.smtp.socketFactory.port", "465"); - props.put("mail.smtp.socketFactory.class", SSLSocketFactory.class.getName()); - props.put("mail.smtp.socketFactory.fallback", "false"); - } else { - props.put("mail.smtp.starttls.enable", "true"); - } - return props; - } - - private String buildBody(OrderNotifyData data) { - String now = LocalDateTime.now().format(FORMATTER); - String recipient = (data.toName != null && !data.toName.isBlank()) ? data.toName : "用户"; - StringBuilder sb = new StringBuilder(); - sb.append("尊敬的 ").append(recipient).append(",\n\n"); - sb.append(" 您好!感谢您在萌芽小店的支持与购买。\n\n"); - sb.append("────────────────────────────────\n"); - sb.append(" 订单信息\n"); - sb.append("────────────────────────────────\n"); - sb.append(" 商品名称:").append(data.productName).append("\n"); - sb.append(" 订单编号:").append(data.orderId).append("\n"); - sb.append(" 购买数量:").append(data.quantity).append(" 件\n"); - sb.append(" 通知时间:").append(now).append("\n"); - sb.append("────────────────────────────────\n\n"); - if (data.isManual) { - sb.append(" 您的订单已成功提交,目前正在等待人工审核与处理。\n"); - sb.append(" 工作人员将尽快为您安排发货,请耐心等候。\n"); - sb.append(" 发货完成后,我们将另行发送邮件通知。\n\n"); - } else { - sb.append(" 您的订单已完成自动发货,发货内容如下:\n\n"); - if (data.codes != null) { - for (int i = 0; i < data.codes.size(); i++) { - sb.append(" [").append(i + 1).append("] ").append(data.codes.get(i)).append("\n"); - } - sb.append("\n"); - } - sb.append(" 请妥善保管以上发货内容,切勿泄露给他人。\n\n"); - } - sb.append(" 如有任何疑问,请联系在线客服,我们将竭诚为您服务。\n\n"); - sb.append("────────────────────────────────\n"); - sb.append(" 此邮件由系统自动发送,请勿直接回复。\n"); - sb.append("────────────────────────────────\n"); - return sb.toString(); - } - - public static class OrderNotifyData { - public String toEmail; - public String toName; - public String productName; - public String orderId; - public int quantity; - public List codes; - public boolean isManual; - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/OrderService.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/OrderService.java deleted file mode 100644 index 15d913b..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/OrderService.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.service; - -import com.smyhub.store.mengyastorebackendjava.dto.request.CheckoutRequest; -import com.smyhub.store.mengyastorebackendjava.dto.response.OrderResponse; -import com.smyhub.store.mengyastorebackendjava.entity.Order; -import com.smyhub.store.mengyastorebackendjava.entity.Product; -import com.smyhub.store.mengyastorebackendjava.exception.BusinessException; -import com.smyhub.store.mengyastorebackendjava.mq.OrderEmailPayload; -import com.smyhub.store.mengyastorebackendjava.mq.OrderEmailProducer; -import com.smyhub.store.mengyastorebackendjava.repository.OrderRepository; -import com.smyhub.store.mengyastorebackendjava.security.SproutGateUser; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -@Slf4j -@Service -@RequiredArgsConstructor -public class OrderService { - - private final OrderRepository orderRepository; - private final ProductService productService; - private final SiteService siteService; - private final EmailService emailService; - private final OrderEmailProducer orderEmailProducer; - - @Transactional - public CheckoutResult checkout(CheckoutRequest req, SproutGateUser optionalUser) { - String userAccount = optionalUser != null ? optionalUser.getAccount() : ""; - String userName = optionalUser != null ? optionalUser.getUsername() : ""; - String userEmail = optionalUser != null ? optionalUser.getEmail() : ""; - - int qty = req.getQuantity() <= 0 ? 1 : req.getQuantity(); - Product product = productService.findById(req.getProductId().strip()); - - if (!product.isActive()) { - throw new BusinessException("商品暂时无法购买"); - } - if (product.isRequireLogin() && !StringUtils.hasText(userAccount)) { - throw new BusinessException("该商品需要登录后才能购买", HttpStatus.UNAUTHORIZED); - } - if (product.getMaxPerAccount() > 0 && StringUtils.hasText(userAccount)) { - int purchased = orderRepository.sumQuantityByUserAccountAndProductId(userAccount, product.getId()); - if (purchased + qty > product.getMaxPerAccount()) { - int remain = product.getMaxPerAccount() - purchased; - if (remain <= 0) { - throw new BusinessException(String.format("每个账户最多购买 %d 个,您已达上限", product.getMaxPerAccount())); - } - throw new BusinessException(String.format("每个账户最多购买 %d 个,您还可购买 %d 个", product.getMaxPerAccount(), remain)); - } - } - - List deliveredCodes; - boolean isFixed = "fixed".equalsIgnoreCase(product.getFulfillmentType()); - if (isFixed) { - String content = product.getFixedContent() != null ? product.getFixedContent().strip() : ""; - if (content.isEmpty()) { - throw new BusinessException("该商品为固定内容发货,但未配置发货内容,请联系管理员"); - } - deliveredCodes = Collections.nCopies(qty, content); - } else { - deliveredCodes = productService.popCodes(product.getId(), qty); - } - - String notifyEmail = StringUtils.hasText(userEmail) ? userEmail - : StringUtils.hasText(req.getNotifyEmail()) ? req.getNotifyEmail().strip() - : StringUtils.hasText(req.getContactEmail()) ? req.getContactEmail().strip() - : ""; - - Order order = new Order(); - order.setId(UUID.randomUUID().toString()); - order.setProductId(product.getId()); - order.setProductName(product.getName()); - order.setUserAccount(userAccount); - order.setUserName(userName); - order.setQuantity(qty); - order.setDeliveredCodes(new ArrayList<>(deliveredCodes)); - order.setStatus("completed"); - order.setDeliveryMode("auto"); - order.setNote(req.getNote() != null ? req.getNote().strip() : ""); - order.setContactPhone(req.getContactPhone() != null ? req.getContactPhone().strip() : ""); - order.setContactEmail(req.getContactEmail() != null ? req.getContactEmail().strip() : ""); - order.setNotifyEmail(notifyEmail); - order.setCreatedAt(LocalDateTime.now()); - orderRepository.save(order); - - productService.incrementSold(product.getId(), qty); - - if (StringUtils.hasText(notifyEmail)) { - sendOrderEmail(notifyEmail, userName, product.getName(), order.getId(), qty, new ArrayList<>(deliveredCodes), false); - } - - return new CheckoutResult(order.getId(), product.getId(), qty, product.getViewCount(), order.getStatus()); - } - - public OrderResponse confirmOrder(String orderId) { - Order order = orderRepository.findById(orderId) - .orElseThrow(() -> new BusinessException("订单不存在", HttpStatus.NOT_FOUND)); - order.setStatus("confirmed"); - orderRepository.save(order); - - boolean isManual = "manual".equals(order.getDeliveryMode()); - if (isManual) { - String email = StringUtils.hasText(order.getNotifyEmail()) - ? order.getNotifyEmail() : order.getContactEmail(); - if (StringUtils.hasText(email)) { - sendOrderEmail(email, order.getUserName(), order.getProductName(), - order.getId(), order.getQuantity(), order.getDeliveredCodes(), false); - } - } - return toResponse(order); - } - - public List getMyOrders(String userAccount) { - return orderRepository.findByUserAccountOrderByCreatedAtDesc(userAccount) - .stream().map(this::toResponse).toList(); - } - - public List listAllOrders() { - return orderRepository.findAllByOrderByCreatedAtDesc() - .stream().map(this::toResponse).toList(); - } - - @Transactional - public void deleteOrder(String orderId) { - if (!orderRepository.existsById(orderId)) { - throw new BusinessException("订单不存在", HttpStatus.NOT_FOUND); - } - orderRepository.deleteById(orderId); - } - - private void sendOrderEmail(String toEmail, String toName, String productName, - String orderId, int qty, List codes, boolean isManual) { - SiteService.SmtpConfig cfg = siteService.getSmtpConfig(); - if (!cfg.isConfigured()) return; - - OrderEmailPayload payload = new OrderEmailPayload(toEmail, toName, productName, orderId, qty, codes, isManual); - boolean queued = orderEmailProducer.publish(payload); - if (!queued) { - EmailService.OrderNotifyData data = new EmailService.OrderNotifyData(); - data.toEmail = toEmail; - data.toName = toName; - data.productName = productName; - data.orderId = orderId; - data.quantity = qty; - data.codes = codes; - data.isManual = isManual; - Thread emailThread = new Thread(() -> emailService.sendOrderNotify(cfg, data)); - emailThread.setDaemon(true); - emailThread.start(); - } - } - - private OrderResponse toResponse(Order o) { - return OrderResponse.builder() - .id(o.getId()) - .productId(o.getProductId()) - .productName(o.getProductName()) - .userAccount(o.getUserAccount()) - .userName(o.getUserName()) - .quantity(o.getQuantity()) - .deliveredCodes(o.getDeliveredCodes()) - .status(o.getStatus()) - .deliveryMode(o.getDeliveryMode()) - .note(o.getNote()) - .contactPhone(o.getContactPhone()) - .contactEmail(o.getContactEmail()) - .notifyEmail(o.getNotifyEmail()) - .createdAt(o.getCreatedAt()) - .build(); - } - - public record CheckoutResult(String orderId, String productId, int qty, int viewCount, String status) {} -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/ProductService.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/ProductService.java deleted file mode 100644 index f131593..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/ProductService.java +++ /dev/null @@ -1,246 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.service; - -import com.smyhub.store.mengyastorebackendjava.dto.request.AdminProductRequest; -import com.smyhub.store.mengyastorebackendjava.dto.response.ProductResponse; -import com.smyhub.store.mengyastorebackendjava.entity.Product; -import com.smyhub.store.mengyastorebackendjava.entity.ProductCode; -import com.smyhub.store.mengyastorebackendjava.exception.BusinessException; -import com.smyhub.store.mengyastorebackendjava.repository.ProductCodeRepository; -import com.smyhub.store.mengyastorebackendjava.repository.ProductRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; - -import java.security.MessageDigest; -import java.time.LocalDateTime; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -@Service -@RequiredArgsConstructor -public class ProductService { - - private static final String DEFAULT_COVER_URL = "https://img.shumengya.top/i/2026/01/04/695a55058c37f.png"; - private static final int VIEW_COOLDOWN_HOURS = 6; - private static final int MAX_SCREENSHOT_URLS = 5; - - private final ProductRepository productRepository; - private final ProductCodeRepository productCodeRepository; - private final Map recentViews = new ConcurrentHashMap<>(); - - public List listPublicProducts() { - return productRepository.findByActiveTrueOrderByCreatedAtDesc().stream() - .map(p -> toResponse(p, loadCodeCount(p), false, false)) - .toList(); - } - - public List listAllProducts() { - return productRepository.findAllByOrderByCreatedAtDesc().stream() - .map(p -> toResponse(p, loadCodeCount(p), true, true)) - .toList(); - } - - public ProductResponse getProductForAdmin(String id) { - Product p = findById(id); - List codes = productCodeRepository.findByProductId(id).stream() - .map(ProductCode::getCode).toList(); - return toResponse(p, (long) codes.size(), true, true, codes); - } - - @Transactional - public ProductResponse createProduct(AdminProductRequest req) { - Product p = new Product(); - p.setId(UUID.randomUUID().toString()); - p.setCreatedAt(LocalDateTime.now()); - applyRequest(p, req); - productRepository.save(p); - replaceCodes(p.getId(), sanitizeCodes(req.getCodes())); - return getProductForAdmin(p.getId()); - } - - @Transactional - public ProductResponse updateProduct(String id, AdminProductRequest req) { - Product p = findById(id); - applyRequest(p, req); - productRepository.save(p); - replaceCodes(id, sanitizeCodes(req.getCodes())); - return getProductForAdmin(id); - } - - @Transactional - public ProductResponse toggleStatus(String id, boolean active) { - Product p = findById(id); - p.setActive(active); - productRepository.save(p); - long count = productCodeRepository.countByProductId(id); - return toResponse(p, count, true, false); - } - - @Transactional - public void deleteProduct(String id) { - findById(id); - productCodeRepository.deleteByProductId(id); - productRepository.deleteById(id); - } - - public ProductResponse recordView(String id, String fingerprint) { - cleanupRecentViews(); - String key = buildViewKey(id, fingerprint); - LocalDateTime last = recentViews.get(key); - LocalDateTime now = LocalDateTime.now(); - boolean counted = false; - if (last == null || last.plusHours(VIEW_COOLDOWN_HOURS).isBefore(now)) { - productRepository.incrementViewCount(id); - recentViews.put(key, now); - counted = true; - } - Product p = findById(id); - return ProductResponse.builder() - .id(p.getId()) - .viewCount(p.getViewCount()) - .build(); - } - - @Transactional - public void incrementSold(String id, int count) { - productRepository.incrementTotalSold(id, count); - } - - public Product findById(String id) { - return productRepository.findById(id) - .orElseThrow(() -> new BusinessException("商品不存在", HttpStatus.NOT_FOUND)); - } - - @Transactional - public List popCodes(String productId, int count) { - List codes = productCodeRepository.findByProductId(productId); - if (codes.size() < count) { - throw new BusinessException("卡密不足"); - } - List popped = codes.subList(0, count).stream().map(ProductCode::getCode).toList(); - codes.subList(0, count).forEach(c -> productCodeRepository.deleteById(c.getId())); - return popped; - } - - private long loadCodeCount(Product p) { - if ("fixed".equalsIgnoreCase(p.getFulfillmentType())) { - return -1; - } - return productCodeRepository.countByProductId(p.getId()); - } - - private void replaceCodes(String productId, List codes) { - productCodeRepository.deleteByProductId(productId); - if (!codes.isEmpty()) { - List rows = codes.stream() - .map(c -> new ProductCode(productId, c)).toList(); - productCodeRepository.saveAll(rows); - } - } - - private void applyRequest(Product p, AdminProductRequest req) { - p.setName(req.getName()); - p.setPrice(req.getPrice()); - double discount = req.getDiscountPrice(); - p.setDiscountPrice((discount > 0 && discount < req.getPrice()) ? discount : 0); - p.setTags(sanitizeTags(req.getTags())); - String cover = req.getCoverUrl(); - p.setCoverUrl(StringUtils.hasText(cover) ? cover : DEFAULT_COVER_URL); - List screenshots = req.getScreenshotUrls(); - if (screenshots != null && screenshots.size() > MAX_SCREENSHOT_URLS) { - screenshots = screenshots.subList(0, MAX_SCREENSHOT_URLS); - } - p.setScreenshotUrls(screenshots != null ? screenshots : new ArrayList<>()); - p.setVerificationUrl(req.getVerificationUrl() != null ? req.getVerificationUrl().strip() : ""); - p.setDescription(req.getDescription()); - p.setActive(req.isActive()); - p.setRequireLogin(req.isRequireLogin()); - p.setMaxPerAccount(req.getMaxPerAccount()); - p.setDeliveryMode(StringUtils.hasText(req.getDeliveryMode()) ? req.getDeliveryMode() : "auto"); - String ft = normalizeFullfillmentType(req.getFulfillmentType()); - p.setFulfillmentType(ft); - if ("fixed".equals(ft)) { - p.setFixedContent(req.getFixedContent() != null ? req.getFixedContent().strip() : ""); - } else { - p.setFixedContent(""); - } - p.setShowNote(req.isShowNote()); - p.setShowContact(req.isShowContact()); - } - - private ProductResponse toResponse(Product p, long codeCount, boolean includeCodes, boolean includeFixedContent) { - return toResponse(p, codeCount, includeCodes, includeFixedContent, null); - } - - private ProductResponse toResponse(Product p, long codeCount, boolean includeCodes, boolean includeFixedContent, List codes) { - int qty = "fixed".equalsIgnoreCase(p.getFulfillmentType()) ? -1 : (int) codeCount; - return ProductResponse.builder() - .id(p.getId()) - .name(p.getName()) - .price(p.getPrice()) - .discountPrice(p.getDiscountPrice()) - .tags(p.getTags()) - .quantity(qty) - .coverUrl(p.getCoverUrl()) - .screenshotUrls(p.getScreenshotUrls()) - .verificationUrl(p.getVerificationUrl()) - .codes(includeCodes ? codes : null) - .viewCount(p.getViewCount()) - .description(p.getDescription()) - .active(p.isActive()) - .requireLogin(p.isRequireLogin()) - .maxPerAccount(p.getMaxPerAccount()) - .totalSold(p.getTotalSold()) - .deliveryMode(p.getDeliveryMode()) - .fulfillmentType(p.getFulfillmentType()) - .fixedContent(includeFixedContent ? p.getFixedContent() : null) - .showNote(p.isShowNote()) - .showContact(p.isShowContact()) - .createdAt(p.getCreatedAt()) - .build(); - } - - private static String normalizeFullfillmentType(String t) { - if (t != null && "fixed".equalsIgnoreCase(t.strip())) return "fixed"; - return "card"; - } - - private static List sanitizeCodes(List codes) { - if (codes == null) return new ArrayList<>(); - Set seen = new LinkedHashSet<>(); - for (String code : codes) { - String trimmed = code != null ? code.strip() : ""; - if (!trimmed.isEmpty()) seen.add(trimmed); - } - return new ArrayList<>(seen); - } - - private static List sanitizeTags(List tags) { - if (tags == null) return new ArrayList<>(); - Set seen = new LinkedHashSet<>(); - for (String tag : tags) { - String t = tag != null ? tag.strip() : ""; - if (!t.isEmpty() && seen.size() < 20) seen.add(t); - } - return new ArrayList<>(seen); - } - - private static String buildViewKey(String id, String fingerprint) { - try { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] hash = md.digest((id + "|" + fingerprint).getBytes()); - StringBuilder hex = new StringBuilder(); - for (byte b : hash) hex.append(String.format("%02x", b)); - return hex.toString(); - } catch (Exception e) { - return id + "|" + fingerprint; - } - } - - private void cleanupRecentViews() { - LocalDateTime cutoff = LocalDateTime.now().minusHours(VIEW_COOLDOWN_HOURS); - recentViews.entrySet().removeIf(e -> e.getValue().isBefore(cutoff)); - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/SiteService.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/SiteService.java deleted file mode 100644 index 56080d6..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/SiteService.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.service; - -import com.smyhub.store.mengyastorebackendjava.dto.request.SmtpConfigRequest; -import com.smyhub.store.mengyastorebackendjava.dto.response.MaintenanceResponse; -import com.smyhub.store.mengyastorebackendjava.dto.response.SmtpConfigResponse; -import com.smyhub.store.mengyastorebackendjava.entity.SiteSetting; -import com.smyhub.store.mengyastorebackendjava.repository.SiteSettingRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -public class SiteService { - - private static final String KEY_TOTAL_VISITS = "totalVisits"; - private static final String KEY_MAINTENANCE = "maintenance"; - private static final String KEY_MAINTENANCE_REASON = "maintenanceReason"; - private static final String KEY_SMTP_ENABLED = "smtpEnabled"; - private static final String KEY_SMTP_EMAIL = "smtpEmail"; - private static final String KEY_SMTP_PASSWORD = "smtpPassword"; - private static final String KEY_SMTP_FROM_NAME = "smtpFromName"; - private static final String KEY_SMTP_HOST = "smtpHost"; - private static final String KEY_SMTP_PORT = "smtpPort"; - - private final SiteSettingRepository repo; - - private String get(String key) { - return repo.findById(key).map(SiteSetting::getValue).orElse(null); - } - - private void set(String key, String value) { - repo.save(new SiteSetting(key, value)); - } - - public int getTotalVisits() { - String v = get(KEY_TOTAL_VISITS); - if (v == null || v.isBlank()) return 0; - try { return Integer.parseInt(v); } catch (NumberFormatException e) { return 0; } - } - - @Transactional - public int incrementVisits() { - int current = getTotalVisits() + 1; - set(KEY_TOTAL_VISITS, String.valueOf(current)); - return current; - } - - public MaintenanceResponse getMaintenance() { - String enabled = get(KEY_MAINTENANCE); - String reason = get(KEY_MAINTENANCE_REASON); - return MaintenanceResponse.builder() - .maintenance("true".equals(enabled)) - .reason(reason != null ? reason : "") - .build(); - } - - @Transactional - public void setMaintenance(boolean enabled, String reason) { - set(KEY_MAINTENANCE, enabled ? "true" : "false"); - set(KEY_MAINTENANCE_REASON, reason != null ? reason : ""); - } - - public SmtpConfig getSmtpConfig() { - SmtpConfig cfg = new SmtpConfig(); - String enabledStr = get(KEY_SMTP_ENABLED); - cfg.enabled = !"false".equals(enabledStr); - cfg.email = nvl(get(KEY_SMTP_EMAIL), ""); - cfg.password = nvl(get(KEY_SMTP_PASSWORD), ""); - cfg.fromName = nvl(get(KEY_SMTP_FROM_NAME), "萌芽小店"); - cfg.host = nvl(get(KEY_SMTP_HOST), "smtp.qq.com"); - cfg.port = nvl(get(KEY_SMTP_PORT), "465"); - return cfg; - } - - @Transactional - public void setSmtpConfig(SmtpConfigRequest req) { - set(KEY_SMTP_ENABLED, req.isEnabled() ? "true" : "false"); - set(KEY_SMTP_EMAIL, nvl(req.getEmail(), "")); - set(KEY_SMTP_PASSWORD, nvl(req.getPassword(), "")); - set(KEY_SMTP_FROM_NAME, nvl(req.getFromName(), "")); - set(KEY_SMTP_HOST, nvl(req.getHost(), "smtp.qq.com")); - set(KEY_SMTP_PORT, nvl(req.getPort(), "465")); - } - - public SmtpConfigResponse getSmtpConfigResponse(boolean maskPassword) { - SmtpConfig cfg = getSmtpConfig(); - return SmtpConfigResponse.builder() - .enabled(cfg.enabled) - .email(cfg.email) - .password(maskPassword ? maskStr(cfg.password) : cfg.password) - .fromName(cfg.fromName) - .host(cfg.host) - .port(cfg.port) - .build(); - } - - private static String nvl(String s, String def) { - return (s != null && !s.isBlank()) ? s : def; - } - - private static String maskStr(String s) { - if (s == null || s.length() <= 2) return "***"; - return s.substring(0, 1) + "***" + s.substring(s.length() - 1); - } - - public static class SmtpConfig { - public boolean enabled = true; - public String email = ""; - public String password = ""; - public String fromName = "萌芽小店"; - public String host = "smtp.qq.com"; - public String port = "465"; - - public boolean isConfigured() { - return enabled && !email.isBlank() && !password.isBlank() && !host.isBlank(); - } - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/SproutGateAuthService.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/SproutGateAuthService.java deleted file mode 100644 index 3ff175f..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/SproutGateAuthService.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.service; - -import com.smyhub.store.mengyastorebackendjava.config.AppProperties; -import com.smyhub.store.mengyastorebackendjava.security.SproutGateVerifyResult; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import java.util.Map; - -@Slf4j -@Service -@RequiredArgsConstructor -public class SproutGateAuthService { - - private final RestTemplate restTemplate; - private final AppProperties appProperties; - - public SproutGateVerifyResult verifyToken(String token) { - try { - String url = appProperties.getAuthApiUrl() + "/api/auth/verify"; - ResponseEntity resp = restTemplate.postForEntity( - url, Map.of("token", token), SproutGateVerifyResult.class - ); - if (resp.getStatusCode().is2xxSuccessful() && resp.getBody() != null) { - return resp.getBody(); - } - } catch (Exception e) { - log.warn("[SproutGate] token verify failed: {}", e.getMessage()); - } - SproutGateVerifyResult invalid = new SproutGateVerifyResult(); - invalid.setValid(false); - return invalid; - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/SystemStatusService.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/SystemStatusService.java deleted file mode 100644 index d0aae74..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/SystemStatusService.java +++ /dev/null @@ -1,296 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.service; - -import com.smyhub.store.mengyastorebackendjava.config.AppProperties; -import com.smyhub.store.mengyastorebackendjava.mq.RabbitMQTopology; -import com.zaxxer.hikari.HikariDataSource; -import com.zaxxer.hikari.HikariPoolMXBean; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.connection.RedisConnection; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; - -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.Statement; -import java.time.Instant; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Builds the same JSON shape as Go {@code admin_status.go} for the admin system-status panel. - */ -@Service -@RequiredArgsConstructor -public class SystemStatusService { - - private static final Instant START_TIME = Instant.now(); - - private final AppProperties appProperties; - private final DataSource dataSource; - private final RedisConnectionFactory redisConnectionFactory; - private final RabbitTemplate rabbitTemplate; - - @Value("${spring.datasource.url:}") - private String datasourceUrl; - - @Value("${spring.datasource.username:}") - private String datasourceUsername; - - @Value("${server.port:8080}") - private int serverPort; - - @Value("${server.address:}") - private String serverAddress; - - @Value("${spring.rabbitmq.host:}") - private String rabbitHost; - - @Value("${spring.rabbitmq.port:5672}") - private int rabbitPort; - - @Value("${spring.rabbitmq.virtual-host:/}") - private String rabbitVhost; - - @Value("${spring.rabbitmq.username:}") - private String rabbitUser; - - @Value("${spring.data.redis.host:}") - private String redisHost; - - @Value("${spring.data.redis.port:6379}") - private int redisPort; - - @Value("${spring.data.redis.database:0}") - private int redisDb; - - public Map buildStatus(HttpServletRequest request) { - Map root = new LinkedHashMap<>(); - root.put("backend", backendInfo(request)); - root.put("mysql", mysqlInfo()); - root.put("redis", redisInfo()); - root.put("rabbitmq", rabbitmqInfo()); - return root; - } - - private Map backendInfo(HttpServletRequest request) { - Map m = new LinkedHashMap<>(); - m.put("status", "ok"); - m.put("appEnv", normalizeAppEnv(appProperties.getAppEnv())); - m.put("ginMode", "—"); - String host = request.getServerName(); - int port = request.getServerPort(); - String hostHeader = request.getHeader("Host"); - m.put("requestHost", StringUtils.hasText(hostHeader) ? hostHeader : host + ":" + port); - - String listen = formatListenAddr(); - m.put("listenAddr", listen); - - String publicBase = appProperties.getPublicApiBaseUrl() != null - ? appProperties.getPublicApiBaseUrl().strip() : ""; - if (publicBase.isEmpty()) { - String proto = request.getHeader("X-Forwarded-Proto"); - if (!StringUtils.hasText(proto)) { - proto = request.isSecure() ? "https" : "http"; - } - if (StringUtils.hasText(hostHeader)) { - publicBase = proto + "://" + hostHeader; - } else { - publicBase = proto + "://" + host + ":" + port; - } - } - m.put("publicBaseUrl", publicBase); - - m.put("uptimeSeconds", (int) (Instant.now().getEpochSecond() - START_TIME.getEpochSecond())); - m.put("authApiConfigured", StringUtils.hasText(appProperties.getAuthApiUrl())); - m.put("rabbitmqEnabled", appProperties.isRabbitmqEnabled()); - m.put("redisEnabled", appProperties.isRedisEnabled()); - return m; - } - - private String normalizeAppEnv(String raw) { - if (raw == null) return "development"; - return switch (raw.strip().toLowerCase()) { - case "production", "prod" -> "production"; - default -> "development"; - }; - } - - private String formatListenAddr() { - String addr = serverAddress != null ? serverAddress.strip() : ""; - if (addr.isEmpty() || "0.0.0.0".equals(addr)) { - return ":" + serverPort; - } - return addr + ":" + serverPort; - } - - private Map mysqlInfo() { - Map m = new LinkedHashMap<>(); - parseJdbcMysqlUrl(datasourceUrl, datasourceUsername, m); - - try (Connection conn = dataSource.getConnection(); - Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT VERSION()")) { - if (rs.next()) { - m.put("version", rs.getString(1).trim()); - } - m.put("status", "ok"); - appendPoolStats(m); - } catch (Exception e) { - m.put("status", "error"); - m.put("message", e.getMessage()); - } - return m; - } - - private void appendPoolStats(Map m) { - try { - if (dataSource instanceof HikariDataSource hikari) { - HikariPoolMXBean pool = hikari.getHikariPoolMXBean(); - if (pool != null) { - int active = pool.getActiveConnections(); - int idle = pool.getIdleConnections(); - m.put("openConnections", active + idle); - m.put("inUse", active); - m.put("idle", idle); - m.put("waitCount", pool.getThreadsAwaitingConnection()); - } - } - } catch (Exception ignored) { - // optional metrics - } - } - - private static void parseJdbcMysqlUrl(String jdbcUrl, String username, Map out) { - if (jdbcUrl == null || jdbcUrl.isBlank()) { - out.put("configured", false); - return; - } - try { - String rest = jdbcUrl.substring("jdbc:mysql://".length()); - int q = rest.indexOf('?'); - String core = q >= 0 ? rest.substring(0, q) : rest; - int slash = core.indexOf('/'); - String hostPort = slash >= 0 ? core.substring(0, slash) : core; - String database = ""; - if (slash >= 0 && slash < core.length() - 1) { - database = core.substring(slash + 1); - } - String host; - String port; - if (hostPort.contains(":")) { - int c = hostPort.lastIndexOf(':'); - host = hostPort.substring(0, c); - port = hostPort.substring(c + 1); - } else { - host = hostPort; - port = "3306"; - } - out.put("configured", true); - out.put("host", host); - out.put("port", port); - out.put("database", database); - out.put("user", username != null ? username : ""); - } catch (Exception e) { - out.put("configured", false); - out.put("dsnParseError", e.getMessage()); - } - } - - private Map redisInfo() { - Map m = new LinkedHashMap<>(); - m.put("enabled", appProperties.isRedisEnabled()); - m.put("env", appProperties.getRedisEnv()); - m.put("host", redisHost); - m.put("port", redisPort); - m.put("dbIndex", redisDb); - - if (!appProperties.isRedisEnabled()) { - m.put("status", "disabled"); - return m; - } - if (!StringUtils.hasText(redisHost)) { - m.put("status", "misconfigured"); - m.put("message", "已启用但未配置 spring.data.redis.host"); - return m; - } - - try (RedisConnection conn = redisConnectionFactory.getConnection()) { - String pong = new String(conn.ping()); - if (!"PONG".equalsIgnoreCase(pong)) { - m.put("status", "degraded"); - m.put("message", "PING 返回异常: " + pong); - return m; - } - java.util.Properties infoProps = conn.info(); - if (infoProps == null || infoProps.isEmpty()) { - m.put("status", "degraded"); - m.put("message", "PING 成功但无法读取 INFO"); - return m; - } - Long dbSize = conn.dbSize(); - m.put("status", "ok"); - m.put("redisVersion", nullToEmpty(infoProps.getProperty("redis_version"))); - m.put("usedMemoryHuman", nullToEmpty(infoProps.getProperty("used_memory_human"))); - m.put("keysApprox", dbSize); - } catch (Exception e) { - m.put("status", "error"); - m.put("message", e.getMessage()); - } - return m; - } - - private static String nullToEmpty(String s) { - return s != null ? s : ""; - } - - private Map rabbitmqInfo() { - String env = appProperties.getRabbitmqEnv(); - Map m = new LinkedHashMap<>(); - m.put("enabled", appProperties.isRabbitmqEnabled()); - m.put("env", env); - m.put("exchange", RabbitMQTopology.exchangeName(env)); - m.put("queue", RabbitMQTopology.queueName(env)); - m.put("routingKey", RabbitMQTopology.ORDER_EMAIL_ROUTING_KEY); - m.put("brokerHost", rabbitHost); - m.put("brokerPort", String.valueOf(rabbitPort)); - m.put("vhost", rabbitVhost); - m.put("brokerUser", rabbitUser); - - if (!appProperties.isRabbitmqEnabled()) { - m.put("status", "disabled"); - return m; - } - if (!StringUtils.hasText(rabbitHost)) { - m.put("status", "misconfigured"); - m.put("message", "已启用但未配置 spring.rabbitmq.host"); - return m; - } - - try { - int[] mqStats = rabbitTemplate.execute(channel -> { - var ok = channel.queueDeclarePassive(RabbitMQTopology.queueName(env)); - return new int[]{ok.getMessageCount(), ok.getConsumerCount()}; - }); - if (mqStats != null && mqStats.length >= 2) { - m.put("messagesReady", mqStats[0]); - m.put("consumers", mqStats[1]); - } - m.put("status", "ok"); - } catch (Exception e) { - String msg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName(); - if (msg.contains("NOT_FOUND") || msg.contains("404")) { - m.put("status", "degraded"); - m.put("message", "已连接但队列不存在或未声明: " + msg); - } else { - m.put("status", "error"); - m.put("message", msg); - } - } - return m; - } -} diff --git a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/WishlistService.java b/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/WishlistService.java deleted file mode 100644 index 026bac1..0000000 --- a/mengyastore-backend-java/src/main/java/com/smyhub/store/mengyastorebackendjava/service/WishlistService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava.service; - -import com.smyhub.store.mengyastorebackendjava.dto.response.WishlistResponse; -import com.smyhub.store.mengyastorebackendjava.entity.Wishlist; -import com.smyhub.store.mengyastorebackendjava.exception.BusinessException; -import com.smyhub.store.mengyastorebackendjava.repository.WishlistRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -@Service -@RequiredArgsConstructor -public class WishlistService { - - private final WishlistRepository wishlistRepository; - - public List getWishlistIds(String accountId) { - return wishlistRepository.findByAccountId(accountId).stream() - .map(Wishlist::getProductId) - .toList(); - } - - public List getWishlist(String accountId) { - return wishlistRepository.findByAccountId(accountId).stream() - .map(this::toResponse) - .toList(); - } - - @Transactional - public WishlistResponse addToWishlist(String accountId, String productId) { - if (wishlistRepository.existsByAccountIdAndProductId(accountId, productId)) { - throw new BusinessException("该商品已在心愿单中"); - } - try { - Wishlist saved = wishlistRepository.save(new Wishlist(accountId, productId)); - return toResponse(saved); - } catch (DataIntegrityViolationException e) { - throw new BusinessException("该商品已在心愿单中"); - } - } - - @Transactional - public void removeFromWishlist(String accountId, String productId) { - if (!wishlistRepository.existsByAccountIdAndProductId(accountId, productId)) { - throw new BusinessException("心愿单中不存在该商品", HttpStatus.NOT_FOUND); - } - wishlistRepository.deleteByAccountIdAndProductId(accountId, productId); - } - - private WishlistResponse toResponse(Wishlist w) { - return WishlistResponse.builder() - .id(w.getId()) - .accountId(w.getAccountId()) - .productId(w.getProductId()) - .build(); - } -} diff --git a/mengyastore-backend-java/src/main/resources/application.yaml b/mengyastore-backend-java/src/main/resources/application.yaml deleted file mode 100644 index 6bcf749..0000000 --- a/mengyastore-backend-java/src/main/resources/application.yaml +++ /dev/null @@ -1,85 +0,0 @@ -spring: - application: - name: mengyastore-backend-java - profiles: - active: dev - - datasource: - url: jdbc:mysql://10.1.1.100:3306/mengyastore-test?charset=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true - username: mengyastore-test - password: mengyastore-test - driver-class-name: com.mysql.cj.jdbc.Driver - - jpa: - hibernate: - ddl-auto: update - show-sql: false - properties: - hibernate: - dialect: org.hibernate.dialect.MySQLDialect - format_sql: false - - rabbitmq: - host: 10.1.1.233 - port: 5672 - username: admin - # 请将此密码替换为真实的 RabbitMQ 密码,然后将 app.rabbitmq-enabled 设为 true - password: shumengya520 - virtual-host: mengyastore-dev - listener: - simple: - acknowledge-mode: manual - prefetch: 1 - - data: - redis: - host: 10.1.1.100 - port: 6379 - password: tyh@19900420 - database: 1 - timeout: 3000ms - - mail: - host: smtp.qq.com - port: 465 - username: "" - password: "" - properties: - mail: - smtp: - auth: true - ssl: - enable: true - socketFactory: - port: 465 - class: javax.net.ssl.SSLSocketFactory - fallback: false - -server: - port: 8080 - -app: - admin-token: shumengya520 - auth-api-url: https://auth.api.shumengya.top - # 对应 Go 的 APP_ENV:development | production(系统状态页「应用环境」) - app-env: development - # 对应 Go 的 REDIS_ENABLED;false 时系统状态里 Redis 为「未启用」且不再探测 - redis-enabled: true - # 对应 Go 后端的 RABBITMQ_ENABLED;默认关闭,密码配置正确后再改为 true - rabbitmq-enabled: true - rabbitmq-env: dev - redis-env: dev - public-api-base-url: "" - cors-allowed-origins: "http://localhost:5173,http://localhost:3000,http://localhost:5174" - -# SMTP 在库里配置前无用户名密码;关闭邮件健康检查避免启动后打 ERROR 日志 -management: - health: - mail: - enabled: false - -logging: - level: - com.smyhub.store: INFO - org.springframework.amqp: WARN - org.springframework.data.redis: WARN diff --git a/mengyastore-backend-java/src/test/java/com/smyhub/store/mengyastorebackendjava/MengyastoreBackendJavaApplicationTests.java b/mengyastore-backend-java/src/test/java/com/smyhub/store/mengyastorebackendjava/MengyastoreBackendJavaApplicationTests.java deleted file mode 100644 index 60c913a..0000000 --- a/mengyastore-backend-java/src/test/java/com/smyhub/store/mengyastorebackendjava/MengyastoreBackendJavaApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.smyhub.store.mengyastorebackendjava; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class MengyastoreBackendJavaApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/mengyastore-backend-java/后端文档.md b/mengyastore-backend-java/后端文档.md deleted file mode 100644 index 3c59996..0000000 --- a/mengyastore-backend-java/后端文档.md +++ /dev/null @@ -1,112 +0,0 @@ -# 萌芽小店 · 后端(mengyastore-backend-java) - -基于 **Java 17、Spring Boot 3、Spring MVC、Spring Data JPA(Hibernate)** 的 REST API,配套 **Vue 3** 前端联调;可在 **IntelliJ IDEA** 中运行 `MengyastoreBackendJavaApplication` 进行开发与测试。 - -## 技术栈与 Maven 依赖 - -| 依赖 | 用途 | -|------|------| -| spring-boot-starter-web | REST、Jackson | -| spring-boot-starter-data-jpa | 实体、Repository、事务 | -| mysql-connector-j | MySQL JDBC | -| spring-boot-starter-amqp | RabbitMQ | -| spring-boot-starter-data-redis | Redis | -| spring-boot-starter-validation | Bean 校验 | -| spring-boot-starter-mail | Jakarta Mail(业务 SMTP 配置来自数据库) | -| spring-boot-starter-actuator | 健康检查(默认关闭 Mail 指标以免未配 SMTP 报错) | -| lombok | Getter 等样板代码 | - -## 包结构 - -``` -src/main/java/com/smyhub/store/mengyastorebackendjava/ -├── MengyastoreBackendJavaApplication.java -├── config/ # AppProperties、CORS、RabbitMQ、Redis、RestTemplate、WebMvc -├── controller/ # 按域拆分;admin/ 为管理端 -├── dto/request|response/ -├── entity/ # JPA;entity/converter 处理 JSON 列 -├── exception/ # BusinessException、GlobalExceptionHandler -├── mq/ # 拓扑、Payload、Producer、Consumer(随 app.rabbitmq-enabled) -├── repository/ -├── security/ # 管理令牌与用户 Bearer 拦截器、ThreadLocal 用户 -└── service/ # 业务与 SystemStatusService 等 -``` - -## API 路由一览 - -### 公开 - -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/api/health` | 健康检查;含 RabbitMQ 状态(`ok` / `error` / `disabled`) | -| GET | `/api/products` | 上架商品列表(不含卡密与 `fixedContent`) | -| POST | `/api/products/{id}/view` | 记录浏览(进程内去重) | -| GET | `/api/stats` | 订单数、访问量 | -| POST | `/api/site/visit` | 站点访问计数 | -| GET | `/api/site/maintenance` | 维护状态 | -| POST | `/api/checkout` | 创建订单(可选 `Authorization: Bearer`) | -| POST | `/api/orders/{id}/confirm` | 确认订单 | - -### 用户(Bearer,远程认证服务校验) - -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/api/orders` | 当前用户订单 | -| GET / POST | `/api/wishlist` | 列表;POST body `{"productId":"…"}` | -| DELETE | `/api/wishlist/{id}` | 删除,路径参数为商品 ID | -| GET / POST | `/api/chat/messages` | 消息列表;发送(带发送频控) | - -### 管理端(`X-Admin-Token`;兼容 `Authorization`、`?token=`) - -| 方法 | 路径 | 说明 | -|------|------|------| -| POST | `/api/admin/verify` | 请求体 `{"token":"…"}`,**响应顶层** `{"valid":true/false}` | -| GET / POST / PUT / PATCH / DELETE | `/api/admin/products` | 商品 CRUD 与上下架 | -| POST | `/api/admin/site/maintenance` | 维护模式 | -| GET / POST | `/api/admin/site/smtp` | SMTP 配置读/写 | -| GET | `/api/admin/orders` | 订单列表 | -| DELETE | `/api/admin/orders/{id}` | 删除订单 | -| GET / POST / DELETE | `/api/admin/chat`、`/api/admin/chat/{account}` | 会话、回复、清空 | -| GET | `/api/admin/system-status` | 聚合 backend / mysql / redis / rabbitmq;JSON 外层为 `{"data":{…},"error":null}` | - -其它管理接口的成功响应多为 `{"data":…,"error":null}` 形式。 - -## 数据库表(摘要) - -开发环境常用 `spring.jpa.hibernate.ddl-auto: update` 同步表结构。 - -**products:** 商品信息;`fulfillment_type`:`card`(卡密库存)/ `fixed`(固定内容);`fixed_content` 不在公开列表返回;含价格、标签 JSON、封面、截图 JSON、`active`、`require_login`、`max_per_account`、`total_sold`、`view_count`、`delivery_mode` 等。 - -**product_codes:** 每行一条卡密,关联 `product_id`;固定内容商品可不设。 - -**orders:** `delivered_codes` JSON、用户信息、`status`、`delivery_mode`、`notify_email`、联系方式等。 - -**site_settings:** 键值对,如 `totalVisits`、`maintenance`、`smtpHost`、`smtpPassword` 等。 - -**wishlists:** 用户账号与商品 ID 唯一约束。 - -**chat_messages:** 会话消息、`from_admin`、发送时间等。 - -## 配置说明(application.yaml) - -| 项 | 说明 | -|----|------| -| `spring.datasource.*` | MySQL | -| `spring.jpa.hibernate.ddl-auto` | 如 `update` | -| `server.port` | HTTP 端口,需与前端 `VITE_API_BASE_URL` 一致 | -| `spring.rabbitmq.*` | Broker;`app.rabbitmq-enabled=false` 时不注册监听器与拓扑,避免无法连接时启动失败 | -| `spring.data.redis.*` | Redis;`app.redis-enabled=false` 时系统状态中 Redis 标记为未启用 | -| `app.admin-token` | 管理后台令牌 | -| `app.auth-api-url` | 用户 token 远程校验服务根地址(`POST …/api/auth/verify`) | -| `app.app-env` | `development` / `production` | -| `app.rabbitmq-enabled` | 是否启用 RabbitMQ 发信与消费者 | -| `app.redis-enabled` | 是否在系统状态中探测 Redis | -| `app.rabbitmq-env` / `app.redis-env` | `dev` / `prod`,影响交换机与队列名 | -| `app.cors-allowed-origins` | 逗号分隔的允许源 | -| `management.health.mail.enabled` | 建议 `false` 直至配置 SMTP | - -**CORS:** 见 `CorsConfig`,包含 `Authorization`、`X-Admin-Token` 等请求头。 - -## 前端联调 - -前端 axios `baseURL` 来自环境变量或当前页 `origin`,请指向本服务监听的 **协议 + 主机 + 端口**。 diff --git a/mengyastore-frontend/package-lock.json b/mengyastore-frontend/package-lock.json index e20a7eb..17d3219 100644 --- a/mengyastore-frontend/package-lock.json +++ b/mengyastore-frontend/package-lock.json @@ -10,7 +10,6 @@ "dependencies": { "axios": "^1.6.8", "markdown-it": "^14.1.0", - "pinia": "^2.1.7", "vue": "^3.4.21", "vue-router": "^4.3.0" }, @@ -5899,28 +5898,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pinia": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", - "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", - "license": "MIT", - "dependencies": { - "@vue/devtools-api": "^6.6.3", - "vue-demi": "^0.14.10" - }, - "funding": { - "url": "https://github.com/sponsors/posva" - }, - "peerDependencies": { - "typescript": ">=4.4.4", - "vue": "^2.7.0 || ^3.5.11" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -7204,32 +7181,6 @@ } } }, - "node_modules/vue-demi": { - "version": "0.14.10", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "vue-demi-fix": "bin/vue-demi-fix.js", - "vue-demi-switch": "bin/vue-demi-switch.js" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - }, - "peerDependencies": { - "@vue/composition-api": "^1.0.0-rc.1", - "vue": "^3.0.0-0 || ^2.6.0" - }, - "peerDependenciesMeta": { - "@vue/composition-api": { - "optional": true - } - } - }, "node_modules/vue-router": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", diff --git a/mengyastore-frontend/package.json b/mengyastore-frontend/package.json index 31b1b32..13e310b 100644 --- a/mengyastore-frontend/package.json +++ b/mengyastore-frontend/package.json @@ -11,7 +11,6 @@ "dependencies": { "axios": "^1.6.8", "markdown-it": "^14.1.0", - "pinia": "^2.1.7", "vue": "^3.4.21", "vue-router": "^4.3.0" }, diff --git a/mengyastore-frontend/src/App.vue b/mengyastore-frontend/src/App.vue index 7adfbdb..aa26953 100644 --- a/mengyastore-frontend/src/App.vue +++ b/mengyastore-frontend/src/App.vue @@ -1,6 +1,5 @@