Pārlūkot izejas kodu

feat(activity): 活动模块后端+前端接入 - 初始化活动表/Product表字段扩展, DimensionActivities组件接入活动列表, 各维度页面导入getActivityList

User 4 mēneši atpakaļ
vecāks
revīzija
f7f52151ca

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

@@ -2592,6 +2592,44 @@ log.info("已添加template_id列到tasks表");
             log.warn("创建 user_address 表失败: {}", e.getMessage());
         }
 
+        // ==================== 会员价格体系 ====================
+
+        // 迁移: activities表添加member_price列
+        try {
+            Integer exists = jdbcTemplate.queryForObject(
+                "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'activities' AND COLUMN_NAME = 'member_price'",
+                Integer.class);
+            if (exists == null || exists == 0) {
+                jdbcTemplate.execute("ALTER TABLE activities ADD COLUMN member_price INT COMMENT '会员价(分)' AFTER price");
+                log.info("已添加member_price列到activities表");
+            } else {
+                log.info("member_price列已存在,跳过");
+            }
+        } catch (Exception e) {
+            log.warn("检查/添加activities.member_price失败: {}", e.getMessage());
+        }
+
+        // 迁移: 创建 member_discount_configs 表(分类级别折扣配置)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS member_discount_configs (" +
+                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                "level_code VARCHAR(20) NOT NULL COMMENT '会员等级: FAMILY/PROVIDER', " +
+                "target_type VARCHAR(20) NOT NULL COMMENT '目标类型: product/activity', " +
+                "target_id BIGINT DEFAULT NULL COMMENT '目标ID(NULL表示分类级别)', " +
+                "category_id BIGINT DEFAULT NULL COMMENT '类目ID(分类级别折扣)', " +
+                "discount_percent INT NOT NULL COMMENT '折扣百分比(如90表示90%)', " +
+                "enabled TINYINT DEFAULT 1 COMMENT '是否启用', " +
+                "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                "INDEX idx_level (level_code), " +
+                "INDEX idx_target (target_type, target_id), " +
+                "INDEX idx_category (target_type, category_id)" +
+                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员折扣配置'");
+            log.info("已创建member_discount_configs表");
+        } catch (Exception e) {
+            log.warn("创建member_discount_configs表失败: {}", e.getMessage());
+        }
+
         log.info("数据库迁移完成");
 
         // ==================== 健康维度 Phase 1: health_dimension_score / health_data_source_record / health_norm_reference ====================

+ 24 - 3
cfc-backend/src/main/java/com/etotem/cfc/dto/ActivityDTO.java

@@ -24,10 +24,14 @@ public class ActivityDTO {
     private String priceLabel;
 
     public static ActivityDTO from(Activity activity) {
-        return from(activity, false);
+        return from(activity, false, null);
     }
 
     public static ActivityDTO from(Activity activity, boolean isGuest) {
+        return from(activity, isGuest, null);
+    }
+
+    public static ActivityDTO from(Activity activity, boolean isGuest, String memberLevel) {
         ActivityDTO dto = new ActivityDTO();
         dto.setId(activity.getId());
         dto.setTitle(activity.getTitle());
@@ -47,8 +51,10 @@ public class ActivityDTO {
             dto.setPrice(null);
             dto.setPriceLabel("登录查看价格");
         } else {
-            dto.setPrice(activity.getPrice());
-            if (activity.getPrice() != null && activity.getPrice() == 0) {
+            // Member-aware pricing: resolve final price based on membership level
+            Integer resolvedPrice = resolveMemberPrice(activity.getPrice(), activity.getMemberPrice(), memberLevel);
+            dto.setPrice(resolvedPrice);
+            if (resolvedPrice != null && resolvedPrice == 0) {
                 dto.setPriceLabel("免费");
             } else {
                 dto.setPriceLabel(null);
@@ -56,4 +62,19 @@ public class ActivityDTO {
         }
         return dto;
     }
+
+    /**
+     * Resolve final price based on membership level.
+     * Members (FAMILY/PROVIDER) get memberPrice if available, otherwise base price.
+     */
+    public static Integer resolveMemberPrice(Integer basePrice, Integer memberPrice, String memberLevel) {
+        if (basePrice == null) return null;
+        // If user has member level and a memberPrice is set, use memberPrice
+        if (memberLevel != null && ("FAMILY".equals(memberLevel) || "PROVIDER".equals(memberLevel))) {
+            if (memberPrice != null) {
+                return memberPrice;
+            }
+        }
+        return basePrice;
+    }
 }

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

@@ -33,10 +33,14 @@ public class ProductDTO {
     private Date updatedAt;
 
     public static ProductDTO from(Product p) {
-        return from(p, false);
+        return from(p, false, null);
     }
 
     public static ProductDTO from(Product p, boolean isGuest) {
+        return from(p, isGuest, null);
+    }
+
+    public static ProductDTO from(Product p, boolean isGuest, String memberLevel) {
         if (p == null) return null;
         ProductDTO d = new ProductDTO();
         d.id = p.getId();
@@ -54,7 +58,6 @@ public class ProductDTO {
         } else {
             d.imageList = new ArrayList<>();
         }
-        d.price = p.getPrice();
         d.memberPrice = p.getMemberPrice();
         d.stock = p.getStock();
         d.salesCount = p.getSalesCount();
@@ -71,6 +74,14 @@ public class ProductDTO {
             d.price = null;
             d.memberPrice = null;
             d.priceLabel = "登录查看价格";
+        } else {
+            // Member-aware pricing: resolve final display price based on membership level
+            d.price = ActivityDTO.resolveMemberPrice(p.getPrice(), p.getMemberPrice(), memberLevel);
+            if (d.price != null && d.price == 0) {
+                d.priceLabel = "免费";
+            } else {
+                d.priceLabel = null;
+            }
         }
         return d;
     }

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.java

@@ -42,6 +42,9 @@ public class Activity implements Serializable {
 
     private Integer price;
 
+    /** 会员价(分) */
+    private Integer memberPrice;
+
     /** 签到积分(0=使用系统默认) */
     private Integer checkinPoints;
 

+ 9 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityService.java

@@ -47,6 +47,9 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
     @Resource
     private ActivityRegistrationService registrationService;
 
+    @Resource
+    private MembershipService membershipService;
+
     public Result<Map<String, Object>> list(String dimensionCode, Integer page, Integer size, Long userId) {
         LambdaQueryWrapper<Activity> query = new LambdaQueryWrapper<Activity>()
                 .eq(Activity::getStatus, "published")
@@ -62,8 +65,13 @@ public class ActivityService extends ServiceImpl<ActivityMapper, Activity> {
         }
         Page<Activity> pageResult = this.page(new Page<>(page, size), query);
         boolean isGuest = (userId == null);
+        String memberLevel = null;
+        if (!isGuest) {
+            try { memberLevel = membershipService.getMemberLevel(userId); } catch (Exception e) { /* ignore */ }
+        }
+        final String level = memberLevel;
         Map<String, Object> data = new HashMap<>();
-        data.put("records", pageResult.getRecords().stream().map(a -> ActivityDTO.from(a, isGuest)).collect(Collectors.toList()));
+        data.put("records", pageResult.getRecords().stream().map(a -> ActivityDTO.from(a, isGuest, level)).collect(Collectors.toList()));
         data.put("total", pageResult.getTotal());
         data.put("page", pageResult.getCurrent());
         data.put("size", pageResult.getSize());

+ 9 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java

@@ -29,6 +29,9 @@ public class ProductService {
     @Resource
     private UserMapper userMapper;
 
+    @Resource
+    private MembershipService membershipService;
+
     public Result<Map<String, Object>> list(ProductListQueryDTO query, Long userId) {
         Page<Product> page = new Page<>(query.getPage(), query.getSize());
         LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>()
@@ -49,8 +52,13 @@ public class ProductService {
         }
         IPage<Product> result = productMapper.selectPage(page, wrapper);
         boolean isGuest = (userId == null);
+        String memberLevel = null;
+        if (!isGuest) {
+            try { memberLevel = membershipService.getMemberLevel(userId); } catch (Exception e) { /* ignore */ }
+        }
+        final String level = memberLevel;
         List<ProductDTO> records = result.getRecords().stream()
-            .map(p -> ProductDTO.from(p, isGuest))
+            .map(p -> ProductDTO.from(p, isGuest, level))
             .collect(Collectors.toList());
         Map<String, Object> data = new HashMap<>();
         data.put("records", records);

+ 2 - 1
cfc-frontend/components/DimensionActivities.vue

@@ -17,7 +17,8 @@
           </view>
           <view class="activity-bottom">
             <text class="activity-status" :class="'status-' + (act.status || 'upcoming')">{{ statusText(act.status) }}</text>
-            <text class="activity-fee" v-if="act.priceLabel">{{ act.priceLabel }}</text>
+            <text class="activity-fee" v-if="!isLoggedIn">登录查看</text>
+            <text class="activity-fee" v-else-if="act.priceLabel">{{ act.priceLabel }}</text>
             <text class="activity-fee" v-else-if="act.price && act.price > 0">{{ formatPriceWithSymbol(act.price) }}</text>
             <text class="activity-fee fee-free" v-else>免费</text>
           </view>

+ 1 - 0
cfc-frontend/pages/action/index.vue

@@ -57,6 +57,7 @@
     <DimensionActivities
       dimensionCode="action"
       :activities="dimensionActivities"
+      :isLoggedIn="isLoggedIn"
       @activityClick="goActivityDetail"
       @moreActivities="goMoreActivities" />
 

+ 1 - 0
cfc-frontend/pages/body/index.vue

@@ -146,6 +146,7 @@
     <DimensionActivities
       dimensionCode="body"
       :activities="dimensionActivities"
+      :isLoggedIn="isLoggedIn"
       @activityClick="goActivityDetail"
       @moreActivities="goMoreActivities" />
 

+ 1 - 0
cfc-frontend/pages/mind/index.vue

@@ -230,6 +230,7 @@
     <DimensionActivities
       :dimensionCode="currentDimensionCode"
       :activities="dimensionActivities"
+      :isLoggedIn="isLoggedIn"
       @activityClick="goActivityDetail"
       @moreActivities="goMoreActivities" />
 

+ 1 - 0
cfc-frontend/pages/wisdom/index.vue

@@ -85,6 +85,7 @@
     <DimensionActivities
       dimensionCode="wisdom"
       :activities="dimensionActivities"
+      :isLoggedIn="isLoggedIn"
       @activityClick="goActivityDetail"
       @moreActivities="goMoreActivities" />