import { Router, type Request, type Response } from 'express'; import { eq, and, max } from 'drizzle-orm'; import { db } from '../../config/database.js'; import { wwwSectionBlocks, wwwSpecGroups, wwwSpecItems } from '../../schemas/index.js'; import logger from '../../config/logger.js'; import { ApiResponse } from '../../utils/response.js'; import { authMiddleware } from '../../middleware/auth.js'; const router = Router(); router.use(authMiddleware); // ===== 3.1 Section 区块 ===== // GET /api/www/sections?page_type=home&product_id=1 router.get('/api/www/sections', async (req: Request, res: Response) => { try { const pageType = req.query.page_type as string; const productId = req.query.product_id ? Number(req.query.product_id) : undefined; if (!pageType) { res.json(ApiResponse.error('page_type 参数必填')); return; } const conditions = [eq(wwwSectionBlocks.pageType, pageType)]; if (productId) conditions.push(eq(wwwSectionBlocks.productId, productId)); const rows = await db.select().from(wwwSectionBlocks) .where(and(...conditions)) .orderBy(wwwSectionBlocks.sortOrder); res.json(ApiResponse.success({ items: rows, total: rows.length })); } catch (e: unknown) { logger.error(`[www/sections] GET error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('获取 Section 列表失败')); } }); // GET /api/www/sections/:id(含子数据) router.get('/api/www/sections/:id', async (req: Request, res: Response) => { try { const id = Number(req.params.id); const [section] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, id)); if (!section) { res.json(ApiResponse.noData('Section 不存在')); return; } // 如果是 spec_table 布局,附带参数分组数据 let specGroups: unknown[] = []; if (section.layout === 'spec_table') { const groups = await db.select().from(wwwSpecGroups) .where(eq(wwwSpecGroups.sectionBlockId, id)) .orderBy(wwwSpecGroups.sortOrder); specGroups = await Promise.all(groups.map(async (g) => { const items = await db.select().from(wwwSpecItems) .where(eq(wwwSpecItems.specGroupId, g.id)) .orderBy(wwwSpecItems.sortOrder); return { ...g, items }; })); } res.json(ApiResponse.success({ ...section, specGroups })); } catch (e: unknown) { logger.error(`[www/sections/:id] GET error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('获取 Section 详情失败')); } }); // POST /api/www/sections router.post('/api/www/sections', async (req: Request, res: Response) => { try { const b = req.body; if (!b.page_type || !b.layout) { res.json(ApiResponse.error('page_type 和 layout 为必填项')); return; } // 未指定排序时,自动追加到同 page_type(及同产品)已有区块的最后,避免默认 0 导致乱序 let sortOrder = Number(b.sort_order) || 0; if (b.sort_order === undefined || b.sort_order === null) { const scope = [eq(wwwSectionBlocks.pageType, b.page_type)]; if (b.product_id) scope.push(eq(wwwSectionBlocks.productId, b.product_id)); const [agg] = await db.select({ max: max(wwwSectionBlocks.sortOrder) }).from(wwwSectionBlocks).where(and(...scope)); sortOrder = (agg?.max ?? -1) + 1; } const result = await db.insert(wwwSectionBlocks).values({ pageType: b.page_type, productId: b.product_id || null, layout: b.layout, theme: b.theme || 'light', overlineColor: b.overline_color || '', titleColor: b.title_color || '', bodyColor: b.body_color || '', bgType: b.bg_type || 'color', bgValue: b.bg_value || '', overlineZh: b.overline_zh || '', overlineEn: b.overline_en || '', titleZh: b.title_zh || '', titleEn: b.title_en || '', subtitleZh: b.subtitle_zh || '', subtitleEn: b.subtitle_en || '', contentZh: b.content_zh || null, contentEn: b.content_en || null, ctaPrimaryZh: b.cta_primary_zh || '', ctaPrimaryEn: b.cta_primary_en || '', ctaPrimaryUrl: b.cta_primary_url || '', ctaSecondaryZh: b.cta_secondary_zh || '', ctaSecondaryEn: b.cta_secondary_en || '', ctaSecondaryUrl: b.cta_secondary_url || '', mediaUrlZh: b.media_url_zh || '', mediaUrlEn: b.media_url_en || '', videoUrl: b.video_url || '', config: b.config || null, isVisible: b.is_visible !== undefined ? (b.is_visible ? 1 : 0) : 1, sortOrder, }); const newId = result[0].insertId; const [created] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, newId)); res.json(ApiResponse.success(created, 'Section 已创建')); } catch (e: unknown) { logger.error(`[www/sections] POST error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('创建 Section 失败')); } }); // PUT /api/www/sections/:id router.put('/api/www/sections/:id', async (req: Request, res: Response) => { try { const id = Number(req.params.id); const [existing] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, id)); if (!existing) { res.json(ApiResponse.noData('Section 不存在')); return; } const b = req.body; await db.update(wwwSectionBlocks).set({ layout: b.layout ?? existing.layout, theme: b.theme ?? existing.theme, overlineColor: b.overline_color ?? existing.overlineColor, titleColor: b.title_color ?? existing.titleColor, bodyColor: b.body_color ?? existing.bodyColor, bgType: b.bg_type ?? existing.bgType, bgValue: b.bg_value ?? existing.bgValue, overlineZh: b.overline_zh ?? existing.overlineZh, overlineEn: b.overline_en ?? existing.overlineEn, titleZh: b.title_zh ?? existing.titleZh, titleEn: b.title_en ?? existing.titleEn, subtitleZh: b.subtitle_zh ?? existing.subtitleZh, subtitleEn: b.subtitle_en ?? existing.subtitleEn, contentZh: b.content_zh !== undefined ? b.content_zh : existing.contentZh, contentEn: b.content_en !== undefined ? b.content_en : existing.contentEn, ctaPrimaryZh: b.cta_primary_zh ?? existing.ctaPrimaryZh, ctaPrimaryEn: b.cta_primary_en ?? existing.ctaPrimaryEn, ctaPrimaryUrl: b.cta_primary_url ?? existing.ctaPrimaryUrl, ctaSecondaryZh: b.cta_secondary_zh ?? existing.ctaSecondaryZh, ctaSecondaryEn: b.cta_secondary_en ?? existing.ctaSecondaryEn, ctaSecondaryUrl: b.cta_secondary_url ?? existing.ctaSecondaryUrl, mediaUrlZh: b.media_url_zh ?? existing.mediaUrlZh, mediaUrlEn: b.media_url_en ?? existing.mediaUrlEn, videoUrl: b.video_url ?? existing.videoUrl, config: b.config !== undefined ? b.config : existing.config, isVisible: b.is_visible !== undefined ? (b.is_visible ? 1 : 0) : existing.isVisible, sortOrder: b.sort_order ?? existing.sortOrder, }).where(eq(wwwSectionBlocks.id, id)); const [updated] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, id)); res.json(ApiResponse.success(updated, 'Section 已更新')); } catch (e: unknown) { logger.error(`[www/sections/:id] PUT error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('更新 Section 失败')); } }); // DELETE /api/www/sections/:id router.delete('/api/www/sections/:id', async (req: Request, res: Response) => { try { const id = Number(req.params.id); const [existing] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, id)); if (!existing) { res.json(ApiResponse.noData('Section 不存在')); return; } // 级联删除参数规格 const groups = await db.select().from(wwwSpecGroups).where(eq(wwwSpecGroups.sectionBlockId, id)); for (const g of groups) { await db.delete(wwwSpecItems).where(eq(wwwSpecItems.specGroupId, g.id)); } await db.delete(wwwSpecGroups).where(eq(wwwSpecGroups.sectionBlockId, id)); await db.delete(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, id)); res.json(ApiResponse.success(null, '删除成功')); } catch (e: unknown) { logger.error(`[www/sections/:id] DELETE error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('删除 Section 失败')); } }); // PATCH /api/www/sections/sort router.patch('/api/www/sections/sort', async (req: Request, res: Response) => { try { const items: { id: number; sort_order: number }[] = req.body; if (!Array.isArray(items)) { res.json(ApiResponse.error('请求体应为数组')); return; } for (const item of items) { await db.update(wwwSectionBlocks).set({ sortOrder: item.sort_order }).where(eq(wwwSectionBlocks.id, item.id)); } res.json(ApiResponse.success(null, '排序已更新')); } catch (e: unknown) { logger.error(`[www/sections/sort] PATCH error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('更新排序失败')); } }); // POST /api/www/sections/:id/duplicate router.post('/api/www/sections/:id/duplicate', async (req: Request, res: Response) => { try { const id = Number(req.params.id); const [source] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, id)); if (!source) { res.json(ApiResponse.noData('Section 不存在')); return; } const { id: _, createdAt: __, updatedAt: ___, ...data } = source; const result = await db.insert(wwwSectionBlocks).values({ ...data, titleZh: `${source.titleZh} (副本)`, titleEn: `${source.titleEn} (copy)`, sortOrder: source.sortOrder + 1, }); const newId = result[0].insertId; // 复制参数规格 if (source.layout === 'spec_table') { const groups = await db.select().from(wwwSpecGroups).where(eq(wwwSpecGroups.sectionBlockId, id)); for (const g of groups) { const gResult = await db.insert(wwwSpecGroups).values({ sectionBlockId: newId, titleZh: g.titleZh, titleEn: g.titleEn, sortOrder: g.sortOrder, defaultVisible: g.defaultVisible, }); const newGroupId = gResult[0].insertId; const items = await db.select().from(wwwSpecItems).where(eq(wwwSpecItems.specGroupId, g.id)); if (items.length > 0) { await db.insert(wwwSpecItems).values(items.map((item) => ({ specGroupId: newGroupId, nameZh: item.nameZh, nameEn: item.nameEn, valueZh: item.valueZh, valueEn: item.valueEn, sortOrder: item.sortOrder, }))); } } } const [created] = await db.select().from(wwwSectionBlocks).where(eq(wwwSectionBlocks.id, newId)); res.json(ApiResponse.success(created, 'Section 已复制')); } catch (e: unknown) { logger.error(`[www/sections/duplicate] POST error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('复制 Section 失败')); } }); // ===== 3.2 Spec Table 参数规格 ===== // GET /api/www/sections/:section_id/spec-groups router.get('/api/www/sections/:section_id/spec-groups', async (req: Request, res: Response) => { try { const sectionId = Number(req.params.section_id); const groups = await db.select().from(wwwSpecGroups) .where(eq(wwwSpecGroups.sectionBlockId, sectionId)) .orderBy(wwwSpecGroups.sortOrder); // 附带每组的参数项 const result = await Promise.all(groups.map(async (g) => { const items = await db.select().from(wwwSpecItems) .where(eq(wwwSpecItems.specGroupId, g.id)) .orderBy(wwwSpecItems.sortOrder); return { ...g, items }; })); res.json(ApiResponse.success({ items: result, total: result.length })); } catch (e: unknown) { logger.error(`[www/spec-groups] GET error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('获取参数分组失败')); } }); // POST /api/www/sections/:section_id/spec-groups router.post('/api/www/sections/:section_id/spec-groups', async (req: Request, res: Response) => { try { const sectionId = Number(req.params.section_id); const { title_zh, title_en, default_visible } = req.body; if (!title_zh || !title_en) { res.json(ApiResponse.error('title_zh 和 title_en 为必填项')); return; } const result = await db.insert(wwwSpecGroups).values({ sectionBlockId: sectionId, titleZh: title_zh, titleEn: title_en, defaultVisible: default_visible || 5, }); const newId = result[0].insertId; const [created] = await db.select().from(wwwSpecGroups).where(eq(wwwSpecGroups.id, newId)); res.json(ApiResponse.success(created, '参数分组已创建')); } catch (e: unknown) { logger.error(`[www/spec-groups] POST error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('创建参数分组失败')); } }); // PUT /api/www/sections/:section_id/spec-groups/:group_id router.put('/api/www/sections/:section_id/spec-groups/:group_id', async (req: Request, res: Response) => { try { const groupId = Number(req.params.group_id); const [existing] = await db.select().from(wwwSpecGroups).where(eq(wwwSpecGroups.id, groupId)); if (!existing) { res.json(ApiResponse.noData('参数分组不存在')); return; } const { title_zh, title_en, default_visible } = req.body; await db.update(wwwSpecGroups).set({ titleZh: title_zh ?? existing.titleZh, titleEn: title_en ?? existing.titleEn, defaultVisible: default_visible ?? existing.defaultVisible, }).where(eq(wwwSpecGroups.id, groupId)); const [updated] = await db.select().from(wwwSpecGroups).where(eq(wwwSpecGroups.id, groupId)); res.json(ApiResponse.success(updated, '参数分组已更新')); } catch (e: unknown) { logger.error(`[www/spec-groups] PUT error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('更新参数分组失败')); } }); // DELETE /api/www/sections/:section_id/spec-groups/:group_id router.delete('/api/www/sections/:section_id/spec-groups/:group_id', async (req: Request, res: Response) => { try { const groupId = Number(req.params.group_id); const [existing] = await db.select().from(wwwSpecGroups).where(eq(wwwSpecGroups.id, groupId)); if (!existing) { res.json(ApiResponse.noData('参数分组不存在')); return; } await db.delete(wwwSpecItems).where(eq(wwwSpecItems.specGroupId, groupId)); await db.delete(wwwSpecGroups).where(eq(wwwSpecGroups.id, groupId)); res.json(ApiResponse.success(null, '删除成功')); } catch (e: unknown) { logger.error(`[www/spec-groups] DELETE error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('删除参数分组失败')); } }); // PATCH /api/www/sections/:section_id/spec-groups/sort router.patch('/api/www/sections/:section_id/spec-groups/sort', async (req: Request, res: Response) => { try { const items: { id: number; sort_order: number }[] = req.body; if (!Array.isArray(items)) { res.json(ApiResponse.error('请求体应为数组')); return; } for (const item of items) { await db.update(wwwSpecGroups).set({ sortOrder: item.sort_order }).where(eq(wwwSpecGroups.id, item.id)); } res.json(ApiResponse.success(null, '排序已更新')); } catch (e: unknown) { logger.error(`[www/spec-groups/sort] PATCH error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('更新排序失败')); } }); // GET /api/www/spec-groups/:group_id/items router.get('/api/www/spec-groups/:group_id/items', async (req: Request, res: Response) => { try { const groupId = Number(req.params.group_id); const items = await db.select().from(wwwSpecItems) .where(eq(wwwSpecItems.specGroupId, groupId)) .orderBy(wwwSpecItems.sortOrder); res.json(ApiResponse.success({ items, total: items.length })); } catch (e: unknown) { logger.error(`[www/spec-items] GET error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('获取参数项失败')); } }); // POST /api/www/spec-groups/:group_id/items router.post('/api/www/spec-groups/:group_id/items', async (req: Request, res: Response) => { try { const groupId = Number(req.params.group_id); const { name_zh, name_en, value_zh, value_en } = req.body; if (!name_zh || !name_en || !value_zh || !value_en) { res.json(ApiResponse.error('name_zh、name_en、value_zh、value_en 为必填项')); return; } const result = await db.insert(wwwSpecItems).values({ specGroupId: groupId, nameZh: name_zh, nameEn: name_en, valueZh: value_zh, valueEn: value_en, }); const newId = result[0].insertId; const [created] = await db.select().from(wwwSpecItems).where(eq(wwwSpecItems.id, newId)); res.json(ApiResponse.success(created, '参数项已创建')); } catch (e: unknown) { logger.error(`[www/spec-items] POST error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('创建参数项失败')); } }); // PUT /api/www/spec-items/:id router.put('/api/www/spec-items/:id', async (req: Request, res: Response) => { try { const id = Number(req.params.id); const [existing] = await db.select().from(wwwSpecItems).where(eq(wwwSpecItems.id, id)); if (!existing) { res.json(ApiResponse.noData('参数项不存在')); return; } const { name_zh, name_en, value_zh, value_en } = req.body; await db.update(wwwSpecItems).set({ nameZh: name_zh ?? existing.nameZh, nameEn: name_en ?? existing.nameEn, valueZh: value_zh ?? existing.valueZh, valueEn: value_en ?? existing.valueEn, }).where(eq(wwwSpecItems.id, id)); const [updated] = await db.select().from(wwwSpecItems).where(eq(wwwSpecItems.id, id)); res.json(ApiResponse.success(updated, '参数项已更新')); } catch (e: unknown) { logger.error(`[www/spec-items] PUT error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('更新参数项失败')); } }); // DELETE /api/www/spec-items/:id router.delete('/api/www/spec-items/:id', async (req: Request, res: Response) => { try { const id = Number(req.params.id); const [existing] = await db.select().from(wwwSpecItems).where(eq(wwwSpecItems.id, id)); if (!existing) { res.json(ApiResponse.noData('参数项不存在')); return; } await db.delete(wwwSpecItems).where(eq(wwwSpecItems.id, id)); res.json(ApiResponse.success(null, '删除成功')); } catch (e: unknown) { logger.error(`[www/spec-items] DELETE error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('删除参数项失败')); } }); // PATCH /api/www/spec-groups/:group_id/items/sort router.patch('/api/www/spec-groups/:group_id/items/sort', async (req: Request, res: Response) => { try { const items: { id: number; sort_order: number }[] = req.body; if (!Array.isArray(items)) { res.json(ApiResponse.error('请求体应为数组')); return; } for (const item of items) { await db.update(wwwSpecItems).set({ sortOrder: item.sort_order }).where(eq(wwwSpecItems.id, item.id)); } res.json(ApiResponse.success(null, '排序已更新')); } catch (e: unknown) { logger.error(`[www/spec-items/sort] PATCH error: ${e instanceof Error ? e.message : e}`); res.json(ApiResponse.error('更新排序失败')); } }); export default router;