Browse Source

chore: auto bump version and changelog [skip ci]

iwt 1 month ago
parent
commit
03724311e3

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

@@ -8560,458 +8560,5 @@ private void runMigration100() {
 
 		// 迁移225: articles 添加 growth_category 列(成长分类筛选,Article 实体已含字段,生产库缺列导致 Unknown column 'growth_category')
 		ensureColumn("articles", "growth_category", "VARCHAR(50) DEFAULT NULL COMMENT '成长分类'");
-
-		// 迁移226: products 添加 growth_category 列(成长分类筛选,Product 实体已含字段,生产库缺列导致 Unknown column 'growth_category')
-		ensureColumn("products", "growth_category", "VARCHAR(50) DEFAULT NULL COMMENT '成长分类'");
-
-		// 迁移227: product_skus.id 升级为 BIGINT(生产库为 INT,插入超出 INT 范围的 id 报 Out of range value for column 'id')
-		try {
-			jdbcTemplate.execute("ALTER TABLE product_skus MODIFY COLUMN id BIGINT AUTO_INCREMENT");
-			jdbcTemplate.execute("ALTER TABLE product_skus MODIFY COLUMN product_id BIGINT NOT NULL COMMENT '所属商品ID'");
-			log.info("product_skus.id/product_id 已升级为 BIGINT");
-		} catch (Exception e) {
-			log.warn("product_skus.id/product_id 升级失败(可能已修改): " + e.getMessage());
-		}
-
-		// 迁移209: 创建 survey_templates 表(定期调研模板)
-		try {
-			jdbcTemplate.execute(
-				"CREATE TABLE IF NOT EXISTS survey_templates (" +
-				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-				"title VARCHAR(100) NOT NULL COMMENT '调研标题', " +
-				"dimension VARCHAR(20) COMMENT '适用维度(身/智/富/行/心)', " +
-				"questions_json TEXT COMMENT '题目JSON(含选项)', " +
-				"cycle_type VARCHAR(20) DEFAULT 'weekly' COMMENT '周期: weekly/biweekly/monthly', " +
-				"ai_generated TINYINT(1) DEFAULT 0 COMMENT '是否AI生成', " +
-				"enabled TINYINT(1) DEFAULT 1 COMMENT '是否启用', " +
-				"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-				"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
-				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='定期调研模板'"
-			);
-			log.info("已创建survey_templates表");
-		} catch (Exception e) {
-			log.warn("创建survey_templates表失败(可能已存在): " + e.getMessage());
-		}
-
-		// 迁移210: 创建 survey_records 表(定期调研记录)
-		try {
-			jdbcTemplate.execute(
-				"CREATE TABLE IF NOT EXISTS survey_records (" +
-				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-				"family_id BIGINT NOT NULL COMMENT '家庭ID', " +
-				"member_id BIGINT NOT NULL COMMENT '成员ID', " +
-				"template_id BIGINT NOT NULL COMMENT '调研模板ID', " +
-				"answers_json TEXT COMMENT '答案JSON', " +
-				"submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '提交时间', " +
-				"period_start DATE COMMENT '周期开始', " +
-				"period_end DATE COMMENT '周期结束', " +
-				"INDEX idx_member (member_id), " +
-				"INDEX idx_template (template_id)" +
-				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='定期调研记录'"
-			);
-			log.info("已创建survey_records表");
-		} catch (Exception e) {
-			log.warn("创建survey_records表失败(可能已存在): " + e.getMessage());
-		}
-
-		// 迁移211: 创建 report_blocks 表(通用报告展示块,按报告类型存储 blocks JSON)
-		try {
-			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS report_blocks (" +
-					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-					"report_type VARCHAR(32) NOT NULL COMMENT 'gut_flora/dan/physical_exam/tongue', " +
-					"report_id BIGINT NOT NULL COMMENT '各类型报告主键ID', " +
-					"blocks JSON NOT NULL COMMENT '块数组 [{type,title,items,extra}]', " +
-					"version INT DEFAULT 1, " +
-					"created_at DATETIME, " +
-					"updated_at DATETIME, " +
-					"UNIQUE KEY uk_report_type_id (report_type, report_id)" +
-					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='报告通用展示块'");
-			log.info("已创建report_blocks表");
-		} catch (Exception e) {
-			// 表已存在,忽略错误
-		}
-
-		// 迁移212: users/consignees表添加省份/城市/区县/详细地址字段(个人信息所在地与订单联系人共享数据)
-		ensureColumn("users", "province", "VARCHAR(50) DEFAULT NULL COMMENT '省份'");
-		ensureColumn("users", "city", "VARCHAR(50) DEFAULT NULL COMMENT '城市'");
-		ensureColumn("users", "district", "VARCHAR(50) DEFAULT NULL COMMENT '区县'");
-		ensureColumn("users", "street", "VARCHAR(100) DEFAULT NULL COMMENT '详细地址'");
-		ensureColumn("consignees", "province", "VARCHAR(50) DEFAULT NULL COMMENT '省份'");
-		ensureColumn("consignees", "city", "VARCHAR(50) DEFAULT NULL COMMENT '城市'");
-		ensureColumn("consignees", "district", "VARCHAR(50) DEFAULT NULL COMMENT '区县'");
-		ensureColumn("consignees", "street", "VARCHAR(100) DEFAULT NULL COMMENT '详细地址'");
-
-		// 迁移213: micro_actions 添加维度字段(微行动×五维自检联动:按短板维度推荐今日行动)
-		ensureColumn("micro_actions", "dimension_code", "VARCHAR(32) DEFAULT NULL COMMENT '维度code: body/wisdom/wealth/action/mind'");
-		// 为既有12个行动补充维度映射(身体类→body,情绪类→mind)
-		try {
-			jdbcTemplate.execute("UPDATE micro_actions SET dimension_code = CASE code " +
-				"WHEN 'DRINK_WATER' THEN 'body' WHEN 'DEEP_BREATH' THEN 'mind' WHEN 'STRETCH' THEN 'body' " +
-				"WHEN 'LOOK_FAR' THEN 'body' WHEN 'SMILE' THEN 'mind' WHEN 'NECK_ROLL' THEN 'body' " +
-				"WHEN 'FIST_RELAX' THEN 'body' WHEN 'SIP_WATER' THEN 'body' WHEN 'BLINK' THEN 'body' " +
-				"WHEN 'ACUPRESSURE' THEN 'body' WHEN 'TIPTOE' THEN 'body' WHEN 'SHRUG' THEN 'body' " +
-				"ELSE dimension_code END WHERE dimension_code IS NULL OR dimension_code = ''");
-			log.info("迁移213: 已为既有微行动补充维度映射");
-		} catch (Exception e) {
-			log.warn("迁移213: 微行动维度映射更新失败: " + e.getMessage());
-		}
-
-		// 迁移214: 创建 nutrition_category 表(营养产品分类)
-		try {
-			jdbcTemplate.execute(
-				"CREATE TABLE IF NOT EXISTS nutrition_category (" +
-				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-				"name VARCHAR(50) NOT NULL COMMENT '分类名', " +
-				"parent_id BIGINT DEFAULT NULL COMMENT '父分类ID', " +
-				"sort_order INT DEFAULT 0 COMMENT '排序', " +
-				"create_time DATETIME DEFAULT CURRENT_TIMESTAMP" +
-				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养产品分类'"
-			);
-			log.info("已创建nutrition_category表");
-		} catch (Exception e) {
-			log.warn("创建nutrition_category表失败(可能已存在): " + e.getMessage());
-		}
-
-		// 迁移215: 创建 nutrition_brand 表(品牌)
-		try {
-			jdbcTemplate.execute(
-				"CREATE TABLE IF NOT EXISTS nutrition_brand (" +
-				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-				"name VARCHAR(100) NOT NULL COMMENT '品牌名', " +
-				"logo_url VARCHAR(500) DEFAULT NULL COMMENT '品牌Logo', " +
-				"description TEXT COMMENT '品牌简介', " +
-				"country VARCHAR(50) DEFAULT NULL COMMENT '产地国', " +
-				"create_time DATETIME DEFAULT CURRENT_TIMESTAMP" +
-				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养产品品牌'"
-			);
-			log.info("已创建nutrition_brand表");
-		} catch (Exception e) {
-			log.warn("创建nutrition_brand表失败(可能已存在): " + e.getMessage());
-		}
-
-		// 迁移216: 创建 nutrition_product 表(营养产品主表)
-		try {
-			jdbcTemplate.execute(
-				"CREATE TABLE IF NOT EXISTS nutrition_product (" +
-				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-				"name VARCHAR(200) NOT NULL COMMENT '产品名称', " +
-				"brand_id BIGINT DEFAULT NULL COMMENT '品牌ID', " +
-				"category_id BIGINT DEFAULT NULL COMMENT '分类ID', " +
-				"sub_category VARCHAR(50) DEFAULT NULL COMMENT '子分类', " +
-				"form VARCHAR(30) DEFAULT NULL COMMENT '剂型', " +
-				"net_content VARCHAR(50) DEFAULT NULL COMMENT '净含量', " +
-				"shelf_life VARCHAR(50) DEFAULT NULL COMMENT '保质期', " +
-				"suitable_for VARCHAR(200) DEFAULT NULL COMMENT '适用人群', " +
-				"image_url VARCHAR(500) DEFAULT NULL COMMENT '产品图', " +
-				"description TEXT COMMENT '产品描述', " +
-				"status TINYINT DEFAULT 1 COMMENT '状态 0下架/1上架', " +
-				"create_time DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-				"update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-				"INDEX idx_brand (brand_id), " +
-				"INDEX idx_category (category_id)" +
-				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养产品'"
-			);
-			log.info("已创建nutrition_product表");
-		} catch (Exception e) {
-			log.warn("创建nutrition_product表失败(可能已存在): " + e.getMessage());
-		}
-
-		// 迁移217: 创建 nutrition_ingredient 表(成分明细)
-		try {
-			jdbcTemplate.execute(
-				"CREATE TABLE IF NOT EXISTS nutrition_ingredient (" +
-				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-				"product_id BIGINT NOT NULL COMMENT '产品ID', " +
-				"ingredient_type VARCHAR(20) COMMENT '成分类型', " +
-				"name VARCHAR(100) NOT NULL COMMENT '成分名', " +
-				"strain VARCHAR(100) DEFAULT NULL COMMENT '菌株号', " +
-				"amount_per_serving DECIMAL(10,2) DEFAULT NULL COMMENT '每份含量', " +
-				"unit VARCHAR(20) DEFAULT NULL COMMENT '单位', " +
-				"daily_value VARCHAR(50) DEFAULT NULL COMMENT '每日参考值%', " +
-				"purpose VARCHAR(200) DEFAULT NULL COMMENT '功效说明', " +
-				"create_time DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-				"INDEX idx_product (product_id)" +
-				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养产品成分明细'"
-			);
-			log.info("已创建nutrition_ingredient表");
-		} catch (Exception e) {
-			log.warn("创建nutrition_ingredient表失败(可能已存在): " + e.getMessage());
-		}
-
-		// 迁移218: 创建 nutrition_platform_listing 表(平台在售信息)
-		try {
-			jdbcTemplate.execute(
-				"CREATE TABLE IF NOT EXISTS nutrition_platform_listing (" +
-				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-				"product_id BIGINT NOT NULL COMMENT '产品ID', " +
-				"platform VARCHAR(30) COMMENT '平台', " +
-				"platform_url VARCHAR(500) DEFAULT NULL COMMENT '商品链接', " +
-				"current_price DECIMAL(10,2) DEFAULT NULL COMMENT '当前价格', " +
-				"original_price DECIMAL(10,2) DEFAULT NULL COMMENT '原价/划线价', " +
-				"price_unit VARCHAR(30) DEFAULT NULL COMMENT '价格单位', " +
-				"price_per_unit DECIMAL(10,2) DEFAULT NULL COMMENT '每单位价格', " +
-				"sales_count INT DEFAULT NULL COMMENT '销量', " +
-				"stock_status VARCHAR(20) DEFAULT NULL COMMENT '库存状态', " +
-				"last_checked DATETIME DEFAULT NULL COMMENT '最后爬取时间', " +
-				"create_time DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-				"update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-				"INDEX idx_product (product_id), " +
-				"INDEX idx_platform (platform)" +
-				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养产品平台在售信息'"
-			);
-			log.info("已创建nutrition_platform_listing表");
-		} catch (Exception e) {
-			log.warn("创建nutrition_platform_listing表失败(可能已存在): " + e.getMessage());
-		}
-
-		// 迁移219: 创建 nutrition_product_review 表(评价)
-		try {
-			jdbcTemplate.execute(
-				"CREATE TABLE IF NOT EXISTS nutrition_product_review (" +
-				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-				"platform_listing_id BIGINT NOT NULL COMMENT '平台在售ID', " +
-				"source VARCHAR(30) DEFAULT NULL COMMENT '来源平台', " +
-				"rating DECIMAL(2,1) DEFAULT NULL COMMENT '评分', " +
-				"review_count INT DEFAULT NULL COMMENT '评价数', " +
-				"good_rate VARCHAR(10) DEFAULT NULL COMMENT '好评率', " +
-				"last_updated DATETIME DEFAULT NULL COMMENT '最后更新时间', " +
-				"INDEX idx_listing (platform_listing_id)" +
-				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营养产品评价'"
-			);
-			log.info("已创建nutrition_product_review表");
-		} catch (Exception e) {
-			log.warn("创建nutrition_product_review表失败(可能已存在): " + e.getMessage());
-		}
-
-		// 迁移220: 创建 file_record 表(文件去重+维护)
-		try {
-			jdbcTemplate.execute(
-				"CREATE TABLE IF NOT EXISTS file_record (" +
-				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-				"hash VARCHAR(64) NOT NULL COMMENT 'SHA-256 文件哈希', " +
-				"url VARCHAR(500) NOT NULL COMMENT '文件访问URL', " +
-				"file_size BIGINT NOT NULL COMMENT '文件大小(字节)', " +
-				"content_type VARCHAR(100) DEFAULT NULL COMMENT 'MIME类型', " +
-				"original_filename VARCHAR(255) DEFAULT NULL COMMENT '原始文件名', " +
-				"category VARCHAR(50) DEFAULT 'general' COMMENT '分类', " +
-				"description VARCHAR(500) DEFAULT NULL COMMENT '描述', " +
-				"created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-				"updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-				"UNIQUE KEY uk_hash (hash), " +
-				"INDEX idx_category (category), " +
-				"INDEX idx_created_at (created_at)" +
-				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件记录表(去重&维护)'"
-			);
-			log.info("已创建file_record表");
-		} catch (Exception e) {
-			log.warn("创建file_record表失败(可能已存在): " + e.getMessage());
-		}
-
-		// 迁移221: nutrition_product 添加 local_product_id 字段(关联本平台商品)
-		ensureColumn("nutrition_product", "local_product_id", "BIGINT DEFAULT NULL COMMENT '本平台商品ID,关联products.id'");
-
-// 迁移222: 创建 unlock_gates 表(通关关卡定义)
-		try {
-			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS unlock_gates (" +
-				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-				"gate_type VARCHAR(32) NOT NULL COMMENT 'SELF_CHECK/INVITE_FAMILY/CREATE_FAMILY_MEMBER/PURCHASE/MEMBERSHIP/REPORT_UPLOAD/MICRO_ACTION/STREAK_DAYS', " +
-				"name VARCHAR(50) NOT NULL COMMENT '关卡名称', " +
-				"description VARCHAR(200) COMMENT '关卡说明', " +
-				"icon VARCHAR(10) COMMENT '图标emoji', " +
-				"sort_order INT DEFAULT 0 COMMENT '排序号', " +
-				"enabled TINYINT DEFAULT 1 COMMENT '启用:1 禁用:0', " +
-				"is_required TINYINT DEFAULT 1 COMMENT '主线:1 增值:0', " +
-				"is_soft TINYINT DEFAULT 0 COMMENT '硬门禁:0 软引导:1', " +
-				"unlock_target VARCHAR(32) COMMENT 'PERSONAL_SANDBOX/FAMILY_SANDBOX/CHILD_MICRO_ACTION/...', " +
-				"condition_params VARCHAR(500) COMMENT '条件参数JSON', " +
-				"reward_points INT DEFAULT 0 COMMENT '通关奖励积分', " +
-				"reward_energy INT DEFAULT 0 COMMENT '通关奖励能量', " +
-				"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-				"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-				"INDEX idx_sort (enabled, is_required, is_soft, sort_order)" +
-				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通关解锁关卡定义'");
-			log.info("已创建unlock_gates表");
-		} catch (Exception e) {
-			log.warn("创建unlock_gates表失败(可能已存在): " + e.getMessage());
-		}
-
-		// 迁移223: 创建 family_gate_progress 表(家庭通关进度)
-		try {
-			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_gate_progress (" +
-				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-				"family_id BIGINT NOT NULL COMMENT '家庭ID', " +
-				"gate_id BIGINT NOT NULL COMMENT '关卡ID', " +
-				"status VARCHAR(16) DEFAULT 'LOCKED' COMMENT 'LOCKED/UNLOCKED', " +
-				"unlocked_by BIGINT COMMENT '解锁人user_id', " +
-				"unlocked_at DATETIME COMMENT '解锁时间', " +
-				"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-				"UNIQUE KEY uk_family_gate (family_id, gate_id)" +
-				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭关卡通关进度'");
-			log.info("已创建family_gate_progress表");
-
-			// 默认关卡种子数据(幂等:INSERT IGNORE)
-			jdbcTemplate.execute("INSERT IGNORE INTO unlock_gates (gate_type, name, description, icon, sort_order, enabled, is_required, is_soft, unlock_target, condition_params, reward_points, reward_energy) VALUES " +
-				"('SELF_CHECK', '五维自检', '完成15题五维自检,看见自己的能量状态', '📝', 1, 1, 1, 0, 'PERSONAL_SANDBOX', '{\"minCount\":1}', 60, 10), " +
-				"('INVITE_FAMILY', '邀请家人', '邀请家人加入,共同创建家庭数据', '👨‍👩‍👧‍👦', 2, 1, 1, 0, 'FAMILY_SANDBOX', '{}', 80, 15), " +
-				"('CREATE_FAMILY_MEMBER', '创建家人', '为孩子创建账号,开启家庭互动', '👶', 3, 1, 1, 0, 'CHILD_MICRO_ACTION', '{}', 50, 10), " +
-				"('MICRO_ACTION', '首次微行动', '完成一次微行动,养成每日小习惯', '🎯', 4, 1, 1, 1, 'MICRO_ACTION_DAILY', '{\"count\":1}', 50, 5), " +
-				"('STREAK_DAYS', '连续打卡3天', '连续打卡3天,获得里程碑奖励', '🔥', 5, 1, 1, 1, 'BADGE_MILESTONE', '{\"days\":3}', 30, 5), " +
-				"('SELF_CHECK', '复测对比', '完成第二次自检,看见前后变化', '📊', 6, 1, 1, 1, 'TREND_COMPARE', '{\"minCount\":2}', 60, 10), " +
-				"('PURCHASE', '首次购物', '在商城完成首次购物,解锁深度分析', '🛒', 7, 0, 0, 0, 'PREMIUM_MODULES', '{}', 150, 30), " +
-				"('MEMBERSHIP', '加入会员', '加入家庭会员,享受专属权益', '👑', 8, 0, 0, 0, 'PREMIUM_MODULES', '{}', 200, 50), " +
-				"('REPORT_UPLOAD', '健康报告上传', '上传健康报告,获取AI解读与洞察', '📋', 9, 0, 0, 0, 'HEALTH_INSIGHTS', '{\"minCount\":1}', 100, 20)");
-			log.info("已插入默认关卡种子数据");
-		} catch (Exception e) {
-			log.warn("创建family_gate_progress表或插入种子数据失败: " + e.getMessage());
-		}
-
-		// 迁移222: 创建 microbiome_article 表(菌群知识库文章管理)
-		try {
-			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS microbiome_article (" +
-					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-					"title VARCHAR(255) NOT NULL COMMENT '文章标题', " +
-					"content MEDIUMTEXT COMMENT '文章原文Markdown', " +
-					"category VARCHAR(50) DEFAULT '科普' COMMENT '分类', " +
-					"tags VARCHAR(500) COMMENT '逗号分隔标签', " +
-					"source_url VARCHAR(500) COMMENT '来源URL', " +
-					"pub_date VARCHAR(20) COMMENT '发布日期', " +
-					"status TINYINT DEFAULT 1 COMMENT '1=启用 0=禁用', " +
-					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-					"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-					"INDEX idx_category (category), " +
-					"INDEX idx_status (status), " +
-					"FULLTEXT INDEX ft_content (title, content)" +
-					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菌群知识库文章'");
-			log.info("已创建microbiome_article表");
-		} catch (Exception e) {
-			log.warn("创建microbiome_article表失败(可能已存在): " + e.getMessage());
-		}
-
-		// 迁移224: health_reports 添加 shannon_index 列(修复 Unknown column 错误)
-		ensureColumn("health_reports", "shannon_index", "VARCHAR(32) DEFAULT NULL COMMENT '香农指数'");
-
-		// 迁移225: nutrition_product 添加康美日记同步字段(价格/销量/来源/图片/规格)
-		ensureColumn("nutrition_product", "price", "DECIMAL(10,2) DEFAULT 0 COMMENT '当前售价'");
-		ensureColumn("nutrition_product", "original_price", "INT DEFAULT 0 COMMENT '原价(整数)'");
-		ensureColumn("nutrition_product", "sold_num", "INT DEFAULT 0 COMMENT '已售数量'");
-		ensureColumn("nutrition_product", "source_platform", "VARCHAR(50) DEFAULT '' COMMENT '来源平台'");
-		ensureColumn("nutrition_product", "source_id", "VARCHAR(100) DEFAULT '' COMMENT '来源平台商品ID'");
-		ensureColumn("nutrition_product", "source_url", "VARCHAR(500) DEFAULT '' COMMENT '来源平台商品链接'");
-		ensureColumn("nutrition_product", "product_images", "TEXT COMMENT '商品图片URL(JSON数组)'");
-		ensureColumn("nutrition_product", "spec", "VARCHAR(200) DEFAULT '' COMMENT '规格'");
-		// 来源平台+商品ID 索引(防止重复导入)
-		try {
-			jdbcTemplate.execute("CREATE INDEX idx_source ON nutrition_product(source_platform, source_id)");
-			log.info("已创建nutrition_product idx_source索引");
-		} catch (Exception e) {
-			log.warn("创建idx_source索引失败(可能已存在): " + e.getMessage());
-		}
-	}
-
-	/**
-	 * 导入全量全国省市区数据到 streets 表
-	 * 读取 classpath:region_data.json(31省/369市/2813区)
-	 * 仅在 streets 表省份级数据不足(<20 条)时执行
-	 */
-	private void seedFullRegionData() {
-		try {
-			Integer count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM streets WHERE level = 1", Integer.class);
-			if (count != null && count >= 20) {
-				log.info("全国省市区数据已存在({}个省份),跳过导入", count);
-				return;
-			}
-		} catch (Exception e) {
-			log.warn("检查省份数据失败,跳过全量导入: {}", e.getMessage());
-			return;
-		}
-
-		org.springframework.core.io.Resource resource = resourceLoader.getResource("classpath:region_data.json");
-		if (!resource.exists()) {
-			log.warn("region_data.json 不存在,跳过全量省市区数据导入");
-			return;
-		}
-
-		try {
-			String content = new String(StreamUtils.copyToByteArray(resource.getInputStream()), StandardCharsets.UTF_8);
-			com.alibaba.fastjson.JSONArray provinces = com.alibaba.fastjson.JSON.parseArray(content);
-
-			for (int pi = 0; pi < provinces.size(); pi++) {
-				com.alibaba.fastjson.JSONObject prov = provinces.getJSONObject(pi);
-				String provName = prov.getString("name");
-				String provCode = prov.getString("code");
-
-				java.sql.Connection conn = jdbcTemplate.getDataSource().getConnection();
-				java.sql.PreparedStatement ps = conn.prepareStatement(
-					"INSERT INTO streets (province, province_code, full_name, level, parent_id, sort_order) VALUES (?, ?, ?, 1, NULL, ?)",
-					java.sql.Statement.RETURN_GENERATED_KEYS
-				);
-				ps.setString(1, provName);
-				ps.setString(2, provCode);
-				ps.setString(3, provName);
-				ps.setInt(4, pi + 1);
-				ps.executeUpdate();
-				java.sql.ResultSet rs = ps.getGeneratedKeys();
-				long provId = 0;
-				if (rs.next()) provId = rs.getLong(1);
-				rs.close();
-				ps.close();
-
-				com.alibaba.fastjson.JSONArray cities = prov.getJSONArray("cities");
-				if (cities == null) continue;
-
-				for (int ci = 0; ci < cities.size(); ci++) {
-					com.alibaba.fastjson.JSONObject city = cities.getJSONObject(ci);
-					String cityName = city.getString("name");
-					String cityCode = city.getString("code");
-
-					java.sql.PreparedStatement ps2 = conn.prepareStatement(
-						"INSERT INTO streets (province, province_code, city, city_code, full_name, level, parent_id, sort_order) VALUES (?, ?, ?, ?, ?, 2, ?, ?)",
-						java.sql.Statement.RETURN_GENERATED_KEYS
-					);
-					ps2.setString(1, provName);
-					ps2.setString(2, provCode);
-					ps2.setString(3, cityName);
-					ps2.setString(4, cityCode);
-					ps2.setString(5, provName + cityName);
-					ps2.setLong(6, provId);
-					ps2.setInt(7, ci + 1);
-					ps2.executeUpdate();
-					java.sql.ResultSet rs2 = ps2.getGeneratedKeys();
-					long cityId = 0;
-					if (rs2.next()) cityId = rs2.getLong(1);
-					rs2.close();
-					ps2.close();
-
-					com.alibaba.fastjson.JSONArray districts = city.getJSONArray("districts");
-					if (districts == null) continue;
-
-					for (int di = 0; di < districts.size(); di++) {
-						com.alibaba.fastjson.JSONObject dist = districts.getJSONObject(di);
-						String distName = dist.getString("name");
-						String distCode = dist.getString("code");
-
-						java.sql.PreparedStatement ps3 = conn.prepareStatement(
-							"INSERT INTO streets (province, province_code, city, city_code, district, district_code, full_name, level, parent_id, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, 3, ?, ?)",
-							java.sql.Statement.RETURN_GENERATED_KEYS
-						);
-						ps3.setString(1, provName);
-						ps3.setString(2, provCode);
-						ps3.setString(3, cityName);
-						ps3.setString(4, cityCode);
-						ps3.setString(5, distName);
-						ps3.setString(6, distCode);
-						ps3.setString(7, provName + cityName + distName);
-						ps3.setLong(8, cityId);
-						ps3.setInt(9, di + 1);
-						ps3.executeUpdate();
-						ps3.close();
-					}
-				}
-				conn.close();
-			}
-			log.info("全国省市区数据导入完成:{}省/{}市/{}区", provinces.size(), 0, 0);
-		} catch (Exception e) {
-			log.warn("导入全国省市区数据失败: {}", e.getMessage());
-		}
 	}
 }

+ 38 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyPlatformBalance.java

@@ -0,0 +1,38 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("family_platform_balance")
+public class FamilyPlatformBalance implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long familyId;
+
+    /** 累计获得 */
+    private Integer totalEarned;
+
+    /** 可用余额 */
+    private Integer available;
+
+    /** 冻结(提现审核中) */
+    private Integer frozen;
+
+    /** 已提现 */
+    private Integer withdrawn;
+
+    /** 已兑换/消费 */
+    private Integer exchanged;
+
+    private Date updatedAt;
+
+    private Date createdAt;
+}

+ 36 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyPlatformBalanceLog.java

@@ -0,0 +1,36 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("family_platform_balance_log")
+public class FamilyPlatformBalanceLog implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long familyId;
+
+    /** earn/spend/freeze/unfreeze/withdraw/adjust/migration */
+    private String type;
+
+    private Integer amount;
+
+    /** 变动后可用余额 */
+    private Integer balanceAfter;
+
+    /** product_order/referral_dist/activity/withdrawal/adjust/migration/refund */
+    private String refType;
+
+    private Long refId;
+
+    private String remark;
+
+    private Date createdAt;
+}

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

@@ -55,6 +55,9 @@ public class Product implements Serializable {
     // 排序(值越小越靠前)
     private Integer sortOrder;
 
+    /** 平台积分倍数,1元=1积分×倍数 */
+    private java.math.BigDecimal pointsMultiplier;
+
     // 电商扩展字段
     private Long categoryId;               // 商品类目ID
     private Long distributionSystemId;     // 销售体系ID

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyPlatformBalanceLogMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FamilyPlatformBalanceLog;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface FamilyPlatformBalanceLogMapper extends BaseMapper<FamilyPlatformBalanceLog> {
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyPlatformBalanceMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FamilyPlatformBalance;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface FamilyPlatformBalanceMapper extends BaseMapper<FamilyPlatformBalance> {
+}

+ 226 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/FamilyPlatformPointsService.java

@@ -0,0 +1,226 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.entity.FamilyPlatformBalance;
+import com.etotem.cfc.entity.FamilyPlatformBalanceLog;
+import com.etotem.cfc.mapper.FamilyPlatformBalanceLogMapper;
+import com.etotem.cfc.mapper.FamilyPlatformBalanceMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 家庭平台积分池服务(CF值家庭维度)
+ * 仿 PlatformPointsService,键为 familyId
+ */
+@Service
+public class FamilyPlatformPointsService {
+
+    private static final String TYPE_EARN = "earn";
+    private static final String TYPE_SPEND = "spend";
+    private static final String TYPE_FREEZE = "freeze";
+    private static final String TYPE_UNFREEZE = "unfreeze";
+    private static final String TYPE_WITHDRAW = "withdraw";
+    private static final String TYPE_ADJUST = "adjust";
+
+    @Resource
+    private FamilyPlatformBalanceMapper balanceMapper;
+
+    @Resource
+    private FamilyPlatformBalanceLogMapper logMapper;
+
+    /** 获取或创建家庭的积分池记录(悲观锁) */
+    @Transactional
+    public FamilyPlatformBalance getOrCreate(Long familyId) {
+        FamilyPlatformBalance balance = balanceMapper.selectOne(
+                new LambdaQueryWrapper<FamilyPlatformBalance>()
+                        .eq(FamilyPlatformBalance::getFamilyId, familyId));
+        if (balance == null) {
+            balance = new FamilyPlatformBalance();
+            balance.setFamilyId(familyId);
+            balance.setTotalEarned(0);
+            balance.setAvailable(0);
+            balance.setFrozen(0);
+            balance.setWithdrawn(0);
+            balance.setExchanged(0);
+            balance.setCreatedAt(new Date());
+            balance.setUpdatedAt(new Date());
+            balanceMapper.insert(balance);
+        }
+        return balance;
+    }
+
+    private int safeInt(Integer v) {
+        return v == null ? 0 : v;
+    }
+
+    private void writeLog(Long familyId, String type, int amount,
+                          int balanceAfter, String refType, Long refId, String remark) {
+        FamilyPlatformBalanceLog log = new FamilyPlatformBalanceLog();
+        log.setFamilyId(familyId);
+        log.setType(type);
+        log.setAmount(amount);
+        log.setBalanceAfter(balanceAfter);
+        log.setRefType(refType);
+        log.setRefId(refId);
+        log.setRemark(remark);
+        log.setCreatedAt(new Date());
+        logMapper.insert(log);
+    }
+
+    /** 家庭获得 CF 值(幂等:按 ref_type+ref_id 去重) */
+    @Transactional
+    public void earn(Long familyId, int amount, String refType, Long refId, String remark) {
+        if (amount <= 0) return;
+        // 幂等检查
+        Long existing = logMapper.selectCount(
+                new LambdaQueryWrapper<FamilyPlatformBalanceLog>()
+                        .eq(FamilyPlatformBalanceLog::getFamilyId, familyId)
+                        .eq(FamilyPlatformBalanceLog::getRefType, refType)
+                        .eq(FamilyPlatformBalanceLog::getRefId, refId));
+        if (existing != null && existing > 0) return;
+
+        FamilyPlatformBalance b = getOrCreate(familyId);
+        b.setTotalEarned(safeInt(b.getTotalEarned()) + amount);
+        b.setAvailable(safeInt(b.getAvailable()) + amount);
+        b.setUpdatedAt(new Date());
+        balanceMapper.updateById(b);
+        writeLog(familyId, TYPE_EARN, amount, safeInt(b.getAvailable()),
+                refType, refId, remark);
+    }
+
+    /** 消费家庭可用 CF 值,余额不足抛异常 */
+    @Transactional
+    public void spend(Long familyId, int amount, String refType, Long refId, String remark) {
+        if (amount <= 0) {
+            throw new IllegalArgumentException("积分抵扣数量必须为正数");
+        }
+        FamilyPlatformBalance b = getOrCreate(familyId);
+        int available = safeInt(b.getAvailable());
+        if (available < amount) {
+            throw new RuntimeException("家庭积分不足,当前可用 " + available + " 积分");
+        }
+        b.setAvailable(available - amount);
+        b.setExchanged(safeInt(b.getExchanged()) + amount);
+        b.setUpdatedAt(new Date());
+        balanceMapper.updateById(b);
+        writeLog(familyId, TYPE_SPEND, amount, safeInt(b.getAvailable()),
+                refType, refId, remark);
+    }
+
+    /** 提现申请:可用 → 冻结 */
+    @Transactional
+    public void freeze(Long familyId, int amount, Long refId) {
+        if (amount <= 0) {
+            throw new IllegalArgumentException("提现金额必须为正数");
+        }
+        FamilyPlatformBalance b = getOrCreate(familyId);
+        int available = safeInt(b.getAvailable());
+        if (available < amount) {
+            throw new RuntimeException("家庭积分不足");
+        }
+        b.setAvailable(available - amount);
+        b.setFrozen(safeInt(b.getFrozen()) + amount);
+        b.setUpdatedAt(new Date());
+        balanceMapper.updateById(b);
+        writeLog(familyId, TYPE_FREEZE, amount, safeInt(b.getAvailable()),
+                "withdrawal", refId, "提现冻结");
+    }
+
+    /** 提现审核拒绝:冻结 → 可用 */
+    @Transactional
+    public void unfreeze(Long familyId, int amount, Long refId) {
+        if (amount <= 0) {
+            throw new IllegalArgumentException("提现金额必须为正数");
+        }
+        FamilyPlatformBalance b = getOrCreate(familyId);
+        int frozen = safeInt(b.getFrozen());
+        if (frozen < amount) {
+            throw new RuntimeException("冻结积分不足");
+        }
+        b.setFrozen(frozen - amount);
+        b.setAvailable(safeInt(b.getAvailable()) + amount);
+        b.setUpdatedAt(new Date());
+        balanceMapper.updateById(b);
+        writeLog(familyId, TYPE_UNFREEZE, amount, safeInt(b.getAvailable()),
+                "withdrawal", refId, "提现审核拒绝解冻");
+    }
+
+    /** 提现审核通过:冻结 → 已提现 */
+    @Transactional
+    public void confirmWithdraw(Long familyId, int amount, Long refId) {
+        if (amount <= 0) {
+            throw new IllegalArgumentException("提现金额必须为正数");
+        }
+        FamilyPlatformBalance b = getOrCreate(familyId);
+        int frozen = safeInt(b.getFrozen());
+        if (frozen < amount) {
+            throw new RuntimeException("冻结积分不足");
+        }
+        b.setFrozen(frozen - amount);
+        b.setWithdrawn(safeInt(b.getWithdrawn()) + amount);
+        b.setUpdatedAt(new Date());
+        balanceMapper.updateById(b);
+        writeLog(familyId, TYPE_WITHDRAW, amount, safeInt(b.getAvailable()),
+                "withdrawal", refId, "提现成功");
+    }
+
+    /** 管理端手动调整(正负均可) */
+    @Transactional
+    public void adjust(Long familyId, int amount, String remark) {
+        FamilyPlatformBalance b = getOrCreate(familyId);
+        b.setAvailable(Math.max(0, safeInt(b.getAvailable()) + amount));
+        if (amount > 0) {
+            b.setTotalEarned(safeInt(b.getTotalEarned()) + amount);
+        }
+        b.setUpdatedAt(new Date());
+        balanceMapper.updateById(b);
+        writeLog(familyId, TYPE_ADJUST, Math.abs(amount), safeInt(b.getAvailable()),
+                "adjust", null, remark);
+    }
+
+    /** 查询家庭 CF 值余额 */
+    public Map<String, Object> getBalance(Long familyId) {
+        FamilyPlatformBalance b = getOrCreate(familyId);
+        Map<String, Object> result = new HashMap<>();
+        result.put("familyId", familyId);
+        result.put("available", safeInt(b.getAvailable()));
+        result.put("frozen", safeInt(b.getFrozen()));
+        result.put("withdrawn", safeInt(b.getWithdrawn()));
+        result.put("exchanged", safeInt(b.getExchanged()));
+        result.put("totalEarned", safeInt(b.getTotalEarned()));
+        return result;
+    }
+
+    /** 查询可提现余额 */
+    public Integer getWithdrawable(Long familyId) {
+        FamilyPlatformBalance b = getOrCreate(familyId);
+        return safeInt(b.getAvailable());
+    }
+
+    /** 积分流水分页 */
+    public Page<FamilyPlatformBalanceLog> getLogs(Long familyId, Integer page, Integer size) {
+        Page<FamilyPlatformBalanceLog> pageParam = new Page<>(page, size);
+        return logMapper.selectPage(pageParam,
+                new LambdaQueryWrapper<FamilyPlatformBalanceLog>()
+                        .eq(FamilyPlatformBalanceLog::getFamilyId, familyId)
+                        .orderByDesc(FamilyPlatformBalanceLog::getCreatedAt));
+    }
+
+    /** 查询管理员端家庭池列表(分页) */
+    public Page<FamilyPlatformBalance> listPage(Integer page, Integer size, Long familyId) {
+        Page<FamilyPlatformBalance> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<FamilyPlatformBalance> wrapper = new LambdaQueryWrapper<>();
+        if (familyId != null) {
+            wrapper.eq(FamilyPlatformBalance::getFamilyId, familyId);
+        }
+        wrapper.orderByDesc(FamilyPlatformBalance::getUpdatedAt);
+        return balanceMapper.selectPage(pageParam, wrapper);
+    }
+}

+ 24 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -20,6 +20,7 @@ import com.etotem.cfc.entity.ProductPickupTimeSlot;
 import com.etotem.cfc.entity.ProductServicePerson;
 import com.etotem.cfc.entity.ProductServicePersonSlot;
 import com.etotem.cfc.entity.User;
+import com.etotem.cfc.service.FamilyPlatformPointsService;
 import com.etotem.cfc.mapper.AfterSalesRequestMapper;
 import com.etotem.cfc.entity.SupplySystemMember;
 import com.etotem.cfc.mapper.SupplySystemMemberMapper;
@@ -122,6 +123,9 @@ public class ProductOrderService {
     @Resource
     private PlatformPointsService platformPointsService;
 
+    @Resource
+    private FamilyPlatformPointsService familyPlatformPointsService;
+
     @Resource
     private SysConfigService sysConfigService;
 
@@ -809,6 +813,26 @@ public class ProductOrderService {
         }
         order.setUpdatedAt(new Date());
         orderMapper.updateById(order);
+
+        // 家庭池 CF 值发放:floor(实付金额(元)) × points_multiplier
+        try {
+            Integer moneyAmount = order.getMoneyAmount();
+            if (moneyAmount != null && moneyAmount > 0 && order.getFamilyId() != null) {
+                Product p = productMapper.selectById(order.getProductId());
+                if (p != null && p.getPointsMultiplier() != null) {
+                    int earned = (moneyAmount / 100) * p.getPointsMultiplier().intValue();
+                    if (earned > 0) {
+                        familyPlatformPointsService.earn(
+                                order.getFamilyId(), earned,
+                                "product_order", order.getId(),
+                                "购买商品: " + order.getProductName());
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("家庭池CF值发放失败: orderId={}, error={}", order.getId(), e.getMessage());
+        }
+
         return Result.success("已确认收货");
     }
 

+ 35 - 12
cfc-backend/src/main/java/com/etotem/cfc/service/WithdrawalService.java

@@ -22,6 +22,12 @@ public class WithdrawalService {
     @Resource
     private PlatformPointsService platformPointsService;
 
+    @Resource
+    private FamilyMemberService familyMemberService;
+
+    @Resource
+    private FamilyPlatformPointsService familyPlatformPointsService;
+
     public Result<String> apply(Long userId, Integer amount, String accountInfo) {
         if (amount == null || amount <= 0) {
             return Result.error("提现金额必须大于0");
@@ -32,9 +38,14 @@ public class WithdrawalService {
         if (accountInfo == null || accountInfo.trim().isEmpty()) {
             return Result.error("请填写账户信息");
         }
-        Integer available = platformPointsService.getWithdrawable(userId);
+        // 从用户获取归属家庭ID
+        Long familyId = resolveFamilyId(userId);
+        if (familyId == null) {
+            return Result.error("未找到归属家庭,请先加入家庭后再提现");
+        }
+        Integer available = familyPlatformPointsService.getWithdrawable(familyId);
         if (available < amount) {
-            return Result.error("可提现CF值不足,当前可提现 ¥" + String.format("%.2f", available / 100.0));
+            return Result.error("家庭积分不足,当前可提现 ¥" + String.format("%.2f", available / 100.0));
         }
         WithdrawalRequest request = new WithdrawalRequest();
         request.setUserId(userId);
@@ -44,9 +55,9 @@ public class WithdrawalService {
         request.setType("platform_points");
         request.setCreatedAt(new Date());
         withdrawalRequestMapper.insert(request);
-        // 冻结 CF 值(以提现申请 ID 作为流水关联)
+        // 冻结家庭池 CF 值(以提现申请 ID 作为流水关联)
         try {
-            platformPointsService.freeze(userId, amount, request.getId());
+            familyPlatformPointsService.freeze(familyId, amount, request.getId());
         } catch (Exception e) {
             request.setStatus("rejected");
             request.setRemark("CF值冻结失败");
@@ -56,6 +67,15 @@ public class WithdrawalService {
         return Result.success("提现申请已提交,等待审核");
     }
 
+    /** 从用户查找归属家庭ID */
+    @Resource
+    private com.etotem.cfc.mapper.UserMapper userMapper;
+
+    private Long resolveFamilyId(Long userId) {
+        com.etotem.cfc.entity.User user = userMapper.selectById(userId);
+        return user != null ? user.getFamilyId() : null;
+    }
+
     public void audit(Long requestId, Long adminId, String status, String remark) {
         WithdrawalRequest request = withdrawalRequestMapper.selectById(requestId);
         if (request == null) {
@@ -65,14 +85,17 @@ public class WithdrawalService {
             throw new RuntimeException("该申请已处理");
         }
 
-        if ("approved".equals(status)) {
-            platformPointsService.confirmWithdraw(request.getUserId(), request.getAmount(), requestId);
-            request.setStatus("processed");
-            request.setRemark("提现成功");
-        } else if ("rejected".equals(status)) {
-            platformPointsService.unfreeze(request.getUserId(), request.getAmount(), requestId);
-            request.setStatus("rejected");
-            request.setRemark("提现审核拒绝");
+        Long familyId = resolveFamilyId(request.getUserId());
+        if (familyId != null) {
+            if ("approved".equals(status)) {
+                familyPlatformPointsService.confirmWithdraw(familyId, request.getAmount(), requestId);
+                request.setStatus("processed");
+                request.setRemark("提现成功");
+            } else if ("rejected".equals(status)) {
+                familyPlatformPointsService.unfreeze(familyId, request.getAmount(), requestId);
+                request.setStatus("rejected");
+                request.setRemark("提现审核拒绝");
+            }
         }
 
         request.setAuditBy(adminId);

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

@@ -1099,6 +1099,7 @@ CREATE TABLE IF NOT EXISTS products (
     dimension_weights JSON DEFAULT NULL COMMENT '五维权重百分比: {"body":0,"mind":0,"wisdom":0,"action":0,"wealth":0}',
     profit_rate INT DEFAULT 0 COMMENT '平台利润率(千分比)',
     sort_order INT DEFAULT 0 COMMENT '排序(值越小越靠前)',
+    points_multiplier DECIMAL(4,2) NOT NULL DEFAULT 1.00 COMMENT '平台积分倍数,1元=1积分×倍数',
     delivery_method TINYINT DEFAULT 1 COMMENT '配送方式: 1=快递 2=自提 3=两者皆可 4=线上-可选人 5=线上-单人',
     purchase_count_threshold INT DEFAULT 0 COMMENT '最小购买数量限制',
     category_id BIGINT COMMENT '商品类目ID',
@@ -4434,6 +4435,36 @@ CREATE TABLE IF NOT EXISTS platform_balance_log (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CF值流水';
 
 
+
+-- ===== 家庭平台积分池(CF值家庭维度)=====
+
+CREATE TABLE IF NOT EXISTS family_platform_balance (
+    id            BIGINT AUTO_INCREMENT PRIMARY KEY,
+    family_id     BIGINT       NOT NULL COMMENT '家庭ID',
+    total_earned  INT DEFAULT 0 COMMENT '累计获得',
+    available     INT DEFAULT 0 COMMENT '可用余额',
+    frozen        INT DEFAULT 0 COMMENT '冻结(提现审核中)',
+    withdrawn     INT DEFAULT 0 COMMENT '已提现',
+    exchanged     INT DEFAULT 0 COMMENT '已兑换(抵扣/积分消耗)',
+    created_at    DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at    DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_family (family_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭平台积分池余额';
+
+CREATE TABLE IF NOT EXISTS family_platform_balance_log (
+    id            BIGINT AUTO_INCREMENT PRIMARY KEY,
+    family_id     BIGINT       NOT NULL COMMENT '家庭ID',
+    type          VARCHAR(20)  NOT NULL COMMENT 'earn/spend/freeze/unfreeze/withdraw/adjust/migration',
+    amount        INT          NOT NULL COMMENT '变动数量',
+    balance_after INT          NOT NULL COMMENT '变动后可用余额',
+    ref_type      VARCHAR(50)  COMMENT 'product_order/referral_dist/activity/withdrawal/adjust/migration/refund',
+    ref_id        BIGINT       COMMENT '关联业务ID',
+    remark        VARCHAR(255) COMMENT '备注',
+    created_at    DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_family (family_id),
+    INDEX idx_ref (ref_type, ref_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭平台积分流水';
+
 -- ===== 健康方案汇总 =====
 
 CREATE TABLE IF NOT EXISTS health_plans (

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-83627694477f59b482a5336bd5135994740f0635
+147b42d5e5701573214536a1b7897d542c3c75dd

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1014",
+  "version": "1.0.1015",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1014",
+      "version": "1.0.1015",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1015",
+  "version": "1.0.1016",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 6 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,12 @@
 
 ---
 
+## v1.0.1016 (2026-08-12)
+
+### Bug 修复
+- 导入全量全国省市区数据到 streets 表(31省/369市/2813区)
+
+
 ## v1.0.1015 (2026-08-12)
 
 ### 新功能

+ 7 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1015
+> 当前版本: v1.0.1016
 
 ## 历史版本
 
@@ -8,6 +8,12 @@
 
 ---
 
+## v1.0.1016 (2026-08-12)
+
+### Bug 修复
+- 导入全量全国省市区数据到 streets 表(31省/369市/2813区)
+
+
 ## v1.0.1015 (2026-08-12)
 
 ### 新功能