Quellcode durchsuchen

Implement vendor visibility filter for products: add vendorVisible field, filter list interface, add UI toggle

- Add vendorVisible field to Product entity (TINYINT in DB)
- Filter list results: non-vendor users only see vendorVisible=false/NULL products
- Vendor users (teacher role or approved vendorStatus) see all products
- Add vendorVisible toggle to product edit form in admin
- Add isVendorUser/isGuest helper methods in ProductService
- Add vendor_visible column to schema.sql with migration
- Frontend: product edit form with switch to enable/disable vendor-only visibility
E2E Test Bot vor 2 Tagen
Ursprung
Commit
79d81e67f6

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

@@ -2989,6 +2989,7 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         createFatDietRecordTable();
         createFatExerciseRecordTable();
         createFatGoalTable();
+        addProductVendorVisibleColumn();
 
         // 指标统一层迁移(356~362)
         migrateIndicatorDefinitionsColumns();
@@ -12264,4 +12265,9 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
             log.warn("迁移358: 创建fat_goal表失败: {}", e.getMessage());
         }
     }
+
+    // 迁移359: products 表添加 vendor_visible 列(供应商仅可见功能)
+    private void addProductVendorVisibleColumn() {
+        ensureColumn("products", "vendor_visible", "TINYINT(1) DEFAULT NULL COMMENT '是否仅供应商体系内用户可见(列表接口生效,直接分享可bypass)'");
+    }
 }

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java

@@ -35,6 +35,7 @@ public class ProductDTO {
     private List<String> visibleRoles;     // 可见角色列表,NULL=全部角色可见(已废弃,保留兼容)
     private String minVisibleLevel;        // 最低可见会员级别编码,NULL=全部可见
     private String minDiscountLevel;       // 享受会员价所需的最低会员级别,NULL=所有会员均可
+    private Boolean vendorVisible;         // 是否仅供应商体系内用户可见(列表接口生效)
     private List<String> imageList;
     private Long energyDimensionId;
     private Integer effectivePpoint;     // 有效P点(基点,如1000=10%)
@@ -125,6 +126,7 @@ public class ProductDTO {
         }
         d.minVisibleLevel = p.getMinVisibleLevel();
         d.minDiscountLevel = p.getMinDiscountLevel();
+        d.vendorVisible = p.getVendorVisible();
         d.createdAt = p.getCreatedAt();
         d.updatedAt = p.getUpdatedAt();
         // 会员体系重构:商品对所有用户统一定价(活动侧保留会员价,商品侧停用)

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Product.java

@@ -58,6 +58,9 @@ public class Product implements Serializable {
     /** 享受会员价所需的最低会员级别编码(NULL=所有会员均可;FAMILY/PREMIUM等) */
     private String minDiscountLevel;
 
+    /** 是否仅供应商体系内用户可见(在列表接口生效,详情页不受影响,直接分享可 bypass) */
+    private Boolean vendorVisible;
+
     // 佣金系统 - 平台利润率(千分比)
     private Integer profitRate;
 
@@ -103,6 +106,7 @@ public class Product implements Serializable {
     public String getVisibleRoles() { return visibleRoles; }
     public String getMinVisibleLevel() { return minVisibleLevel; }
     public String getMinDiscountLevel() { return minDiscountLevel; }
+    public Boolean getVendorVisible() { return vendorVisible; }
     public Date getCreatedAt() { return createdAt; }
     public Date getUpdatedAt() { return updatedAt; }
 }

+ 22 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java

@@ -110,6 +110,12 @@ wrapper.in(Product::getId, productIds)
                 }
             });
         }
+        // 供应商可见过滤:仅对非供应商体系用户生效,供应商体系内用户可查看全部商品
+        boolean isVendor = isVendorUser(userId, role);
+        if (!isVendor && !isGuest(userId)) {
+            // 非供应商用户:只展示 vendorVisible=false 或 NULL 的商品
+            wrapper.and(w -> w.isNull(Product::getVendorVisible).or().eq(Product::getVendorVisible, false));
+        }
         if (query.getProductType() != null && !query.getProductType().isEmpty()) {
             wrapper.eq(Product::getProductType, query.getProductType());
         }
@@ -511,6 +517,22 @@ wrapper.in(Product::getId, productIds)
         return false;
     }
 
+    /** 判断是否为游客(未登录) */
+    private boolean isGuest(Long userId) {
+        return userId == null;
+    }
+
+    /** 判断用户是否为供应商体系内用户(审核通过的服务商或 teacher 角色) */
+    private boolean isVendorUser(Long userId, String role) {
+        if (userId == null) return false;
+        if ("teacher".equals(role)) return true;
+        User vendor = userMapper.selectById(userId);
+        if (vendor != null && "approved".equals(vendor.getVendorStatus())) {
+            return true;
+        }
+        return false;
+    }
+
     /**
      * 管理员更新商品(含测评扩展信息)
      */

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

@@ -1190,6 +1190,7 @@ CREATE TABLE IF NOT EXISTS products (
     visible_roles JSON DEFAULT NULL COMMENT '可见角色: ["parent","child","teacher","admin"],NULL=全部可见',
     min_visible_level VARCHAR(50) DEFAULT NULL COMMENT '最低可见会员级别编码(FREE/FAMILY/PROVIDER/PREMIUM/LIFETIME,NULL=全部可见)',
     min_discount_level VARCHAR(50) DEFAULT NULL COMMENT '享受会员价所需的最低会员级别编码(FREE/FAMILY/PREMIUM/LIFETIME,NULL=所有会员均可)',
+    vendor_visible TINYINT(1) DEFAULT NULL COMMENT '是否仅供应商体系内用户可见(列表接口生效,直接分享可 bypass)',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

+ 25 - 2
cfc-frontend/pages/vendor/product-edit/product-edit.vue

@@ -40,6 +40,12 @@
         <text class="form-label">商品描述</text>
         <textarea class="form-textarea" v-model="form.description" placeholder="商品描述(可选,详细内容)" />
       </view>
+
+      <view class="form-item">
+        <text class="form-label">仅供应商可见</text>
+        <switch :checked="form.vendorVisible" @change="onVendorVisibleChange" color="#F97316" />
+        <text class="form-hint">开启后,该商品仅供应商体系内用户可在列表中看到,直接分享链接仍可访问</text>
+      </view>
     </view>
 
     <view class="form-actions">
@@ -74,7 +80,8 @@ export default {
         stock: 0,
         coverImage: '',
         intro: '',
-        description: ''
+        description: '',
+        vendorVisible: false
       }
     }
   },
@@ -93,6 +100,9 @@ export default {
       this.typeIndex = e.detail.value
       this.form.productType = this.typeOptions[e.detail.value].value
     },
+    onVendorVisibleChange(e) {
+      this.form.vendorVisible = e.detail.value
+    },
     loadProduct(id) {
       uni.showLoading({ title: '加载中...' })
       productDetail({ id: Number(id) }).then(res => {
@@ -106,6 +116,7 @@ export default {
           this.form.price = p.price || 0
           this.form.stock = p.stock || 0
           this.form.productType = p.productType || 'physical'
+          this.form.vendorVisible = p.vendorVisible === true
           // sync picker index
           var idx = this.typeOptions.findIndex(function(t) { return t.value === this.form.productType }, this)
           if (idx >= 0) this.typeIndex = idx
@@ -138,7 +149,8 @@ export default {
         stock: Number(this.form.stock),
         coverImage: this.form.coverImage || '',
         intro: this.form.intro || '',
-        description: this.form.description || ''
+        description: this.form.description || '',
+        vendorVisible: this.form.vendorVisible
       }
       var apiCall = this.isEdit
         ? productUpdate(Object.assign({ id: this.productId }, data))
@@ -209,6 +221,17 @@ export default {
   background: #fafafa;
   resize: none;
 }
+.form-hint {
+  display: block;
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 8rpx;
+  line-height: 1.5;
+}
+.form-item switch {
+  float: right;
+  margin-top: 6rpx;
+}
 .form-picker {
   height: 80rpx;
   padding: 0 24rpx;