feat: 官网代码全量更新 (2026-04-18)

这个提交包含在:
wx
2026-04-18 15:13:28 +08:00
父节点 fcac8f88db
当前提交 f91ecbb093
共修改 135 个文件,包含 10307 行新增和 3349 行删除
+39
查看文件
@@ -0,0 +1,39 @@
import { execFileSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { requireAdmin } from '../../utils/auth.js'
/**
* 对 public/uploads 下的图片跑 tesseract chi_sim OCR。
* Body: { image: '/uploads/reviews/xxx.jpg' }
* 仅允许 /uploads/ 前缀,防止任意文件读取。
*/
export default defineEventHandler(async (event) => {
requireAdmin(event)
const { image } = await readBody(event)
if (!image || typeof image !== 'string' || !image.startsWith('/uploads/')) {
throw createError({ statusCode: 422, message: '仅支持 /uploads/ 下的图片' })
}
const rel = image.replace(/^\/+/, '')
const abs = resolve(join(process.cwd(), 'public', rel))
const allowedRoot = resolve(join(process.cwd(), 'public/uploads'))
if (!abs.startsWith(allowedRoot + '/') && abs !== allowedRoot) {
throw createError({ statusCode: 400, message: '非法路径' })
}
if (!existsSync(abs)) {
throw createError({ statusCode: 404, message: '图片不存在' })
}
try {
const text = execFileSync('tesseract', [abs, 'stdout', '-l', 'chi_sim'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
maxBuffer: 10 * 1024 * 1024,
})
return { text: text || '' }
} catch (e) {
throw createError({ statusCode: 500, message: 'OCR 执行失败(请确认已安装 tesseract + chi_sim 语言包)' })
}
})
+27 -7
查看文件
@@ -1,4 +1,4 @@
import { desc, like, or, sql } from 'drizzle-orm'
import { desc, asc, eq, like, or, and, sql, isNotNull, ne, isNull } from 'drizzle-orm'
import { useDB, schema } from '../../utils/db.js'
import { requireAdmin } from '../../utils/auth.js'
import { parsePagination } from '../../utils/pagination.js'
@@ -8,15 +8,28 @@ export default defineEventHandler(async (event) => {
const db = useDB()
const { page, limit, offset, search } = parsePagination(event)
const q = getQuery(event)
let whereClause = sql`1=1`
const conditions = []
if (search) {
whereClause = or(
conditions.push(or(
like(schema.reviews.author, `%${search}%`),
like(schema.reviews.content, `%${search}%`),
like(schema.reviews.title, `%${search}%`)
)
like(schema.reviews.title, `%${search}%`),
))
}
if (q.published === 'true' || q.published === '1') conditions.push(eq(schema.reviews.published, true))
if (q.published === 'false' || q.published === '0') conditions.push(eq(schema.reviews.published, false))
if (q.hasScreenshot === 'true' || q.hasScreenshot === '1') {
conditions.push(and(isNotNull(schema.reviews.screenshot), ne(schema.reviews.screenshot, '')))
}
if (q.hasScreenshot === 'false' || q.hasScreenshot === '0') {
conditions.push(or(isNull(schema.reviews.screenshot), eq(schema.reviews.screenshot, '')))
}
if (q.scene) conditions.push(like(schema.reviews.scenes, `%"${q.scene}"%`))
if (q.concern) conditions.push(like(schema.reviews.concerns, `%"${q.concern}"%`))
const whereClause = conditions.length ? and(...conditions) : sql`1=1`
const [{ count: total }] = db
.select({ count: sql`count(*)` })
@@ -24,14 +37,21 @@ export default defineEventHandler(async (event) => {
.where(whereClause)
.all()
const items = db
const rows = db
.select()
.from(schema.reviews)
.where(whereClause)
.orderBy(desc(schema.reviews.id))
.orderBy(asc(schema.reviews.sortOrder), desc(schema.reviews.id))
.limit(limit)
.offset(offset)
.all()
// 解析 JSON 字段方便前端展示
const items = rows.map(r => ({
...r,
scenes: (() => { try { return JSON.parse(r.scenes || '[]') } catch { return [] } })(),
concerns: (() => { try { return JSON.parse(r.concerns || '[]') } catch { return [] } })(),
}))
return { items, total, page, limit, totalPages: Math.ceil(total / limit) }
})
+5
查看文件
@@ -16,7 +16,12 @@ export default defineEventHandler(async (event) => {
title: body.title || '',
content: body.content,
date: body.date || '',
screenshot: body.screenshot || '',
scenes: JSON.stringify(Array.isArray(body.scenes) ? body.scenes : []),
concerns: JSON.stringify(Array.isArray(body.concerns) ? body.concerns : []),
sortOrder: body.sortOrder || 0,
verified: body.verified ? 1 : 0,
published: body.published === false ? 0 : 1,
}).run()
return { success: true, id: result.lastInsertRowid }
+5 -1
查看文件
@@ -8,5 +8,9 @@ export default defineEventHandler(async (event) => {
const db = useDB()
const item = db.select().from(schema.reviews).where(eq(schema.reviews.id, id)).get()
if (!item) throw createError({ statusCode: 404, message: '评价不存在' })
return item
return {
...item,
scenes: (() => { try { return JSON.parse(item.scenes || '[]') } catch { return [] } })(),
concerns: (() => { try { return JSON.parse(item.concerns || '[]') } catch { return [] } })(),
}
})
+3
查看文件
@@ -15,6 +15,9 @@ export default defineEventHandler(async (event) => {
if (body.title !== undefined) updateData.title = body.title
if (body.content !== undefined) updateData.content = body.content
if (body.date !== undefined) updateData.date = body.date
if (body.screenshot !== undefined) updateData.screenshot = body.screenshot
if (body.scenes !== undefined) updateData.scenes = Array.isArray(body.scenes) ? JSON.stringify(body.scenes) : body.scenes
if (body.concerns !== undefined) updateData.concerns = Array.isArray(body.concerns) ? JSON.stringify(body.concerns) : body.concerns
if (body.verified !== undefined) updateData.verified = body.verified ? 1 : 0
if (body.published !== undefined) updateData.published = body.published ? 1 : 0
if (body.sortOrder !== undefined) updateData.sortOrder = body.sortOrder
+31
查看文件
@@ -0,0 +1,31 @@
import { inArray } from 'drizzle-orm'
import { useDB, schema } from '../../../utils/db.js'
import { requireAdmin } from '../../../utils/auth.js'
/**
* 批量操作:{ action: 'publish'|'unpublish'|'delete', ids: number[] }
*/
export default defineEventHandler(async (event) => {
requireAdmin(event)
const { action, ids } = await readBody(event)
if (!Array.isArray(ids) || ids.length === 0) {
throw createError({ statusCode: 422, message: 'ids 不能为空' })
}
const db = useDB()
switch (action) {
case 'publish':
db.update(schema.reviews).set({ published: true }).where(inArray(schema.reviews.id, ids)).run()
break
case 'unpublish':
db.update(schema.reviews).set({ published: false }).where(inArray(schema.reviews.id, ids)).run()
break
case 'delete':
db.delete(schema.reviews).where(inArray(schema.reviews.id, ids)).run()
break
default:
throw createError({ statusCode: 422, message: '未知 action' })
}
return { success: true, affected: ids.length }
})
+2 -1
查看文件
@@ -17,7 +17,8 @@ export default defineEventHandler(async (event) => {
.get()
if (!item) {
throw createError({ statusCode: 404, message: '未找到该配置' })
// 返回空配置壳,前端按默认值渲染;保存时会自动创建
return { key: query.key, label: '', data: {} }
}
return { ...item, data: JSON.parse(item.data) }
+59 -15
查看文件
@@ -1,4 +1,4 @@
import { count } from 'drizzle-orm'
import { count, eq, desc, sql, gte } from 'drizzle-orm'
import { useDB, schema } from '../../utils/db.js'
import { requireAdmin } from '../../utils/auth.js'
@@ -7,21 +7,65 @@ export default defineEventHandler(async (event) => {
const db = useDB()
const [articles] = db.select({ count: count() }).from(schema.articles).all()
const [reviews] = db.select({ count: count() }).from(schema.reviews).all()
const [faqs] = db.select({ count: count() }).from(schema.faqs).all()
const [gallery] = db.select({ count: count() }).from(schema.galleryItems).all()
const [stories] = db.select({ count: count() }).from(schema.stories).all()
const [submissions] = db.select({ count: count() }).from(schema.submissions).all()
const [siteContent] = db.select({ count: count() }).from(schema.siteContent).all()
const [newsRow] = db.select({ c: count() }).from(schema.articles).where(eq(schema.articles.type, 'news')).all()
const [blogRow] = db.select({ c: count() }).from(schema.articles).where(eq(schema.articles.type, 'blog')).all()
const [reviewsRow] = db.select({ c: count() }).from(schema.reviews).all()
const [faqsRow] = db.select({ c: count() }).from(schema.faqs).all()
const [galleryRow] = db.select({ c: count() }).from(schema.galleryItems).all()
const [storiesRow] = db.select({ c: count() }).from(schema.stories).all()
const [subTotalRow] = db.select({ c: count() }).from(schema.submissions).all()
const [subUnreadRow] = db.select({ c: count() }).from(schema.submissions).where(eq(schema.submissions.read, false)).all()
const [siteContentRow] = db.select({ c: count() }).from(schema.siteContent).all()
// 产品线数量(从 site_content 的 product-lines 键解析)
let productLines = 0
try {
const pl = db.select().from(schema.siteContent).where(eq(schema.siteContent.key, 'product-lines')).all()
if (pl[0]?.data) {
const arr = JSON.parse(pl[0].data)
if (Array.isArray(arr)) productLines = arr.length
}
} catch {}
// 近 7 天真实提交(仅统计用户表单,文章/评价为后台手动录入不计入)
const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()
const [recentSubRow] = db.select({ c: count() }).from(schema.submissions).where(gte(schema.submissions.createdAt, weekAgo)).all()
const [recentContactRow] = db.select({ c: count() }).from(schema.submissions).where(sql`${schema.submissions.type} = 'contact' AND ${schema.submissions.createdAt} >= ${weekAgo}`).all()
const [recentCustomizeRow] = db.select({ c: count() }).from(schema.submissions).where(sql`${schema.submissions.type} = 'customize' AND ${schema.submissions.createdAt} >= ${weekAgo}`).all()
// 最近 5 条提交 / 最近 5 条评价
const recentSubmissions = db.select({
id: schema.submissions.id, type: schema.submissions.type,
read: schema.submissions.read, createdAt: schema.submissions.createdAt,
}).from(schema.submissions).orderBy(desc(schema.submissions.id)).limit(5).all()
const recentReviews = db.select({
id: schema.reviews.id, author: schema.reviews.author, rating: schema.reviews.rating,
title: schema.reviews.title, createdAt: schema.reviews.createdAt,
}).from(schema.reviews).orderBy(desc(schema.reviews.id)).limit(5).all()
return {
articles: articles.count,
reviews: reviews.count,
faqs: faqs.count,
gallery: gallery.count,
stories: stories.count,
submissions: submissions.count,
siteContent: siteContent.count,
articles: {
total: newsRow.c + blogRow.c,
news: newsRow.c,
blog: blogRow.c,
},
reviews: reviewsRow.c,
faqs: faqsRow.c,
gallery: galleryRow.c,
stories: storiesRow.c,
submissions: {
total: subTotalRow.c,
unread: subUnreadRow.c,
},
siteContent: siteContentRow.c,
productLines,
recent: {
submissions7d: recentSubRow.c,
contact7d: recentContactRow.c,
customize7d: recentCustomizeRow.c,
},
recentSubmissions,
recentReviews,
}
})
+4
查看文件
@@ -20,6 +20,10 @@ export default defineEventHandler(async (event) => {
tripProduct: body.tripProduct || '',
travelDate: body.travelDate || '',
coverImage: body.coverImage || '',
summary: body.summary || '',
author: body.author || '',
sourceUrl: body.sourceUrl || '',
tags: JSON.stringify(Array.isArray(body.tags) ? body.tags : []),
sections: body.sections ? JSON.stringify(body.sections) : '[]',
keyMoments: body.keyMoments ? JSON.stringify(body.keyMoments) : '[]',
seoTitle: body.seoTitle || '',
+6 -1
查看文件
@@ -8,5 +8,10 @@ export default defineEventHandler(async (event) => {
const db = useDB()
const item = db.select().from(schema.stories).where(eq(schema.stories.id, id)).get()
if (!item) throw createError({ statusCode: 404, message: '故事不存在' })
return item
return {
...item,
tags: (() => { try { return JSON.parse(item.tags || '[]') } catch { return [] } })(),
sections: (() => { try { return JSON.parse(item.sections || '[]') } catch { return [] } })(),
keyMoments: (() => { try { return JSON.parse(item.keyMoments || '[]') } catch { return [] } })(),
}
})
+4
查看文件
@@ -19,6 +19,10 @@ export default defineEventHandler(async (event) => {
if (body.tripProduct !== undefined) updateData.tripProduct = body.tripProduct
if (body.travelDate !== undefined) updateData.travelDate = body.travelDate
if (body.coverImage !== undefined) updateData.coverImage = body.coverImage
if (body.summary !== undefined) updateData.summary = body.summary
if (body.author !== undefined) updateData.author = body.author
if (body.sourceUrl !== undefined) updateData.sourceUrl = body.sourceUrl
if (body.tags !== undefined) updateData.tags = Array.isArray(body.tags) ? JSON.stringify(body.tags) : body.tags
if (body.sections !== undefined) updateData.sections = JSON.stringify(body.sections)
if (body.keyMoments !== undefined) updateData.keyMoments = JSON.stringify(body.keyMoments)
if (body.seoTitle !== undefined) updateData.seoTitle = body.seoTitle
@@ -0,0 +1,30 @@
import { eq } from 'drizzle-orm'
import { useDB, schema } from '../../../utils/db.js'
import { requireAdmin } from '../../../utils/auth.js'
export default defineEventHandler(async (event) => {
requireAdmin(event)
const id = Number(getRouterParam(event, 'id'))
if (!id || isNaN(id)) {
throw createError({ statusCode: 400, message: '无效的 id 参数' })
}
const db = useDB()
const [existing] = db
.select()
.from(schema.submissions)
.where(eq(schema.submissions.id, id))
.all()
if (!existing) {
throw createError({ statusCode: 404, message: '记录不存在' })
}
db.delete(schema.submissions)
.where(eq(schema.submissions.id, id))
.run()
return { success: true, id }
})
+21 -4
查看文件
@@ -10,9 +10,26 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 400, message: '无效的 id 参数' })
}
const body = await readBody(event)
if (typeof body.read !== 'boolean') {
throw createError({ statusCode: 400, message: 'read 字段必须是布尔值' })
const body = await readBody(event) || {}
const patch = {}
if (typeof body.read === 'boolean') {
patch.read = body.read
}
if (typeof body.replied === 'boolean') {
patch.replied = body.replied
patch.repliedAt = body.replied ? new Date().toISOString() : null
// 标记为已回复时自动视为已读
if (body.replied) patch.read = true
}
if (body.data && typeof body.data === 'object' && !Array.isArray(body.data)) {
patch.data = JSON.stringify(body.data)
}
if (Object.keys(patch).length === 0) {
throw createError({ statusCode: 400, message: '请求体必须包含 read/replied/data 中的至少一个字段' })
}
const db = useDB()
@@ -28,7 +45,7 @@ export default defineEventHandler(async (event) => {
}
db.update(schema.submissions)
.set({ read: body.read })
.set(patch)
.where(eq(schema.submissions.id, id))
.run()
+28 -9
查看文件
@@ -24,12 +24,30 @@ function buildFromEntityTable(db, key, siteData) {
scenes: r.scenes ? JSON.parse(r.scenes) : [],
concerns: r.concerns ? JSON.parse(r.concerns) : [],
}))
// 合并 site_content 的 summary(如果有/��
const summary = siteData?.summary || {
totalCount: items.length,
approvalRate: '98%',
// 高频关键词:从评价正文中按预设词表统计出现频次
const KEYWORD_DICT = [
'领队', '服务', '贴心', '耐心', '细心', '专业',
'摄影', '拍照', '旅拍', '骑马', '越野车', '滑草',
'营地', '烧烤', '住宿', '行程', '错峰', '不排队',
'亲子', '小朋友', '孩子', '家人', '老人',
'草原', '风景', '美', '惊艳', '难忘', '舒适',
'安排', '推荐', '值得', '良心', '安心', '用心',
]
const freq = {}
for (const it of items) {
const c = it.content || ''
for (const kw of KEYWORD_DICT) if (c.includes(kw)) freq[kw] = (freq[kw] || 0) + 1
}
const keywords = Object.entries(freq)
.sort((a, b) => b[1] - a[1])
.slice(0, 12)
.map(([w]) => w)
const summary = {
totalCount: siteData?.summary?.totalCount || items.length,
approvalRate: siteData?.summary?.approvalRate || '98%',
keywords: siteData?.summary?.keywords?.length ? siteData.summary.keywords : keywords,
}
// 用实体表 items 覆盖 site_content 的 items
return { summary, items }
}
@@ -82,6 +100,7 @@ function buildFromEntityTable(db, key, siteData) {
.all()
.map(s => ({
...s,
tags: s.tags ? (() => { try { return JSON.parse(s.tags) } catch { return [] } })() : [],
sections: s.sections ? JSON.parse(s.sections) : [],
keyMoments: s.keyMoments ? JSON.parse(s.keyMoments) : [],
}))
@@ -91,7 +110,7 @@ function buildFromEntityTable(db, key, siteData) {
case 'news': {
const items = db.select().from(schema.articles)
.where(eq(schema.articles.published, true))
.orderBy(desc(schema.articles.id))
.orderBy(desc(schema.articles.createdAt), desc(schema.articles.id))
.all()
.filter(a => a.type === 'news')
.map(a => ({
@@ -153,7 +172,7 @@ export default defineEventHandler(async (event) => {
if (ENTITY_KEYS.includes(key)) {
const result = buildFromEntityTable(db, key, siteData)
if (result) {
setHeader(event, 'Cache-Control', 'public, max-age=60')
setHeader(event, 'Cache-Control', 'no-store, must-revalidate')
return result
}
}
@@ -165,7 +184,7 @@ export default defineEventHandler(async (event) => {
.get()
const productsData = productsItem ? JSON.parse(productsItem.data) : null
if (productsData?.summerCamp) {
setHeader(event, 'Cache-Control', 'public, max-age=60')
setHeader(event, 'Cache-Control', 'no-store, must-revalidate')
return productsData.summerCamp
}
}
@@ -175,6 +194,6 @@ export default defineEventHandler(async (event) => {
throw createError({ statusCode: 404, message: `未找到配置项: ${key}` })
}
setHeader(event, 'Cache-Control', 'public, max-age=60')
setHeader(event, 'Cache-Control', 'no-store, must-revalidate')
return siteData
})
+16
查看文件
@@ -52,6 +52,9 @@ CREATE TABLE IF NOT EXISTS reviews (
title TEXT,
content TEXT,
date TEXT,
screenshot TEXT,
scenes TEXT,
concerns TEXT,
verified INTEGER DEFAULT 0,
published INTEGER DEFAULT 1,
sort_order INTEGER DEFAULT 0,
@@ -104,6 +107,8 @@ CREATE TABLE IF NOT EXISTS submissions (
data TEXT NOT NULL,
ip TEXT,
read INTEGER DEFAULT 0,
replied INTEGER DEFAULT 0,
replied_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
@@ -125,4 +130,15 @@ CREATE TABLE IF NOT EXISTS sessions (
db.exec(tables)
console.log('✅ 数据库表创建完成:', DB_PATH)
// ── 增量迁移:为已有表补齐新列(幂等)───────────────
function addColumnIfMissing(table, column, ddl) {
const info = db.prepare(`PRAGMA table_info(${table})`).all()
if (!info.some(c => c.name === column)) {
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddl}`)
console.log(` + ${table}.${column} 列已添加`)
}
}
addColumnIfMissing('submissions', 'replied', 'INTEGER DEFAULT 0')
addColumnIfMissing('submissions', 'replied_at', 'TEXT')
db.close()
+6
查看文件
@@ -81,6 +81,10 @@ export const stories = sqliteTable('stories', {
tripProduct: text('trip_product'),
travelDate: text('travel_date'),
coverImage: text('cover_image'),
summary: text('summary'), // 卡片展示的 2 行摘要
author: text('author'), // 客户昵称/作者
sourceUrl: text('source_url'), // 小红书等外链(有则前台卡片外跳)
tags: text('tags'), // JSON 数组(标签)
// sections 存为 JSON 字符串: [{ heading, content }]
sections: text('sections'),
keyMoments: text('key_moments'), // JSON 数组
@@ -98,6 +102,8 @@ export const submissions = sqliteTable('submissions', {
data: text('data').notNull(), // JSON 字符串,存完整表单数据
ip: text('ip'),
read: integer('read', { mode: 'boolean' }).default(false),
replied: integer('replied', { mode: 'boolean' }).default(false),
repliedAt: text('replied_at'),
createdAt: text('created_at').notNull().$defaultFn(() => new Date().toISOString()),
})
+3
查看文件
@@ -140,6 +140,7 @@ console.log(`✅ 客户故事导入: ${storiesData.stories.length} 条`)
// ── 导入站点配置(复杂 JSON 直接存)──────────────────
const siteConfigs = [
{ key: 'product-lines', label: '产品线列表', file: 'product-lines.json' },
{ key: 'products', label: '产品管理', file: 'products.json' },
{ key: 'autumn-products', label: '秋季产品', file: 'autumn-products.json' },
{ key: 'winter-products', label: '冬季产品', file: 'winter-products.json' },
@@ -149,6 +150,8 @@ const siteConfigs = [
{ key: 'courses', label: '研学课程', file: 'courses.json' },
{ key: 'calendar', label: '出行日历', file: 'calendar.json' },
{ key: 'guides', label: '出行指南', file: 'guides.json' },
{ key: 'guide-custom', label: '定制游怎么选 (文章)', file: 'guide-custom.json' },
{ key: 'section-titles', label: '栏目标题', file: 'section-titles.json' },
{ key: 'navigation', label: '导航配置', file: 'navigation.json' },
{ key: 'brand', label: '品牌信息', file: 'brand.json' },
{ key: 'contact', label: '联系方式', file: 'contact.json' },
+1 -22
查看文件
@@ -1,22 +1 @@
[
{
"id": "CT1774007263178",
"createdAt": "2026-03-20T11:47:43.178Z",
"name": "李女士",
"phone": "13711112222",
"subject": "询问秋季团期",
"message": "请问9月底有哪些团期可以选择?",
"ip": "::1",
"source": "contact-form"
},
{
"id": "CT1774023293117",
"createdAt": "2026-03-20T16:14:53.117Z",
"name": "测试用户",
"phone": "13800138000",
"subject": "行程咨询",
"message": "请问七月份有空档吗,想带家人来",
"ip": "::1",
"source": "contact-form"
}
]
[]
+1 -107
查看文件
@@ -1,107 +1 @@
[
{
"id": "CZ1774007128348",
"createdAt": "2026-03-20T11:45:28.348Z",
"name": "测试用户",
"phone": "13800138000",
"wechat": "",
"adults": 2,
"children": 1,
"childAges": [
5
],
"date": "2026-07-01",
"duration": "5d4n",
"activities": [
"骑马",
"露营"
],
"budget": "1-2万",
"remarks": "这是测试提交",
"ip": "::1",
"source": "customize-form"
},
{
"id": "CZ1774007220100",
"createdAt": "2026-03-20T11:47:00.100Z",
"name": "王小明",
"phone": "13912345678",
"wechat": "wxid_test",
"adults": 2,
"children": 2,
"childAges": [
6,
9
],
"date": "2026-08-15",
"duration": "6d5n",
"activities": [
"骑马",
"露营",
"捕鱼"
],
"budget": "2-3万",
"remarks": "希望住蒙古包",
"ip": "::1",
"source": "customize-form"
},
{
"id": "CZ1774023293092",
"createdAt": "2026-03-20T16:14:53.092Z",
"name": "测试用户",
"phone": "13800138000",
"wechat": "",
"adults": 2,
"children": 1,
"childAges": [
5
],
"date": "2026-07-15",
"duration": "6d5n",
"activities": [],
"budget": "8000-12000",
"remarks": "",
"ip": "::1",
"source": "customize-form"
},
{
"id": "CZ1774057315225",
"createdAt": "2026-03-21T01:41:55.225Z",
"name": "测试用户",
"phone": "13800138000",
"wechat": "",
"adults": 2,
"children": 1,
"childAges": [],
"date": "",
"duration": "6天5晚",
"activities": [
"骑马",
"旅拍"
],
"budget": "品质优先",
"remarks": "",
"ip": "::1",
"source": "customize-form"
},
{
"id": "CZ1774335344590",
"createdAt": "2026-03-24T06:55:44.590Z",
"name": "发电公司",
"phone": "17600545225",
"wechat": "",
"adults": 2,
"children": 0,
"childAges": [],
"date": "2026-03-26",
"duration": "5天4晚",
"activities": [
"亲子活动",
"旅拍"
],
"budget": "5000-8000元",
"remarks": "第三方公司代发",
"ip": "::1",
"source": "customize-form"
}
]
[]