Explorar o código

feat(membership): 会员体系迁移数据库 — 前端改读接口等级/名称/图标/levelOrder,移除硬编码映射

- benefits/index/upgrade/pay: 等级名、图标、levelOrder、推荐档改从 /api/membership/levels 或 /api/membership/plans 读取,删除前端维护的等级名/emoji/rank 映射
- index/upgrade: 权益对比表按接口 columns + rows.cells 动态渲染(列名/列高亮随 levelOrder)
- upgrade: 等级排序改由接口返回的 levelOrder 比较高低
- pay: 默认档位取推荐档或价格>0 的最低付费等级,移除写死的 FAMILY/家庭会员
- utils/api.js 对 getMembershipLevels/Plans/Benefits/Compare/Current 的全量封装已存在,路径与冻结契约一致(本次未改动)
Sisyphus Agent hai 1 semana
pai
achega
dbd94c59be

+ 23 - 6
cfc-frontend/pages/membership/benefits.vue

@@ -109,7 +109,7 @@
 </template>
 
 <script>
-import { getMyMembership, getMembershipBenefits } from '../../utils/api.js'
+import { getMyMembership, getMembershipBenefits, getMembershipLevels } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 import fontScaleMixin from '../../mixins/font-scale.js'
 
@@ -119,19 +119,20 @@ export default {
     return {
       subscription: null,
       activeTab: 'available',
-      benefits: []
+      benefits: [],
+      levelMap: {}
     }
   },
   computed: {
     levelIcon() {
       if (!this.subscription || !this.subscription.level) return '○'
-      var map = { BASIC: '🧡', FAMILY: '👑', PREMIUM: '💎', LIFETIME: '🌟' }
-      return map[this.subscription.level] || '○'
+      var item = this.levelMap && this.levelMap[this.subscription.level]
+      return (item && item.icon) ? item.icon : '○'
     },
     levelName() {
       if (!this.subscription || !this.subscription.level) return '未开通'
-      var map = { FREE: '免费用户', BASIC: '体验会员', FAMILY: '家庭会员', PREMIUM: '高级会员', LIFETIME: '终身会员' }
-      return map[this.subscription.level] || this.subscription.level
+      var item = this.levelMap && this.levelMap[this.subscription.level]
+      return (item && item.levelName) ? item.levelName : (this.subscription.level || '未开通')
     },
     availableBenefits() {
       return this.benefits.filter(function(b) { return !b.used })
@@ -155,7 +156,23 @@ export default {
   methods: {
     // 读取当前用户实际有效会员等级(getMyMembership 返回 memberLevel=个人与家庭身份取高者),
     // 免费用户展示体验会员权益(与会员中心 index.vue 一致)。
+    // 等级名/图标改从 /api/membership/levels 读取,前端不再维护任何等级名/emoji 映射。
     loadData() {
+      var self = this
+      getMembershipLevels().then(function(res) {
+        var list = (res && res.data) || []
+        var levelMap = {}
+        for (var i = 0; i < list.length; i++) {
+          levelMap[list[i].levelCode] = list[i]
+        }
+        self.levelMap = levelMap
+        self.loadMyMembership()
+      }).catch(function() {
+        self.levelMap = {}
+        self.loadMyMembership()
+      })
+    },
+    loadMyMembership() {
       var self = this
       getMyMembership().then(function(res) {
         var m = res.data || {}

+ 37 - 16
cfc-frontend/pages/membership/index.vue

@@ -61,17 +61,11 @@
         <view class="compare-table">
           <view class="compare-row header-row">
             <text class="compare-cell cell-feature">权益项</text>
-            <text class="compare-cell cell-free">免费</text>
-            <text class="compare-cell cell-basic">体验会员</text>
-            <text class="compare-cell cell-family">家庭会员</text>
-            <text class="compare-cell cell-premium">高级会员</text>
+            <text class="compare-cell" v-for="(col, cidx) in compareColumns" :key="cidx">{{ col.levelName || col.levelCode }}</text>
           </view>
           <view class="compare-row" v-for="(item, idx) in compareData" :key="idx">
-            <text class="compare-cell cell-feature">{{ item.name }}</text>
-            <text class="compare-cell cell-free">{{ item.free }}</text>
-            <text class="compare-cell cell-basic">{{ item.basic }}</text>
-            <text class="compare-cell cell-family">{{ item.family }}</text>
-            <text class="compare-cell cell-premium">{{ item.premium }}</text>
+            <text class="compare-cell cell-feature">{{ item.benefitName || item.name }}</text>
+            <text class="compare-cell" v-for="(col, cidx) in compareColumns" :key="cidx">{{ getCompareCell(item, col.levelCode) }}</text>
           </view>
         </view>
       </view>
@@ -80,7 +74,7 @@
 </template>
 
 <script>
-import { getMyMembership, getMyButler, reportAppEnter, getMembershipBenefits, getMembershipCompare
+import { getMyMembership, getMyButler, reportAppEnter, getMembershipBenefits, getMembershipCompare, getMembershipLevels
 } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 import fontScaleMixin from '../../mixins/font-scale.js'
@@ -91,9 +85,11 @@ export default {
     return {
       membership: null,
       isFree: true,
-      currentLevelName: '免费用户',
+      currentLevelName: '',
+      levelMap: {},
       features: ['折扣购物', '家庭管理', '推荐佣金', '专属活动', '任务特权', '成长报告'],
       compareData: [],
+      compareColumns: [],
       myButlerInfo: null,
       isL2Active: false
     }
@@ -115,6 +111,22 @@ export default {
   },
   methods: {
     loadData() {
+      var that = this
+      // 等级名改从 /api/membership/levels 读取,前端不再维护任何等级名映射
+      getMembershipLevels().then(function(res) {
+        var list = (res && res.data) || []
+        var levelMap = {}
+        for (var i = 0; i < list.length; i++) {
+          levelMap[list[i].levelCode] = list[i]
+        }
+        that.levelMap = levelMap
+        that.loadMyMembership()
+      }).catch(function() {
+        that.levelMap = {}
+        that.loadMyMembership()
+      })
+    },
+    loadMyMembership() {
       var that = this
       getMyMembership().then(function(res) {
         var m = res.data || {}
@@ -125,7 +137,7 @@ export default {
           that.currentLevelName = that.getLevelName(levelCode)
         } else {
           that.isFree = true
-          that.currentLevelName = '免费用户'
+          that.currentLevelName = that.getLevelName('FREE')
           levelCode = 'BASIC'
         }
         // 动态加载权益功能列表(免费用户展示 BASIC 体验会员功能)
@@ -140,16 +152,25 @@ export default {
     loadCompare() {
       var self = this
       getMembershipCompare().then(function(res) {
-        if (res.code === 200 && res.data && res.data.rows) {
-          self.compareData = res.data.rows
+        if (res.code === 200 && res.data) {
+          self.compareColumns = res.data.columns || []
+          self.compareData = res.data.rows || []
         }
       }).catch(function(e) {
         console.error('加载权益对比表失败', e)
       })
     },
     getLevelName(code) {
-      var map = { FREE: '免费用户', BASIC: '体验会员', FAMILY: '家庭会员', PREMIUM: '高级会员', LIFETIME: '终身会员' }
-      return map[code] || code || ''
+      if (!code) return ''
+      var item = this.levelMap && this.levelMap[code]
+      return (item && item.levelName) ? item.levelName : code
+    },
+    getCompareCell(item, levelCode) {
+      if (!item || !item.cells) return ''
+      var cell = item.cells[levelCode]
+      if (!cell) return ''
+      if (cell.value != null && cell.value !== '') return cell.value
+      return cell.included ? '✓' : '—'
     },
     goUpgrade() {
       uni.navigateTo({ url: '/pages/membership/upgrade' })

+ 35 - 5
cfc-frontend/pages/membership/pay.vue

@@ -91,16 +91,16 @@
 </template>
 
 <script>
-import { getMembershipPlans, getCouponList, createOrder, requestVirtualPayment, nextOrder } from '../../utils/api.js'
+import { getMembershipLevels, getMembershipPlans, getCouponList, createOrder, requestVirtualPayment, nextOrder } from '../../utils/api.js'
 import fontScaleMixin from '../../mixins/font-scale.js'
 
 export default {
   mixins: [fontScaleMixin],
   data() {
     return {
-      levelCode: 'FAMILY',
+      levelCode: '',
       period: 'yearly',
-      levelName: '家庭会员',
+      levelName: '',
       periodLabel: '年度',
       levels: [],
       coupons: [],
@@ -167,14 +167,44 @@ export default {
     }
   },
   onLoad: function(options) {
-    this.levelCode = options.levelCode || 'FAMILY'
+    this.levelCode = options.levelCode || ''
     this.period = options.period || 'yearly'
     this.periodLabel = this.getPeriodLabel(this.period)
-    this.loadPlans()
     this.loadCoupons()
     this.checkMemberStatus()
+    if (options.levelCode) {
+      this.loadPlans()
+    } else {
+      this.loadDefaultLevel()
+    }
   },
   methods: {
+    // 推荐档优先;否则取价格>0 的最低付费等级。前端不再写死任何等级名/编码。
+    loadDefaultLevel: function() {
+      var self = this
+      getMembershipLevels().then(function(res) {
+        var list = (res && res.data) || []
+        var target = null
+        for (var i = 0; i < list.length; i++) {
+          var lv = list[i]
+          if (lv.levelCode === 'FREE') continue
+          var price = lv.priceMonthly || lv.priceQuarterly || lv.priceYearly || 0
+          if (price <= 0) continue
+          if (lv.recommended) { target = lv; break }
+          if (!target || (lv.levelOrder != null && lv.levelOrder < target.levelOrder)) {
+            target = lv
+          }
+        }
+        if (target) {
+          self.levelCode = target.levelCode
+          self.levelName = target.levelName || target.levelCode
+        }
+        self.loadPlans()
+      }).catch(function(e) {
+        console.error('加载等级列表失败', e)
+        self.loadPlans()
+      })
+    },
     getPeriodLabel: function(period) {
       var map = { monthly: '月度', quarterly: '季度', yearly: '年度' }
       return map[period] || '年度'

+ 44 - 26
cfc-frontend/pages/membership/upgrade.vue

@@ -61,17 +61,11 @@
         <view class="compare-table">
           <view class="compare-row header-row">
             <text class="compare-cell cell-feature">权益项</text>
-            <text class="compare-cell cell-free">免费</text>
-            <text class="compare-cell cell-basic" :class="selectedLevel === 'BASIC' ? 'active' : ''">体验会员</text>
-            <text class="compare-cell cell-family" :class="selectedLevel === 'FAMILY' ? 'active' : ''">家庭会员</text>
-            <text class="compare-cell cell-premium" :class="selectedLevel === 'PREMIUM' ? 'active' : ''">高级会员</text>
+            <text class="compare-cell" v-for="(col, cidx) in compareColumns" :key="cidx" :class="col.levelCode === selectedLevel ? 'active' : ''">{{ col.levelName || col.levelCode }}</text>
           </view>
           <view class="compare-row" v-for="(item, idx) in compareData" :key="idx">
-            <text class="compare-cell cell-feature">{{ item.name }}</text>
-            <text class="compare-cell cell-free">{{ item.free }}</text>
-            <text class="compare-cell cell-basic" :class="selectedLevel === 'BASIC' ? 'active' : ''">{{ item.basic }}</text>
-            <text class="compare-cell cell-family" :class="selectedLevel === 'FAMILY' ? 'active' : ''">{{ item.family }}</text>
-            <text class="compare-cell cell-premium" :class="selectedLevel === 'PREMIUM' ? 'active' : ''">{{ item.premium }}</text>
+            <text class="compare-cell cell-feature">{{ item.benefitName || item.name }}</text>
+            <text class="compare-cell" v-for="(col, cidx) in compareColumns" :key="cidx" :class="col.levelCode === selectedLevel ? 'active' : ''">{{ getCompareCell(item, col.levelCode) }}</text>
           </view>
         </view>
       </view>
@@ -121,8 +115,9 @@ export default {
         { key: 'yearly', label: '年度', unit: '/年' }
       ],
       selectedPeriod: 'yearly',
-      selectedLevel: 'FAMILY',
+      selectedLevel: '',
       joinRules: [],
+      compareColumns: [],
     }
   },
   computed: {
@@ -172,13 +167,13 @@ export default {
       })
     },
     getLevelName(code) {
-      var map = {
-        FREE: '免费版',
-        BASIC: '体验会员',
-        FAMILY: '家庭会员',
-        PREMIUM: '高级会员'
+      if (!code) return ''
+      for (var i = 0; i < this.levels.length; i++) {
+        if (this.levels[i].levelCode === code) {
+          return this.levels[i].levelName || code
+        }
       }
-      return map[code] || code || ''
+      return code || ''
     },
     getSelectedLevelName() {
       var name = ''
@@ -206,10 +201,10 @@ export default {
         this.membership = membershipRes.data
         if (this.membership && this.membership.memberLevel && this.membership.memberLevel !== 'FREE') {
           this.isFree = false
-          this.currentLevelName = this.membership.levelName || '家庭会员'
+          this.currentLevelName = this.membership.levelName || this.getLevelName(this.membership.memberLevel)
         } else {
           this.isFree = true
-          this.currentLevelName = '免费用户'
+          this.currentLevelName = (this.membership && this.membership.levelName) || ''
         }
       } catch (e) {
         console.error('加载会员信息失败', e)
@@ -218,6 +213,9 @@ export default {
         const levelsRes = await getMembershipPlans()
         this.levels = levelsRes.data || []
         this.applyDefaultLevel()
+        // 等级名改从接口返回的 levelName 读取(含免费用户),前端不再维护等级名映射
+        var lvCode = (this.membership && this.membership.memberLevel) ? this.membership.memberLevel : 'FREE'
+        this.currentLevelName = (this.membership && this.membership.levelName) || this.getLevelName(lvCode)
       } catch (e) {
         console.error('加载会员等级失败', e)
       }
@@ -234,13 +232,21 @@ export default {
     loadCompare() {
       var self = this
       getMembershipCompare().then(function(res) {
-        if (res.code === 200 && res.data && res.data.rows) {
-          self.compareData = res.data.rows
+        if (res.code === 200 && res.data) {
+          self.compareColumns = res.data.columns || []
+          self.compareData = res.data.rows || []
         }
       }).catch(function(e) {
         console.error('加载权益对比表失败', e)
       })
     },
+    getCompareCell(item, levelCode) {
+      if (!item || !item.cells) return ''
+      var cell = item.cells[levelCode]
+      if (!cell) return ''
+      if (cell.value != null && cell.value !== '') return cell.value
+      return cell.included ? '✓' : '—'
+    },
     selectLevel: function(code) {
       this.selectedLevel = code
       // 选中第一个有价格的周期,避免切换到无价格的周期导致空档
@@ -259,16 +265,23 @@ export default {
     selectPeriod: function(key) {
       this.selectedPeriod = key
     },
+    getLevelOrder(code) {
+      // 等级高低改由接口返回的 levelOrder 比较,前端不再维护任何等级排序映射
+      for (var i = 0; i < this.levels.length; i++) {
+        if (this.levels[i].levelCode === code) {
+          var o = this.levels[i].levelOrder
+          return (o != null) ? o : 0
+        }
+      }
+      return 0
+    },
     applyDefaultLevel: function() {
       var current = this.membership && this.membership.memberLevel ? this.membership.memberLevel : 'FREE'
-      // level_order 与 DB membership_levels.level_order 一致:FREE=1 BASIC=2 FAMILY=3 PREMIUM=4 LIFETIME=5;
-      // PROVIDER 为服务商独立角色(不参与会员权益),level_order=1 与 FREE 同级
-      var rank = { 'FREE': 1, 'BASIC': 2, 'FAMILY': 3, 'PROVIDER': 1, 'PREMIUM': 4, 'LIFETIME': 5 }
-      var currentRank = rank[current] !== undefined ? rank[current] : 0
+      var currentOrder = this.getLevelOrder(current)
       var target = null
       for (var i = 0; i < this.buyableLevels.length; i++) {
-        var r = rank[this.buyableLevels[i].levelCode]
-        if (r !== undefined && r > currentRank) { target = this.buyableLevels[i].levelCode; break }
+        var r = this.getLevelOrder(this.buyableLevels[i].levelCode)
+        if (r > currentOrder) { target = this.buyableLevels[i].levelCode; break }
       }
       this.selectedLevel = target || current
     },
@@ -533,6 +546,11 @@ export default {
   background: $violet-tint-100;
   color: $facade-violet;
 }
+.compare-cell.active {
+  background: $facade-brand-tint-2;
+  color: $brand-primary;
+  font-weight: 600;
+}
 
 /* ===== 等级切换 Tabs ===== */
 .level-tabs {