|
@@ -181,13 +181,22 @@ const fullCourse = async (row: any, includeCreator = false) => {
|
|
|
if (row.digital_instructor_id) {
|
|
if (row.digital_instructor_id) {
|
|
|
const digital = await pool.query('SELECT * FROM digital_instructors WHERE id=$1', [row.digital_instructor_id])
|
|
const digital = await pool.query('SELECT * FROM digital_instructors WHERE id=$1', [row.digital_instructor_id])
|
|
|
if (digital.rows[0]) {
|
|
if (digital.rows[0]) {
|
|
|
- const imageRows = await pool.query("SELECT stored_name FROM digital_instructor_files WHERE instructor_id=$1 AND kind='IMAGE' ORDER BY sort_order", [row.digital_instructor_id])
|
|
|
|
|
- instructors = [{ ...digital.rows[0], image_urls: imageRows.rows.map(f => `/uploads/${f.stored_name}`) }]
|
|
|
|
|
|
|
+ const imageRows = await pool.query("SELECT stored_name FROM digital_instructor_files WHERE instructor_id=$1 AND kind='IMAGE' AND is_active=TRUE ORDER BY sort_order", [row.digital_instructor_id])
|
|
|
|
|
+ const audioRows = await pool.query("SELECT stored_name FROM digital_instructor_files WHERE instructor_id=$1 AND kind='AUDIO' AND is_active=TRUE ORDER BY sort_order", [row.digital_instructor_id])
|
|
|
|
|
+ instructors = [{
|
|
|
|
|
+ ...digital.rows[0],
|
|
|
|
|
+ image_urls: imageRows.rows.map(f => `/uploads/${f.stored_name}`),
|
|
|
|
|
+ audio_urls: audioRows.rows.map(f => `/uploads/${f.stored_name}`)
|
|
|
|
|
+ }]
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
if (row.status !== 'DRAFT' && row.digital_instructor_snapshot) {
|
|
if (row.status !== 'DRAFT' && row.digital_instructor_snapshot) {
|
|
|
const snapshot = row.digital_instructor_snapshot
|
|
const snapshot = row.digital_instructor_snapshot
|
|
|
- instructors = [{ ...snapshot, image_urls: (snapshot.images || []).map((image: any) => image.url) }]
|
|
|
|
|
|
|
+ instructors = [{
|
|
|
|
|
+ ...snapshot,
|
|
|
|
|
+ image_urls: (snapshot.images || []).map((image: any) => image.url),
|
|
|
|
|
+ audio_urls: (snapshot.audio || []).map((audio: any) => audio.url)
|
|
|
|
|
+ }]
|
|
|
}
|
|
}
|
|
|
return mapCourse({ ...row, progress_events: progress.rows }, a.rows, instructors, d.rows, creatorUser, episodesRows)
|
|
return mapCourse({ ...row, progress_events: progress.rows }, a.rows, instructors, d.rows, creatorUser, episodesRows)
|
|
|
}
|
|
}
|
|
@@ -427,7 +436,6 @@ app.post('/api/me/password', { preHandler: auth }, async (req, reply) => {
|
|
|
|
|
|
|
|
const courseInput = z.object({
|
|
const courseInput = z.object({
|
|
|
name: z.string().trim().min(1, '请输入课程名称').max(100),
|
|
name: z.string().trim().min(1, '请输入课程名称').max(100),
|
|
|
- category: z.string().max(50).default(''),
|
|
|
|
|
description: z.string().max(500).default(''),
|
|
description: z.string().max(500).default(''),
|
|
|
digitalInstructorId: z.string().uuid().nullable().optional()
|
|
digitalInstructorId: z.string().uuid().nullable().optional()
|
|
|
})
|
|
})
|
|
@@ -471,8 +479,8 @@ app.post('/api/courses', { preHandler: auth }, async (req, reply) => {
|
|
|
}
|
|
}
|
|
|
if (d.digitalInstructorId && !await validDigitalInstructor(d.digitalInstructorId, organizationId, userId(req))) return reply.code(400).send({ message: '请选择本工作区的数字讲师' })
|
|
if (d.digitalInstructorId && !await validDigitalInstructor(d.digitalInstructorId, organizationId, userId(req))) return reply.code(400).send({ message: '请选择本工作区的数字讲师' })
|
|
|
const { rows } = await pool.query(
|
|
const { rows } = await pool.query(
|
|
|
- 'INSERT INTO courses(id, user_id, organization_id, name, category, description,digital_instructor_id) VALUES($1, $2, $3, $4, $5, $6,$7) RETURNING *',
|
|
|
|
|
- [id, userId(req), organizationId, d.name, d.category, d.description,d.digitalInstructorId || null]
|
|
|
|
|
|
|
+ 'INSERT INTO courses(id, user_id, organization_id, name, description, digital_instructor_id) VALUES($1, $2, $3, $4, $5, $6) RETURNING *',
|
|
|
|
|
+ [id, userId(req), organizationId, d.name, d.description, d.digitalInstructorId || null]
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
// 课程创建后默认创建第 1 集内容,确保分集不为空
|
|
// 课程创建后默认创建第 1 集内容,确保分集不为空
|
|
@@ -507,8 +515,8 @@ app.post('/api/courses/template', { preHandler: auth }, async (req, reply) => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const { rows } = await pool.query(
|
|
const { rows } = await pool.query(
|
|
|
- 'INSERT INTO courses(id, user_id, organization_id, name, category, description) VALUES($1, $2, $3, $4, $5, $6) RETURNING *',
|
|
|
|
|
- [courseId, userId(req), organizationId, template.name, template.category, template.description]
|
|
|
|
|
|
|
+ 'INSERT INTO courses(id, user_id, organization_id, name, description) VALUES($1, $2, $3, $4, $5) RETURNING *',
|
|
|
|
|
+ [courseId, userId(req), organizationId, template.name, template.description]
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
await populateCourseFromTemplate(pool, courseId, template.id, uploadDir, sampleDir)
|
|
await populateCourseFromTemplate(pool, courseId, template.id, uploadDir, sampleDir)
|
|
@@ -533,7 +541,7 @@ app.patch('/api/courses/:id', { preHandler: auth }, async (req, reply) => {
|
|
|
const row = await ownedCourse(id, userId(req))
|
|
const row = await ownedCourse(id, userId(req))
|
|
|
if (!row) return reply.code(404).send({ message: '课程不存在' })
|
|
if (!row) return reply.code(404).send({ message: '课程不存在' })
|
|
|
|
|
|
|
|
- const p = courseInput.pick({ name: true, category: true, description: true, digitalInstructorId: true }).safeParse(req.body)
|
|
|
|
|
|
|
+ const p = courseInput.pick({ name: true, description: true, digitalInstructorId: true }).safeParse(req.body)
|
|
|
if (!p.success) {
|
|
if (!p.success) {
|
|
|
return reply.code(400).send({ message: p.error.issues[0]?.message || '请检查课程信息' })
|
|
return reply.code(400).send({ message: p.error.issues[0]?.message || '请检查课程信息' })
|
|
|
}
|
|
}
|
|
@@ -541,11 +549,11 @@ app.patch('/api/courses/:id', { preHandler: auth }, async (req, reply) => {
|
|
|
if (p.data.digitalInstructorId !== undefined && row.status !== 'DRAFT') return reply.code(409).send({ message: '只有草稿课程可以更换讲师' })
|
|
if (p.data.digitalInstructorId !== undefined && row.status !== 'DRAFT') return reply.code(409).send({ message: '只有草稿课程可以更换讲师' })
|
|
|
if (p.data.digitalInstructorId && !await validDigitalInstructor(p.data.digitalInstructorId,row.organization_id,userId(req))) return reply.code(400).send({ message: '请选择本工作区的数字讲师' })
|
|
if (p.data.digitalInstructorId && !await validDigitalInstructor(p.data.digitalInstructorId,row.organization_id,userId(req))) return reply.code(400).send({ message: '请选择本工作区的数字讲师' })
|
|
|
const { rows } = await pool.query(
|
|
const { rows } = await pool.query(
|
|
|
- 'UPDATE courses SET name = $1, category = $2, description = $3, digital_instructor_id=COALESCE($5,digital_instructor_id), updated_at = NOW() WHERE id = $4 RETURNING *',
|
|
|
|
|
- [p.data.name, p.data.category, p.data.description, id,p.data.digitalInstructorId || null]
|
|
|
|
|
|
|
+ 'UPDATE courses SET name = $1, description = $2, digital_instructor_id=COALESCE($4,digital_instructor_id), updated_at = NOW() WHERE id = $3 RETURNING *',
|
|
|
|
|
+ [p.data.name, p.data.description, id, p.data.digitalInstructorId || null]
|
|
|
)
|
|
)
|
|
|
await refreshCoursePoints(id)
|
|
await refreshCoursePoints(id)
|
|
|
- await addProgressEvent(id, 'USER', 'CONTENT_UPDATED', '课程基本信息已修改', '课程名称、分类或需求说明已更新。')
|
|
|
|
|
|
|
+ await addProgressEvent(id, 'USER', 'CONTENT_UPDATED', '课程基本信息已修改', '课程名称或需求说明已更新。')
|
|
|
return { course: await fullCourse(rows[0]) }
|
|
return { course: await fullCourse(rows[0]) }
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -887,7 +895,7 @@ app.get('/api/admin/courses', { preHandler: adminAuth }, async (req) => {
|
|
|
if (query.keyword && query.keyword.trim()) {
|
|
if (query.keyword && query.keyword.trim()) {
|
|
|
params.push(`%${query.keyword.trim()}%`)
|
|
params.push(`%${query.keyword.trim()}%`)
|
|
|
const idx = params.length
|
|
const idx = params.length
|
|
|
- conditions.push(`(c.name ILIKE $${idx} OR c.category ILIKE $${idx} OR u.phone ILIKE $${idx} OR u.contact_name ILIKE $${idx})`)
|
|
|
|
|
|
|
+ conditions.push(`(c.name ILIKE $${idx} OR u.phone ILIKE $${idx} OR u.contact_name ILIKE $${idx})`)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const whereClause = conditions.length ? 'WHERE ' + conditions.join(' AND ') : ''
|
|
const whereClause = conditions.length ? 'WHERE ' + conditions.join(' AND ') : ''
|