完善本地质量门禁与资金链路测试
This commit is contained in:
@@ -170,13 +170,19 @@ func TestArbitrationReleaseDepositDoesNotRepublishListing(t *testing.T) {
|
||||
t.Fatalf("读取仲裁后订单模型失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "仲裁退款状态", orderRow.RefundStatus, "refunded")
|
||||
assertEqual(t, "仲裁退款金额", orderRow.RefundAmountCent, int64(15000))
|
||||
assertEqual(t, "仲裁退款金额", orderRow.RefundAmountCent, int64(25800))
|
||||
|
||||
walletAccount, err := services.wallet.Account(t.Context(), owner.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("读取号主钱包失败: %v", err)
|
||||
}
|
||||
assertEqual(t, "仲裁后号主可用余额", walletAccount.AvailableBalanceCent, int64(35300))
|
||||
assertEqual(t, "仲裁后号主可用余额", walletAccount.AvailableBalanceCent, int64(24500))
|
||||
assertEqual(
|
||||
t,
|
||||
"仲裁资金守恒",
|
||||
orderRow.RefundAmountCent+walletAccount.AvailableBalanceCent+3000,
|
||||
orderRow.RentAmountCent+orderRow.DepositAmountCent,
|
||||
)
|
||||
|
||||
var listingRow model.RentalListing
|
||||
if err := db.First(&listingRow, approved.ID).Error; err != nil {
|
||||
@@ -432,6 +438,10 @@ func createListingUnderReview(t *testing.T, service *listing.Service, ownerID ui
|
||||
ScreenshotURLS: []string{"https://example.com/e2e-account.png"},
|
||||
AssetSummary: map[string]any{
|
||||
"fire_level": 50,
|
||||
"online_time": map[string]any{
|
||||
"start": "09:00",
|
||||
"end": "23:00",
|
||||
},
|
||||
"price_breakdown": map[string]any{
|
||||
"seller_coin_base_price": 233,
|
||||
"seller_total_price": 353,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -9,16 +11,55 @@ import (
|
||||
|
||||
type HealthHandler struct {
|
||||
startedAt time.Time
|
||||
checks map[string]ReadinessCheck
|
||||
}
|
||||
|
||||
func NewHealthHandler() *HealthHandler {
|
||||
return &HealthHandler{startedAt: time.Now()}
|
||||
type ReadinessCheck func(context.Context) error
|
||||
|
||||
func NewHealthHandler(checks map[string]ReadinessCheck) *HealthHandler {
|
||||
return &HealthHandler{startedAt: time.Now(), checks: checks}
|
||||
}
|
||||
|
||||
func (h *HealthHandler) Check(c *gin.Context) {
|
||||
func (h *HealthHandler) Live(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"service": "hfb-api",
|
||||
"started_at": h.startedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *HealthHandler) Ready(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
names := make([]string, 0, len(h.checks))
|
||||
for name := range h.checks {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
ready := true
|
||||
dependencies := make(map[string]string, len(names))
|
||||
for _, name := range names {
|
||||
check := h.checks[name]
|
||||
if check == nil || check(ctx) != nil {
|
||||
dependencies[name] = "unavailable"
|
||||
ready = false
|
||||
continue
|
||||
}
|
||||
dependencies[name] = "ok"
|
||||
}
|
||||
|
||||
statusCode := http.StatusOK
|
||||
status := "ok"
|
||||
if !ready {
|
||||
statusCode = http.StatusServiceUnavailable
|
||||
status = "unavailable"
|
||||
}
|
||||
c.JSON(statusCode, gin.H{
|
||||
"status": status,
|
||||
"service": "hfb-api",
|
||||
"started_at": h.startedAt.Format(time.RFC3339),
|
||||
"dependencies": dependencies,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestHealthLiveDoesNotDependOnReadiness(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
h := NewHealthHandler(map[string]ReadinessCheck{
|
||||
"mysql": func(context.Context) error { return errors.New("down") },
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
|
||||
h.Live(ctx)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthReadyReportsDependencyState(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
tests := []struct {
|
||||
name string
|
||||
checks map[string]ReadinessCheck
|
||||
wantStatus int
|
||||
wantBody string
|
||||
}{
|
||||
{
|
||||
name: "all dependencies ready",
|
||||
checks: map[string]ReadinessCheck{
|
||||
"mysql": func(context.Context) error { return nil },
|
||||
"redis": func(context.Context) error { return nil },
|
||||
},
|
||||
wantStatus: http.StatusOK,
|
||||
wantBody: `"status":"ok"`,
|
||||
},
|
||||
{
|
||||
name: "dependency unavailable",
|
||||
checks: map[string]ReadinessCheck{
|
||||
"mysql": func(context.Context) error { return errors.New("down") },
|
||||
"redis": func(context.Context) error { return nil },
|
||||
},
|
||||
wantStatus: http.StatusServiceUnavailable,
|
||||
wantBody: `"mysql":"unavailable"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
h := NewHealthHandler(tt.checks)
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(recorder)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, "/health/ready", nil)
|
||||
|
||||
h.Ready(ctx)
|
||||
|
||||
if recorder.Code != tt.wantStatus {
|
||||
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, tt.wantStatus, recorder.Body.String())
|
||||
}
|
||||
if body := recorder.Body.String(); !strings.Contains(body, tt.wantBody) {
|
||||
t.Fatalf("body = %s, want substring %s", body, tt.wantBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -161,6 +161,51 @@ func TestResetRetryClearsRetryFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncRefundPaymentsMarksFailedAttemptForRetry(t *testing.T) {
|
||||
db := setupRefundRetryTestDB(t)
|
||||
repo := payment.NewRepository(db, nil, nil)
|
||||
job := New(db, nil, zap.NewNop(), repo)
|
||||
now := time.Date(2026, 9, 2, 12, 0, 0, 0, time.UTC)
|
||||
row := model.PaymentOrder{
|
||||
PaymentNo: "PAY-RETRY-FAIL",
|
||||
OrderID: 1,
|
||||
OrderNo: "ORD-RETRY-FAIL",
|
||||
UserID: 2,
|
||||
Provider: "lakala",
|
||||
MerchantID: "merchant-missing-config",
|
||||
ThirdOrderID: "REF-RETRY-FAIL",
|
||||
AmountCent: 1000,
|
||||
BizType: "admin_refund",
|
||||
Status: "refunding",
|
||||
UpdatedAt: now.Add(-10 * time.Minute),
|
||||
RetryCount: 1,
|
||||
}
|
||||
if err := db.Create(&row).Error; err != nil {
|
||||
t.Fatalf("create refund payment: %v", err)
|
||||
}
|
||||
|
||||
processed, err := job.syncRefundPayments(t.Context(), now)
|
||||
if err != nil {
|
||||
t.Fatalf("syncRefundPayments() error = %v", err)
|
||||
}
|
||||
if processed != 0 {
|
||||
t.Fatalf("processed = %d, want 0 when channel config is unavailable", processed)
|
||||
}
|
||||
var latest model.PaymentOrder
|
||||
if err := db.First(&latest, row.ID).Error; err != nil {
|
||||
t.Fatalf("find refund payment: %v", err)
|
||||
}
|
||||
if latest.RetryCount != 2 {
|
||||
t.Fatalf("retry_count = %d, want 2", latest.RetryCount)
|
||||
}
|
||||
if latest.LastRetryAt == nil || !latest.LastRetryAt.Equal(now) {
|
||||
t.Fatalf("last_retry_at = %v, want %v", latest.LastRetryAt, now)
|
||||
}
|
||||
if latest.NextRetryAt == nil || !latest.NextRetryAt.After(now) {
|
||||
t.Fatalf("next_retry_at = %v, want a future retry time", latest.NextRetryAt)
|
||||
}
|
||||
}
|
||||
|
||||
// createArbitrationOrphanFixture 构造一个仲裁孤儿订单:已支付原单 + 仲裁已结案但退款单缺失。
|
||||
func createArbitrationOrphanFixture(t *testing.T, db *gorm.DB, suffix string, settlementStatus string) model.RentalOrder {
|
||||
t.Helper()
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -694,6 +696,75 @@ func TestConfirmPaidRollsBackOrderWhenPaymentUpdateFails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentPaidNotificationsAreIdempotent(t *testing.T) {
|
||||
db := database.NewTestDBWithName("payment-concurrent")
|
||||
if err := database.MigrateRentalTransactionTestSchema(db); err != nil {
|
||||
t.Fatalf("数据库迁移失败: %v", err)
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
t.Fatalf("get sqlite db: %v", err)
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(8)
|
||||
orderRepo := ordermodule.NewRepository(db)
|
||||
repo := NewRepository(db, nil, orderRepo)
|
||||
fixture := createPayableOrderFixture(t, db, "CONCURRENT", true)
|
||||
params := map[string]string{
|
||||
"third_order_id": fixture.Payment.ThirdOrderID,
|
||||
"status": "paid",
|
||||
"amount": "1000",
|
||||
"provider_order_id": "PROVIDER-CONCURRENT",
|
||||
}
|
||||
|
||||
const callers = 8
|
||||
results := make(chan error, callers)
|
||||
var group sync.WaitGroup
|
||||
for i := 0; i < callers; i++ {
|
||||
group.Add(1)
|
||||
go func() {
|
||||
defer group.Done()
|
||||
var callErr error
|
||||
for attempt := 0; attempt < 50; attempt++ {
|
||||
_, callErr = repo.HandleNotify(t.Context(), "mock", params, "status=paid", "application/x-www-form-urlencoded", "")
|
||||
if callErr == nil || !strings.Contains(callErr.Error(), "locked") {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
results <- callErr
|
||||
}()
|
||||
}
|
||||
group.Wait()
|
||||
close(results)
|
||||
for callErr := range results {
|
||||
if callErr != nil {
|
||||
t.Fatalf("concurrent HandleNotify() error = %v", callErr)
|
||||
}
|
||||
}
|
||||
|
||||
var latestPayment model.PaymentOrder
|
||||
if err := db.First(&latestPayment, fixture.Payment.ID).Error; err != nil {
|
||||
t.Fatalf("find payment: %v", err)
|
||||
}
|
||||
if latestPayment.Status != "paid" {
|
||||
t.Fatalf("payment status = %q, want paid", latestPayment.Status)
|
||||
}
|
||||
var latestOrder model.RentalOrder
|
||||
if err := db.First(&latestOrder, fixture.Order.ID).Error; err != nil {
|
||||
t.Fatalf("find order: %v", err)
|
||||
}
|
||||
if latestOrder.Status != "pending_handoff" || latestOrder.HandoffStatus != "pending_owner" {
|
||||
t.Fatalf("order status = %q/%q, want pending_handoff/pending_owner", latestOrder.Status, latestOrder.HandoffStatus)
|
||||
}
|
||||
var paidCount int64
|
||||
if err := db.Model(&model.PaymentOrder{}).Where("id = ? AND status = 'paid'", fixture.Payment.ID).Count(&paidCount).Error; err != nil {
|
||||
t.Fatalf("count paid payment: %v", err)
|
||||
}
|
||||
if paidCount != 1 {
|
||||
t.Fatalf("paid payment rows = %d, want 1", paidCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestQueryClosedPaymentReturnsLocalStatus 验证关闭状态不再依赖渠道配置或继续轮询渠道。
|
||||
func TestQueryClosedPaymentReturnsLocalStatus(t *testing.T) {
|
||||
db := setupPaymentTestDB(t)
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package withdrawal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/wallet"
|
||||
"hfb_sys/backend/pkg/crypto"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type withdrawalFixture struct {
|
||||
db *gorm.DB
|
||||
repo *Repository
|
||||
userID uint64
|
||||
adminID uint64
|
||||
accountID uint64
|
||||
}
|
||||
|
||||
func newWithdrawalFixture(t *testing.T) withdrawalFixture {
|
||||
t.Helper()
|
||||
dsn := fmt.Sprintf("file:withdrawal-%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(
|
||||
&model.User{},
|
||||
&model.AdminUser{},
|
||||
&model.UserPaymentAccount{},
|
||||
&model.WithdrawalRequest{},
|
||||
&model.WalletAccount{},
|
||||
&model.WalletLedger{},
|
||||
); err != nil {
|
||||
t.Fatalf("migrate fixture: %v", err)
|
||||
}
|
||||
|
||||
user := model.User{Phone: "13800000001", Nickname: "test-user", Status: "active"}
|
||||
admin := model.AdminUser{Username: "reviewer", PasswordHash: "test", Nickname: "reviewer", Status: "active"}
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
t.Fatalf("create user: %v", err)
|
||||
}
|
||||
if err := db.Create(&admin).Error; err != nil {
|
||||
t.Fatalf("create admin: %v", err)
|
||||
}
|
||||
account := model.UserPaymentAccount{
|
||||
UserID: user.ID, AccountType: "bank", AccountName: "Test User",
|
||||
AccountNo: "6222021234567890", BankName: "Test Bank", Status: "active",
|
||||
}
|
||||
if err := db.Create(&account).Error; err != nil {
|
||||
t.Fatalf("create payment account: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.WalletAccount{
|
||||
UserID: user.ID, AvailableBalanceCent: 100_000, Status: "active",
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create wallet: %v", err)
|
||||
}
|
||||
|
||||
return withdrawalFixture{
|
||||
db: db, repo: NewRepository(db, wallet.NewRepository(db), &crypto.MockEncryptor{}),
|
||||
userID: user.ID, adminID: admin.ID, accountID: account.ID,
|
||||
}
|
||||
}
|
||||
|
||||
func (f withdrawalFixture) create(t *testing.T, amountCent int64) *WithdrawalDTO {
|
||||
t.Helper()
|
||||
dto, err := f.repo.Create(context.Background(), f.userID, CreateWithdrawalRequest{
|
||||
PaymentAccountID: f.accountID,
|
||||
AmountCent: amountCent,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
return dto
|
||||
}
|
||||
|
||||
func (f withdrawalFixture) assertBalance(t *testing.T, available, frozen int64) {
|
||||
t.Helper()
|
||||
var account model.WalletAccount
|
||||
if err := f.db.Where("user_id = ?", f.userID).First(&account).Error; err != nil {
|
||||
t.Fatalf("load wallet: %v", err)
|
||||
}
|
||||
if account.AvailableBalanceCent != available || account.FrozenBalanceCent != frozen {
|
||||
t.Fatalf("balance = available:%d frozen:%d, want available:%d frozen:%d",
|
||||
account.AvailableBalanceCent, account.FrozenBalanceCent, available, frozen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithdrawalCreateAndCancelPreservesBalance(t *testing.T) {
|
||||
f := newWithdrawalFixture(t)
|
||||
dto := f.create(t, 20_000)
|
||||
if dto.Status != "pending" || dto.FeeCent != 0 || dto.ActualAmountCent != 20_000 {
|
||||
t.Fatalf("created withdrawal = %+v", dto)
|
||||
}
|
||||
f.assertBalance(t, 80_000, 20_000)
|
||||
|
||||
if err := f.repo.Cancel(context.Background(), f.userID, dto.ID); err != nil {
|
||||
t.Fatalf("Cancel: %v", err)
|
||||
}
|
||||
f.assertBalance(t, 100_000, 0)
|
||||
if err := f.repo.Cancel(context.Background(), f.userID, dto.ID); !errors.Is(err, ErrWithdrawalLocked) {
|
||||
t.Fatalf("second Cancel error = %v, want ErrWithdrawalLocked", err)
|
||||
}
|
||||
f.assertBalance(t, 100_000, 0)
|
||||
}
|
||||
|
||||
func TestWithdrawalRejectedReturnsFrozenBalanceOnce(t *testing.T) {
|
||||
f := newWithdrawalFixture(t)
|
||||
dto := f.create(t, 30_000)
|
||||
approved := false
|
||||
result, err := f.repo.Review(context.Background(), f.adminID, dto.ID, ReviewWithdrawalRequest{
|
||||
Approved: &approved,
|
||||
Remark: "account mismatch",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Review rejected: %v", err)
|
||||
}
|
||||
if result.Status != "rejected" || result.ReviewedByName != "reviewer" {
|
||||
t.Fatalf("review result = %+v", result)
|
||||
}
|
||||
f.assertBalance(t, 100_000, 0)
|
||||
if _, err := f.repo.Review(context.Background(), f.adminID, dto.ID, ReviewWithdrawalRequest{Approved: &approved}); !errors.Is(err, ErrWithdrawalLocked) {
|
||||
t.Fatalf("second Review error = %v, want ErrWithdrawalLocked", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithdrawalApprovedAndPaidConsumesFrozenBalanceOnce(t *testing.T) {
|
||||
f := newWithdrawalFixture(t)
|
||||
dto := f.create(t, 40_000)
|
||||
approved := true
|
||||
if _, err := f.repo.Review(context.Background(), f.adminID, dto.ID, ReviewWithdrawalRequest{Approved: &approved}); err != nil {
|
||||
t.Fatalf("Review approved: %v", err)
|
||||
}
|
||||
f.assertBalance(t, 60_000, 40_000)
|
||||
|
||||
result, err := f.repo.ConfirmPayment(context.Background(), f.adminID, dto.ID, ConfirmPaymentRequest{
|
||||
PaymentProofURL: "proof/test.png",
|
||||
Remark: "paid",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ConfirmPayment: %v", err)
|
||||
}
|
||||
if result.Status != "completed" || result.PaidByName != "reviewer" {
|
||||
t.Fatalf("payment result = %+v", result)
|
||||
}
|
||||
f.assertBalance(t, 60_000, 0)
|
||||
if _, err := f.repo.ConfirmPayment(context.Background(), f.adminID, dto.ID, ConfirmPaymentRequest{}); !errors.Is(err, ErrWithdrawalLocked) {
|
||||
t.Fatalf("second ConfirmPayment error = %v, want ErrWithdrawalLocked", err)
|
||||
}
|
||||
f.assertBalance(t, 60_000, 0)
|
||||
}
|
||||
|
||||
func TestWithdrawalCreateRollsBackWhenBalanceIsInsufficient(t *testing.T) {
|
||||
f := newWithdrawalFixture(t)
|
||||
_, err := f.repo.Create(context.Background(), f.userID, CreateWithdrawalRequest{
|
||||
PaymentAccountID: f.accountID,
|
||||
AmountCent: 100_001,
|
||||
})
|
||||
if !errors.Is(err, wallet.ErrInsufficientBalance) {
|
||||
t.Fatalf("Create error = %v, want ErrInsufficientBalance", err)
|
||||
}
|
||||
var count int64
|
||||
if err := f.db.Model(&model.WithdrawalRequest{}).Count(&count).Error; err != nil {
|
||||
t.Fatalf("count withdrawals: %v", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatalf("withdrawal count = %d, want 0", count)
|
||||
}
|
||||
f.assertBalance(t, 100_000, 0)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -62,8 +60,9 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
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) {
|
||||
@@ -399,7 +398,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)
|
||||
@@ -653,31 +652,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)
|
||||
@@ -750,95 +725,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user