Sfoglia il codice sorgente

会员可见级别模型: 增加所有用户档(含未登录)+BASIC独立档位

- 后端: 迁移321 CASE 同步 + 新增迁移350, level_order 整体顺延为
  FREE=1 BASIC=2 FAMILY=3 PROVIDER=4 PREMIUM=5 LIFETIME=6,
  BASIC 从与 FREE 同级调整为独立档位(FREE 与 FAMILY 之间)
- 前端: ProductEdit/CategoryManage 可见级别下拉增加"所有用户(含未登录)"
  选项(值空串,提交转null), 显示全部级别含FREE; ProductManage
  levelLabel NULL 显示"所有用户"
- schema.sql: membership_levels.level_order 注释同步
Xiaogang Liao 5 giorni fa
parent
commit
cd99db48c2

+ 20 - 6
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -10934,6 +10934,7 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         backfillTemplateInstanceFamilyMemberId();
         createReportTransferRequestsTable();
         addProductMinDiscountLevel();
+        backfillMembershipLevelOrder();
 
         // 迁移341: BASIC 体验会员 features JSON 补 referral_commission(迁移315 未包含,前端对比表已显示可参与)
         try {
@@ -11184,15 +11185,15 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
         }
 
         // 迁移321: 商品/分类可见性由「角色」改为「按会员级别」控制
-        // - membership_levels.level_order: 级别数字(数字越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5
-        // - products.min_visible_level: 最低可见会员级别编码(NULL=全部可见)
-        // - shop_categories.min_visible_level: 分类最低可见会员级别编码(NULL=全部可见)
+        // - membership_levels.level_order: 级别数字(数字越小级别越低): FREE=1 BASIC=2 FAMILY=3 PROVIDER=4 PREMIUM=5 LIFETIME=6
+        // - products.min_visible_level: 最低可见会员级别编码(NULL=所有用户含未登录可见)
+        // - shop_categories.min_visible_level: 分类最低可见会员级别编码(NULL=所有用户含未登录可见)
         ensureColumn("membership_levels", "level_order",
-                "INT DEFAULT 1 COMMENT '级别数字(越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5'");
+                "INT DEFAULT 1 COMMENT '级别数字(越小级别越低): FREE=1 BASIC=2 FAMILY=3 PROVIDER=4 PREMIUM=5 LIFETIME=6'");
         try {
             jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
-                    "WHEN 'FREE' THEN 1 WHEN 'FAMILY' THEN 2 WHEN 'PROVIDER' THEN 3 " +
-                    "WHEN 'PREMIUM' THEN 4 WHEN 'LIFETIME' THEN 5 ELSE level_order END");
+                    "WHEN 'FREE' THEN 1 WHEN 'BASIC' THEN 2 WHEN 'FAMILY' THEN 3 WHEN 'PROVIDER' THEN 4 " +
+                    "WHEN 'PREMIUM' THEN 5 WHEN 'LIFETIME' THEN 6 ELSE level_order END");
             log.info("迁移321: 已回填 membership_levels.level_order");
         } catch (Exception e) {
             log.warn("迁移321: 回填 membership_levels.level_order 失败: {}", e.getMessage());
@@ -11659,4 +11660,17 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
             log.debug("迁移350: products.min_discount_level 列已存在,跳过");
         }
     }
+
+    // 迁移351: membership_levels.level_order 顺延,为 BASIC 让出独立档位
+    // 会员级别包含关系(数字越小级别越低、可见范围越大): FREE=1 BASIC=2 FAMILY=3 PROVIDER=4 PREMIUM=5 LIFETIME=6
+    private void backfillMembershipLevelOrder() {
+        try {
+            int updated = jdbcTemplate.update("UPDATE membership_levels SET level_order = CASE level_code " +
+                    "WHEN 'FREE' THEN 1 WHEN 'BASIC' THEN 2 WHEN 'FAMILY' THEN 3 WHEN 'PROVIDER' THEN 4 " +
+                    "WHEN 'PREMIUM' THEN 5 WHEN 'LIFETIME' THEN 6 ELSE level_order END");
+            log.info("迁移351: 已顺延 membership_levels.level_order({} 行),BASIC=2", updated);
+        } catch (Exception e) {
+            log.warn("迁移351: 顺延 membership_levels.level_order 失败: {}", e.getMessage());
+        }
+    }
 }

+ 1 - 1
cfc-backend/src/main/resources/schema.sql

@@ -3945,7 +3945,7 @@ CREATE TABLE IF NOT EXISTS membership_levels (
     ai_review_enabled TINYINT(1) DEFAULT 0 COMMENT 'AI审核功能',
     priority_support TINYINT(1) DEFAULT 0 COMMENT '优先客服',
     teacher_consultation TINYINT(1) DEFAULT 0 COMMENT '老师咨询',
-    level_order INT DEFAULT 1 COMMENT '级别数字(越小级别越低): FREE=1 FAMILY=2 PROVIDER=3 PREMIUM=4 LIFETIME=5',
+    level_order INT DEFAULT 1 COMMENT '级别数字(越小级别越低): FREE=1 BASIC=2 FAMILY=3 PROVIDER=4 PREMIUM=5 LIFETIME=6',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     UNIQUE KEY uk_level_code (level_code)

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-6bf61b0e7001e7bdcf4a46b5d723f1c010c7126e
+6bf61b0e7001e7bdcf4a46b5d723f1c010c7126e

+ 16 - 6
cfc-web/src/views/admin/CategoryManage.vue

@@ -80,15 +80,15 @@
             </el-form-item>
 
             <el-form-item label="最低可见级别">
-              <el-select v-model="form.minVisibleLevel" placeholder="全部可见" clearable style="width: 100%;">
+              <el-select v-model="form.minVisibleLevel" placeholder="所有用户(含未登录)" clearable style="width: 100%;">
                 <el-option
-                  v-for="lv in membershipLevels"
-                  :key="lv.levelCode"
-                  :label="lv.levelName + ' (' + lv.levelCode + ')'"
-                  :value="lv.levelCode"
+                  v-for="opt in visibleLevelOptions"
+                  :key="opt.value"
+                  :label="opt.label"
+                  :value="opt.value"
                 />
               </el-select>
-              <div style="font-size: 12px; color: #999; line-height: 1.5; margin-top: 4px;">留空=全部会员可见;选择后仅该级别及更高级别会员可见</div>
+              <div style="font-size: 12px; color: #999; line-height: 1.5; margin-top: 4px;">所有用户(含未登录)=游客及全部会员可见;选择级别后仅该级别及更高级别会员可见</div>
             </el-form-item>
 
             <el-form-item>
@@ -126,6 +126,7 @@ export default {
         minVisibleLevel: ''
       },
       membershipLevels: [],
+      allMembershipLevels: [],
       selectedDimensions: [],
       selectedProductTypes: [],
       rules: {
@@ -151,6 +152,13 @@ export default {
         return result
       }
       return filterNodes(this.treeData)
+    },
+    visibleLevelOptions() {
+      const options = [{ label: '所有用户(含未登录)', value: '' }]
+      ;(this.allMembershipLevels || []).forEach(lv => {
+        options.push({ label: lv.levelName + ' (' + lv.levelCode + ')', value: lv.levelCode })
+      })
+      return options
     }
   },
   mounted() {
@@ -162,6 +170,7 @@ export default {
       try {
         const res = await listMembershipLevels()
         if (res.code === 200 && Array.isArray(res.data)) {
+          this.allMembershipLevels = res.data
           this.membershipLevels = res.data.filter(l => l.levelCode !== 'FREE')
         }
       } catch (e) {
@@ -228,6 +237,7 @@ export default {
         try {
           const payload = {
             ...this.form,
+            minVisibleLevel: this.form.minVisibleLevel || null,
             dimensionCodes: this.selectedDimensions.join(','),
             productTypes: this.selectedProductTypes.join(',')
           }

+ 17 - 6
cfc-web/src/views/admin/ProductEdit.vue

@@ -260,15 +260,15 @@
         </el-form-item>
 
         <el-form-item label="最低可见级别">
-          <el-select v-model="form.minVisibleLevel" placeholder="全部可见" clearable style="width: 200px;">
+          <el-select v-model="form.minVisibleLevel" placeholder="所有用户(含未登录)" clearable style="width: 220px;">
             <el-option
-              v-for="lv in membershipLevels"
-              :key="lv.levelCode"
-              :label="lv.levelName + ' (' + lv.levelCode + ')'"
-              :value="lv.levelCode"
+              v-for="opt in visibleLevelOptions"
+              :key="opt.value"
+              :label="opt.label"
+              :value="opt.value"
             />
           </el-select>
-          <span style="margin-left: 10px; font-size: 12px; color: #999;">留空=全部会员可见;选择后仅该级别及更高级别会员可见</span>
+          <span style="margin-left: 10px; font-size: 12px; color: #999;">所有用户(含未登录)=游客及全部会员可见;选择级别后仅该级别及更高级别会员可见</span>
         </el-form-item>
 
         <el-form-item label="商品维度">
@@ -412,6 +412,7 @@ export default {
       giftRuleEnabled: false,
       giftRuleLevel: '',
       membershipLevels: [],
+      allMembershipLevels: [],
       // 可选赠品配置:{ [levelCode]: [{giftType,giftProductId,cfValue,sortOrder}] }
       giftConfigsByLevel: {},
       onShelfProducts: [],
@@ -575,6 +576,13 @@ export default {
       const roles = rolesStr ? JSON.parse(rolesStr) : []
       return roles.includes('supplier') || roles.includes('supplier_admin')
     },
+  visibleLevelOptions() {
+      const options = [{ label: '所有用户(含未登录)', value: '' }]
+      ;(this.allMembershipLevels || []).forEach(lv => {
+        options.push({ label: lv.levelName + ' (' + lv.levelCode + ')', value: lv.levelCode })
+      })
+      return options
+    },
   uploadActionUrl() {
     const baseURL = process.env.VUE_APP_BASE_API || ''
     return baseURL + '/api/admin/articles/upload/image'
@@ -765,6 +773,7 @@ async loadGiftRule(productId) {
       try {
         const lvRes = await listMembershipLevels()
         if (lvRes.code === 200 && Array.isArray(lvRes.data)) {
+          this.allMembershipLevels = lvRes.data
           this.membershipLevels = lvRes.data.filter(l => l.levelCode !== 'FREE')
         }
         const ruleRes = await listProductGiftRules(productId)
@@ -802,6 +811,7 @@ async loadGiftRule(productId) {
         try {
           const lvRes = await listMembershipLevels()
           if (lvRes.code === 200 && Array.isArray(lvRes.data)) {
+            this.allMembershipLevels = lvRes.data
             this.membershipLevels = lvRes.data.filter(l => l.levelCode !== 'FREE')
           }
         } catch (e) {
@@ -1046,6 +1056,7 @@ async loadGiftRule(productId) {
         try {
           const productData = {
             ...this.form,
+            minVisibleLevel: this.form.minVisibleLevel || null,
             price: (this.form.price != null) ? Math.round(this.form.price * 100) : 0,
             memberPrice: (this.form.memberPrice != null && this.form.memberPrice > 0) ? Math.round(this.form.memberPrice * 100) : 0,
             images: JSON.stringify(this.imageList)

+ 3 - 3
cfc-web/src/views/admin/ProductManage.vue

@@ -250,7 +250,7 @@
             <i class="pm-status__dot"></i>{{ statusLabel(detail.status) }}
           </span>
         </el-descriptions-item>
-        <el-descriptions-item label="最低可见级别">{{ detail.minVisibleLevel ? levelLabel(detail.minVisibleLevel) : '全部' }}</el-descriptions-item>
+        <el-descriptions-item label="最低可见级别">{{ detail.minVisibleLevel ? levelLabel(detail.minVisibleLevel) : '所有用户' }}</el-descriptions-item>
         <el-descriptions-item label="拒绝原因" v-if="detail.rejectReason" :span="2">{{ detail.rejectReason }}</el-descriptions-item>
         <el-descriptions-item label="商品描述" :span="2">{{ detail.description || '—' }}</el-descriptions-item>
         <el-descriptions-item label="创建时间" :span="2">{{ formatTime(detail.createdAt) }}</el-descriptions-item>
@@ -409,8 +409,8 @@ export default {
       return map[type] || type
     },
     levelLabel(code) {
-      if (!code) return '全部'
-      const map = { FREE: '免费版', FAMILY: '家庭版', PROVIDER: '服务商版', PREMIUM: '高级版', LIFETIME: '终身版' }
+      if (!code) return '所有用户'
+      const map = { FREE: '免费版', BASIC: '体验版', FAMILY: '家庭版', PROVIDER: '服务商版', PREMIUM: '高级版', LIFETIME: '终身版' }
       return map[code] || code
     },
     formatTime(t) {