Browse Source

课程域 04 四个 P1 显示类按 Go 权威对齐

P1-1 端点44 classify int64 + 前端映射(Go courseRowView.Classify int64,只下发 ID 不下发文字):
  mock/DTO classify: string→number;种子数据 '数学'/'物理'/'英语'→1/2/3;视图加 classifyText 静态映射(同 payroll 页口径)
P1-2 端点44 课程 status 枚举 0~3(Go courseStatusFilter 白名单 {0,1,2,3}):
  视图 courseStatusText 由 {0:未开始,1:进行中} 改 {0:未发布,1:发布未开始,2:进行中,3:已结束};segmented 筛选补 2/3;mock 种子 course 1 改 status=2(进行中)
P1-3 端点53 报课 status 枚举 1/2(Go studentStatusFilter 白名单 {1,2}):
  视图 stuStatusText 由 {0:未确认,1:在读,2:结课} 改 {1:正常,2:结束};mock 53 按课程是否已结束给 1/2(不再恒 1)
P1-4 端点46 时分只取 HH:mm(Go classRowView.Start/End *time.Time 注释 date 恒 1970-01-01 只取时分;端点47 Start max=5 即 HH:mm):
  data.ts CourseRecord 加 startHM/endHM(HH:mm 5字符);mock 46 改下发 r.startHM/r.endHM(不再下发完整 RFC3339);
  视图 46 列改直显时分(不再 formatDateTime);端点47 提交去掉 todayStr 拼日期(Go 只认 HH:mm 5字符);删未用 todayStr 函数

B 类回报后端(frontend-sync/issues/2026-11-10-课程域B类3项-回报后端.md):
  B-1 端点48 时间窗闭区间(Go course_handler.go:397 坐实'两端都含',建议契约补显式例外)
  B-2 端点44 classify 下发 int64(确认前端本地映射方案)
  B-3 端点55/56 counteracted 判据(Go 自定,契约未定义,请拍板)

门禁四件全绿:vue-tsc 0 / eslint 0 / vite build 成功 / dist 无 mock 逻辑串
Q 12 hours ago
parent
commit
2e63d60d39

+ 9 - 9
src/api/academic/course.ts

@@ -28,9 +28,9 @@ export interface CourseDTO {
   institutionId: number;
   /** 课程名称 */
   name: string;
-  /** 课程分类(学科) */
-  classify: string;
-  /** 状态数字码(0 未开始 / 1 进行中;前端映射中文) */
+  /** 课程分类 ID(int64;Go courseRowView.Classify int64;前端用 classifyText 映射成学科名——Go 只下发 ID,不下发文字) */
+  classify: number;
+  /** 状态数字码(0 未发布 / 1 发布未开始 / 2 进行中 / 3 已结束;Go courseStatusFilter 白名单;前端映射中文) */
   status: number;
   /** 授课老师 id 列表 */
   teacherIds: string[];
@@ -44,8 +44,8 @@ export type CourseQuery = {
   institutionId: number;
   /** 校区 id(可空) */
   campusId?: number;
-  /** 课程分类筛选(可空) */
-  classify?: string;
+  /** 课程分类筛选(可空;int64 分类 ID;Go 只认数字,不认分类名) */
+  classify?: number;
   /** 状态筛选(可空) */
   status?: number;
   /** 名称模糊搜索(可空) */
@@ -71,9 +71,9 @@ export interface CourseClassDTO {
   teacherId: string;
   /** 周(原值字符串:"-1"=全部周 / "0"~"6"=周日..周六 / "1,3,5"=多值;v1.8) */
   week: string;
-  /** 上课起时间(RFC3339) */
+  /** 上课时分(**HH:mm** 5 字符;Go classRowView.Start *time.Time,注释"date 部分恒 1970-01-01 只取时分";前端只展示时分,勿 formatDateTime 成完整日期) */
   start: string;
-  /** 下课止时间(RFC3339) */
+  /** 下课时分(HH:mm;可空 = 按课程 duration 推算) */
   end: string;
   /** 校区 id(0 = 机构级) */
   campusId: number;
@@ -93,9 +93,9 @@ export type CreateClassBody = {
   teacherId: string;
   /** 周(原值字符串,同 CourseClassDTO.week) */
   week: string;
-  /** 上课起时间(RFC3339) */
+  /** 上课时分(**HH:mm** 5 字符;Go CreateClassReq.Start `validate:"required,max=5"`,必填且最多 5 字符,即 "HH:mm") */
   start: string;
-  /** 下课止时间(RFC3339) */
+  /** 下课时分(HH:mm;可空 → 按课程 duration 推算;给了必须晚于 Start) */
   end: string;
   /** 校区 id(0 = 机构级) */
   campusId: number;

+ 21 - 13
src/api/mock/data.ts

@@ -453,18 +453,23 @@ export interface CourseRecord {
    *  ⚠ `pay`=应收金额(端点53 口径,保留);`paid`=是否已付(端点52 请求 0/1、端点51 perStudent bool 的布尔源,
    *     与 `pay` 金额是两个概念,不可复用同一字段)。 */
   students: { userId: string; type: number; pay: number; cost: number; giveCost: number; paid: boolean }[];
+  /** 排班规则"上课/下课时分"(端点 46 口径,Go 注释 date 恒 1970-01-01 只取时分;`HH:mm` 5 字符,max=5) */
+  startHM: string;
+  endHM: string;
 }
 export interface Course {
   id: number;
   institutionId: number;
   name: string;
-  classify: string;
-  status: 0 | 1;
+  /** 课程分类 ID(int64;Go courseRowView.Classify int64;前端用 classifyText 映射成学科名) */
+  classify: number;
+  /** 课程状态(Go courseStatusFilter 白名单:0 未发布 / 1 发布未开始 / 2 进行中 / 3 已结束) */
+  status: 0 | 1 | 2 | 3;
   teacherIds: string[];
   records: CourseRecord[];
 }
 let recordSeq = 6000;
-function mkRecord(courseId: number, teacherId: string, campusId: number, address: string, week: string, offsetDays: number, status: 0 | 1 | 2): CourseRecord {
+function mkRecord(courseId: number, teacherId: string, campusId: number, address: string, week: string, offsetDays: number, status: 0 | 1 | 2, startHM = '09:00', endHM = '10:30'): CourseRecord {
   const start = BASE_MS + offsetDays * 86400_000;
   const present = (on: boolean): number => (status === 2 && on ? 1 : 0); // 已结束课才有出勤 type=1(正常)
   return {
@@ -477,6 +482,9 @@ function mkRecord(courseId: number, teacherId: string, campusId: number, address
     startTime: rfc3339(start),
     endTime: rfc3339(start + 90 * 60_000),
     status,
+    /** 端点 46 时分(Go 语义:date 恒 1970-01-01 只取时分;HH:mm) */
+    startHM,
+    endHM,
     students: [
       { userId: ulid(201), type: present(true), pay: 10000, cost: 10000, giveCost: 0, paid: true },
       { userId: ulid(202), type: present(true), pay: 10000, cost: 10000, giveCost: 0, paid: true },
@@ -486,24 +494,24 @@ function mkRecord(courseId: number, teacherId: string, campusId: number, address
 }
 const courses: Course[] = [
   {
-    id: 1, institutionId: 1, name: '数学 8 班', classify: '数学', status: 1, teacherIds: [ulid(101)],
+    id: 1, institutionId: 1, name: '数学 8 班', classify: 1, status: 2, teacherIds: [ulid(101)],
     records: [
-      mkRecord(1, ulid(101), 11, '滨江校区 3 楼 305 教室', '1,3,5', -3, 2),
-      mkRecord(1, ulid(101), 11, '滨江校区 3 楼 305 教室', '1,3,5', 0, 0),
-      mkRecord(1, ulid(101), 11, '滨江校区 5 楼 502 教室', '2,4', 2, 0),
-      mkRecord(1, ulid(101), 11, '滨江校区 3 楼 305 教室', '-1', 4, 0),
+      mkRecord(1, ulid(101), 11, '滨江校区 3 楼 305 教室', '1,3,5', -3, 2, '09:00', '10:30'),
+      mkRecord(1, ulid(101), 11, '滨江校区 3 楼 305 教室', '1,3,5', 0, 0, '09:00', '10:30'),
+      mkRecord(1, ulid(101), 11, '滨江校区 5 楼 502 教室', '2,4', 2, 0, '14:00', '15:30'),
+      mkRecord(1, ulid(101), 11, '滨江校区 3 楼 305 教室', '-1', 4, 0, '16:00', '17:30'),
     ],
   },
   {
-    id: 2, institutionId: 1, name: '物理 3 班', classify: '物理', status: 1, teacherIds: [ulid(102)],
-    records: [mkRecord(2, ulid(102), 12, '城西校区 1 楼 102 教室', '3', -1, 1), mkRecord(2, ulid(102), 12, '城西校区 1 楼 102 教室', '3', 1, 0)],
+    id: 2, institutionId: 1, name: '物理 3 班', classify: 2, status: 1, teacherIds: [ulid(102)],
+    records: [mkRecord(2, ulid(102), 12, '城西校区 1 楼 102 教室', '3', -1, 1, '10:00', '11:30'), mkRecord(2, ulid(102), 12, '城西校区 1 楼 102 教室', '3', 1, 0, '10:00', '11:30')],
   },
   {
-    id: 3, institutionId: 1, name: '英语 1 班', classify: '英语', status: 0, teacherIds: [ulid(101)],
-    records: [mkRecord(3, ulid(101), 11, '滨江校区 4 楼 401 教室', '6', -7, 2)],
+    id: 3, institutionId: 1, name: '英语 1 班', classify: 3, status: 0, teacherIds: [ulid(101)],
+    records: [mkRecord(3, ulid(101), 11, '滨江校区 4 楼 401 教室', '6', -7, 2, '15:00', '16:30')],
   },
 ];
-export function queryCourses(f: { classify?: string; status?: number; keyword?: string; institutionId?: number; campusId?: number }): Course[] {
+export function queryCourses(f: { classify?: number; status?: number; keyword?: string; institutionId?: number; campusId?: number }): Course[] {
   return courses.filter((c) => {
     if (f.classify && c.classify !== f.classify) return false;
     if (f.status !== undefined && c.status !== f.status) return false;

+ 10 - 5
src/api/mock/routes/courses.ts

@@ -48,7 +48,7 @@ export const courseRoutes: MockRoute[] = [
       const denied = checkScope(ctx);
       if (denied) return denied;
       const rows = queryCourses({
-        classify: ctx.query.classify,
+        classify: ctx.query.classify !== undefined ? Number(ctx.query.classify) : undefined,
         status: ctx.query.status !== undefined ? Number(ctx.query.status) : undefined,
         keyword: ctx.query.keyword,
         institutionId: ctx.query.institutionId ? Number(ctx.query.institutionId) : 1,
@@ -90,14 +90,15 @@ export const courseRoutes: MockRoute[] = [
       if (denied) return denied;
       const c = findCourse(Number(ctx.params.id));
       if (!c) return fail(3001, '课程不存在');
-      // v1.8:排班规则行 = {classId, courseId, teacherId, week(字符串), start, end, campusId};无 room(源列只有 address)
+      // v1.8 + Go 权威:排班规则行 = {classId, courseId, teacherId, week(字符串), start, end, campusId};
+      // 无 room(源列只有 address);start/end 为**时分**(Go 注释:date 恒 1970-01-01 只取时分;HH:mm 5 字符 max=5)
       const rules = c.records.slice(0, 2).map((r, i) => ({
         classId: r.id * 10 + i,
         courseId: c.id,
         teacherId: r.teacherId,
         week: r.week,
-        start: r.startTime,
-        end: r.endTime,
+        start: r.startHM,
+        end: r.endHM,
         campusId: r.campusId,
       }));
       return page(rules, Number(ctx.query.page ?? 1), Number(ctx.query.pageSize ?? 10));
@@ -274,6 +275,10 @@ export const courseRoutes: MockRoute[] = [
       const rec = c.records[0];
       // v1.6 C3/A1 + v1.8:行 = {userId, realName(可空), pay, cost, giveCost, remainderReceivable, status(数字码)}。
       // realName 查不到给 null(区分"没这个人"与"名字为空");欠费用 remainderReceivable(unpaid 是 2018 停更僵尸列)。
+      // ⚠ status = 报课行状态(Go studentStatusFilter 白名单 1 正常 / 2 结束)——不是 record 上课状态(0/1/2);
+      //    mock 按课程状态映射:课程已结束(record 全 status=2) → 报课行 status=2,否则 1(在读)。
+      const courseEnded = c.records.every((r) => r.status === 2);
+      const stuStatus = courseEnded ? 2 : 1;
       const rows = (rec?.students ?? []).map((s) => {
         const u = findUser(s.userId);
         return {
@@ -283,7 +288,7 @@ export const courseRoutes: MockRoute[] = [
           cost: fmt(s.cost),
           giveCost: fmt(s.giveCost),
           remainderReceivable: fmt(20000), // mock 欠费
-          status: 1, // 数字码(1 在读;中文前端映射)
+          status: stuStatus, // 1 正常 / 2 结束(Go 白名单;前端映射中文)
         };
       });
       return page(rows, Number(ctx.query.page ?? 1), Number(ctx.query.pageSize ?? 10));

+ 15 - 19
src/views/academic/schedule/index.vue

@@ -95,26 +95,22 @@ function weekText(w: string): string {
     .map((x) => WEEK_NAMES[x.trim()] ?? x.trim())
     .join('、');
 }
-/** 本地当天日期(YYYY-MM-DD;拼 RFC3339 用) */
-function todayStr(): string {
-  const d = new Date();
-  const p = (n: number) => String(n).padStart(2, '0');
-  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
-}
-
 /* ================= ① 课程列表(端点 44) ================= */
+/** 课程分类 ID → 学科名(Go 只下发 int64 分类 ID,不下发文字;新契约无独立字典端点,暂静态枚举;同 payroll 页 classifyText) */
+const classifyText: Record<number, string> = { 1: '数学', 2: '物理', 3: '英语' };
 /** 状态筛选:'all' = 全部(el-segmented 哨兵值;fetcher 里剥离,不传 undefined 之外的脏值) */
 const filters = reactive<{ keyword: string; status: string }>({ keyword: '', status: 'all' });
 const { rows: courses, total, page, pageSize, loading, error, fetchList } = useListPage<CourseDTO>({
   fetcher: (p, ps) =>
     listCourses({ ...scope.toQuery(), keyword: filters.keyword, status: filters.status === 'all' ? undefined : Number(filters.status), page: p, pageSize: ps }),
 });
-const courseStatusText: Record<number, string> = { 0: '未开始', 1: '进行中' };
+/** 课程状态(Go courseStatusFilter 白名单 0~3):0 未发布 / 1 发布未开始 / 2 进行中 / 3 已结束 */
+const courseStatusText: Record<number, string> = { 0: '未发布', 1: '发布未开始', 2: '进行中', 3: '已结束' };
 const columns: ListColumn<CourseDTO>[] = [
   { prop: 'id', label: '课程 id', minWidth: 90 },
   { prop: 'name', label: '课程名称', minWidth: 160 },
-  { prop: 'classify', label: '学科', minWidth: 100 },
-  { prop: 'status', label: '状态', minWidth: 90, formatter: (r) => courseStatusText[r.status] ?? String(r.status) },
+  { prop: 'classify', label: '学科', minWidth: 100, formatter: (r) => classifyText[r.classify] ?? String(r.classify) },
+  { prop: 'status', label: '状态', minWidth: 110, formatter: (r) => courseStatusText[r.status] ?? String(r.status) },
   { prop: 'recordCount', label: '排班数', minWidth: 90 },
 ];
 function onSearch(): void {
@@ -215,9 +211,9 @@ async function doCreateClass(): Promise<void> {
     const r = await createClass(selectedCourseId.value, {
       teacherId: classForm.teacherId,
       week: weeksToString(),
-      // 本地日期 + 时分秒 → RFC3339(无时区后缀 = 本地时间,与后端 dev 环境口径一致)
-      start: `${todayStr()}T${classForm.start}`,
-      end: `${todayStr()}T${classForm.end}`,
+      // ⚠ Go CreateClassReq.Start `max=5`(HH:mm 5 字符)——**时分语义,不拼日期**(date 部分恒 1970-01-01 无业务含义)
+      start: classForm.start,
+      end: classForm.end,
       campusId: classForm.campusId,
     });
     if (r.code === 200) {
@@ -443,7 +439,7 @@ async function loadStudents(): Promise<void> {
     stuLoading.value = false;
   }
 }
-const stuStatusText: Record<number, string> = { 0: '未确认', 1: '在读', 2: '结课' };
+const stuStatusText: Record<number, string> = { 1: '正常', 2: '结束' };
 
 /** 报名对话框(端点 54;不写资金流水、不动余额;重复 → 3143) */
 const enrollVisible = ref(false);
@@ -590,7 +586,7 @@ async function doCounteract(): Promise<void> {
         <el-input v-model="filters.keyword" placeholder="搜索课程名称" clearable class="toolbar-input" />
         <el-segmented
           v-model="filters.status"
-          :options="[{ label: '全部', value: 'all' }, { label: '未开始', value: '0' }, { label: '进行中', value: '1' }]"
+          :options="[{ label: '全部', value: 'all' }, { label: '未发布', value: '0' }, { label: '发布未开始', value: '1' }, { label: '进行中', value: '2' }, { label: '已结束', value: '3' }]"
         />
       </template>
     </ListPage>
@@ -611,11 +607,11 @@ async function doCounteract(): Promise<void> {
         <el-table-column prop="week" label="周" min-width="120">
           <template #default="{ row }">{{ weekText(row.week) }}</template>
         </el-table-column>
-        <el-table-column prop="start" label="上课时间" width="150">
-          <template #default="{ row }">{{ formatDateTime(row.start) }}</template>
+        <el-table-column prop="start" label="上课时间" width="120">
+          <template #default="{ row }">{{ row.start || '—' }}</template>
         </el-table-column>
-        <el-table-column prop="end" label="下课时间" width="150">
-          <template #default="{ row }">{{ formatDateTime(row.end) }}</template>
+        <el-table-column prop="end" label="下课时间" width="120">
+          <template #default="{ row }">{{ row.end || '—' }}</template>
         </el-table-column>
         <el-table-column prop="campusId" label="校区" min-width="110">
           <template #default="{ row }">{{ campusName(row.campusId) }}</template>