Pārlūkot izejas kodu

Rebrand: 品牌升级为爱伴·AI之旅 + 后端加 train_course 多课程骨架(L0-L4)

liaoxg 2 nedēļas atpakaļ
vecāks
revīzija
beb35d9d67

+ 2 - 2
AGENTS.md

@@ -1,4 +1,4 @@
-# AGENTS.md — 家立方-AI管家成长营 (train)
+# AGENTS.md — 爱伴·AI之旅 (train)
 
 训练营系统,单 git 仓库(远程 `origin` = http://git.iwintrue.com/liaoxg/train.git,分支 `master`),三个子项目:
 
@@ -17,7 +17,7 @@
 ## 硬约束(用户明示,违反即返工)
 
 - 前端**只允许 Vue2**:train-frontend 必须 uni-app Vue2(@dcloudio 2.x),train-web 必须 Vue2 + vue-cli + Element UI。**禁用 TypeScript、Vite、Vue 3。**
-- 品牌名固定为「家立方-AI管家成长营」(曾叫"AI 管家启航营/半日营",改名已全量应用,勿回退)
+- 品牌名固定为「爱伴·AI之旅」(训练营总品牌),课程体系为 L0/L1/L2/L3/L4 五级。L0 课程名为「家立方-AI管家成长营」(首期课程名,保留为 L0 的 course name)。后续 L1-L4 课程待规划。品牌名勿回退为旧称
 
 ## 命令与验证
 

+ 1 - 1
train-backend/pom.xml

@@ -10,7 +10,7 @@
     <packaging>jar</packaging>
 
     <name>train-backend</name>
-    <description>家立方-AI管家成长营 学员管理与竞赛后台服务</description>
+    <description>爱伴·AI之旅 训练营学员管理与竞赛后台服务</description>
 
     <parent>
         <groupId>org.springframework.boot</groupId>

+ 60 - 0
train-backend/src/main/java/com/train/config/DatabaseInitializer.java

@@ -35,6 +35,7 @@ public class DatabaseInitializer implements CommandLineRunner {
     @Override
     public void run(String... args) throws Exception {
         initSchema();
+        ensureCourseSeed();
         initAdmin();
         if (initTestData) {
             initTestData();
@@ -128,4 +129,63 @@ public class DatabaseInitializer implements CommandLineRunner {
             log.error("初始化{}账号失败", role, e);
         }
     }
+
+    /**
+     * 确保 L0 默认课程存在,并回填存量 train_class.course_id。
+     * 品牌已升级为「爱伴·AI之旅」,L0 课程名仍为「家立方-AI管家成长营」。
+     */
+    private void ensureCourseSeed() {
+        try {
+            // 1. 确保 train_course 表存在(schema.sql 若因故静默失败,此处自愈)
+            jdbcTemplate.execute(
+                    "CREATE TABLE IF NOT EXISTS train_course (" +
+                            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                            "level VARCHAR(20) NOT NULL, " +
+                            "name VARCHAR(100) NOT NULL, " +
+                            "description VARCHAR(500), " +
+                            "status VARCHAR(20) DEFAULT 'draft', " +
+                            "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                            "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                            "UNIQUE KEY uk_level (level), " +
+                            "INDEX idx_status (status)" +
+                            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
+
+            // 2. 确保 L0 课程存在
+            Long courseCount = jdbcTemplate.queryForObject(
+                    "SELECT COUNT(*) FROM train_course WHERE level = 'L0'",
+                    Long.class);
+            Long courseId = null;
+            if (courseCount != null && courseCount > 0) {
+                courseId = jdbcTemplate.queryForObject(
+                        "SELECT id FROM train_course WHERE level = 'L0' LIMIT 1",
+                        Long.class);
+            } else {
+                jdbcTemplate.update(
+                        "INSERT INTO train_course (level, name, description, status) VALUES ('L0', ?, ?, 'active')",
+                        "家立方-AI管家成长营",
+                        "爱伴·AI之旅 L0 首期课程:新人成长训练营");
+                courseId = jdbcTemplate.queryForObject(
+                        "SELECT id FROM train_course WHERE level = 'L0' LIMIT 1",
+                        Long.class);
+                log.info("已初始化 L0 默认课程: 家立方-AI管家成长营 (id={})", courseId);
+            }
+
+            // 3. 回填存量 train_class.course_id 为空的数据
+            if (courseId != null) {
+                try {
+                    int updated = jdbcTemplate.update(
+                            "UPDATE train_class SET course_id = ? WHERE course_id IS NULL",
+                            courseId);
+                    if (updated > 0) {
+                        log.info("已回填 {} 条存量班次的 course_id = L0({})", updated, courseId);
+                    }
+                } catch (Exception e) {
+                    // course_id 列可能尚未补列(ALTER 静默失败),忽略
+                    log.warn("回填 train_class.course_id 失败(列可能不存在): {}", e.getMessage());
+                }
+            }
+        } catch (Exception e) {
+            log.error("初始化课程种子数据失败", e);
+        }
+    }
 }

+ 91 - 0
train-backend/src/main/java/com/train/controller/CourseController.java

@@ -0,0 +1,91 @@
+package com.train.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.train.common.Result;
+import com.train.entity.TrainClass;
+import com.train.entity.TrainCourse;
+import com.train.entity.TrainUser;
+import com.train.mapper.TrainClassMapper;
+import com.train.mapper.TrainCourseMapper;
+import com.train.mapper.TrainUserMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+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.List;
+import java.util.Map;
+
+/**
+ * 训练营课程(L0/L1/L2/L3/L4)相关接口。
+ *
+ * - /api/course/list     列出所有课程
+ * - /api/course/current  当前学员所修课程(按 user.class_id -> train_class.course_id 解析)
+ *
+ * 注意:当前学员所修课程未写入 user,而是从 class_id 反查,因此响应里附带 class 信息。
+ */
+@Tag(name = "课程", description = "训练营课程(L0/L1/.../L4)")
+@RestController
+@RequestMapping("/api/course")
+public class CourseController {
+
+    @Resource
+    private TrainCourseMapper trainCourseMapper;
+
+    @Resource
+    private TrainClassMapper trainClassMapper;
+
+    @Resource
+    private TrainUserMapper trainUserMapper;
+
+    @Operation(summary = "列出所有课程")
+    @PostMapping("/list")
+    public Result<List<TrainCourse>> list() {
+        List<TrainCourse> courses = trainCourseMapper.selectList(
+                new LambdaQueryWrapper<TrainCourse>().orderByAsc(TrainCourse::getLevel));
+        return Result.success(courses);
+    }
+
+    @Operation(summary = "当前学员所修课程")
+    @PostMapping("/current")
+    public Result<Map<String, Object>> current(@RequestAttribute("userId") Long userId) {
+        Map<String, Object> data = new HashMap<>();
+
+        TrainUser user = trainUserMapper.selectById(userId);
+        if (user == null) {
+            return Result.error("用户不存在");
+        }
+
+        TrainCourse course = null;
+        TrainClass trainClass = null;
+        if (user.getClassId() != null) {
+            trainClass = trainClassMapper.selectById(user.getClassId());
+            if (trainClass != null && trainClass.getCourseId() != null) {
+                course = trainCourseMapper.selectById(trainClass.getCourseId());
+            }
+        }
+
+        if (course == null) {
+            // 兜底:未进班或课程未关联时,返回首个 active 课程(一般是 L0)
+            course = trainCourseMapper.selectOne(
+                    new LambdaQueryWrapper<TrainCourse>()
+                            .eq(TrainCourse::getStatus, "active")
+                            .orderByAsc(TrainCourse::getLevel)
+                            .last("LIMIT 1"));
+        }
+
+        data.put("course", course);
+        if (trainClass != null) {
+            data.put("classId", trainClass.getId());
+            data.put("className", trainClass.getName());
+            data.put("classCode", trainClass.getCode());
+            data.put("classTime", trainClass.getTime());
+            data.put("classPlace", trainClass.getPlace());
+        }
+        return Result.success(data);
+    }
+}

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

@@ -42,7 +42,7 @@ public class AdminClassController {
         try {
             CfcActivity activity = new CfcActivity();
             activity.setTitle(c.getName());
-            activity.setDescription("家立方-AI管家成长营班次:" + c.getName());
+            activity.setDescription("爱伴·AI之旅班次:" + c.getName());
             activity.setActivityType("offline");
             activity.setDimensionCode("wealth,health,growth");
             activity.setStatus("published");

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

@@ -18,6 +18,9 @@ public class TrainClass implements Serializable {
     /** 关联的 cfc 活动 ID(activities.id,位于 cfc 同库) */
     private Long activityId;
 
+    /** 关联的课程 ID(train_course.id);未填写时按 L0 默认课程兜底 */
+    private Long courseId;
+
     private String name;
     private String code;
     private String time;

+ 37 - 0
train-backend/src/main/java/com/train/entity/TrainCourse.java

@@ -0,0 +1,37 @@
+package com.train.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;
+
+/**
+ * 训练营课程(L0/L1/L2/L3/L4)。
+ * 一个课程可对应多个 train_class 班次,班次通过 course_id 关联课程。
+ * 学员从 user.class_id -> train_class.course_id 解析当前所修课程。
+ */
+@Data
+@TableName("train_course")
+public class TrainCourse implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 课程等级:L0/L1/L2/L3/L4 */
+    private String level;
+
+    /** 课程名称(如"家立方-AI管家成长营") */
+    private String name;
+
+    /** 课程简介 */
+    private String description;
+
+    /** 状态:draft/active/finished */
+    private String status;
+
+    private Date createdAt;
+    private Date updatedAt;
+}

+ 7 - 0
train-backend/src/main/java/com/train/mapper/TrainCourseMapper.java

@@ -0,0 +1,7 @@
+package com.train.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.train.entity.TrainCourse;
+
+public interface TrainCourseMapper extends BaseMapper<TrainCourse> {
+}

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

@@ -1,10 +1,26 @@
--- 家立方-AI管家成长营 数据库初始化脚本
+-- 爱伴·AI之旅 训练营 数据库初始化脚本
 -- 数据库: cfc 同库 (zxyj) —— 训练营运营表 train_* 与 cfc 会员/活动/账本同库共存
+-- 训练营品牌:爱伴·AI之旅
+-- 课程等级:L0/L1/L2/L3/L4;其中 L0 = 「家立方-AI管家成长营」(首期课程)
+
+-- 课程表(L0~L4 课程主数据)
+CREATE TABLE IF NOT EXISTS train_course (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    level VARCHAR(20) NOT NULL COMMENT '课程等级:L0/L1/L2/L3/L4',
+    name VARCHAR(100) NOT NULL COMMENT '课程名称(如 L0 = 家立方-AI管家成长营)',
+    description VARCHAR(500) COMMENT '课程简介',
+    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,
+    UNIQUE KEY uk_level (level),
+    INDEX idx_status (status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='训练营课程(L0/L1/L2/L3/L4)';
 
 -- 班次表
 CREATE TABLE IF NOT EXISTS train_class (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
     activity_id BIGINT COMMENT '关联 cfc 活动 ID(activities.id,cfc 同库)',
+    course_id BIGINT COMMENT '关联课程 ID(train_course.id,L0/L1/L2…)',
     name VARCHAR(100) NOT NULL COMMENT '班次名称',
     code VARCHAR(20) UNIQUE COMMENT '班级码(学员进班用)',
     time VARCHAR(100) COMMENT '培训时间描述',
@@ -14,7 +30,8 @@ CREATE TABLE IF NOT EXISTS train_class (
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
     INDEX idx_code (code),
-    INDEX idx_activity_id (activity_id)
+    INDEX idx_activity_id (activity_id),
+    INDEX idx_course_id (course_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='班次';
 
 -- 学员用户表
@@ -45,6 +62,7 @@ CREATE TABLE IF NOT EXISTS train_user (
 -- 兼容存量库:为新版本新增的 cfc 关联列补列(列已存在时静默跳过)
 -- 下方语句由 DatabaseInitializer 逐条执行并容错,重复执行不报致命错误
 ALTER TABLE train_class ADD COLUMN activity_id BIGINT COMMENT '关联 cfc 活动 ID(activities.id,cfc 同库)';
+ALTER TABLE train_class ADD COLUMN course_id BIGINT COMMENT '关联课程 ID(train_course.id,L0/L1/L2…)';
 ALTER TABLE train_user ADD COLUMN cfc_user_id BIGINT COMMENT 'cfc 统一会员 ID(users.id,cfc 同库)';
 
 -- 小组表

+ 1 - 1
train-frontend/index.html

@@ -3,7 +3,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
-    <title>家立方-AI管家成长营</title>
+    <title>爱伴·AI之旅</title>
     <!--preload-links-->
     <!--app-context-->
     <link rel="stylesheet" href="/static/index.css" />

+ 2 - 2
train-frontend/manifest.json

@@ -1,7 +1,7 @@
 {
-    "name" : "家立方-AI管家成长营",
+    "name" : "爱伴·AI之旅",
     "appid" : "__UNI__D0CAMP0",
-    "description" : "家立方-AI管家成长营L0训练营管理",
+    "description" : "爱伴·AI之旅训练营小程序(L0-L4 课程体系)",
     "versionName" : "1.0.0",
     "versionCode" : "100",
     "transformPx" : false,

+ 1 - 1
train-frontend/package.json

@@ -1,7 +1,7 @@
 {
   "name": "train-frontend",
   "version": "1.0.0",
-  "description": "家立方-AI管家成长营小程序前端",
+  "description": "爱伴·AI之旅训练营小程序前端",
   "main": "main.js",
   "lazyCodeLoading": "requiredComponents",
   "scripts": {

+ 1 - 1
train-frontend/pages.json

@@ -94,7 +94,7 @@
   ],
   "globalStyle": {
     "navigationBarTextStyle": "black",
-    "navigationBarTitleText": "家立方-AI管家成长营",
+    "navigationBarTitleText": "爱伴·AI之旅",
     "navigationBarBackgroundColor": "#FFFFFF",
     "backgroundColor": "#F8F8F8"
   },

+ 14 - 3
train-frontend/pages/index/index.vue

@@ -2,8 +2,8 @@
   <view class="home-page">
     <view class="banner">
       <text class="banner-icon">🚀</text>
-      <text class="banner-title">家立方-AI管家成长营</text>
-      <text class="banner-sub">L0 新人成长训练营</text>
+      <text class="banner-title">爱伴·AI之旅</text>
+      <text class="banner-sub">{{ courseLabel }}</text>
       <button v-if="!isLoggedIn" class="login-entry-btn" @click="showLoginSheet">点击登录</button>
     </view>
 
@@ -96,13 +96,14 @@
 </template>
 
 <script>
-import { getMyGroup, getClassInfo, wechatLogin } from '@/utils/api.js'
+import { getMyGroup, getClassInfo, wechatLogin, getMyCourse } from '@/utils/api.js'
 
 export default {
   data() {
     return {
       groupInfo: null,
       classInfo: null,
+      courseName: '',
       loginSheetVisible: false,
       loginLoading: false
     }
@@ -111,6 +112,9 @@ export default {
     isLoggedIn() {
       return this.$store.getters.isLoggedIn
     },
+    courseLabel() {
+      return this.courseName || 'L0 家立方-AI管家成长营'
+    },
     verifyLabel() {
       var v = this.$store.state.alumniVerify
       if (v === 'accepted') return '✅ 已通过'
@@ -159,6 +163,13 @@ export default {
       getMyGroup().then(function(resp) {
         self.groupInfo = resp.data
       }).catch(function() {})
+      getMyCourse().then(function(resp) {
+        var d = resp.data || {}
+        var c = d.course
+        if (c && c.name) {
+          self.courseName = (c.level ? c.level + ' ' : '') + c.name
+        }
+      }).catch(function() {})
     },
     showLoginSheet() {
       this.loginSheetVisible = true

+ 2 - 2
train-frontend/pages/login/index.vue

@@ -4,8 +4,8 @@
       <view class="login-content">
         <view class="logo-area">
           <view class="logo-icon">🤖</view>
-          <text class="logo-title">家立方-AI管家成长营</text>
-          <text class="logo-subtitle">L0 新人成长训练营</text>
+      <text class="logo-title">爱伴·AI之旅</text>
+      <text class="logo-subtitle">L0 家立方-AI管家成长营</text>
         </view>
         <view class="login-btn-area">
           <button class="login-btn" @click="handleLogin" :loading="loading" :disabled="loading">

+ 8 - 0
train-frontend/utils/api.js

@@ -101,6 +101,14 @@ export const getClassInfo = () => {
   return request('/api/class/info', 'POST')
 }
 
+// 课程(L0/L1/L2/L3/L4)
+export const getCourseList = () => {
+  return request('/api/course/list', 'POST')
+}
+export const getMyCourse = () => {
+  return request('/api/course/current', 'POST')
+}
+
 // 分组
 export const joinGroup = (groupId) => {
   return request('/api/group/join', 'POST', { groupId })

+ 1 - 1
train-web/public/index.html

@@ -4,7 +4,7 @@
     <meta charset="utf-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width,initial-scale=1.0">
-    <title>家立方-AI管家成长营 L0 - 管理后台</title>
+    <title>爱伴·AI之旅 - 管理后台</title>
   </head>
   <body>
     <noscript>

+ 1 - 1
train-web/src/router/index.js

@@ -87,7 +87,7 @@ const router = new VueRouter({
 })
 
 router.beforeEach(function (to, from, next) {
-  document.title = (to.meta && to.meta.title ? to.meta.title + ' - ' : '') + '家立方-AI管家成长营 L0 管理后台'
+  document.title = (to.meta && to.meta.title ? to.meta.title + ' - ' : '') + '爱伴·AI之旅 管理后台'
   var token = localStorage.getItem('token')
   if (to.path === '/login') {
     if (token) {

+ 1 - 1
train-web/src/views/Layout.vue

@@ -1,7 +1,7 @@
 <template>
   <el-container class="layout-container">
     <el-aside width="200px" class="el-aside">
-      <div class="logo">成长营管理</div>
+      <div class="logo">爱伴·AI之旅</div>
       <el-scrollbar wrap-class="sidebar-scrollbar-wrap">
         <el-menu
           :default-active="activeMenu"

+ 1 - 1
train-web/src/views/Login.vue

@@ -3,7 +3,7 @@
     <div class="login-container">
       <div class="login-card">
         <div class="login-header">
-          <h2 class="login-title">家立方-AI管家成长营 L0</h2>
+          <h2 class="login-title">爱伴·AI之旅</h2>
           <p class="login-subtitle">管理后台登录</p>
         </div>