jiapu hai 2 meses
pai
achega
dcd30ecc2b

+ 123 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java

@@ -66,6 +66,9 @@ public class AdminController {
     @Resource
     private AssessmentAppointmentMapper assessmentAppointmentMapper;
 
+    @Resource
+    private AssessmentOrderMapper assessmentOrderMapper;
+
     // 用户管理
     @PostMapping("/users")
     public Result<Page<User>> getUsers(@RequestBody Map<String, Object> params) {
@@ -1053,6 +1056,126 @@ public class AdminController {
         return Result.success(list);
     }
 
+    // ========== 测评订单管理(基于预约表) ==========
+
+    @PostMapping("/assessment/orders")
+    public Result<Map<String, Object>> getAssessmentOrderList(@RequestBody Map<String, Object> params) {
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+        Page<AssessmentAppointment> pageParam = new Page<>(page, size);
+
+        LambdaQueryWrapper<AssessmentAppointment> wrapper = new LambdaQueryWrapper<>();
+        // 按状态筛选
+        if (params.containsKey("status") && params.get("status") != null && !params.get("status").toString().isEmpty()) {
+            wrapper.eq(AssessmentAppointment::getStatus, Integer.parseInt(params.get("status").toString()));
+        }
+        // 按关键词搜索(用户昵称/规划师昵称)
+        if (params.containsKey("keyword") && params.get("keyword") != null && !params.get("keyword").toString().isEmpty()) {
+            // 先查匹配的用户ID和规划师ID
+            String kw = params.get("keyword").toString();
+            LambdaQueryWrapper<User> userWrapper = new LambdaQueryWrapper<>();
+            userWrapper.like(User::getNickname, kw).select(User::getId);
+            java.util.List<User> matchedUsers = userMapper.selectList(userWrapper);
+            java.util.List<Long> matchedIds = new java.util.ArrayList<>();
+            for (User u : matchedUsers) { matchedIds.add(u.getId()); }
+            if (matchedIds.isEmpty()) {
+                matchedIds.add(-1L); // 无匹配时返回空
+            }
+            wrapper.and(w -> w.in(AssessmentAppointment::getUserId, matchedIds)
+                    .or().in(AssessmentAppointment::getGuideId, matchedIds));
+        }
+        wrapper.orderByDesc(AssessmentAppointment::getCreatedAt);
+
+        Page<AssessmentAppointment> result = assessmentAppointmentMapper.selectPage(pageParam, wrapper);
+
+        java.util.List<Map<String, Object>> list = new java.util.ArrayList<>();
+        for (AssessmentAppointment app : result.getRecords()) {
+            Map<String, Object> item = new HashMap<>();
+            item.put("id", app.getId());
+            item.put("familyId", app.getFamilyId());
+            item.put("userId", app.getUserId());
+            item.put("childId", app.getChildId());
+            item.put("guideId", app.getGuideId());
+            item.put("teacherId", app.getTeacherId());
+            item.put("packageId", app.getPackageId());
+            item.put("appointmentDate", app.getAppointmentDate());
+            item.put("appointmentTime", app.getAppointmentTime());
+            item.put("durationMinutes", app.getDurationMinutes());
+            item.put("appointmentType", app.getAppointmentType());
+            item.put("notes", app.getNotes());
+            item.put("contactPhone", app.getContactPhone());
+            item.put("status", app.getStatus());
+            item.put("createdAt", app.getCreatedAt());
+
+            // 关联查询用户昵称
+            User user = userMapper.selectById(app.getUserId());
+            item.put("userName", user != null ? user.getNickname() : "未知用户");
+
+            // 关联查询孩子昵称
+            if (app.getChildId() != null) {
+                FamilyMember child = familyMemberMapper.selectById(app.getChildId());
+                item.put("childName", child != null ? child.getNickname() : "未知");
+            } else {
+                item.put("childName", "");
+            }
+
+            // 关联查询规划师昵称
+            Long guideId = app.getGuideId() != null ? app.getGuideId() : app.getTeacherId();
+            if (guideId != null) {
+                User guide = userMapper.selectById(guideId);
+                item.put("guideName", guide != null ? guide.getNickname() : "未知规划师");
+            } else {
+                item.put("guideName", "");
+            }
+
+            // 关联查询方案名称
+            if (app.getPackageId() != null) {
+                GuidePackage pkg = guidePackageMapper.selectById(app.getPackageId());
+                item.put("packageName", pkg != null ? pkg.getName() : "未知方案");
+            } else {
+                item.put("packageName", "");
+            }
+
+            list.add(item);
+        }
+
+        Map<String, Object> resultMap = new HashMap<>();
+        resultMap.put("records", list);
+        resultMap.put("total", result.getTotal());
+        resultMap.put("current", result.getCurrent());
+        resultMap.put("size", result.getSize());
+        return Result.success(resultMap);
+    }
+
+    @PostMapping("/assessment/change-guide")
+    public Result<Void> changeAssessmentGuide(@RequestBody Map<String, Object> body) {
+        Long appointmentId = body.get("orderId") != null ? Long.valueOf(body.get("orderId").toString()) : null;
+        Long guideId = body.get("guideId") != null ? Long.valueOf(body.get("guideId").toString()) : null;
+        if (appointmentId == null || guideId == null) {
+            return Result.error("appointmentId 和 guideId 不能为空");
+        }
+
+        AssessmentAppointment appointment = assessmentAppointmentMapper.selectById(appointmentId);
+        if (appointment == null) {
+            return Result.error("预约记录不存在");
+        }
+
+        User guide = userMapper.selectById(guideId);
+        if (guide == null || !"teacher".equals(guide.getRole())) {
+            return Result.error("规划师不存在或角色不正确");
+        }
+
+        appointment.setGuideId(guideId);
+        appointment.setTeacherId(guideId);
+        if (appointment.getStatus() != null && appointment.getStatus() == 0) {
+            appointment.setStatus(1); // 待确认 → 已确认
+        }
+        appointment.setUpdatedAt(new Date());
+        assessmentAppointmentMapper.updateById(appointment);
+
+        return Result.success(null);
+    }
+
     @PostMapping("/assessment/assign-guide")
     public Result<Void> assignGuide(@RequestBody Map<String, Object> body) {
         Long appointmentId = body.get("appointmentId") != null ? Long.valueOf(body.get("appointmentId").toString()) : null;

+ 18 - 1
cfc-web/src/api/admin.js

@@ -1358,7 +1358,7 @@ export function reviewSupplyHierarchyChange(data) {
         method: 'post',
         data
     })
-}
+}
 
 // ========== 家庭会员订阅管理 API ==========
 
@@ -1620,3 +1620,20 @@ export function assignGuide(appointmentId, guideId) {
     data: { appointmentId, guideId }
   })
 }
+
+// ===== 测评订单管理 =====
+export function getAssessmentOrderList(params) {
+  return request({
+    url: '/api/admin/assessment/orders',
+    method: 'post',
+    data: params || {}
+  })
+}
+
+export function changeAssessmentGuide(orderId, guideId) {
+  return request({
+    url: '/api/admin/assessment/change-guide',
+    method: 'post',
+    data: { orderId, guideId }
+  })
+}

+ 8 - 2
cfc-web/src/router/index.js

@@ -192,6 +192,12 @@ const routes = [
         component: () => import('@/views/admin/AssessmentAssign.vue'),
         meta: { title: '待分配规划师', perm: 'assessment:dan' }
       },
+      {
+        path: 'assessment-orders',
+        name: 'AssessmentOrders',
+        component: () => import('@/views/admin/AssessmentOrders.vue'),
+        meta: { title: '测评订单', perm: 'assessment:dan' }
+      },
       {
         path: 'product-manage',
         name: 'ProductManage',
@@ -327,7 +333,7 @@ const routes = [
     component: () => import('@/views/admin/SupplierProductManage.vue'),
     meta: { title: '商品管理', perm: 'commerce:product' }
   },
-  {
+  {
             path: 'supply-system/:id/edit',
             name: 'SupplySystemEdit',
             component: () => import('@/views/admin/supply-system/Form.vue'),
@@ -339,7 +345,7 @@ const routes = [
             name: 'SupplyHierarchyChangeRequests',
             component: () => import('@/views/admin/supply-hierarchy/ChangeRequest.vue'),
             meta: { title: '更换上级审核', perm: 'system:supply' }
-        },
+        },
       // ========== 供应商管理 (supplier_admin) ==========
       {
         path: 'supply-manage',

+ 1 - 0
cfc-web/src/views/Layout.vue

@@ -295,6 +295,7 @@ export default {
             { title: '测评管理', icon: 'el-icon-edit', perm: 'assessment:dan',
               children: [
                 { path: '/assessment-admin', label: '测评管理', icon: 'el-icon-edit', perm: 'assessment:dan' },
+                { path: '/assessment-orders', label: '测评订单', icon: 'el-icon-s-order', perm: 'assessment:dan' },
                 { path: '/assessment-assign', label: '待分配规划师', icon: 'el-icon-user', perm: 'assessment:dan' },
               ]},
             // --- 虚拟团队 ---

+ 258 - 0
cfc-web/src/views/admin/AssessmentOrders.vue

@@ -0,0 +1,258 @@
+<template>
+  <div class="assessment-orders admin-page">
+    <el-card>
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">测评预约管理</span>
+        <div class="filter-bar">
+          <el-input
+            v-model="keyword"
+            placeholder="搜索用户/规划师..."
+            prefix-icon="el-icon-search"
+            clearable
+            style="width: 200px; margin-right: 10px;"
+            @keyup.enter.native="handleSearch"
+            @clear="handleSearch"
+          />
+          <el-select v-model="statusFilter" placeholder="预约状态" clearable @change="handleSearch" style="width: 140px; margin-right: 10px;">
+            <el-option label="全部状态" value="" />
+            <el-option label="待确认" :value="0" />
+            <el-option label="已确认" :value="1" />
+            <el-option label="已完成" :value="2" />
+            <el-option label="已取消" :value="3" />
+            <el-option label="未到" :value="4" />
+          </el-select>
+          <el-button type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
+        </div>
+      </div>
+
+      <el-table :max-height="tableHeight" :data="list" v-loading="loading" border stripe empty-text="暂无测评预约">
+        <el-table-column label="用户" width="110">
+          <template slot-scope="{ row }">{{ row.userName || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="孩子" width="100">
+          <template slot-scope="{ row }">{{ row.childName || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="测评方案" min-width="140" show-overflow-tooltip>
+          <template slot-scope="{ row }">{{ row.packageName || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="当前规划师" width="120">
+          <template slot-scope="{ row }">
+            <span v-if="row.guideName">{{ row.guideName }}</span>
+            <el-tag v-else size="mini" type="info">未分配</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="预约日期" width="120">
+          <template slot-scope="{ row }">{{ row.appointmentDate || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="预约时间" width="100">
+          <template slot-scope="{ row }">{{ row.appointmentTime || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="预约类型" width="100">
+          <template slot-scope="{ row }">{{ row.appointmentType || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="状态" width="100">
+          <template slot-scope="{ row }">
+            <el-tag :type="statusType(row.status)" size="mini">{{ statusLabel(row.status) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" width="155">
+          <template slot-scope="{ row }">{{ formatTime(row.createdAt) }}</template>
+        </el-table-column>
+        <el-table-column label="操作" width="130" fixed="right">
+          <template slot-scope="{ row }">
+            <el-button
+              size="mini"
+              type="primary"
+              plain
+              @click="openChangeGuideDialog(row)"
+              :disabled="row.status === 3"
+            >修改规划师</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-pagination
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+        :current-page="page"
+        :page-sizes="[10, 20, 50]"
+        :page-size="size"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        class="pagination-wrap"
+      />
+    </el-card>
+
+    <!-- 修改规划师 Dialog -->
+    <el-dialog title="修改规划师" :visible.sync="changeGuideDialogVisible" width="500px">
+      <div v-if="currentOrder" v-loading="guidesLoading">
+        <el-descriptions :column="1" border size="small" style="margin-bottom: 20px;">
+          <el-descriptions-item label="用户">{{ currentOrder.userName }}</el-descriptions-item>
+          <el-descriptions-item label="孩子">{{ currentOrder.childName }}</el-descriptions-item>
+          <el-descriptions-item label="测评方案">{{ currentOrder.packageName || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="预约日期">{{ currentOrder.appointmentDate || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="当前规划师">
+            <span v-if="currentOrder.guideName">{{ currentOrder.guideName }}</span>
+            <span v-else style="color: #999;">未分配</span>
+          </el-descriptions-item>
+        </el-descriptions>
+
+        <el-form label-width="100px">
+          <el-form-item label="新规划师">
+            <el-select
+              v-model="selectedGuideId"
+              placeholder="请选择规划师"
+              style="width: 100%;"
+              filterable
+            >
+              <el-option
+                v-for="g in guides"
+                :key="g.id"
+                :label="g.nickname + (g.realName ? ' (' + g.realName + ')' : '') + (g.teacherNo ? ' - ' + g.teacherNo : '')"
+                :value="g.id"
+              />
+            </el-select>
+          </el-form-item>
+        </el-form>
+      </div>
+      <div slot="footer">
+        <el-button @click="changeGuideDialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmChangeGuide" :loading="changing">确认修改</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getAssessmentOrderList, changeAssessmentGuide, getAvailableGuides } from '@/api/admin'
+
+export default {
+  name: 'AssessmentOrders',
+  computed: {
+    tableHeight() {
+      return window.innerHeight - 320
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      list: [],
+      page: 1,
+      size: 20,
+      total: 0,
+      keyword: '',
+      statusFilter: '',
+      // 修改规划师
+      changeGuideDialogVisible: false,
+      currentOrder: null,
+      selectedGuideId: null,
+      guides: [],
+      guidesLoading: false,
+      changing: false
+    }
+  },
+  mounted() {
+    this.loadList()
+  },
+  methods: {
+    handleSearch() {
+      this.page = 1
+      this.loadList()
+    },
+    async loadList() {
+      this.loading = true
+      try {
+        var params = { page: this.page, size: this.size }
+        if (this.statusFilter) {
+          params.status = this.statusFilter
+        }
+        if (this.keyword) {
+          params.keyword = this.keyword
+        }
+        var res = await getAssessmentOrderList(params)
+        this.list = res.data.records || []
+        this.total = res.data.total || 0
+      } catch (e) {
+        this.$message.error(e.message || '加载测评订单失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    handleSizeChange(val) {
+      this.size = val
+      this.page = 1
+      this.loadList()
+    },
+    handleCurrentChange(val) {
+      this.page = val
+      this.loadList()
+    },
+    async openChangeGuideDialog(row) {
+      this.currentOrder = row
+      this.selectedGuideId = row.guideId || null
+      this.changeGuideDialogVisible = true
+
+      this.guidesLoading = true
+      try {
+        var res = await getAvailableGuides()
+        this.guides = res.data || []
+      } catch (e) {
+        this.$message.error(e.message || '加载规划师列表失败')
+      } finally {
+        this.guidesLoading = false
+      }
+    },
+    async confirmChangeGuide() {
+      if (!this.selectedGuideId) {
+        this.$message.warning('请选择规划师')
+        return
+      }
+      if (this.currentOrder.guideId === this.selectedGuideId) {
+        this.$message.warning('新规划师与当前规划师相同')
+        return
+      }
+      this.changing = true
+      try {
+        await changeAssessmentGuide(this.currentOrder.id, this.selectedGuideId)
+        this.$message.success('规划师修改成功')
+        this.changeGuideDialogVisible = false
+        this.loadList()
+      } catch (e) {
+        this.$message.error(e.message || '修改规划师失败')
+      } finally {
+        this.changing = false
+      }
+    },
+    statusType(status) {
+      var map = { 0: 'warning', 1: 'primary', 2: 'success', 3: 'info', 4: 'danger' }
+      return map[status] !== undefined ? map[status] : 'info'
+    },
+    statusLabel(status) {
+      var map = { 0: '待确认', 1: '已确认', 2: '已完成', 3: '已取消', 4: '未到' }
+      return map[status] !== undefined ? map[status] : (status !== null && status !== undefined ? status : '-')
+    },
+    formatTime(t) {
+      if (!t) return '-'
+      return t.replace ? t.replace('T', ' ').substring(0, 19) : t
+    }
+  }
+}
+</script>
+
+<style scoped>
+.assessment-orders .admin-page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+.filter-bar {
+  display: flex;
+  align-items: center;
+}
+.pagination-wrap {
+  margin-top: 20px;
+  text-align: right;
+}
+</style>