فهرست منبع

feat: 浠宝/福宝IP形象系统 - 用户可选择AI助手形象,所有AI对话携带形象标识

User 2 ماه پیش
والد
کامیت
b614abe682

+ 8 - 243
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -1937,249 +1937,6 @@ log.info("已添加template_id列到tasks表");
             log.warn("检查/添加 users 表 show_to_family 列失败: {}", e.getMessage());
         }
 
-        // ==================== DanShop 电商模块建表 ====================
-        // 1. danshop_categories (商品分类表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_categories (" +
-                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                "parent_id BIGINT DEFAULT NULL COMMENT '父分类ID(null=一级)', " +
-                "name VARCHAR(64) NOT NULL COMMENT '分类名称', " +
-                "image VARCHAR(512) DEFAULT NULL COMMENT '分类图标URL', " +
-                "sort_order INT DEFAULT 0 COMMENT '排序(越小越靠前)', " +
-                "energy_dimension_id BIGINT DEFAULT NULL COMMENT '关联五维维度ID', " +
-                "status INT DEFAULT 1 COMMENT '1启用/0禁用', " +
-                "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-                "INDEX idx_parent (parent_id), " +
-                "INDEX idx_status (status)" +
-                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品分类表'");
-            log.info("已创建 danshop_categories 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_categories 表失败: {}", e.getMessage());
-        }
-
-        // 2. danshop_products (商品表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_products (" +
-                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                "category_id BIGINT NOT NULL COMMENT '所属分类ID', " +
-                "name VARCHAR(128) NOT NULL COMMENT '商品名称', " +
-                "description TEXT COMMENT '商品详细描述', " +
-                "cover_image VARCHAR(512) COMMENT '封面图URL', " +
-                "images TEXT COMMENT '多图(JSON数组)', " +
-                "price INT NOT NULL COMMENT '售价(分)', " +
-                "member_price INT DEFAULT NULL COMMENT '会员价(分)', " +
-                "cost_price INT DEFAULT NULL COMMENT '成本价(分)', " +
-                "stock INT DEFAULT 0 COMMENT '库存数量', " +
-                "sales_count INT DEFAULT 0 COMMENT '销量', " +
-                "product_type VARCHAR(32) NOT NULL COMMENT '类型: physical/virtual/coupon/course', " +
-                "vendor_id BIGINT DEFAULT NULL COMMENT '供应商ID', " +
-                "vendor_name VARCHAR(64) COMMENT '供应商名称', " +
-                "status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/approved/rejected/on_shelf/off_shelf', " +
-                "reject_reason VARCHAR(255) DEFAULT NULL COMMENT '审核拒绝原因', " +
-                "profit_rate INT DEFAULT 0 COMMENT '平台利润率(bps,100=1%)', " +
-                "external_source VARCHAR(32) COMMENT '外部商品来源', " +
-                "external_id VARCHAR(64) COMMENT '外部商品ID', " +
-                "external_data TEXT COMMENT '外部商品完整数据(JSON)', " +
-                "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-                "INDEX idx_category (category_id), " +
-                "INDEX idx_vendor (vendor_id), " +
-                "INDEX idx_status (status)" +
-                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'");
-            log.info("已创建 danshop_products 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_products 表失败: {}", e.getMessage());
-        }
-
-        // 3. danshop_orders (主订单表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_orders (" +
-                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                "order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号(DS+时间戳+6位随机)', " +
-                "buyer_id BIGINT NOT NULL COMMENT '买家ID', " +
-                "family_id BIGINT DEFAULT NULL COMMENT '家庭ID', " +
-                "total_amount INT NOT NULL COMMENT '订单总金额(分)', " +
-                "discount_amount INT DEFAULT 0 COMMENT '优惠金额(分)', " +
-                "actual_amount INT NOT NULL COMMENT '实付金额(分)', " +
-                "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid/shipped/completed/refunding/refunded/cancelled', " +
-                "payment_method VARCHAR(16) DEFAULT 'wechat' COMMENT 'wechat/alipay', " +
-                "transaction_id VARCHAR(64) DEFAULT NULL COMMENT '微信/支付宝交易号', " +
-                "paid_at DATETIME DEFAULT NULL COMMENT '支付时间', " +
-                "shipped_at DATETIME DEFAULT NULL COMMENT '发货时间', " +
-                "completed_at DATETIME DEFAULT NULL COMMENT '完成时间', " +
-                "remark VARCHAR(255) DEFAULT NULL COMMENT '买家备注', " +
-                "cancel_reason VARCHAR(255) DEFAULT NULL COMMENT '取消原因', " +
-                "receiver_name VARCHAR(32) DEFAULT NULL COMMENT '收件人姓名', " +
-                "receiver_phone VARCHAR(16) DEFAULT NULL COMMENT '收件人电话', " +
-                "receiver_address VARCHAR(256) DEFAULT NULL COMMENT '收件人地址', " +
-                "freight INT DEFAULT 0 COMMENT '运费(分)', " +
-                "profit_amount INT DEFAULT 0 COMMENT '分润金额(分)', " +
-                "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-                "INDEX idx_buyer (buyer_id), " +
-                "INDEX idx_family (family_id), " +
-                "INDEX idx_status (status), " +
-                "INDEX idx_order_no (order_no)" +
-                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主订单表'");
-            log.info("已创建 danshop_orders 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_orders 表失败: {}", e.getMessage());
-        }
-
-        // 4. danshop_order_items (订单明细表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_order_items (" +
-                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                "order_id BIGINT NOT NULL COMMENT '所属订单ID', " +
-                "product_id BIGINT NOT NULL COMMENT '商品ID', " +
-                "product_name VARCHAR(128) NOT NULL COMMENT '商品名称(快照)', " +
-                "cover_image VARCHAR(512) COMMENT '封面图(快照)', " +
-                "unit_price INT NOT NULL COMMENT '单价(分,快照)', " +
-                "price INT NOT NULL COMMENT '单价(分,别名兼容)', " +
-                "quantity INT NOT NULL DEFAULT 1 COMMENT '购买数量', " +
-                "subtotal INT NOT NULL COMMENT '小计(分)', " +
-                "product_type VARCHAR(32) COMMENT '商品类型(快照)', " +
-                "vendor_id BIGINT COMMENT '供应商ID(快照)', " +
-                "vendor_name VARCHAR(64) COMMENT '供应商名称(快照)', " +
-                "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                "INDEX idx_order (order_id), " +
-                "INDEX idx_product (product_id)" +
-                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表'");
-            log.info("已创建 danshop_order_items 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_order_items 表失败: {}", e.getMessage());
-        }
-
-        // 5. danshop_order_logs (订单操作日志表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_order_logs (" +
-                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                "order_id BIGINT NOT NULL COMMENT '订单ID', " +
-                "action VARCHAR(32) NOT NULL COMMENT '操作: create/pay/ship/confirm/refund/cancel', " +
-                "operator_id BIGINT DEFAULT NULL COMMENT '操作人ID', " +
-                "operator_type VARCHAR(16) DEFAULT NULL COMMENT 'buyer/vendor/admin/system', " +
-                "detail TEXT DEFAULT NULL COMMENT '操作详情(JSON)', " +
-                "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                "INDEX idx_order (order_id)" +
-                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单操作日志表'");
-            log.info("已创建 danshop_order_logs 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_order_logs 表失败: {}", e.getMessage());
-        }
-
-        // 6. danshop_logistics (物流信息表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_logistics (" +
-                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                "order_id BIGINT NOT NULL COMMENT '订单ID', " +
-                "order_item_id BIGINT DEFAULT NULL COMMENT '订单明细ID', " +
-                "logistics_no VARCHAR(64) NOT NULL COMMENT '物流单号', " +
-                "logistics_company VARCHAR(32) NOT NULL COMMENT '物流公司编码: sf/yto/zt/sto/yd/ttkd', " +
-                "logistics_name VARCHAR(32) DEFAULT NULL COMMENT '物流公司名称', " +
-                "sender_name VARCHAR(32) DEFAULT NULL COMMENT '发货人姓名', " +
-                "sender_phone VARCHAR(16) DEFAULT NULL COMMENT '发货人电话', " +
-                "sender_address VARCHAR(256) DEFAULT NULL COMMENT '发货人地址', " +
-                "receiver_name VARCHAR(32) NOT NULL COMMENT '收件人姓名', " +
-                "receiver_phone VARCHAR(16) NOT NULL COMMENT '收件人电话', " +
-                "receiver_address VARCHAR(256) NOT NULL COMMENT '收件人地址', " +
-                "status VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/shipped/signing/completed', " +
-                "tracking_data TEXT DEFAULT NULL COMMENT '物流轨迹(JSON数组)', " +
-                "shipped_at DATETIME DEFAULT NULL COMMENT '发货时间', " +
-                "signed_at DATETIME DEFAULT NULL COMMENT '签收时间', " +
-                "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-                "INDEX idx_order (order_id), " +
-                "INDEX idx_logistics_no (logistics_no), " +
-                "INDEX idx_status (status)" +
-                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物流信息表'");
-            log.info("已创建 danshop_logistics 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_logistics 表失败: {}", e.getMessage());
-        }
-
-        // 7. danshop_profit_rules (分润规则配置表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_profit_rules (" +
-                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                "rule_name VARCHAR(64) NOT NULL COMMENT '规则名称', " +
-                "rule_type VARCHAR(32) NOT NULL COMMENT '规则类型: product_category/vendor_level/global', " +
-                "target_id BIGINT DEFAULT NULL COMMENT '关联目标ID', " +
-                "platform_profit_rate INT NOT NULL COMMENT '平台利润率(bps,100=1%)', " +
-                "referrer_l1_rate INT DEFAULT 0 COMMENT '直接推荐人分润比例(bps,占利润%)', " +
-                "referrer_l2_rate INT DEFAULT 0 COMMENT '间接推荐人分润比例(bps,占利润%)', " +
-                "platform_service_rate INT DEFAULT 0 COMMENT '平台服务费率(bps,占订单金额%)', " +
-                "is_active INT DEFAULT 1 COMMENT '1启用/0禁用', " +
-                "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-                "INDEX idx_type (rule_type, target_id), " +
-                "INDEX idx_active (is_active)" +
-                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分润规则配置表'");
-            log.info("已创建 danshop_profit_rules 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_profit_rules 表失败: {}", e.getMessage());
-        }
-
-        // 8. danshop_profit_logs (分润流水表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_profit_logs (" +
-                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                "order_id BIGINT NOT NULL COMMENT '订单ID', " +
-                "order_no VARCHAR(32) NOT NULL COMMENT '订单号', " +
-                "order_amount INT NOT NULL COMMENT '订单实付金额(分)', " +
-                "cost_amount INT DEFAULT 0 COMMENT '成本金额(分)', " +
-                "profit_amount INT NOT NULL COMMENT '利润金额(分)', " +
-                "platform_retained INT NOT NULL COMMENT '平台留存金额(分)', " +
-                "vendor_income INT NOT NULL COMMENT '供应商实收金额(分)', " +
-                "commission_l1 INT DEFAULT 0 COMMENT 'L1推荐人佣金(分)', " +
-                "commission_l2 INT DEFAULT 0 COMMENT 'L2推荐人佣金(分)', " +
-                "rule_id BIGINT DEFAULT NULL COMMENT '使用的分润规则ID', " +
-                "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                "INDEX idx_order (order_id), " +
-                "INDEX idx_order_no (order_no)" +
-                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分润流水表'");
-            log.info("已创建 danshop_profit_logs 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_profit_logs 表失败: {}", e.getMessage());
-        }
-
-        // 9. danshop_favorites (商品收藏表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_favorites (" +
-                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                "user_id BIGINT NOT NULL COMMENT '用户ID', " +
-                "product_id BIGINT NOT NULL COMMENT '商品ID', " +
-                "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                "UNIQUE INDEX idx_user_product (user_id, product_id), " +
-                "INDEX idx_user (user_id)" +
-                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品收藏表'");
-            log.info("已创建 danshop_favorites 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_favorites 表失败: {}", e.getMessage());
-        }
-
-        // 10. danshop_addresses (收货地址表)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS danshop_addresses (" +
-                "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                "user_id BIGINT NOT NULL COMMENT '用户ID', " +
-                "receiver_name VARCHAR(32) NOT NULL COMMENT '收货人姓名', " +
-                "phone VARCHAR(16) NOT NULL COMMENT '收货人电话', " +
-                "province VARCHAR(32) NOT NULL COMMENT '省份', " +
-                "city VARCHAR(32) NOT NULL COMMENT '城市', " +
-                "district VARCHAR(32) NOT NULL COMMENT '区县', " +
-                "street VARCHAR(256) NOT NULL COMMENT '详细地址', " +
-                "is_default INT DEFAULT 0 COMMENT '是否默认地址 1=是 0=否', " +
-                "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
-                "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-                "INDEX idx_user (user_id), " +
-                "INDEX idx_user_default (user_id, is_default)" +
-                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收货地址表'");
-            log.info("已创建 danshop_addresses 表");
-        } catch (Exception e) {
-            log.warn("创建 danshop_addresses 表失败: {}", e.getMessage());
-        }
-
         // ==================== 迁移: activities表添加visible_scope列 ====================
         try {
             Integer colExists = jdbcTemplate.queryForObject(
@@ -2673,6 +2430,14 @@ log.info("已添加template_id列到tasks表");
         } catch (Exception e) {
             log.warn("旧维度评分数据迁移跳过: {}", e.getMessage());
         }
+
+        // 迁移: users表添加mascot字段(AI助手形象)
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN mascot VARCHAR(20) COMMENT 'AI助手形象: xibao(浠宝-女孩)/fubao(福宝-男孩)'");
+            log.info("已添加mascot列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
     }
 
     private void seedHealthNorms() {

+ 40 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java

@@ -8,6 +8,7 @@ import com.etotem.cfc.service.AIService;
 import com.etotem.cfc.service.FamilyContextService;
 import com.etotem.cfc.service.HealthAnalysisService;
 import com.etotem.cfc.service.RecommendationService;
+import com.etotem.cfc.service.UserService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.extern.slf4j.Slf4j;
@@ -43,6 +44,9 @@ public class AIChatController {
     @Resource
     private com.etotem.cfc.service.GrowthTaskService growthTaskService;
 
+    @Resource
+    private UserService userService;
+
     @Operation(summary = "发送聊天消息(支持传入reportId以解读报告)")
     @PostMapping("/chat/send")
     public Result<Map<String, Object>> sendMessage(
@@ -57,6 +61,10 @@ public class AIChatController {
             return Result.error("消息不能为空");
         }
 
+        // 获取用户mascot设置
+        com.etotem.cfc.entity.User user = userService.getUserInfo(userId);
+        String mascotCode = user != null ? user.getMascot() : null;
+
         // 组装家庭上下文(如果指定reportId/surveyId,注入报告+问卷数据)
         Long reportId = null;
         Long surveyId = null;
@@ -75,6 +83,20 @@ public class AIChatController {
             inputs = familyContextService.buildContext(userId);
         }
 
+        // 注入mascot信息到Dify inputs
+        if (mascotCode != null && !mascotCode.isEmpty()) {
+            com.etotem.cfc.enums.MascotEnum mascot = com.etotem.cfc.enums.MascotEnum.fromCode(mascotCode);
+            if (mascot != null) {
+                inputs.put("mascot_name", mascot.name());
+                inputs.put("mascot_gender", mascot.gender());
+                inputs.put("mascot_persona", mascot.persona());
+            }
+        } else {
+            inputs.put("mascot_name", "小助手");
+            inputs.put("mascot_gender", "");
+            inputs.put("mascot_persona", "");
+        }
+
         // 调用 Dify
         Map<String, Object> difyResp = aiService.sendMessage(
                 query, String.valueOf(userId),
@@ -140,6 +162,10 @@ public class AIChatController {
             return Result.error("需要reportId进行营养分析");
         }
 
+        // 获取用户mascot设置
+        com.etotem.cfc.entity.User user = userService.getUserInfo(userId);
+        String mascotCode = user != null ? user.getMascot() : null;
+
         Long reportId = Long.valueOf(reportIdStr);
 
         // 1. 菌群分析
@@ -152,6 +178,20 @@ public class AIChatController {
         Map<String, Object> inputs = familyContextService.buildContext(userId);
         inputs.put("health_analysis", analysis);
 
+        // 注入mascot信息到Dify inputs
+        if (mascotCode != null && !mascotCode.isEmpty()) {
+            com.etotem.cfc.enums.MascotEnum mascot = com.etotem.cfc.enums.MascotEnum.fromCode(mascotCode);
+            if (mascot != null) {
+                inputs.put("mascot_name", mascot.name());
+                inputs.put("mascot_gender", mascot.gender());
+                inputs.put("mascot_persona", mascot.persona());
+            }
+        } else {
+            inputs.put("mascot_name", "小助手");
+            inputs.put("mascot_gender", "");
+            inputs.put("mascot_persona", "");
+        }
+
         // 3. 调用精准营养助手
         Map<String, Object> difyResp = aiService.sendNutritionMessage(
                 query, String.valueOf(userId), conversationId, inputs);

+ 54 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/user/UserMascotController.java

@@ -0,0 +1,54 @@
+package com.etotem.cfc.controller.user;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.service.UserService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/user/mascot")
+public class UserMascotController {
+
+    @Resource
+    private UserService userService;
+
+    @PostMapping("/set")
+    public Result<String> setMascot(@RequestAttribute("userId") Long userId,
+                                    @RequestBody Map<String, Object> params) {
+        Object mascotObj = params.get("mascot");
+        String mascotCode = mascotObj != null ? mascotObj.toString() : null;
+
+        // Validate: only allow "xibao", "fubao", or null
+        if (mascotCode != null && !mascotCode.isEmpty()
+                && !"xibao".equals(mascotCode) && !"fubao".equals(mascotCode)) {
+            return Result.error("无效的吉祥物代码,仅支持 xibao/fubao");
+        }
+
+        if (mascotCode != null && mascotCode.isEmpty()) {
+            mascotCode = null;
+        }
+
+        userService.setMascot(userId, mascotCode);
+        return Result.success("设置成功");
+    }
+
+    @PostMapping("/get")
+    public Result<Map<String, Object>> getMascot(@RequestAttribute("userId") Long userId) {
+        User user = userService.getUserInfo(userId);
+        Map<String, Object> result = new HashMap<>();
+        if (user != null) {
+            result.put("mascot", user.getMascot());
+        } else {
+            result.put("mascot", null);
+        }
+        return Result.success(result);
+    }
+}

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

@@ -91,6 +91,9 @@ public class User implements Serializable {
     // 是否对家庭成员可见: 1=可见, 0=不可见
     private Integer showToFamily;
 
+    // AI助手形象: xibao(浠宝-女孩)/fubao(福宝-男孩)/null(未选择)
+    private String mascot;
+
     // 家庭管理员标记
     private Boolean isFamilyAdmin;
 

+ 50 - 0
cfc-backend/src/main/java/com/etotem/cfc/enums/MascotEnum.java

@@ -0,0 +1,50 @@
+package com.etotem.cfc.enums;
+
+/**
+ * 吉祥物(IP形象)枚举
+ * 用于注入AI对话上下文,让Dify感知用户选择的mascot角色
+ */
+public enum MascotEnum {
+
+    XIBAO("xibao", "女", "温柔可爱的浠宝小女孩"),
+    FUBAO("fubao", "男", "聪明活泼的福宝小男孩");
+
+    private final String code;
+    private final String gender;
+    private final String persona;
+
+    MascotEnum(String code, String gender, String persona) {
+        this.code = code;
+        this.gender = gender;
+        this.persona = persona;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public String gender() {
+        return gender;
+    }
+
+    public String persona() {
+        return persona;
+    }
+
+    /** 返回中文显示名称 */
+    public String displayName() {
+        return this == XIBAO ? "浠宝" : "福宝";
+    }
+
+    public static MascotEnum fromCode(String code) {
+        if (code == null || code.isEmpty()) {
+            return null;
+        }
+        for (MascotEnum m : values()) {
+            if (m.code.equals(code)) {
+                return m;
+            }
+        }
+        return null;
+    }
+}

+ 15 - 21
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -43,9 +43,6 @@ private VerificationCodeService verificationCodeService;
 @Resource
 private WechatService wechatService;
 
-    @Resource
-    private DanshopSyncService danshopSyncService;
-
     @Resource
     private OnboardingService onboardingService;
 
@@ -96,7 +93,6 @@ private WechatService wechatService;
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
         populateTeacherFields(result, user);
-        syncToDanshop(result, user);
 
         return result;
     }
@@ -263,7 +259,6 @@ private WechatService wechatService;
         // 新用户需要完善信息(仅当需要选择角色时)
         result.setNeedsRoleSelect(isNewUser);
         populateTeacherFields(result, user);
-        syncToDanshop(result, user);
 
         return result;
     }
@@ -303,7 +298,6 @@ private WechatService wechatService;
         result.setIsNewUser(false);
         result.setOpenid(openid);
         populateTeacherFields(result, user);
-        syncToDanshop(result, user);
 
         return result;
     }
@@ -1185,6 +1179,21 @@ private WechatService wechatService;
         return userMapper.updateById(user) > 0;
     }
 
+    /**
+     * 设置用户的AI助手形象(吉祥物)
+     * @param userId 用户ID
+     * @param mascotCode 吉祥物代码: "xibao"/"fubao"/null
+     */
+    public void setMascot(Long userId, String mascotCode) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+        user.setMascot(mascotCode);
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+    }
+
     private void populateTeacherFields(LoginResultDTO result, User user) {
         result.setTeacherStatus(user.getTeacherStatus() != null ? user.getTeacherStatus() : "pending");
         result.setTeacherRejectReason(user.getTeacherRejectReason());
@@ -1192,19 +1201,4 @@ private WechatService wechatService;
         result.setHasTeacherRole(hasTeacher);
     }
 
-    /**
-     * 同步用户到 danshop,设置 danshop token
-     */
-    private void syncToDanshop(LoginResultDTO result, User user) {
-        try {
-            Map<String, String> danshopToken = danshopSyncService.syncMember(user);
-            if (danshopToken != null) {
-                result.setDanshopAccessToken(danshopToken.get("accessToken"));
-                result.setDanshopRefreshToken(danshopToken.get("refreshToken"));
-            }
-        } catch (Exception e) {
-            // Don't fail login if danshop sync fails
-            System.err.println("Danshop sync failed for user " + user.getId() + ": " + e.getMessage());
-        }
-    }
 }

+ 104 - 1
cfc-frontend/pages/profile/profile.vue

@@ -19,6 +19,24 @@
       <!-- 用户卡片 + 富沛能量 + 切换 -->
       <ProfileHeader />
 
+      <!-- AI小助手形象选择 -->
+      <view class="mascot-selector">
+        <view class="selector-header">
+          <text class="selector-title">AI小助手</text>
+          <text class="selector-subtitle">{{ currentMascotName }}为你服务</text>
+        </view>
+        <view class="mascot-cards">
+          <view class="mascot-card" :class="{active: selectedMascot === 'xibao'}" @click="changeMascot('xibao')">
+            <view class="card-icon xibao">浠</view>
+            <text class="card-name">浠宝</text>
+          </view>
+          <view class="mascot-card" :class="{active: selectedMascot === 'fubao'}" @click="changeMascot('fubao')">
+            <view class="card-icon fubao">福</view>
+            <text class="card-name">福宝</text>
+          </view>
+        </view>
+      </view>
+
       <!-- 行动数据 + 财富数据 -->
       <ProfileStats />
 
@@ -56,7 +74,9 @@ export default {
   data() {
     return {
       shareData: null,
-      showTabTransition: false
+      showTabTransition: false,
+      selectedMascot: '',
+      currentMascotName: '小助手'
     }
   },
   computed: {
@@ -85,6 +105,7 @@ export default {
     }, 1100)
 
     if (!uni.getStorageSync('token')) return
+    this.loadMascot()
     const currentRole = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
     if (currentRole === 'teacher') {
       uni.redirectTo({ url: '/pages/teacher/teacher-profile' })
@@ -96,6 +117,29 @@ export default {
     },
     onInviteGenerate(shareData) {
       this.shareData = shareData
+    },
+    changeMascot(mascot) {
+      if (this.selectedMascot === mascot) return;
+      uni.request({
+        url: this.$config.API_BASE_URL + '/api/user/mascot/set',
+        method: 'POST',
+        header: { Authorization: 'Bearer ' + uni.getStorageSync('token') },
+        data: { mascot },
+        success: (res) => {
+          if (res.data.code === 200 || res.data.code === 0) {
+            this.selectedMascot = mascot;
+            this.currentMascotName = mascot === 'xibao' ? '浠宝' : '福宝';
+            uni.showToast({ title: '已更换' });
+          }
+        }
+      });
+    },
+    loadMascot() {
+      var userInfo = uni.getStorageSync('userInfo');
+      if (userInfo && userInfo.mascot) {
+        this.selectedMascot = userInfo.mascot;
+        this.currentMascotName = userInfo.mascot === 'xibao' ? '浠宝' : '福宝';
+      }
     }
   }
 }
@@ -127,4 +171,63 @@ export default {
   padding: 24rpx;
   line-height: 1.5;
 }
+
+.mascot-selector {
+  margin: 20rpx 0;
+  padding: 24rpx;
+  background: #fff;
+  border-radius: 16rpx;
+}
+.selector-header {
+  margin-bottom: 20rpx;
+}
+.selector-title {
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #1F2937;
+}
+.selector-subtitle {
+  font-size: 24rpx;
+  color: #6366F1;
+  margin-left: 16rpx;
+}
+.mascot-cards {
+  display: flex;
+  gap: 20rpx;
+}
+.mascot-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 24rpx 0;
+  border: 2rpx solid #E5E7EB;
+  border-radius: 12rpx;
+}
+.mascot-card.active {
+  border-color: #6366F1;
+  background: #EEF2FF;
+}
+.card-icon {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #fff;
+  margin-bottom: 12rpx;
+}
+.card-icon.xibao {
+  background: linear-gradient(135deg, #FF8C42, #FF6B9D);
+}
+.card-icon.fubao {
+  background: linear-gradient(135deg, #6366F1, #10B981);
+}
+.card-name {
+  font-size: 26rpx;
+  color: #374151;
+}
 </style>