Files
hfb_sys/backend/internal/router/router.go
T

655 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package router
import (
"context"
"strings"
"hfb_sys/backend/internal/config"
"hfb_sys/backend/internal/handler"
smsintegration "hfb_sys/backend/internal/integrations/sms"
"hfb_sys/backend/internal/middleware"
"hfb_sys/backend/internal/modules/adminaudit"
"hfb_sys/backend/internal/modules/adminauth"
"hfb_sys/backend/internal/modules/admindashboard"
"hfb_sys/backend/internal/modules/adminfinance"
"hfb_sys/backend/internal/modules/adminmgr"
"hfb_sys/backend/internal/modules/adminrole"
"hfb_sys/backend/internal/modules/adminuser"
"hfb_sys/backend/internal/modules/announcement"
"hfb_sys/backend/internal/modules/auth"
"hfb_sys/backend/internal/modules/chat"
"hfb_sys/backend/internal/modules/chathub"
"hfb_sys/backend/internal/modules/dispute"
filemodule "hfb_sys/backend/internal/modules/file"
"hfb_sys/backend/internal/modules/listing"
"hfb_sys/backend/internal/modules/notification"
"hfb_sys/backend/internal/modules/order"
"hfb_sys/backend/internal/modules/payment"
"hfb_sys/backend/internal/modules/paymentaccount"
"hfb_sys/backend/internal/modules/paymentconfig"
"hfb_sys/backend/internal/modules/realname"
"hfb_sys/backend/internal/modules/supportgroup"
"hfb_sys/backend/internal/modules/systemconfig"
"hfb_sys/backend/internal/modules/user"
"hfb_sys/backend/internal/modules/wallet"
"hfb_sys/backend/internal/modules/withdrawal"
_ "hfb_sys/backend/docs" // Swagger 文档
"hfb_sys/backend/pkg/crypto"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"go.uber.org/zap"
)
func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
if config.IsProductionEnv(cfg.AppEnv) {
gin.SetMode(gin.ReleaseMode)
}
engine := gin.New()
// 生产环境前置 Caddy,已用实测对端地址覆盖 X-Real-IP,
// 直接据此取真实客户端 IP,避免客户端伪造 X-Forwarded-For。
engine.TrustedPlatform = "X-Real-IP"
engine.Use(middleware.RequestID())
engine.Use(middleware.RequestLogger(logger))
engine.Use(middleware.Recovery(logger))
health := handler.NewHealthHandler()
engine.GET("/health", health.Check)
// Swagger 文档路由(仅在非生产环境)
if !config.IsProductionEnv(cfg.AppEnv) {
engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
if cfg.RateLimit.Enabled {
engine.Use(middleware.RateLimitPerMinute(cfg.RateLimit.RequestsPerMinute, deps.Redis))
}
// 业务字段加密器(实名/收款账号):主密钥 + legacy 回退,用于密钥轮换期间透明解出旧密文。
// 生产环境必须配置 FIELD_ENCRYPTION_KEYValidateProductionSecurity 已校验)。
// 开发/测试缺 FIELD_ENCRYPTION_KEY 时用 legacy 密钥构造单密钥加密器,
// 保证零配置兼容历史硬编码密钥加密的存量密文(非生产 legacy 默认填历史硬编码值)。
var fieldEncryptor crypto.Encryptor
primary := cfg.FieldEncryptionKey
legacy := cfg.FieldEncryptionLegacyKey
switch {
case primary != "":
encryptor, err := crypto.NewFieldEncryptor(primary, legacy)
if err != nil {
if config.IsProductionEnv(cfg.AppEnv) {
logger.Fatal("FIELD_ENCRYPTION_KEY invalid", zap.Error(err))
}
logger.Warn("FIELD_ENCRYPTION_KEY invalid, fallback to legacy-only", zap.Error(err))
// primary 非法时退回 legacy 单密钥(若有),否则 MockEncryptor
if legacy != "" {
if e, eErr := crypto.NewFieldEncryptor(legacy); eErr == nil {
fieldEncryptor = e
}
}
} else {
fieldEncryptor = encryptor
}
case legacy != "":
// 开发态零配置:用 legacy 单密钥,能解历史存量密文。
encryptor, err := crypto.NewFieldEncryptor(legacy)
if err == nil {
fieldEncryptor = encryptor
logger.Warn("FIELD_ENCRYPTION_KEY not set, using legacy-only encryptor for dev compatibility")
}
}
if fieldEncryptor == nil {
if config.IsProductionEnv(cfg.AppEnv) {
logger.Fatal("FIELD_ENCRYPTION_KEY not set")
}
fieldEncryptor = &crypto.MockEncryptor{}
logger.Warn("FIELD_ENCRYPTION_KEY and legacy both unset, using MockEncryptor")
}
jwtManager := auth.NewJWTManager(cfg.JWTSecret)
var userRepo *auth.UserRepository
if deps.DB != nil {
userRepo = auth.NewUserRepository(deps.DB)
}
smsProvider := newSMSProvider(cfg, logger)
authService := auth.NewService(userRepo, deps.Redis, jwtManager, smsProvider, logger)
authHandler := auth.NewHandler(authService)
var adminAuthRepo *adminauth.Repository
if deps.DB != nil {
adminAuthRepo = adminauth.NewRepository(deps.DB, deps.Redis, jwtManager)
}
adminAuthService := adminauth.NewService(adminAuthRepo, jwtManager)
adminAuthHandler := adminauth.NewHandler(adminAuthService)
var adminDashboardRepo *admindashboard.Repository
if deps.DB != nil {
adminDashboardRepo = admindashboard.NewRepository(deps.DB)
}
adminDashboardService := admindashboard.NewService(adminDashboardRepo)
adminDashboardHandler := admindashboard.NewHandler(adminDashboardService)
var adminFinanceRepo *adminfinance.Repository
if deps.DB != nil {
adminFinanceRepo = adminfinance.NewRepository(deps.DB)
}
adminFinanceService := adminfinance.NewService(adminFinanceRepo)
adminFinanceHandler := adminfinance.NewHandler(adminFinanceService)
var adminUserRepo *adminuser.Repository
if deps.DB != nil {
adminUserRepo = adminuser.NewRepository(deps.DB)
}
adminUserService := adminuser.NewService(adminUserRepo)
adminUserHandler := adminuser.NewHandler(adminUserService)
var adminAuditRepo *adminaudit.Repository
if deps.DB != nil {
adminAuditRepo = adminaudit.NewRepository(deps.DB)
}
adminAuditService := adminaudit.NewService(adminAuditRepo)
adminAuditHandler := adminaudit.NewHandler(adminAuditService)
userHandler := user.NewHandler(userRepo)
var realnameRepo *realname.Repository
if deps.DB != nil {
realnameRepo = realname.NewRepository(deps.DB)
}
realnameService := realname.NewService(realnameRepo, newRealnameProvider(cfg, fieldEncryptor, logger), logger)
realnameHandler := realname.NewHandler(realnameService)
var chatHub *chathub.Hub
if deps.DB != nil {
chatHub = chathub.NewHub(deps.DB)
}
var chatRepo *chat.Repository
if deps.DB != nil {
chatRepo = chat.NewRepository(deps.DB, chatHub)
}
// 创建 chat 适配器用于 listing
chatCreator := chat.NewListingChatCreatorAdapter()
var listingRepo *listing.Repository
if deps.DB != nil {
listingRepo = listing.NewRepository(deps.DB, chatCreator)
}
var paymentRepo *payment.Repository
var orderRepo *order.Repository
if deps.DB != nil {
orderRepo = order.NewRepository(deps.DB, order.Dependencies{
ChatNotifier: chatRepo,
RefundStarter: order.RefundStarterFunc(func(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string, remark string) (string, error) {
if paymentRepo == nil {
return "", order.ErrDependencyUnavailable
}
dto, err := paymentRepo.StartRefund(ctx, orderID, refundAmountCent, bizType, remark)
if err != nil {
return "", err
}
return dto.Status, nil
}),
})
}
orderService := order.NewService(orderRepo)
orderHandler := order.NewHandler(orderService)
var walletRepo *wallet.Repository
if deps.DB != nil {
walletRepo = wallet.NewRepository(deps.DB)
}
walletService := wallet.NewService(walletRepo)
walletHandler := wallet.NewHandler(walletService)
var paymentAccountRepo *paymentaccount.Repository
if deps.DB != nil {
paymentAccountRepo = paymentaccount.NewRepository(deps.DB, fieldEncryptor)
}
paymentAccountService := paymentaccount.NewService(paymentAccountRepo)
paymentAccountHandler := paymentaccount.NewHandler(paymentAccountService)
var withdrawalRepo *withdrawal.Repository
if deps.DB != nil {
withdrawalRepo = withdrawal.NewRepository(deps.DB, walletRepo, fieldEncryptor)
}
withdrawalService := withdrawal.NewService(withdrawalRepo)
withdrawalHandler := withdrawal.NewHandler(withdrawalService)
// 支付配置管理
var paymentConfigRepo *paymentconfig.Repository
var paymentConfigService *paymentconfig.Service
var paymentConfigHandler *paymentconfig.Handler
if deps.DB != nil {
// 生产环境必须配置有效加密密钥;开发/测试缺失时才降级 MockEncryptor。
encryptionKey := cfg.PaymentConfigEncryptionKey
var encryptor paymentconfig.Encryptor
if encryptionKey != "" {
if aesEncryptor, err := paymentconfig.NewAESEncryptor(encryptionKey); err == nil {
encryptor = aesEncryptor
}
}
if encryptor == nil {
if config.IsProductionEnv(cfg.AppEnv) {
logger.Fatal("PAYMENT_CONFIG_ENCRYPTION_KEY not set or invalid")
}
encryptor = &paymentconfig.MockEncryptor{}
logger.Warn("PAYMENT_CONFIG_ENCRYPTION_KEY not set or invalid, using MockEncryptor")
}
paymentConfigRepo = paymentconfig.NewRepository(deps.DB, encryptor)
paymentConfigService = paymentconfig.NewService(paymentConfigRepo)
paymentConfigHandler = paymentconfig.NewHandler(paymentConfigService)
}
if deps.DB != nil {
paymentRepo = payment.NewRepository(deps.DB, paymentConfigRepo, orderRepo)
}
paymentService := payment.NewService(paymentRepo)
paymentHandler := payment.NewHandler(paymentService)
var notificationRepo *notification.Repository
if deps.DB != nil {
notificationRepo = notification.NewRepository(deps.DB)
}
notificationService := notification.NewService(notificationRepo)
notificationHandler := notification.NewHandler(notificationService)
chatService := chat.NewService(chatRepo)
chatHandler := chat.NewHandler(chatService)
var chatHubHandler *chathub.Handler
if chatHub != nil {
chatHubHandler = chathub.NewHandler(chatHub)
}
var disputeRepo *dispute.Repository
if deps.DB != nil {
disputeRepo = dispute.NewRepository(deps.DB, dispute.Dependencies{
RefundStarter: dispute.RefundStarterFunc(func(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string, remark string) (string, error) {
if paymentRepo == nil {
return "", dispute.ErrDependencyUnavailable
}
dto, err := paymentRepo.StartRefund(ctx, orderID, refundAmountCent, bizType, remark)
if err != nil {
return "", err
}
return dto.Status, nil
}),
})
}
disputeService := dispute.NewService(disputeRepo)
disputeHandler := dispute.NewHandler(disputeService)
var systemConfigRepo *systemconfig.Repository
if deps.DB != nil {
systemConfigRepo = systemconfig.NewRepository(deps.DB)
}
systemConfigService := systemconfig.NewService(systemConfigRepo)
systemConfigHandler := systemconfig.NewHandler(systemConfigService)
var adminRoleRepo *adminrole.Repository
if deps.DB != nil {
adminRoleRepo = adminrole.NewRepository(deps.DB, deps.Redis)
}
adminRoleService := adminrole.NewService(adminRoleRepo)
adminRoleHandler := adminrole.NewHandler(adminRoleService)
var adminMgrRepo *adminmgr.Repository
if deps.DB != nil {
adminMgrRepo = adminmgr.NewRepository(deps.DB, deps.Redis)
}
adminMgrService := adminmgr.NewService(adminMgrRepo)
adminMgrHandler := adminmgr.NewHandler(adminMgrService)
var supportGroupRepo *supportgroup.Repository
if deps.DB != nil {
supportGroupRepo = supportgroup.NewRepository(deps.DB)
}
supportGroupService := supportgroup.NewService(supportGroupRepo)
supportGroupHandler := supportgroup.NewHandler(supportGroupService)
var fileStorage *filemodule.Storage
if cfg.Storage.Endpoint != "" && cfg.Storage.Bucket != "" {
var err error
fileStorage, err = filemodule.NewStorage(cfg.Storage)
if err != nil {
logger.Warn("file storage bucket is not ready; file APIs will retry on request", zap.Error(err))
}
}
fileService := filemodule.NewService(fileStorage)
fileHandler := filemodule.NewHandler(fileService, fileStorage)
listingService := listing.NewService(listingRepo, systemConfigRepo)
listingHandler := listing.NewHandler(listingService, fileStorage, listing.HandlerOptions{
ExternalUploadSecret: cfg.ExternalUploadSecret,
ExternalUploadAllowedIPs: cfg.ExternalUploadAllowedIPs,
})
var announcementRepo *announcement.Repository
if deps.DB != nil {
announcementRepo = announcement.NewRepository(deps.DB)
}
announcementService := announcement.NewService(announcementRepo)
announcementHandler := announcement.NewHandler(announcementService)
requireAuth := middleware.Auth(jwtManager)
var validateAdminToken middleware.AdminTokenValidatorFunc
if adminAuthRepo != nil {
validateAdminToken = func(ctx context.Context, adminID uint64, tokenVersion int64) (middleware.AdminTokenContext, error) {
admin, err := adminAuthRepo.FindActiveForPasswordGate(ctx, adminID, tokenVersion)
if err != nil {
return middleware.AdminTokenContext{}, err
}
return middleware.AdminTokenContext{
Username: admin.Username,
PasswordMustChange: admin.PasswordMustChange,
}, nil
}
}
requireAdmin := middleware.AdminAuth(jwtManager, validateAdminToken)
requireRealname := middleware.RequireRealname(userRepo)
requirePerm := func(code string) gin.HandlerFunc {
return middleware.RequirePermission(code, deps.Redis)
}
api := engine.Group("/api")
{
api.GET("/health", health.Check)
api.GET("/listing-publish-options", systemConfigHandler.PublishOptions)
api.GET("/listing-publish-agreements", systemConfigHandler.ListingPublishAgreements)
api.GET("/listing-sale-price-config", systemConfigHandler.SalePriceConfig)
api.GET("/order-agreements", systemConfigHandler.OrderAgreements)
api.GET("/post-rental-notice", systemConfigHandler.PostRentalNotice)
api.GET("/home-announcements", systemConfigHandler.HomeAnnouncements)
api.GET("/mobile-home-config", systemConfigHandler.HomeConfig)
api.GET("/public/files/object", fileHandler.PublicObject)
api.POST("/payments/leshua/notify", paymentHandler.LeshuaNotify)
api.POST("/payments/lakala/notify", paymentHandler.LakalaNotify)
api.POST("/payments/shuncheng/notify", paymentHandler.ShunchengNotify)
openRoutes := api.Group("/open")
{
openRoutes.POST("/listing-uploads", listingHandler.ImportExternalUpload)
}
authRoutes := api.Group("/auth")
{
authRoutes.GET("/captcha", authHandler.Captcha)
authRoutes.POST("/sms/send", authHandler.SendSMS)
authRoutes.POST("/sms/login", authHandler.Login)
authRoutes.POST("/refresh", authHandler.Refresh)
authRoutes.POST("/logout", authHandler.Logout)
}
api.GET("/me", requireAuth, userHandler.Me)
api.PUT("/me", requireAuth, userHandler.UpdateMe)
listingRoutes := api.Group("/listings")
{
listingRoutes.GET("", listingHandler.ListPublic)
listingRoutes.GET("/default-upload-screenshot", listingHandler.DefaultUploadScreenshot)
listingRoutes.GET("/:id/cover", listingHandler.Cover)
listingRoutes.GET("/:id/screenshots/:index", listingHandler.Screenshot)
listingRoutes.GET("/:id", listingHandler.FindPublic)
listingRoutes.POST("", requireAuth, requireRealname, listingHandler.Create)
listingRoutes.PUT("/:id", requireAuth, requireRealname, listingHandler.Update)
listingRoutes.POST("/:id/submit-review", requireAuth, requireRealname, listingHandler.SubmitReview)
listingRoutes.DELETE("/:id", requireAuth, requireRealname, listingHandler.Offline)
}
sellerRoutes := api.Group("/seller", requireAuth, requireRealname)
{
sellerRoutes.GET("/listings", listingHandler.ListMine)
sellerRoutes.GET("/listings/:id", listingHandler.FindMine)
}
orderRoutes := api.Group("/orders", requireAuth)
{
orderRoutes.POST("", requireRealname, orderHandler.Create)
orderRoutes.GET("", orderHandler.List)
orderRoutes.GET("/:id", orderHandler.Detail)
orderRoutes.GET("/:id/chat", chatHandler.OrderConversation)
orderRoutes.POST("/:id/pay", orderHandler.Pay)
orderRoutes.POST("/:id/start-payment", paymentHandler.Start)
orderRoutes.GET("/:id/query-payment", paymentHandler.Query)
orderRoutes.POST("/:id/cancel", orderHandler.Cancel)
orderRoutes.POST("/:id/handoff", orderHandler.SubmitHandoff)
orderRoutes.GET("/:id/handoff-records", orderHandler.HandoffRecords)
orderRoutes.POST("/:id/confirm-receive", orderHandler.ConfirmReceive)
orderRoutes.POST("/:id/return", orderHandler.SubmitReturn)
orderRoutes.POST("/:id/confirm-return", orderHandler.ConfirmReturn)
orderRoutes.POST("/:id/checkout", orderHandler.SubmitCheckout)
orderRoutes.POST("/:id/checkout/confirm", orderHandler.ConfirmCheckout)
orderRoutes.POST("/:id/checkout/counter", orderHandler.CounterCheckout)
orderRoutes.POST("/:id/checkout/accept", orderHandler.AcceptCheckout)
orderRoutes.POST("/:id/dispute", disputeHandler.Create)
orderRoutes.POST("/:id/dispute/cancel", disputeHandler.CancelByOrder)
orderRoutes.GET("/:id/refund-status", paymentHandler.QueryRefundStatus)
}
disputeRoutes := api.Group("/disputes", requireAuth)
{
disputeRoutes.GET("", disputeHandler.List)
disputeRoutes.GET("/:id", disputeHandler.Detail)
disputeRoutes.POST("/:id/cancel", disputeHandler.Cancel)
}
walletRoutes := api.Group("/wallet", requireAuth)
{
walletRoutes.GET("/balance", walletHandler.Balance)
walletRoutes.GET("/ledger", walletHandler.Ledger)
walletRoutes.POST("/withdraw", walletHandler.Withdraw)
}
paymentAccountRoutes := api.Group("/payment-accounts", requireAuth)
{
paymentAccountRoutes.GET("", paymentAccountHandler.List)
paymentAccountRoutes.GET("/:id", paymentAccountHandler.FindByID)
paymentAccountRoutes.POST("", paymentAccountHandler.Create)
paymentAccountRoutes.PUT("/:id", paymentAccountHandler.Update)
paymentAccountRoutes.DELETE("/:id", paymentAccountHandler.Delete)
paymentAccountRoutes.POST("/:id/set-default", paymentAccountHandler.SetDefault)
}
withdrawalRoutes := api.Group("/withdrawals", requireAuth)
{
withdrawalRoutes.POST("", withdrawalHandler.Create)
withdrawalRoutes.GET("", withdrawalHandler.List)
withdrawalRoutes.GET("/:id", withdrawalHandler.FindByID)
withdrawalRoutes.POST("/:id/cancel", withdrawalHandler.Cancel)
}
fileRoutes := api.Group("/files", requireAuth)
{
fileRoutes.POST("/upload", fileHandler.Upload)
fileRoutes.GET("/object", fileHandler.Object)
}
notificationRoutes := api.Group("/notifications", requireAuth)
{
notificationRoutes.GET("", notificationHandler.List)
notificationRoutes.POST("/:id/read", notificationHandler.MarkRead)
}
chatRoutes := api.Group("/chats", requireAuth)
{
if chatHubHandler != nil {
chatRoutes.GET("/events", chatHubHandler.UserEvents)
}
chatRoutes.POST("/support", chatHandler.EnsureSupportConversation)
chatRoutes.GET("/unread-count", chatHandler.UnreadCount)
chatRoutes.GET("", chatHandler.List)
chatRoutes.GET("/:id", chatHandler.Detail)
chatRoutes.GET("/:id/messages", chatHandler.Messages)
chatRoutes.POST("/:id/messages", chatHandler.Send)
chatRoutes.POST("/:id/read", chatHandler.MarkRead)
}
realnameRoutes := api.Group("/realname", requireAuth)
{
realnameRoutes.POST("/start", realnameHandler.Start)
realnameRoutes.GET("/status", realnameHandler.Status)
}
announcementRoutes := api.Group("/announcements")
{
announcementRoutes.GET("", announcementHandler.List)
announcementRoutes.GET("/:id", announcementHandler.GetByID)
}
adminAuthRoutes := api.Group("/admin/auth")
{
adminAuthRoutes.GET("/captcha", adminAuthHandler.Captcha)
adminAuthRoutes.POST("/login", adminAuthHandler.Login)
adminAuthRoutes.POST("/refresh", adminAuthHandler.Refresh)
}
adminRoutes := api.Group("/admin", requireAdmin, middleware.RequireAdminPasswordChanged())
{
adminRoutes.GET("/me", adminAuthHandler.Me)
adminRoutes.PUT("/me/support-status", adminAuthHandler.UpdateSupportStatus)
adminRoutes.POST("/auth/logout", adminAuthHandler.Logout)
adminRoutes.POST("/files/upload", fileHandler.Upload)
adminRoutes.GET("/dashboard", requirePerm("dashboard:view"), adminDashboardHandler.Summary)
adminRoutes.GET("/files/object", fileHandler.Object)
adminRoutes.GET("/users", requirePerm("user:view"), adminUserHandler.List)
adminRoutes.POST("/users/:id/freeze", requirePerm("user:freeze"), adminUserHandler.Freeze)
adminRoutes.POST("/users/:id/unfreeze", requirePerm("user:unfreeze"), adminUserHandler.Unfreeze)
adminRoutes.POST("/users/:id/deposit-free-quota", requirePerm("user:deposit_free"), adminUserHandler.SetDepositFreeQuota)
adminRoutes.POST("/users/:id/revoke-realname", requirePerm("user:revoke_realname"), adminUserHandler.RevokeRealname)
adminRoutes.GET("/orders", requirePerm("order:view"), orderHandler.AdminList)
adminRoutes.GET("/listing-orders/:id/latest", requirePerm("order:view"), orderHandler.AdminLatestByListing)
adminRoutes.GET("/orders/:id", requirePerm("order:view"), orderHandler.AdminDetail)
adminRoutes.GET("/orders/:id/handoff-records", requirePerm("order:view"), orderHandler.AdminHandoffRecords)
adminRoutes.POST("/orders/:id/close", requirePerm("order:close"), orderHandler.AdminClose)
adminRoutes.POST("/orders/:id/mark-abnormal", requirePerm("order:mark_abnormal"), orderHandler.AdminMarkAbnormal)
adminRoutes.POST("/orders/:id/reset-handoff", requirePerm("order:mark_abnormal"), orderHandler.AdminResetHandoff)
adminRoutes.POST("/orders/:id/refund", requirePerm("order:close"), orderHandler.AdminRefund)
adminRoutes.GET("/orders/:id/refund-status", requirePerm("order:view"), orderHandler.AdminRefundStatus)
adminRoutes.GET("/listings", requirePerm("listing:view"), listingHandler.ListAdmin)
adminRoutes.GET("/listings/pending", requirePerm("listing:approve"), listingHandler.ListPendingReview)
adminRoutes.GET("/listings/:id", requirePerm("listing:view"), listingHandler.FindAdmin)
adminRoutes.POST("/listings/:id/approve", requirePerm("listing:approve"), listingHandler.Approve)
adminRoutes.POST("/listings/:id/adjust-price", requirePerm("listing:approve"), listingHandler.AdjustReviewPrice)
adminRoutes.POST("/listings/:id/transfer-owner", requirePerm("listing:approve"), listingHandler.TransferOwner)
adminRoutes.POST("/listings/:id/reject", requirePerm("listing:reject"), listingHandler.Reject)
adminRoutes.POST("/listings/:id/offline", requirePerm("listing:offline"), listingHandler.AdminOffline)
adminRoutes.POST("/listings/:id/mark-abnormal", requirePerm("listing:offline"), listingHandler.AdminMarkAbnormal)
adminRoutes.GET("/disputes", requirePerm("dispute:view"), disputeHandler.AdminList)
adminRoutes.POST("/disputes/:id/arbitrate", requirePerm("dispute:arbitrate"), disputeHandler.AdminArbitrate)
adminRoutes.GET("/finance/dashboard", requirePerm("wallet:view"), adminFinanceHandler.Dashboard)
adminRoutes.GET("/finance/details", requirePerm("wallet:view"), adminFinanceHandler.Details)
adminRoutes.GET("/wallet/ledger", requirePerm("wallet:view"), walletHandler.AdminLedger)
adminRoutes.GET("/payments", requirePerm("wallet:view"), paymentHandler.AdminList)
// 提现管理
adminRoutes.GET("/withdrawals", requirePerm("withdrawal:list"), withdrawalHandler.AdminList)
adminRoutes.GET("/withdrawals/:id", requirePerm("withdrawal:detail"), withdrawalHandler.AdminFindByID)
adminRoutes.POST("/withdrawals/:id/review", requirePerm("withdrawal:review"), withdrawalHandler.Review)
adminRoutes.POST("/withdrawals/:id/confirm-payment", requirePerm("withdrawal:pay"), withdrawalHandler.ConfirmPayment)
// 支付配置管理
if paymentConfigHandler != nil {
adminRoutes.GET("/payment-configs", requirePerm("payment_config:list"), paymentConfigHandler.List)
adminRoutes.GET("/payment-configs/export", requirePerm("payment_config:view_secret"), paymentConfigHandler.ExportBackup)
adminRoutes.POST("/payment-configs/import", requirePerm("payment_config:view_secret"), paymentConfigHandler.ImportBackup)
adminRoutes.GET("/payment-configs/:id", requirePerm("payment_config:list"), middleware.RequirePermissionIf("payment_config:view_secret", deps.Redis, func(c *gin.Context) bool {
return c.Query("include_secret") == "true"
}), paymentConfigHandler.Get)
adminRoutes.POST("/payment-configs", requirePerm("payment_config:create"), paymentConfigHandler.Create)
adminRoutes.PUT("/payment-configs/:id", requirePerm("payment_config:update"), paymentConfigHandler.Update)
adminRoutes.DELETE("/payment-configs/:id", requirePerm("payment_config:delete"), paymentConfigHandler.Delete)
}
adminRoutes.GET("/system-configs", requirePerm("system_config:view"), systemConfigHandler.List)
adminRoutes.PUT("/system-configs/:key", requirePerm("system_config:update"), systemConfigHandler.Update)
adminRoutes.GET("/audit-logs", requirePerm("audit_log:view"), adminAuditHandler.List)
if chatHubHandler != nil {
adminRoutes.GET("/chats/events", requirePerm("chat:view"), chatHubHandler.AdminEvents)
}
adminRoutes.GET("/chats", requirePerm("chat:view"), chatHandler.AdminList)
adminRoutes.GET("/chats/:id", requirePerm("chat:view"), chatHandler.AdminDetail)
adminRoutes.GET("/chats/:id/messages", requirePerm("chat:view"), chatHandler.AdminMessages)
adminRoutes.POST("/chats/:id/messages", requirePerm("chat:send"), chatHandler.AdminSend)
adminRoutes.POST("/chats/:id/read", requirePerm("chat:view"), chatHandler.AdminMarkRead)
adminRoutes.POST("/chats/:id/transfer", requirePerm("chat:send"), chatHandler.AdminTransfer)
adminRoutes.GET("/chats/support-admins", requirePerm("chat:view"), chatHandler.AdminSupportAdmins)
adminRoutes.PUT("/chats/:id/remark", requirePerm("chat:send"), chatHandler.AdminUpdateRemark)
adminRoutes.GET("/chats/quick-replies", requirePerm("chat:view"), chatHandler.AdminListQuickReplies)
adminRoutes.POST("/chats/quick-replies", requirePerm("chat:send"), chatHandler.AdminCreateQuickReply)
adminRoutes.PUT("/chats/quick-replies/:id", requirePerm("chat:send"), chatHandler.AdminUpdateQuickReply)
adminRoutes.DELETE("/chats/quick-replies/:id", requirePerm("chat:send"), chatHandler.AdminDeleteQuickReply)
adminRoutes.GET("/chats/auto-welcome", requirePerm("system_config:view"), chatHandler.AdminGetAutoWelcome)
adminRoutes.PUT("/chats/auto-welcome", requirePerm("system_config:update"), chatHandler.AdminUpdateAutoWelcome)
adminRoutes.GET("/chats/listing-group-welcome", requirePerm("system_config:view"), chatHandler.AdminGetListingGroupWelcome)
adminRoutes.PUT("/chats/listing-group-welcome", requirePerm("system_config:update"), chatHandler.AdminUpdateListingGroupWelcome)
// 二维码池管理
adminRoutes.POST("/chats/qrcodes", requirePerm("chat:manage"), chatHandler.CreateQrCodeHandler)
adminRoutes.POST("/chats/qrcodes/batch", requirePerm("chat:manage"), chatHandler.BatchCreateQrCodeHandler)
adminRoutes.GET("/chats/qrcodes", requirePerm("chat:view"), chatHandler.ListQrCodesHandler)
adminRoutes.GET("/chats/qrcodes/stats", requirePerm("chat:view"), chatHandler.GetQrCodeStatsHandler)
adminRoutes.POST("/chats/qrcodes/ocr-group-name", requirePerm("chat:manage"), chatHandler.RecognizeQrCodeGroupNameHandler)
adminRoutes.PATCH("/chats/qrcodes/:id", requirePerm("chat:manage"), chatHandler.UpdateQrCodeHandler)
adminRoutes.DELETE("/chats/qrcodes/:id", requirePerm("chat:manage"), chatHandler.DeleteQrCodeHandler)
// 客服分组
adminRoutes.GET("/chat-support-groups", requirePerm("chat:manage"), supportGroupHandler.List)
adminRoutes.GET("/chat-support-groups/support-admins", requirePerm("chat:manage"), supportGroupHandler.ListSupportAdmins)
adminRoutes.GET("/chat-support-groups/:id", requirePerm("chat:manage"), supportGroupHandler.FindByID)
adminRoutes.POST("/chat-support-groups", requirePerm("chat:manage"), supportGroupHandler.Create)
adminRoutes.PUT("/chat-support-groups/:id", requirePerm("chat:manage"), supportGroupHandler.Update)
adminRoutes.DELETE("/chat-support-groups/:id", requirePerm("chat:manage"), supportGroupHandler.Delete)
adminRoutes.PUT("/chat-support-groups/:id/members", requirePerm("chat:manage"), supportGroupHandler.AssignMembers)
// 角色管理
adminRoutes.GET("/roles", requirePerm("role:manage"), adminRoleHandler.List)
adminRoutes.GET("/roles/:id", requirePerm("role:manage"), adminRoleHandler.FindByID)
adminRoutes.POST("/roles", requirePerm("role:manage"), adminRoleHandler.Create)
adminRoutes.PUT("/roles/:id", requirePerm("role:manage"), adminRoleHandler.Update)
adminRoutes.DELETE("/roles/:id", requirePerm("role:manage"), adminRoleHandler.Delete)
adminRoutes.PUT("/roles/:id/permissions", requirePerm("role:manage"), adminRoleHandler.AssignPermissions)
adminRoutes.GET("/permissions", requirePerm("role:manage"), adminRoleHandler.ListPermissions)
// 管理员管理
adminRoutes.GET("/admin-users", requirePerm("admin_user:manage"), adminMgrHandler.List)
adminRoutes.GET("/admin-users/:id", requirePerm("admin_user:manage"), adminMgrHandler.FindByID)
adminRoutes.POST("/admin-users", requirePerm("admin_user:manage"), adminMgrHandler.Create)
adminRoutes.PUT("/admin-users/:id", requirePerm("admin_user:manage"), adminMgrHandler.Update)
adminRoutes.DELETE("/admin-users/:id", requirePerm("admin_user:manage"), adminMgrHandler.Delete)
adminRoutes.PUT("/admin-users/:id/roles", requirePerm("admin_user:manage"), adminMgrHandler.AssignRoles)
adminRoutes.PUT("/admin-users/me/password", adminMgrHandler.ChangeOwnPassword)
adminRoutes.PUT("/admin-users/:id/password", requirePerm("admin_user:manage"), adminMgrHandler.ResetPassword)
// 公告管理
adminRoutes.GET("/announcements", requirePerm("announcement:view"), announcementHandler.AdminList)
adminRoutes.GET("/announcements/:id", requirePerm("announcement:view"), announcementHandler.AdminGetByID)
adminRoutes.POST("/announcements", requirePerm("announcement:manage"), announcementHandler.Create)
adminRoutes.PUT("/announcements/:id", requirePerm("announcement:manage"), announcementHandler.Update)
adminRoutes.POST("/announcements/:id/publish", requirePerm("announcement:manage"), announcementHandler.Publish)
adminRoutes.POST("/announcements/:id/archive", requirePerm("announcement:manage"), announcementHandler.Archive)
adminRoutes.POST("/announcements/:id/unarchive", requirePerm("announcement:manage"), announcementHandler.Unarchive)
adminRoutes.DELETE("/announcements/:id", requirePerm("announcement:manage"), announcementHandler.Delete)
}
}
return engine
}
func newSMSProvider(cfg config.Config, logger *zap.Logger) smsintegration.Provider {
switch strings.ToLower(strings.TrimSpace(cfg.SMS.Provider)) {
case "aliyun":
provider, err := smsintegration.NewAliyunProvider(smsintegration.AliyunConfig{
AccessKeyID: cfg.SMS.AliyunAccessKeyID,
AccessKeySecret: cfg.SMS.AliyunAccessKeySecret,
Endpoint: cfg.SMS.AliyunEndpoint,
SignName: cfg.SMS.AliyunSignName,
LoginTemplateCode: cfg.SMS.AliyunLoginTemplateCode,
}, logger)
if err != nil {
logger.Warn("aliyun sms provider unavailable; sms send will fail", zap.Error(err))
return smsintegration.NewUnavailableProvider(err)
}
return provider
default:
return smsintegration.NewMockProvider(logger)
}
}
func newRealnameProvider(cfg config.Config, encryptor crypto.Encryptor, logger *zap.Logger) realname.Provider {
switch strings.ToLower(strings.TrimSpace(cfg.Realname.Provider)) {
case "cloudmarket", "aliyun_cloudmarket":
provider, err := realname.NewCloudMarketProvider(realname.CloudMarketConfig{
URL: cfg.Realname.CloudMarketURL,
AppCode: cfg.Realname.CloudMarketAppCode,
Encryptor: encryptor,
})
if err != nil {
logger.Warn("cloud market realname provider unavailable; realname verify will fail", zap.Error(err))
return realname.NewUnavailableProvider(err)
}
return provider
default:
return realname.NewMockProvider(encryptor)
}
}