From 90b83972ec05ca82e9a6733875d03ca9928e0833 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 4 Nov 2026 20:12:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(benefits):=20=E5=A2=9E=E5=8A=A0=E6=9D=83?= =?UTF-8?q?=E7=9B=8A=E4=B8=AD=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/pointsmall/dto.go | 23 +- .../internal/modules/pointsmall/repository.go | 13 + .../modules/pointsmall/repository_test.go | 30 + .../benefits/views/BenefitsCenterView.vue | 994 ++++++++++++++++++ .../src/features/points/api/pointsMall.ts | 11 + frontend/src/layouts/AppLayout.css | 28 + frontend/src/layouts/AppLayout.vue | 10 + frontend/src/router/accountRoutes.ts | 7 + frontend/src/router/mobileRoutes.ts | 7 + 9 files changed, 1117 insertions(+), 6 deletions(-) create mode 100644 frontend/src/features/benefits/views/BenefitsCenterView.vue diff --git a/backend/internal/modules/pointsmall/dto.go b/backend/internal/modules/pointsmall/dto.go index 7711ae5..b9edab4 100644 --- a/backend/internal/modules/pointsmall/dto.go +++ b/backend/internal/modules/pointsmall/dto.go @@ -17,12 +17,23 @@ var ( ) type SummaryDTO struct { - AvailablePoints int64 `json:"available_points"` - LifetimePoints int64 `json:"lifetime_points"` - LevelCode string `json:"level_code"` - LevelName string `json:"level_name"` - PointsPerYuan int64 `json:"points_per_yuan"` - PointsMultiplierBps int `json:"points_multiplier_bps"` + AvailablePoints int64 `json:"available_points"` + LifetimePoints int64 `json:"lifetime_points"` + LevelCode string `json:"level_code"` + LevelName string `json:"level_name"` + PointsPerYuan int64 `json:"points_per_yuan"` + PointsMultiplierBps int `json:"points_multiplier_bps"` + GrowthEnabled bool `json:"growth_enabled"` + Levels []GrowthLevelDTO `json:"levels"` +} + +type GrowthLevelDTO struct { + Code string `json:"code"` + Name string `json:"name"` + MinPoints int64 `json:"min_points"` + PointsMultiplierBps int `json:"points_multiplier_bps"` + DiscountBps int `json:"discount_bps"` + DepositFreeQuotaCent int64 `json:"deposit_free_quota_cent"` } type ProductDTO struct { diff --git a/backend/internal/modules/pointsmall/repository.go b/backend/internal/modules/pointsmall/repository.go index 6f577c1..1b02558 100644 --- a/backend/internal/modules/pointsmall/repository.go +++ b/backend/internal/modules/pointsmall/repository.go @@ -35,6 +35,17 @@ func (r *Repository) Summary(ctx context.Context, userID uint64) (*SummaryDTO, e return nil, err } level := rentergrowth.LevelForPoints(cfg, user.RenterGrowthPoints) + levels := make([]GrowthLevelDTO, 0, len(cfg.Levels)) + for _, item := range cfg.Levels { + levels = append(levels, GrowthLevelDTO{ + Code: item.Code, + Name: item.Name, + MinPoints: item.MinPoints, + PointsMultiplierBps: item.PointsMultiplierBps, + DiscountBps: item.DiscountBps, + DepositFreeQuotaCent: item.DepositFreeQuotaCent, + }) + } return &SummaryDTO{ AvailablePoints: user.PointsAvailable, LifetimePoints: user.RenterGrowthPoints, @@ -42,6 +53,8 @@ func (r *Repository) Summary(ctx context.Context, userID uint64) (*SummaryDTO, e LevelName: level.Name, PointsPerYuan: cfg.PointsPerYuan, PointsMultiplierBps: level.PointsMultiplierBps, + GrowthEnabled: cfg.Enabled, + Levels: levels, }, nil } diff --git a/backend/internal/modules/pointsmall/repository_test.go b/backend/internal/modules/pointsmall/repository_test.go index bc0a062..48a23a7 100644 --- a/backend/internal/modules/pointsmall/repository_test.go +++ b/backend/internal/modules/pointsmall/repository_test.go @@ -76,3 +76,33 @@ func TestRedeemDeductsAvailablePointsAndStock(t *testing.T) { t.Fatalf("redemption status = %s, want completed", completed.Status) } } + +func TestSummaryIncludesGrowthLevelRules(t *testing.T) { + db := database.NewTestDB() + user := model.User{ + Phone: "13900009602", + RenterGrowthPoints: 1000, + PointsAvailable: 432, + } + if err := db.Create(&user).Error; err != nil { + t.Fatalf("create user failed: %v", err) + } + + summary, err := NewRepository(db).Summary(t.Context(), user.ID) + if err != nil { + t.Fatalf("summary failed: %v", err) + } + if !summary.GrowthEnabled || len(summary.Levels) != 4 { + t.Fatalf("growth config = enabled %v levels %d", summary.GrowthEnabled, len(summary.Levels)) + } + if summary.LevelCode != "diamond" || summary.PointsMultiplierBps != 15_000 { + t.Fatalf( + "current level = %s multiplier %d, want diamond/15000", + summary.LevelCode, + summary.PointsMultiplierBps, + ) + } + if summary.Levels[2].DiscountBps != 9800 || summary.Levels[2].DepositFreeQuotaCent != 100_000 { + t.Fatalf("diamond rules = %+v", summary.Levels[2]) + } +} diff --git a/frontend/src/features/benefits/views/BenefitsCenterView.vue b/frontend/src/features/benefits/views/BenefitsCenterView.vue new file mode 100644 index 0000000..1f7931f --- /dev/null +++ b/frontend/src/features/benefits/views/BenefitsCenterView.vue @@ -0,0 +1,994 @@ + + + + + diff --git a/frontend/src/features/points/api/pointsMall.ts b/frontend/src/features/points/api/pointsMall.ts index b7c9fa4..6207c1c 100644 --- a/frontend/src/features/points/api/pointsMall.ts +++ b/frontend/src/features/points/api/pointsMall.ts @@ -8,6 +8,17 @@ export interface PointsSummary { level_name: string points_per_yuan: number points_multiplier_bps: number + growth_enabled: boolean + levels: GrowthLevelRule[] +} + +export interface GrowthLevelRule { + code: string + name: string + min_points: number + points_multiplier_bps: number + discount_bps: number + deposit_free_quota_cent: number } export interface PointsLedgerItem { diff --git a/frontend/src/layouts/AppLayout.css b/frontend/src/layouts/AppLayout.css index 9967524..ac91361 100644 --- a/frontend/src/layouts/AppLayout.css +++ b/frontend/src/layouts/AppLayout.css @@ -101,6 +101,7 @@ .pc-nav-link { position: relative; display: inline-flex; + flex-shrink: 0; align-items: center; gap: 6px; padding: 8px 16px; @@ -109,6 +110,7 @@ font-size: 14px; font-weight: 700; text-decoration: none; + white-space: nowrap; transition: all 0.18s ease; } @@ -801,3 +803,29 @@ display: none; } } + +@media (max-width: 1500px) { + .pc-topbar { + gap: 14px; + padding: 0 20px; + } + + .pc-nav { + gap: 2px; + margin-left: 0; + } + + .pc-nav-link { + padding: 8px 10px; + font-size: 13px; + } + + .pc-nav-link .el-icon { + display: none; + } + + .pc-search { + min-width: 160px; + padding: 0 12px; + } +} diff --git a/frontend/src/layouts/AppLayout.vue b/frontend/src/layouts/AppLayout.vue index 8e272fe..b0cf6a8 100644 --- a/frontend/src/layouts/AppLayout.vue +++ b/frontend/src/layouts/AppLayout.vue @@ -11,6 +11,7 @@ import { DocumentChecked, EditPen, Finished, + GoldMedal, House, Search, Service, @@ -41,6 +42,7 @@ const navItems = [ { label: '消息', to: '/messages', icon: ChatDotRound }, { label: '钱包', to: '/wallet', icon: Wallet }, { label: '邀请返利', to: '/invitations', icon: Coin }, + { label: '权益中心', to: '/benefits', icon: GoldMedal }, { label: '公告', to: '/announcements', icon: Bell }, ] const buyerServiceItems = [ @@ -329,6 +331,14 @@ async function handleSupportClick() { 邀请返利 + + + 权益中心 + diff --git a/frontend/src/router/accountRoutes.ts b/frontend/src/router/accountRoutes.ts index a2f1f65..db893d8 100644 --- a/frontend/src/router/accountRoutes.ts +++ b/frontend/src/router/accountRoutes.ts @@ -19,6 +19,13 @@ export const accountRoutes: RouteRecordRaw[] = [ component: () => import('@/features/auth/views/ProfileView.vue'), meta: { requiresAuth: true }, }, + { + path: '/benefits', + name: 'benefits', + component: () => import('@/features/benefits/views/BenefitsCenterView.vue'), + meta: { requiresAuth: true }, + props: { mobile: false }, + }, { path: '/points-mall', name: 'points-mall', diff --git a/frontend/src/router/mobileRoutes.ts b/frontend/src/router/mobileRoutes.ts index 38fdc3d..6037f0d 100644 --- a/frontend/src/router/mobileRoutes.ts +++ b/frontend/src/router/mobileRoutes.ts @@ -89,6 +89,13 @@ export const mobileRoutes: RouteRecordRaw[] = [ component: () => import('@/features/invitation/views/UserInvitationView.vue'), meta: { layout: 'blank', requiresAuth: true }, }, + { + path: '/m/benefits', + name: 'mobile-benefits', + component: () => import('@/features/benefits/views/BenefitsCenterView.vue'), + meta: { layout: 'blank', requiresAuth: true }, + props: { mobile: true }, + }, { path: '/m/profile', name: 'mobile-profile',