Browse Source

Phase1: 与 E:\cfc 统一会员身份,营作为 cfc 活动容器接入

- 引入 dynamic-datasource,train 主库 + cfc 同库(zxyj) 双数据源
- 新增 CfcUser/CfcActivity 实体与 Mapper,@DS(cfc) 直连 cfc 表
- 登录:按 openid 在 cfc users 表查找/创建统一会员,回写 train_user.cfc_user_id
- 建班:同步在 cfc activities 表创建活动内容,回写 train_class.activity_id
- JWT 密钥与 cfc 对齐,便于营员 token 与 cfc 体系互通
liaoxg 2 weeks ago
parent
commit
b20c487

+ 7 - 0
train-backend/pom.xml

@@ -47,6 +47,13 @@
             <version>${mybatis-plus.version}</version>
             <version>${mybatis-plus.version}</version>
         </dependency>
         </dependency>
 
 
+        <!-- 多数据源(train 主库 + cfc 从库) -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+            <version>3.6.1</version>
+        </dependency>
+
         <!-- MySQL -->
         <!-- MySQL -->
         <dependency>
         <dependency>
             <groupId>mysql</groupId>
             <groupId>mysql</groupId>

+ 104 - 0
train-backend/src/main/java/com/train/cfc/entity/CfcActivity.java

@@ -0,0 +1,104 @@
+package com.train.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;
+
+/**
+ * cfc 活动容器(activities 表,位于 cfc 同库 zxyj)。
+ * 训练营(班级)作为一条 cfc 活动内容出现;字段与 E:\cfc 的 Activity 实体对齐。
+ * 通过 @DS("cfc") 路由到 cfc 数据源。
+ */
+@Data
+@TableName("activities")
+public class CfcActivity implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String title;
+
+    private String description;
+
+    private String coverImage;
+
+    /** body/mind/wisdom/action/wealth */
+    private String dimensionCode;
+
+    /** JSON: {"body":0,"mind":0,"wisdom":0,"action":0,"wealth":0} */
+    private String dimensionWeights;
+
+    /** offline/online/campaign */
+    private String activityType;
+
+    /** draft(草稿)/pending(待审核)/rejected(已驳回)/published(发布中)/withdrawn(已撤回) */
+    private String status;
+
+    /** approved/pending/rejected */
+    private String auditStatus;
+
+    private String auditReason;
+
+    private Long auditorId;
+
+    private Date auditedAt;
+
+    private Date startTime;
+
+    private Date endTime;
+
+    private String location;
+
+    private Integer maxParticipants;
+
+    private Integer currentParticipants;
+
+    private Integer price;
+
+    private Integer memberPrice;
+
+    private Date registrationDeadline;
+
+    private Integer childFee;
+
+    private Integer childMemberFee;
+
+    private Integer parentFee;
+
+    private Integer parentMemberFee;
+
+    /** 创建人userId(普通用户发布者, admin创建可为空) */
+    private Long creatorId;
+
+    private Integer familyScope;
+
+    /** 签到积分(0=使用系统默认) */
+    private Integer checkinPoints;
+
+    /** 活动奖励CF值(0=不发) */
+    private Integer platformPoints;
+
+    /** 是否需要报名(0=否,1=是) */
+    private Integer requireRegistration;
+
+    /** 是否需要审核签到(0=否,1=是) */
+    private Integer requireCheckinReview;
+
+    /** 未报名签到扣减能量值(正数) */
+    private Integer energyPenaltyUnregistered;
+
+    /** public(公开)/restricted(私密)/all(默认)/member */
+    private String visibility;
+
+    private Long vendorId;
+
+    private String vendorName;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 70 - 0
train-backend/src/main/java/com/train/cfc/entity/CfcUser.java

@@ -0,0 +1,70 @@
+package com.train.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * cfc 统一会员身份(users 表,位于 cfc 同库 zxyj)。
+ * 字段与 E:\cfc 的 User 实体对齐(仅本营所需子集),供 train 直接读写 cfc 会员。
+ * 通过 @DS("cfc") 路由到 cfc 数据源。
+ */
+@Data
+@TableName("users")
+public class CfcUser implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String openid;
+
+    private String unionid;
+
+    private Long familyId;
+
+    /** parent/child/teacher */
+    private String role;
+
+    private String nickname;
+
+    private String avatar;
+
+    private String realName;
+
+    private String phone;
+
+    /** 佣金系统 - 推荐人ID */
+    private Long referrerId;
+
+    /** 佣金系统 - 个人邀请码(6位字母数字) */
+    private String referralCode;
+
+    /** 会员等级: FREE/FAMILY/PROVIDER */
+    private String memberLevel;
+
+    /** 家长积分(孩子没有积分,用积分奖励家长) */
+    private Integer totalPoints;
+
+    @TableField("session_key")
+    private String sessionKey;
+
+    private Date birthday;
+
+    private String gender;
+
+    private String mascot;
+
+    private Boolean isFamilyAdmin;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+
+    @TableField("deleted")
+    private Integer deleted;
+}

+ 12 - 0
train-backend/src/main/java/com/train/cfc/mapper/CfcActivityMapper.java

@@ -0,0 +1,12 @@
+package com.train.cfc.mapper;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.train.cfc.entity.CfcActivity;
+
+/**
+ * cfc 活动容器(activities 表)Mapper,路由到 cfc 数据源。
+ */
+@DS("cfc")
+public interface CfcActivityMapper extends BaseMapper<CfcActivity> {
+}

+ 12 - 0
train-backend/src/main/java/com/train/cfc/mapper/CfcUserMapper.java

@@ -0,0 +1,12 @@
+package com.train.cfc.mapper;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.train.cfc.entity.CfcUser;
+
+/**
+ * cfc 统一会员(users 表)Mapper,路由到 cfc 数据源。
+ */
+@DS("cfc")
+public interface CfcUserMapper extends BaseMapper<CfcUser> {
+}

+ 31 - 0
train-backend/src/main/java/com/train/controller/admin/AdminClassController.java

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.train.common.Result;
 import com.train.common.Result;
 import com.train.entity.*;
 import com.train.entity.*;
 import com.train.mapper.*;
 import com.train.mapper.*;
+import com.train.cfc.entity.CfcActivity;
+import com.train.cfc.mapper.CfcActivityMapper;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
@@ -24,6 +26,8 @@ public class AdminClassController {
     private TrainUserMapper trainUserMapper;
     private TrainUserMapper trainUserMapper;
     @Resource
     @Resource
     private TrainGroupMemberMapper trainGroupMemberMapper;
     private TrainGroupMemberMapper trainGroupMemberMapper;
+    @Resource
+    private CfcActivityMapper cfcActivityMapper;
 
 
     // ============ 班级 ============
     // ============ 班级 ============
     @Operation(summary = "创建班级")
     @Operation(summary = "创建班级")
@@ -34,6 +38,33 @@ public class AdminClassController {
         }
         }
         c.setStatus("active");
         c.setStatus("active");
         trainClassMapper.insert(c);
         trainClassMapper.insert(c);
+        // 营 = cfc 活动:在 cfc 同库 activities 表中同步创建一条活动内容
+        try {
+            CfcActivity activity = new CfcActivity();
+            activity.setTitle(c.getName());
+            activity.setDescription("家立方-AI管家成长营班次:" + c.getName());
+            activity.setActivityType("offline");
+            activity.setDimensionCode("wealth,health,growth");
+            activity.setStatus("published");
+            activity.setAuditStatus("approved");
+            activity.setLocation(c.getPlace());
+            activity.setMaxParticipants(c.getCapacity());
+            activity.setCheckinPoints(5);
+            activity.setPlatformPoints(10);
+            activity.setRequireRegistration(1);
+            activity.setVisibility("restricted");
+            java.util.Date now = new java.util.Date();
+            activity.setCreatedAt(now);
+            activity.setUpdatedAt(now);
+            cfcActivityMapper.insert(activity);
+            c.setActivityId(activity.getId());
+            trainClassMapper.updateById(c);
+            c.setActivityId(activity.getId());
+        } catch (Exception e) {
+            // 同步 cfc 活动失败不阻塞建班,仅记录
+            org.slf4j.LoggerFactory.getLogger(getClass())
+                    .warn("同步 cfc 活动失败: {}", e.getMessage());
+        }
         return Result.success(c);
         return Result.success(c);
     }
     }
 
 

+ 31 - 0
train-backend/src/main/java/com/train/controller/auth/AuthController.java

@@ -5,6 +5,8 @@ import com.train.config.JwtConfig;
 import com.train.entity.TrainUser;
 import com.train.entity.TrainUser;
 import com.train.mapper.TrainClassMapper;
 import com.train.mapper.TrainClassMapper;
 import com.train.mapper.TrainUserMapper;
 import com.train.mapper.TrainUserMapper;
+import com.train.cfc.entity.CfcUser;
+import com.train.cfc.mapper.CfcUserMapper;
 import com.train.dto.LoginResultDTO;
 import com.train.dto.LoginResultDTO;
 import com.train.dto.AlumniVerifyDTO;
 import com.train.dto.AlumniVerifyDTO;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Operation;
@@ -18,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.bind.annotation.RestController;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 import java.util.UUID;
 import java.util.UUID;
@@ -34,6 +37,9 @@ public class AuthController {
     @Resource
     @Resource
     private TrainClassMapper trainClassMapper;
     private TrainClassMapper trainClassMapper;
 
 
+    @Resource
+    private CfcUserMapper cfcUserMapper;
+
     @Resource
     @Resource
     private JwtConfig jwtConfig;
     private JwtConfig jwtConfig;
 
 
@@ -42,6 +48,8 @@ public class AuthController {
 
 
     /**
     /**
      * 微信登录(用 code 换取 openid;test-mode 下用模拟 openid)
      * 微信登录(用 code 换取 openid;test-mode 下用模拟 openid)
+     * <p>统一会员身份:以 cfc 同库的 users 表为准,按 openid 查找/创建 cfc 会员,
+     * 并将 cfcUser.id 回写至本库 train_user.cfc_user_id,实现与 cfc 体系同一会员。
      */
      */
     @Operation(summary = "微信code登录")
     @Operation(summary = "微信code登录")
     @PostMapping("/wechat-login")
     @PostMapping("/wechat-login")
@@ -57,6 +65,24 @@ public class AuthController {
             openid = mockOpenidByCode(code); // 生产接入微信 jscode2session
             openid = mockOpenidByCode(code); // 生产接入微信 jscode2session
         }
         }
 
 
+        // 1) 在 cfc 同库确保统一会员身份
+        CfcUser cfcUser = cfcUserMapper.selectOne(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CfcUser>()
+                        .eq(CfcUser::getOpenid, openid)
+                        .last("LIMIT 1"));
+        if (cfcUser == null) {
+            cfcUser = new CfcUser();
+            cfcUser.setOpenid(openid);
+            cfcUser.setRole("parent");
+            cfcUser.setNickname("营员" + openid.substring(Math.max(0, openid.length() - 6)));
+            cfcUser.setMemberLevel("FREE");
+            cfcUser.setTotalPoints(0);
+            cfcUser.setCreatedAt(new Date());
+            cfcUser.setUpdatedAt(new Date());
+            cfcUserMapper.insert(cfcUser);
+        }
+
+        // 2) 本库 train_user 作为训练营运营档案,回写 cfcUser.id
         TrainUser user = trainUserMapper.selectOne(
         TrainUser user = trainUserMapper.selectOne(
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TrainUser>()
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TrainUser>()
                         .eq(TrainUser::getOpenid, openid)
                         .eq(TrainUser::getOpenid, openid)
@@ -64,12 +90,17 @@ public class AuthController {
         boolean isNew = false;
         boolean isNew = false;
         if (user == null) {
         if (user == null) {
             user = new TrainUser();
             user = new TrainUser();
+            user.setCfcUserId(cfcUser.getId());
             user.setOpenid(openid);
             user.setOpenid(openid);
             user.setAlumniVerify("pending");
             user.setAlumniVerify("pending");
             user.setRole("student");
             user.setRole("student");
             user.setStatus("active");
             user.setStatus("active");
             trainUserMapper.insert(user);
             trainUserMapper.insert(user);
             isNew = true;
             isNew = true;
+        } else if (user.getCfcUserId() == null) {
+            // 存量营员补齐 cfc 关联,保持统一身份
+            user.setCfcUserId(cfcUser.getId());
+            trainUserMapper.updateById(user);
         }
         }
 
 
         String token = jwtConfig.generateToken(user.getId(), user.getRole());
         String token = jwtConfig.generateToken(user.getId(), user.getRole());

+ 3 - 0
train-backend/src/main/java/com/train/entity/TrainClass.java

@@ -15,6 +15,9 @@ public class TrainClass implements Serializable {
     @TableId(type = IdType.AUTO)
     @TableId(type = IdType.AUTO)
     private Long id;
     private Long id;
 
 
+    /** 关联的 cfc 活动 ID(activities.id,位于 cfc 同库) */
+    private Long activityId;
+
     private String name;
     private String name;
     private String code;
     private String code;
     private String time;
     private String time;

+ 3 - 0
train-backend/src/main/java/com/train/entity/TrainUser.java

@@ -15,6 +15,9 @@ public class TrainUser implements Serializable {
     @TableId(type = IdType.AUTO)
     @TableId(type = IdType.AUTO)
     private Long id;
     private Long id;
 
 
+    /** cfc 统一会员 ID(users.id,位于 cfc 同库) */
+    private Long cfcUserId;
+
     private String openid;
     private String openid;
     private String phone;
     private String phone;
     private String name;
     private String name;

+ 32 - 12
train-backend/src/main/resources/application.yml

@@ -15,17 +15,36 @@ spring:
   application:
   application:
     name: train-backend
     name: train-backend
   datasource:
   datasource:
-    driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://localhost:3306/train?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
-    username: root
-    password: root
-    hikari:
-      maximum-pool-size: 50
-      minimum-idle: 10
-      connection-timeout: 30000
-      idle-timeout: 600000
-      max-lifetime: 1800000
-      pool-name: TrainHikariPool
+    dynamic:
+      primary: master
+      strict: false
+      datasource:
+        # train 主库:训练营自身的运营表
+        master:
+          driver-class-name: com.mysql.cj.jdbc.Driver
+          url: ${TRAIN_DB_URL:jdbc:mysql://localhost:3306/train?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true}
+          username: ${TRAIN_DB_USERNAME:root}
+          password: ${TRAIN_DB_PASSWORD:root}
+          hikari:
+            maximum-pool-size: 50
+            minimum-idle: 10
+            connection-timeout: 30000
+            idle-timeout: 600000
+            max-lifetime: 1800000
+            pool-name: TrainHikariPool
+        # cfc 从库:承接统一会员身份、活动容器、能量/财富账本(与 E:\cfc 同库)
+        cfc:
+          driver-class-name: com.mysql.cj.jdbc.Driver
+          url: ${CFC_DB_URL:jdbc:mysql://192.168.16.251:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true}
+          username: ${CFC_DB_USERNAME:zxyj}
+          password: ${CFC_DB_PASSWORD:zxyj@123}
+          hikari:
+            maximum-pool-size: 20
+            minimum-idle: 5
+            connection-timeout: 30000
+            idle-timeout: 600000
+            max-lifetime: 1800000
+            pool-name: CfcHikariPool
 
 
 mybatis-plus:
 mybatis-plus:
   mapper-locations: classpath*:/mapper/**/*.xml
   mapper-locations: classpath*:/mapper/**/*.xml
@@ -38,7 +57,8 @@ mybatis-plus:
       id-type: auto
       id-type: auto
 
 
 jwt:
 jwt:
-  secret: ${JWT_SECRET:train-prod-jwt-secret-2026-keep-it-safe!}
+  # 与 cfc(E:\cfc)对齐同一密钥,使 camp 学员 token 可与 cfc 体系互通
+  secret: ${JWT_SECRET:cfc-prod-jwt-secret-2026-keep-it-safe!}
   expiration: 86400000
   expiration: 86400000
 
 
 wechat:
 wechat:

+ 11 - 2
train-backend/src/main/resources/schema.sql

@@ -4,6 +4,7 @@
 -- 班次表
 -- 班次表
 CREATE TABLE IF NOT EXISTS train_class (
 CREATE TABLE IF NOT EXISTS train_class (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    activity_id BIGINT COMMENT '关联 cfc 活动 ID(activities.id,cfc 同库)',
     name VARCHAR(100) NOT NULL COMMENT '班次名称',
     name VARCHAR(100) NOT NULL COMMENT '班次名称',
     code VARCHAR(20) UNIQUE COMMENT '班级码(学员进班用)',
     code VARCHAR(20) UNIQUE COMMENT '班级码(学员进班用)',
     time VARCHAR(100) COMMENT '培训时间描述',
     time VARCHAR(100) COMMENT '培训时间描述',
@@ -12,12 +13,14 @@ CREATE TABLE IF NOT EXISTS train_class (
     status VARCHAR(20) DEFAULT 'draft' COMMENT 'draft/active/finished',
     status VARCHAR(20) DEFAULT 'draft' COMMENT 'draft/active/finished',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    INDEX idx_code (code)
+    INDEX idx_code (code),
+    INDEX idx_activity_id (activity_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班次';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班次';
 
 
 -- 学员用户表
 -- 学员用户表
 CREATE TABLE IF NOT EXISTS train_user (
 CREATE TABLE IF NOT EXISTS train_user (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    cfc_user_id BIGINT COMMENT 'cfc 统一会员 ID(users.id,cfc 同库)',
     openid VARCHAR(64) COMMENT '微信openid',
     openid VARCHAR(64) COMMENT '微信openid',
     phone VARCHAR(11) COMMENT '手机号',
     phone VARCHAR(11) COMMENT '手机号',
     name VARCHAR(50) COMMENT '姓名',
     name VARCHAR(50) COMMENT '姓名',
@@ -35,9 +38,15 @@ CREATE TABLE IF NOT EXISTS train_user (
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     INDEX idx_openid (openid),
     INDEX idx_openid (openid),
     INDEX idx_phone (phone),
     INDEX idx_phone (phone),
-    INDEX idx_class_id (class_id)
+    INDEX idx_class_id (class_id),
+    INDEX idx_cfc_user_id (cfc_user_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学员用户';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学员用户';
 
 
+-- 兼容存量库:为新版本新增的 cfc 关联列补列(列已存在时静默跳过)
+-- 下方语句由 DatabaseInitializer 逐条执行并容错,重复执行不报致命错误
+ALTER TABLE train_class ADD COLUMN activity_id BIGINT COMMENT '关联 cfc 活动 ID(activities.id,cfc 同库)';
+ALTER TABLE train_user ADD COLUMN cfc_user_id BIGINT COMMENT 'cfc 统一会员 ID(users.id,cfc 同库)';
+
 -- 小组表
 -- 小组表
 CREATE TABLE IF NOT EXISTS train_group (
 CREATE TABLE IF NOT EXISTS train_group (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
     id BIGINT AUTO_INCREMENT PRIMARY KEY,