Quellcode durchsuchen

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 vor 2 Wochen
Ursprung
Commit
b20c487377

+ 7 - 0
train-backend/pom.xml

@@ -47,6 +47,13 @@
             <version>${mybatis-plus.version}</version>
         </dependency>
 
+        <!-- 多数据源(train 主库 + cfc 从库) -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+            <version>3.6.1</version>
+        </dependency>
+
         <!-- MySQL -->
         <dependency>
             <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.entity.*;
 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.tags.Tag;
 import org.springframework.web.bind.annotation.*;
@@ -24,6 +26,8 @@ public class AdminClassController {
     private TrainUserMapper trainUserMapper;
     @Resource
     private TrainGroupMemberMapper trainGroupMemberMapper;
+    @Resource
+    private CfcActivityMapper cfcActivityMapper;
 
     // ============ 班级 ============
     @Operation(summary = "创建班级")
@@ -34,6 +38,33 @@ public class AdminClassController {
         }
         c.setStatus("active");
         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);
     }
 

+ 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.mapper.TrainClassMapper;
 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.AlumniVerifyDTO;
 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 javax.annotation.Resource;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
@@ -34,6 +37,9 @@ public class AuthController {
     @Resource
     private TrainClassMapper trainClassMapper;
 
+    @Resource
+    private CfcUserMapper cfcUserMapper;
+
     @Resource
     private JwtConfig jwtConfig;
 
@@ -42,6 +48,8 @@ public class AuthController {
 
     /**
      * 微信登录(用 code 换取 openid;test-mode 下用模拟 openid)
+     * <p>统一会员身份:以 cfc 同库的 users 表为准,按 openid 查找/创建 cfc 会员,
+     * 并将 cfcUser.id 回写至本库 train_user.cfc_user_id,实现与 cfc 体系同一会员。
      */
     @Operation(summary = "微信code登录")
     @PostMapping("/wechat-login")
@@ -57,6 +65,24 @@ public class AuthController {
             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(
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<TrainUser>()
                         .eq(TrainUser::getOpenid, openid)
@@ -64,12 +90,17 @@ public class AuthController {
         boolean isNew = false;
         if (user == null) {
             user = new TrainUser();
+            user.setCfcUserId(cfcUser.getId());
             user.setOpenid(openid);
             user.setAlumniVerify("pending");
             user.setRole("student");
             user.setStatus("active");
             trainUserMapper.insert(user);
             isNew = true;
+        } else if (user.getCfcUserId() == null) {
+            // 存量营员补齐 cfc 关联,保持统一身份
+            user.setCfcUserId(cfcUser.getId());
+            trainUserMapper.updateById(user);
         }
 
         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)
     private Long id;
 
+    /** 关联的 cfc 活动 ID(activities.id,位于 cfc 同库) */
+    private Long activityId;
+
     private String name;
     private String code;
     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)
     private Long id;
 
+    /** cfc 统一会员 ID(users.id,位于 cfc 同库) */
+    private Long cfcUserId;
+
     private String openid;
     private String phone;
     private String name;

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

@@ -15,17 +15,36 @@ spring:
   application:
     name: train-backend
   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:
   mapper-locations: classpath*:/mapper/**/*.xml
@@ -38,7 +57,8 @@ mybatis-plus:
       id-type: auto
 
 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
 
 wechat:

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

@@ -4,6 +4,7 @@
 -- 班次表
 CREATE TABLE IF NOT EXISTS train_class (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    activity_id BIGINT COMMENT '关联 cfc 活动 ID(activities.id,cfc 同库)',
     name VARCHAR(100) NOT NULL COMMENT '班次名称',
     code VARCHAR(20) UNIQUE 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',
     created_at DATETIME DEFAULT 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='班次';
 
 -- 学员用户表
 CREATE TABLE IF NOT EXISTS train_user (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    cfc_user_id BIGINT COMMENT 'cfc 统一会员 ID(users.id,cfc 同库)',
     openid VARCHAR(64) COMMENT '微信openid',
     phone VARCHAR(11) 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,
     INDEX idx_openid (openid),
     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='学员用户';
 
+-- 兼容存量库:为新版本新增的 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 (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,