فهرست منبع

课程调研: 课程表加需要调研配置-进班学员按课程完成调研并防重复

liaoxg 5 روز پیش
والد
کامیت
e948365865

+ 25 - 4
train-backend/src/main/java/com/train/controller/PlanController.java

@@ -54,14 +54,36 @@ public class PlanController {
         return Result.success(plan);
     }
 
-    // ============ 测评兴趣调研 ============
-    @Operation(summary = "提交测评兴趣调研")
+    // ============ 测评兴趣调研(报名后按课程绑定) ============
+    @Operation(summary = "提交测评兴趣调研(课程调研携带 courseId,同 uid+courseId 防重复)")
     @PostMapping("/survey")
     public Result<Boolean> survey(@RequestBody Map<String, Object> body,
                                   @org.springframework.web.bind.annotation.RequestAttribute("userId") Long userId) {
+        String interest = (String) body.get("interest");
+        if (interest == null || interest.trim().isEmpty()) {
+            return Result.error("请选择调研选项");
+        }
+        Long courseId = null;
+        if (body.get("courseId") != null) {
+            try {
+                courseId = Long.valueOf(body.get("courseId").toString());
+            } catch (NumberFormatException ignored) {
+            }
+        }
+        // 同 uid + courseId 已提交过则拒绝重复(防重复领卡券)
+        if (courseId != null) {
+            Long exists = trainSurveyMapper.selectCount(
+                    new LambdaQueryWrapper<TrainSurvey>()
+                            .eq(TrainSurvey::getUid, userId)
+                            .eq(TrainSurvey::getCourseId, courseId));
+            if (exists != null && exists > 0) {
+                return Result.error("该课程调研已完成,无需重复提交");
+            }
+        }
         TrainSurvey survey = new TrainSurvey();
         survey.setUid(userId);
-        survey.setInterest((String) body.get("interest"));
+        survey.setCourseId(courseId);
+        survey.setInterest(interest);
         if (body.get("classId") != null) {
             survey.setClassId(Long.valueOf(body.get("classId").toString()));
         } else {
@@ -72,7 +94,6 @@ public class PlanController {
         }
         trainSurveyMapper.insert(survey);
         // 调研勾选健康/成长 -> etotem 测评兑换卡
-        String interest = (String) body.get("interest");
         if ("health".equals(interest) || "growth".equals(interest)) {
             grantCoupon(userId, "etotem", "research", "测评兴趣调研");
         }

+ 6 - 0
train-backend/src/main/java/com/train/controller/admin/AdminCourseController.java

@@ -74,6 +74,9 @@ public class AdminCourseController {
         if (c.getMemberPrice() == null) {
             c.setMemberPrice(0);
         }
+        if (c.getNeedsSurvey() == null) {
+            c.setNeedsSurvey(0);
+        }
         trainCourseMapper.insert(c);
         return Result.success(c);
     }
@@ -113,6 +116,9 @@ public class AdminCourseController {
         if (c.getTrack() != null) {
             exist.setTrack(c.getTrack());
         }
+        if (c.getNeedsSurvey() != null) {
+            exist.setNeedsSurvey(c.getNeedsSurvey());
+        }
         trainCourseMapper.updateById(exist);
         return Result.success(true);
     }

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

@@ -44,6 +44,9 @@ public class TrainCourse implements Serializable {
     /** 课程会员价(分) */
     private Integer memberPrice;
 
+    /** 是否需要完成调研(1=报名后需调研,0=不需要) */
+    private Integer needsSurvey;
+
     /** 学习路径/轨道:wealth/health/growth(模块化方向,空=主线通用课程如 L0) */
     private String track;
 

+ 1 - 0
train-backend/src/main/java/com/train/entity/TrainSurvey.java

@@ -17,6 +17,7 @@ public class TrainSurvey implements Serializable {
 
     private Long uid;
     private Long classId;
+    private Long courseId;
     private String interest;
     private Date submittedAt;
 }

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

@@ -15,12 +15,16 @@ CREATE TABLE IF NOT EXISTS train_course (
     end_time DATETIME COMMENT '课程结束时间(状态自动切换依据)',
     price INT DEFAULT 0 COMMENT '课程报名价(分,报名=报课程,价格挂在课程上)',
     member_price INT DEFAULT 0 COMMENT '课程会员价(分)',
+    needs_survey TINYINT DEFAULT 0 COMMENT '是否需要完成调研(1=报名后需调研)',
     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)';
 
+-- 课程调研配置(存量库补列,重复执行报错被吞)
+ALTER TABLE train_course ADD COLUMN needs_survey TINYINT DEFAULT 0 COMMENT '是否需要完成调研(1=报名后需调研)';
+
 -- 课程版块表(管理端维护:版块增删改/排序/启用)
 CREATE TABLE IF NOT EXISTS train_course_section (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
@@ -282,16 +286,21 @@ CREATE TABLE IF NOT EXISTS train_coupon (
     INDEX idx_type (type)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='卡券';
 
--- 测评兴趣调研表
+-- 测评兴趣调研表(报名后按课程绑定调研,course_id 关联 train_course.id)
 CREATE TABLE IF NOT EXISTS train_survey (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
     uid BIGINT NOT NULL,
     class_id BIGINT,
+    course_id BIGINT COMMENT '调研所属课程(train_course.id,报名后按课程调研)',
     interest VARCHAR(100) COMMENT 'interest_health/interest_growth',
     submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
-    INDEX idx_uid (uid)
+    INDEX idx_uid (uid),
+    INDEX idx_course_id (course_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评兴趣调研';
 
+-- 调研绑定课程(存量库补列,重复执行报错被吞)
+ALTER TABLE train_survey ADD COLUMN course_id BIGINT COMMENT '调研所属课程(train_course.id,报名后按课程调研)';
+
 -- 管理后台账号表
 CREATE TABLE IF NOT EXISTS train_admin (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,

+ 14 - 4
train-frontend/pages/course/detail.vue

@@ -153,26 +153,34 @@ export default {
       return this.enrollment.status
     },
     stageMenus() {
+      // 课程配置了"需要完成调研":进班后按课程绑定调研,三个阶段都展示入口
+      var surveyMenu = null
+      if (this.course.needsSurvey === 1 && this.course.id) {
+        surveyMenu = { icon: '📝', text: '课程调研', url: '/pages/survey/index?courseId=' + this.course.id }
+      }
       if (this.stage === 'upcoming') {
         // 课程前:课前准备
-        return [
+        var upcoming = [
           { icon: '📄', text: '课前资料', url: '/pages/material/index' },
-          { icon: '📝', text: '兴趣调研', url: '/pages/survey/index' },
           { icon: '📅', text: '行动计划', url: '/pages/plan/index' },
           { icon: '👥', text: '我的小组', url: '/pages/group/index' }
         ]
+        if (surveyMenu) upcoming.push(surveyMenu)
+        return upcoming
       }
       if (this.stage === 'finished') {
         // 课程后:结业事项
-        return [
+        var finished = [
           { icon: '📋', text: '三本账', url: '/pages/assignment/index' },
           { icon: '📅', text: '行动计划', url: '/pages/plan/index' },
           { icon: '🏆', text: '积分榜', url: '/pages/scoreboard/index' },
           { icon: '🗳️', text: '路演投票', url: '/pages/vote/index' }
         ]
+        if (surveyMenu) finished.push(surveyMenu)
+        return finished
       }
       // 课程中:学习内容
-      return [
+      var active = [
         { icon: '📄', text: '课前资料', url: '/pages/material/index' },
         { icon: '👥', text: '我的小组', url: '/pages/group/index' },
         { icon: '✅', text: '装机打卡', url: '/pages/checkin/index', tab: true },
@@ -184,6 +192,8 @@ export default {
         { icon: '📅', text: '行动计划', url: '/pages/plan/index' },
         { icon: '📋', text: '三本账', url: '/pages/assignment/index' }
       ]
+      if (surveyMenu) active.push(surveyMenu)
+      return active
     }
   },
   onLoad(options) {

+ 16 - 4
train-frontend/pages/survey/index.vue

@@ -1,8 +1,8 @@
 <template>
   <view class="survey-page">
     <view class="section-card">
-      <text class="section-title">测评兴趣调研</text>
-      <text class="card-desc">选择您最感兴趣的领域,提交后有机会获得测评兑换卡</text>
+      <text class="section-title">{{ pageTitle }}</text>
+      <text class="card-desc">{{ pageDesc }}</text>
       <view class="interest-list">
         <label class="interest-item" v-for="(item, idx) in interests" :key="idx">
           <radio :value="item.value" :checked="selected === item.value" @tap="selected = item.value" color="#FC7A57" />
@@ -46,7 +46,17 @@ export default {
       ],
       selected: '',
       loading: false,
-      coupons: []
+      coupons: [],
+      courseId: '',
+      pageTitle: '测评兴趣调研',
+      pageDesc: '选择您最感兴趣的领域,提交后有机会获得测评兑换卡'
+    }
+  },
+  onLoad(options) {
+    this.courseId = (options && options.courseId) ? options.courseId : ''
+    if (this.courseId) {
+      this.pageTitle = '课程调研'
+      this.pageDesc = '完成本次课程调研,帮助我们将课程调整得更贴合您的需求'
     }
   },
   onShow() {
@@ -65,7 +75,9 @@ export default {
       this.loading = true
       var self = this
       var classId = this.$store.state.classId
-      submitSurvey({ interest: this.selected, classId: classId ? parseInt(classId) : null }).then(function() {
+      var payload = { interest: this.selected, classId: classId ? parseInt(classId) : null }
+      if (this.courseId) payload.courseId = parseInt(this.courseId)
+      submitSurvey(payload).then(function() {
         uni.showToast({ title: '提交成功', icon: 'success' })
         self.loadCoupons()
       }).catch(function(err) {

+ 29 - 17
train-web/src/views/Courses.vue

@@ -47,6 +47,11 @@
           <el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
         </template>
       </el-table-column>
+      <el-table-column label="需调研" width="90">
+        <template slot-scope="{ row }">
+          <el-tag :type="row.needsSurvey === 1 ? 'warning' : 'info'" size="small">{{ row.needsSurvey === 1 ? '是' : '否' }}</el-tag>
+        </template>
+      </el-table-column>
       <el-table-column prop="createdAt" label="创建时间" width="170" :formatter="formatTime" />
        <el-table-column label="操作" width="180" fixed="right">
          <template slot-scope="{ row }">
@@ -99,6 +104,10 @@
             <el-option label="已结束" value="finished" />
           </el-select>
         </el-form-item>
+        <el-form-item label="需要调研">
+          <el-switch v-model="form.needsSurvey" :active-value="1" :inactive-value="0" />
+          <span class="tips">开启后,学员报名进班需按课程完成调研</span>
+        </el-form-item>
       </el-form>
       <div slot="footer">
         <el-button @click="showDialog = false">取消</el-button>
@@ -184,7 +193,8 @@ export default {
          memberPrice: 0,
          startTime: null,
          endTime: null,
-         status: 'draft'
+         status: 'draft',
+         needsSurvey: 0
        },
        rules: {
          level: [{ required: true, message: '请选择课程等级', trigger: 'change' }],
@@ -248,26 +258,27 @@ export default {
       this.showDialog = true
     },
      openEdit: function (row) {
-       this.form = {
-         id: row.id,
-         level: row.level,
-         track: row.track || '',
-         name: row.name,
-         description: row.description,
-         price: (row.price || 0) / 100,
-         memberPrice: (row.memberPrice || 0) / 100,
-         startTime: row.startTime || null,
-         endTime: row.endTime || null,
-         status: row.status || 'draft'
-       }
+       form = {
+          id: row.id,
+          level: row.level,
+          track: row.track || '',
+          name: row.name,
+          description: row.description,
+          price: (row.price || 0) / 100,
+          memberPrice: (row.memberPrice || 0) / 100,
+          startTime: row.startTime || null,
+          endTime: row.endTime || null,
+          status: row.status || 'draft',
+          needsSurvey: row.needsSurvey === 1 ? 1 : 0
+        }
        this.showDialog = true
      },
      openModules: function (row) {
        this.$router.push({ path: '/modules', query: { courseId: row.id } })
      },
-     resetForm: function () {
-       this.form = { id: null, level: 'L0', track: '', name: '', description: '', price: 0, memberPrice: 0, startTime: null, endTime: null, status: 'draft' }
-     },
+resetForm: function () {
+        this.form = { id: null, level: 'L0', track: '', name: '', description: '', price: 0, memberPrice: 0, startTime: null, endTime: null, status: 'draft', needsSurvey: 0 }
+      },
     handleSubmit: function () {
       var self = this
       if (self.submitting) return
@@ -283,7 +294,8 @@ self.submitting = true
             memberPrice: Math.round((self.form.memberPrice || 0) * 100),
            startTime: self.form.startTime,
            endTime: self.form.endTime,
-           status: self.form.status
+           status: self.form.status,
+           needsSurvey: self.form.needsSurvey === 1 ? 1 : 0
          }
         var p
         if (self.form.id) {