合并 main 分支并融合订单列表最小化与 SQL 治理

- Auth/权限中间件取 main 演进版(错误细分、token 来源),保留 dev 的用户 cookie 与登出撤销语义
- 后台订单列表:融合 main 的首页免 COUNT 优化与关键词索引路由,保留 dev 的最小化 AdminOrderListItemDTO(结账明细仍由详情接口返回,同步调整对应集成测试)
- 保留 dev 的 token_version 冻结/解冻递增与 main 的状态变化判断及通知
- 前端:保留 dev 登出接口与 main 的 fetchMe silent 参数、SSE 登录跳转;后台订单表按 dev 最小化 DTO 移除号主到手列
This commit is contained in:
yml2213
2026-09-05 11:43:13 +08:00
294 changed files with 30171 additions and 13439 deletions
+97
View File
@@ -0,0 +1,97 @@
package router
import (
"hfb_sys/backend/internal/modules/adminmgr"
"hfb_sys/backend/internal/modules/adminrole"
"hfb_sys/backend/internal/modules/announcement"
"hfb_sys/backend/internal/modules/chat"
"hfb_sys/backend/internal/modules/mohong"
"hfb_sys/backend/internal/modules/payment"
"hfb_sys/backend/internal/modules/supportgroup"
"github.com/gin-gonic/gin"
)
type permissionMiddleware func(string) gin.HandlerFunc
func registerCrashAdminRoutes(routes *gin.RouterGroup, requirePerm permissionMiddleware, mohongHandler *mohong.Handler, paymentHandler *payment.Handler) {
if mohongHandler == nil {
return
}
register := func(prefix string) {
routes.GET(prefix+"/categories", requirePerm("mohong:product_view"), mohongHandler.AdminListCategories)
routes.POST(prefix+"/categories", requirePerm("mohong:category"), mohongHandler.AdminCreateCategory)
routes.PUT(prefix+"/categories/:id", requirePerm("mohong:category"), mohongHandler.AdminUpdateCategory)
routes.DELETE(prefix+"/categories/:id", requirePerm("mohong:category"), mohongHandler.AdminDeleteCategory)
routes.GET(prefix+"/products", requirePerm("mohong:product_view"), mohongHandler.AdminListProducts)
routes.GET(prefix+"/products/:id", requirePerm("mohong:product_view"), mohongHandler.AdminGetProduct)
routes.POST(prefix+"/products", requirePerm("mohong:product_manage"), mohongHandler.AdminCreateProduct)
routes.PUT(prefix+"/products/:id", requirePerm("mohong:product_manage"), mohongHandler.AdminUpdateProduct)
routes.DELETE(prefix+"/products/:id", requirePerm("mohong:product_manage"), mohongHandler.AdminDeleteProduct)
routes.GET(prefix+"/orders", requirePerm("mohong:order_view"), mohongHandler.AdminListOrders)
routes.GET(prefix+"/orders/:id", requirePerm("mohong:order_view"), mohongHandler.AdminGetOrder)
routes.POST(prefix+"/orders/:id/receive", requirePerm("mohong:order_manage"), mohongHandler.AdminReceiveOrder)
routes.POST(prefix+"/orders/:id/complete", requirePerm("mohong:order_manage"), mohongHandler.AdminCompleteOrder)
routes.POST(prefix+"/orders/:id/cancel", requirePerm("mohong:order_manage"), mohongHandler.AdminCancelOrder)
routes.POST(prefix+"/orders/:id/refund", requirePerm("mohong:order_manage"), paymentHandler.AdminRefundMohong)
routes.GET(prefix+"/config", requirePerm("mohong:config"), mohongHandler.AdminGetConfig)
routes.PUT(prefix+"/config", requirePerm("mohong:config"), mohongHandler.AdminUpdateConfig)
}
register("/crash")
register("/mohong")
}
type adminManagementHandlers struct {
chat *chat.Handler
supportGroup *supportgroup.Handler
role *adminrole.Handler
admin *adminmgr.Handler
announcement *announcement.Handler
}
func registerAdminManagementRoutes(routes *gin.RouterGroup, requirePerm permissionMiddleware, handlers adminManagementHandlers) {
routes.POST("/chats/qrcodes", requirePerm("qrcode:manage"), handlers.chat.CreateQrCodeHandler)
routes.POST("/chats/qrcodes/batch", requirePerm("qrcode:manage"), handlers.chat.BatchCreateQrCodeHandler)
routes.POST("/chats/qrcodes/batch-delete", requirePerm("qrcode:manage"), handlers.chat.BatchDeleteQrCodeHandler)
routes.GET("/chats/qrcodes", requirePerm("qrcode:view"), handlers.chat.ListQrCodesHandler)
routes.GET("/chats/qrcodes/stats", requirePerm("qrcode:view"), handlers.chat.GetQrCodeStatsHandler)
routes.POST("/chats/qrcodes/ocr-group-name", requirePerm("qrcode:manage"), handlers.chat.RecognizeQrCodeGroupNameHandler)
routes.POST("/chats/qrcodes/ocr-group-name/async", requirePerm("qrcode:manage"), handlers.chat.SubmitOCRJobHandler)
routes.GET("/chats/qrcodes/ocr-group-name/async/:jobId", requirePerm("qrcode:manage"), handlers.chat.GetOCRJobResultHandler)
routes.PATCH("/chats/qrcodes/:id", requirePerm("qrcode:manage"), handlers.chat.UpdateQrCodeHandler)
routes.DELETE("/chats/qrcodes/:id", requirePerm("qrcode:manage"), handlers.chat.DeleteQrCodeHandler)
routes.GET("/chat-support-groups", requirePerm("chat:manage"), handlers.supportGroup.List)
routes.GET("/chat-support-groups/support-admins", requirePerm("chat:manage"), handlers.supportGroup.ListSupportAdmins)
routes.GET("/chat-support-groups/:id", requirePerm("chat:manage"), handlers.supportGroup.FindByID)
routes.POST("/chat-support-groups", requirePerm("chat:manage"), handlers.supportGroup.Create)
routes.PUT("/chat-support-groups/:id", requirePerm("chat:manage"), handlers.supportGroup.Update)
routes.DELETE("/chat-support-groups/:id", requirePerm("chat:manage"), handlers.supportGroup.Delete)
routes.PUT("/chat-support-groups/:id/members", requirePerm("chat:manage"), handlers.supportGroup.AssignMembers)
routes.GET("/roles", requirePerm("role:manage"), handlers.role.List)
routes.GET("/roles/:id", requirePerm("role:manage"), handlers.role.FindByID)
routes.POST("/roles", requirePerm("role:manage"), handlers.role.Create)
routes.PUT("/roles/:id", requirePerm("role:manage"), handlers.role.Update)
routes.DELETE("/roles/:id", requirePerm("role:manage"), handlers.role.Delete)
routes.PUT("/roles/:id/permissions", requirePerm("role:manage"), handlers.role.AssignPermissions)
routes.GET("/permissions", requirePerm("role:manage"), handlers.role.ListPermissions)
routes.GET("/admin-users", requirePerm("admin_user:manage"), handlers.admin.List)
routes.GET("/admin-users/:id", requirePerm("admin_user:manage"), handlers.admin.FindByID)
routes.POST("/admin-users", requirePerm("admin_user:manage"), handlers.admin.Create)
routes.PUT("/admin-users/:id", requirePerm("admin_user:manage"), handlers.admin.Update)
routes.DELETE("/admin-users/:id", requirePerm("admin_user:manage"), handlers.admin.Delete)
routes.PUT("/admin-users/:id/roles", requirePerm("admin_user:manage"), handlers.admin.AssignRoles)
routes.PUT("/admin-users/me/password", handlers.admin.ChangeOwnPassword)
routes.PUT("/admin-users/:id/password", requirePerm("admin_user:manage"), handlers.admin.ResetPassword)
routes.GET("/announcements", requirePerm("announcement:view"), handlers.announcement.AdminList)
routes.GET("/announcements/:id", requirePerm("announcement:view"), handlers.announcement.AdminGetByID)
routes.POST("/announcements", requirePerm("announcement:manage"), handlers.announcement.Create)
routes.PUT("/announcements/:id", requirePerm("announcement:manage"), handlers.announcement.Update)
routes.POST("/announcements/:id/publish", requirePerm("announcement:manage"), handlers.announcement.Publish)
routes.POST("/announcements/:id/archive", requirePerm("announcement:manage"), handlers.announcement.Archive)
routes.POST("/announcements/:id/unarchive", requirePerm("announcement:manage"), handlers.announcement.Unarchive)
routes.DELETE("/announcements/:id", requirePerm("announcement:manage"), handlers.announcement.Delete)
}
+29
View File
@@ -0,0 +1,29 @@
package router
import (
"context"
"errors"
"hfb_sys/backend/internal/handler"
)
func newReadinessChecks(deps Dependencies) map[string]handler.ReadinessCheck {
return map[string]handler.ReadinessCheck{
"mysql": func(ctx context.Context) error {
if deps.DB == nil {
return errors.New("mysql unavailable")
}
sqlDB, err := deps.DB.DB()
if err != nil {
return err
}
return sqlDB.PingContext(ctx)
},
"redis": func(ctx context.Context) error {
if deps.Redis == nil {
return errors.New("redis unavailable")
}
return deps.Redis.Ping(ctx).Err()
},
}
}
+48
View File
@@ -0,0 +1,48 @@
package router
import (
"strings"
"hfb_sys/backend/internal/config"
smsintegration "hfb_sys/backend/internal/integrations/sms"
"hfb_sys/backend/internal/modules/realname"
"hfb_sys/backend/pkg/crypto"
"go.uber.org/zap"
)
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("阿里云短信服务初始化失败", 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("实名认证服务初始化失败", zap.Error(err))
return realname.NewUnavailableProvider(err)
}
return provider
default:
return realname.NewMockProvider(encryptor)
}
}
+36 -127
View File
@@ -2,11 +2,9 @@ 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"
@@ -19,6 +17,7 @@ import (
"hfb_sys/backend/internal/modules/adminuser"
"hfb_sys/backend/internal/modules/announcement"
"hfb_sys/backend/internal/modules/auth"
"hfb_sys/backend/internal/modules/backupmonitor"
"hfb_sys/backend/internal/modules/chat"
"hfb_sys/backend/internal/modules/chathub"
"hfb_sys/backend/internal/modules/dispute"
@@ -60,16 +59,18 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
engine.Use(middleware.RequestID())
engine.Use(middleware.RequestLogger(logger))
engine.Use(middleware.Recovery(logger))
jwtManager := auth.NewJWTManager(cfg.JWTSecret)
health := handler.NewHealthHandler()
engine.GET("/health", health.Check)
health := handler.NewHealthHandler(newReadinessChecks(deps))
engine.GET("/health", health.Live)
engine.GET("/health/ready", health.Ready)
// 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))
engine.Use(middleware.AdminAwareRateLimitPerMinute(cfg.RateLimit.RequestsPerMinute, deps.Redis, jwtManager))
}
// 业务字段加密器(实名/收款账号):主密钥 + legacy 回退,用于密钥轮换期间透明解出旧密文。
@@ -112,7 +113,6 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
logger.Debug("开发环境使用模拟业务字段加密器")
}
jwtManager := auth.NewJWTManager(cfg.JWTSecret)
var userRepo *auth.UserRepository
if deps.DB != nil {
userRepo = auth.NewUserRepository(deps.DB)
@@ -134,7 +134,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminDashboardHandler := admindashboard.NewHandler(adminDashboardService)
var adminFinanceRepo *adminfinance.Repository
if deps.DB != nil {
adminFinanceRepo = adminfinance.NewRepository(deps.DB)
adminFinanceRepo = adminfinance.NewRepository(deps.DB, deps.Redis)
}
adminFinanceService := adminfinance.NewService(adminFinanceRepo)
adminFinanceHandler := adminfinance.NewHandler(adminFinanceService)
@@ -173,10 +173,11 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
var chatHub *chathub.Hub
if deps.DB != nil {
chatHub = chathub.NewHub(deps.DB)
adminUserService.SetSessionRevoker(chatHub.DisconnectUser)
}
var chatRepo *chat.Repository
if deps.DB != nil {
chatRepo = chat.NewRepository(deps.DB, chatHub, deps.Redis)
chatRepo = chat.NewRepository(deps.DB, chatHub, deps.Redis, chat.WithLogger(logger))
}
// 创建 chat 适配器用于 listing
@@ -284,7 +285,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
chatHandler := chat.NewHandler(chatService)
var chatHubHandler *chathub.Handler
if chatHub != nil {
chatHubHandler = chathub.NewHandler(chatHub)
chatHubHandler = chathub.NewHandler(chatHub, logger)
}
var disputeRepo *dispute.Repository
if deps.DB != nil {
@@ -315,6 +316,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
}
distributionService := distribution.NewService(distributionRepo)
distributionHandler := distribution.NewHandler(distributionService)
backupMonitorHandler := backupmonitor.NewHandler(cfg.BackupStatusFile)
var adminRoleRepo *adminrole.Repository
if deps.DB != nil {
adminRoleRepo = adminrole.NewRepository(deps.DB, deps.Redis)
@@ -374,12 +376,12 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
}
announcementService := announcement.NewService(announcementRepo)
announcementHandler := announcement.NewHandler(announcementService)
var validateUserToken middleware.UserTokenValidatorFunc
if userRepo != nil {
validateUserToken = func(ctx context.Context, userID uint64, tokenVersion int64) error {
_, err := userRepo.FindActiveForToken(ctx, userID, tokenVersion)
return err
validateUserToken := func(ctx context.Context, userID uint64, tokenVersion int64) error {
if userRepo == nil {
return auth.ErrDependencyUnavailable
}
_, err := userRepo.FindActiveForToken(ctx, userID, tokenVersion)
return err
}
requireAuth := middleware.Auth(jwtManager, validateUserToken)
var validateAdminToken middleware.AdminTokenValidatorFunc
@@ -403,7 +405,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
api := engine.Group("/api")
{
api.GET("/health", health.Check)
api.GET("/health", health.Ready)
api.GET("/listing-publish-options", systemConfigHandler.PublishOptions)
api.GET("/listing-publish-agreements", systemConfigHandler.ListingPublishAgreements)
api.GET("/listing-sale-price-config", systemConfigHandler.SalePriceConfig)
@@ -431,6 +433,9 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
authRoutes.POST("/password/reset", authHandler.ResetPassword)
authRoutes.POST("/refresh", authHandler.Refresh)
authRoutes.POST("/logout", requireAuth, authHandler.Logout)
if chatHubHandler != nil {
authRoutes.GET("/events", requireAuth, chatHubHandler.UserEvents)
}
}
api.GET("/me", requireAuth, userHandler.Me)
@@ -627,6 +632,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
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.GET("/orders/:id/process-events", requirePerm("order:view"), orderHandler.AdminProcessEvents)
adminRoutes.POST("/orders/:id/close", requirePerm("order:close"), orderHandler.AdminClose)
adminRoutes.POST("/orders/:id/seal", requirePerm("order:close"), orderHandler.AdminSeal)
adminRoutes.POST("/orders/:id/mark-abnormal", requirePerm("order:mark_abnormal"), orderHandler.AdminMarkAbnormal)
@@ -644,6 +650,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminRoutes.POST("/orders/:id/deposit-hold", requirePerm("order:deposit_hold"), orderHandler.AdminHoldDeposit)
adminRoutes.POST("/orders/:id/deposit-release", requirePerm("order:deposit_hold"), orderHandler.AdminReleaseDeposit)
adminRoutes.GET("/orders/refund-pending", requirePerm("order:close"), orderHandler.ListPendingRefund)
adminRoutes.GET("/orders/refund-pending-count", requirePerm("order:close"), orderHandler.PendingRefundCount)
adminRoutes.POST("/orders/:id/dispute", requirePerm("dispute:arbitrate"), disputeHandler.AdminCreateByOrder)
// 管理员线下提号(独立于正常订单流程)
@@ -652,6 +659,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminRoutes.GET("/pickups/shop-options", requirePerm("order:pickup"), pickupHandler.ShopOptions)
adminRoutes.GET("/pickups/available-listings", requirePerm("order:pickup"), pickupHandler.AvailableListings)
adminRoutes.GET("/pickups/:id/financial-adjustments", requirePerm("order:pickup"), pickupHandler.ListFinancialAdjustments)
adminRoutes.GET("/pickups/:id/process-events", requirePerm("order:pickup"), pickupHandler.ProcessEvents)
adminRoutes.POST("/pickups/:id/financial-adjustments", requirePerm("order:pickup"), pickupHandler.CreateFinancialAdjustment)
adminRoutes.GET("/pickups/:id", requirePerm("order:pickup"), pickupHandler.Detail)
adminRoutes.POST("/pickups/:id/complete", requirePerm("order:pickup"), pickupHandler.Complete)
@@ -671,31 +679,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminRoutes.POST("/listings/:id/offline", requirePerm("listing:offline"), listingHandler.AdminOffline)
adminRoutes.POST("/listings/:id/mark-abnormal", requirePerm("listing:offline"), listingHandler.AdminMarkAbnormal)
// 撞车后台(/admin/crash;兼容 /admin/mohong)
if mohongHandler != nil {
// 商品编辑需要拉分类列表,故 GET 用 product_view
registerCrashAdmin := func(prefix string) {
adminRoutes.GET(prefix+"/categories", requirePerm("mohong:product_view"), mohongHandler.AdminListCategories)
adminRoutes.POST(prefix+"/categories", requirePerm("mohong:category"), mohongHandler.AdminCreateCategory)
adminRoutes.PUT(prefix+"/categories/:id", requirePerm("mohong:category"), mohongHandler.AdminUpdateCategory)
adminRoutes.DELETE(prefix+"/categories/:id", requirePerm("mohong:category"), mohongHandler.AdminDeleteCategory)
adminRoutes.GET(prefix+"/products", requirePerm("mohong:product_view"), mohongHandler.AdminListProducts)
adminRoutes.GET(prefix+"/products/:id", requirePerm("mohong:product_view"), mohongHandler.AdminGetProduct)
adminRoutes.POST(prefix+"/products", requirePerm("mohong:product_manage"), mohongHandler.AdminCreateProduct)
adminRoutes.PUT(prefix+"/products/:id", requirePerm("mohong:product_manage"), mohongHandler.AdminUpdateProduct)
adminRoutes.DELETE(prefix+"/products/:id", requirePerm("mohong:product_manage"), mohongHandler.AdminDeleteProduct)
adminRoutes.GET(prefix+"/orders", requirePerm("mohong:order_view"), mohongHandler.AdminListOrders)
adminRoutes.GET(prefix+"/orders/:id", requirePerm("mohong:order_view"), mohongHandler.AdminGetOrder)
adminRoutes.POST(prefix+"/orders/:id/receive", requirePerm("mohong:order_manage"), mohongHandler.AdminReceiveOrder)
adminRoutes.POST(prefix+"/orders/:id/complete", requirePerm("mohong:order_manage"), mohongHandler.AdminCompleteOrder)
adminRoutes.POST(prefix+"/orders/:id/cancel", requirePerm("mohong:order_manage"), mohongHandler.AdminCancelOrder)
adminRoutes.POST(prefix+"/orders/:id/refund", requirePerm("mohong:order_manage"), paymentHandler.AdminRefundMohong)
adminRoutes.GET(prefix+"/config", requirePerm("mohong:config"), mohongHandler.AdminGetConfig)
adminRoutes.PUT(prefix+"/config", requirePerm("mohong:config"), mohongHandler.AdminUpdateConfig)
}
registerCrashAdmin("/crash")
registerCrashAdmin("/mohong")
}
registerCrashAdminRoutes(adminRoutes, requirePerm, mohongHandler, paymentHandler)
adminRoutes.GET("/disputes", requirePerm("dispute:view"), disputeHandler.AdminList)
adminRoutes.POST("/disputes/:id/arbitrate", requirePerm("dispute:arbitrate"), disputeHandler.AdminArbitrate)
@@ -704,6 +688,9 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminRoutes.GET("/finance/disbursements", requirePerm("wallet:view"), adminFinanceHandler.Disbursements)
adminRoutes.POST("/finance/manual-disbursements", requirePerm("finance:manual_disbursement"), adminFinanceHandler.CreateManualDisbursement)
adminRoutes.POST("/finance/manual-disbursements/:id/void", requirePerm("finance:manual_disbursement"), adminFinanceHandler.VoidManualDisbursement)
adminRoutes.GET("/finance/operating-expenses", requirePerm("finance:operating_expense"), adminFinanceHandler.OperatingExpenses)
adminRoutes.POST("/finance/operating-expenses", requirePerm("finance:operating_expense"), adminFinanceHandler.CreateOperatingExpense)
adminRoutes.POST("/finance/operating-expenses/:id/void", requirePerm("finance:operating_expense"), adminFinanceHandler.VoidOperatingExpense)
adminRoutes.GET("/wallet/ledger", requirePerm("wallet:view"), walletHandler.AdminLedger)
adminRoutes.GET("/payments", requirePerm("wallet:view"), paymentHandler.AdminList)
@@ -728,6 +715,9 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminRoutes.GET("/system-configs", requirePerm("system_config:view"), systemConfigHandler.List)
adminRoutes.PUT("/system-configs/:key", requirePerm("system_config:update"), systemConfigHandler.Update)
adminRoutes.GET("/backups/status", requirePerm("backup:view"), backupMonitorHandler.Status)
adminRoutes.GET("/backups/schedule", requirePerm("backup:view"), systemConfigHandler.BackupSchedule)
adminRoutes.PUT("/backups/schedule", requirePerm("backup:schedule:update"), systemConfigHandler.UpdateBackupSchedule)
adminRoutes.GET("/notifications", requirePerm("notification:view"), adminNotificationHandler.List)
adminRoutes.GET("/notifications/unread-count", requirePerm("notification:view"), adminNotificationHandler.UnreadCount)
adminRoutes.PUT("/notifications/read-all", requirePerm("notification:view"), adminNotificationHandler.MarkAllRead)
@@ -744,6 +734,8 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminRoutes.GET("/chats/events", requirePerm("chat:view"), chatHubHandler.AdminEvents)
}
adminRoutes.GET("/chats", requirePerm("chat:view"), chatHandler.AdminList)
adminRoutes.GET("/chats/counts", requirePerm("chat:view"), chatHandler.AdminCounts)
adminRoutes.POST("/chats/read-all", requirePerm("chat:view"), chatHandler.AdminMarkAllRead)
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)
@@ -760,95 +752,12 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
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("qrcode:manage"), chatHandler.CreateQrCodeHandler)
adminRoutes.POST("/chats/qrcodes/batch", requirePerm("qrcode:manage"), chatHandler.BatchCreateQrCodeHandler)
adminRoutes.POST("/chats/qrcodes/batch-delete", requirePerm("qrcode:manage"), chatHandler.BatchDeleteQrCodeHandler)
adminRoutes.GET("/chats/qrcodes", requirePerm("qrcode:view"), chatHandler.ListQrCodesHandler)
adminRoutes.GET("/chats/qrcodes/stats", requirePerm("qrcode:view"), chatHandler.GetQrCodeStatsHandler)
adminRoutes.POST("/chats/qrcodes/ocr-group-name", requirePerm("qrcode:manage"), chatHandler.RecognizeQrCodeGroupNameHandler)
adminRoutes.POST("/chats/qrcodes/ocr-group-name/async", requirePerm("qrcode:manage"), chatHandler.SubmitOCRJobHandler)
adminRoutes.GET("/chats/qrcodes/ocr-group-name/async/:jobId", requirePerm("qrcode:manage"), chatHandler.GetOCRJobResultHandler)
adminRoutes.PATCH("/chats/qrcodes/:id", requirePerm("qrcode:manage"), chatHandler.UpdateQrCodeHandler)
adminRoutes.DELETE("/chats/qrcodes/:id", requirePerm("qrcode: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)
registerAdminManagementRoutes(adminRoutes, requirePerm, adminManagementHandlers{
chat: chatHandler, supportGroup: supportGroupHandler, role: adminRoleHandler,
admin: adminMgrHandler, announcement: announcementHandler,
})
}
}
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("阿里云短信服务初始化失败", 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("实名认证服务初始化失败", zap.Error(err))
return realname.NewUnavailableProvider(err)
}
return provider
default:
return realname.NewMockProvider(encryptor)
}
}
+68
View File
@@ -0,0 +1,68 @@
package router
import (
"net/http"
"net/http/httptest"
"testing"
"hfb_sys/backend/internal/config"
"go.uber.org/zap"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func testConfig() config.Config {
return config.Config{AppEnv: "test", JWTSecret: "test-secret"}
}
func TestHealthRoutesSeparateLivenessAndReadiness(t *testing.T) {
engine := New(testConfig(), Dependencies{}, zap.NewNop())
for _, tt := range []struct {
path string
wantStatus int
}{
{path: "/health", wantStatus: http.StatusOK},
{path: "/health/ready", wantStatus: http.StatusServiceUnavailable},
{path: "/api/health", wantStatus: http.StatusServiceUnavailable},
} {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, tt.path, nil)
engine.ServeHTTP(recorder, request)
if recorder.Code != tt.wantStatus {
t.Fatalf("GET %s status = %d, want %d; body=%s", tt.path, recorder.Code, tt.wantStatus, recorder.Body.String())
}
}
}
func TestExtractedAdminRoutesRemainRegisteredWithoutDuplicates(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:router-routes?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
engine := New(testConfig(), Dependencies{DB: db}, zap.NewNop())
routes := engine.Routes()
registered := make(map[string]struct{}, len(routes))
for _, route := range routes {
key := route.Method + " " + route.Path
if _, exists := registered[key]; exists {
t.Fatalf("duplicate route: %s", key)
}
registered[key] = struct{}{}
}
for _, key := range []string{
"GET /api/admin/crash/products",
"GET /api/admin/mohong/products",
"POST /api/admin/chats/qrcodes/batch",
"PUT /api/admin/chat-support-groups/:id/members",
"PUT /api/admin/roles/:id/permissions",
"PUT /api/admin/admin-users/:id/password",
"POST /api/admin/announcements/:id/publish",
} {
if _, exists := registered[key]; !exists {
t.Errorf("missing route after extraction: %s", key)
}
}
}