feat: add ticket client demo data
This commit is contained in:
166
src/pages-service/tickets/data.js
Normal file
166
src/pages-service/tickets/data.js
Normal file
@@ -0,0 +1,166 @@
|
||||
import { createPaidOrder } from './model.mjs'
|
||||
|
||||
export const PLACEHOLDER_IMAGE = 'https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/oneFeel/2082673896938569729.jpg'
|
||||
|
||||
export const SCENIC_SPOT = {
|
||||
id: 'libo-xiaoqikong',
|
||||
name: '荔波小七孔景区',
|
||||
level: '国家5A级景区',
|
||||
heritage: '世界自然遗产',
|
||||
address: '贵州省黔南布依族苗族自治州荔波县',
|
||||
phone: '0854-3619810',
|
||||
latitude: 25.2534,
|
||||
longitude: 107.7255,
|
||||
heroImage: PLACEHOLDER_IMAGE,
|
||||
}
|
||||
|
||||
export const GROUPS = [
|
||||
{ id: 'ticket', name: '景点门票', subtitle: '畅游小七孔核心景点', description: '适合自由行游客的景区基础门票', tag: '热门', productIds: ['adult', 'elderly', 'minor', 'free'], sortOrder: 1, enabled: true },
|
||||
{ id: 'vip', name: 'VIP速通', subtitle: '快速入园省时省心', description: '专属快速通道,减少排队等待', tag: '优选', productIds: ['vip-fast'], sortOrder: 2, enabled: true },
|
||||
{ id: 'bus', name: '直通车', subtitle: '往返交通一站直达', description: '景区直通车,出行更轻松', tag: '便捷', productIds: ['direct-bus'], sortOrder: 3, enabled: true },
|
||||
{ id: 'package', name: '门票+观光车', subtitle: '经典组合更划算', description: '门票与观光车联票,覆盖主要游览动线', tag: '套餐', productIds: ['classic'], sortOrder: 4, enabled: true },
|
||||
{ id: 'tour', name: '一日游', subtitle: '跟团畅游无忧', description: '含行程服务的一日游体验', tag: '省心', productIds: ['day-tour'], sortOrder: 5, enabled: true },
|
||||
{ id: 'play', name: '玩乐体验', subtitle: '解锁山水更多乐趣', description: '水上与讲解体验任选', tag: '推荐', productIds: ['boat', 'guide'], sortOrder: 6, enabled: true },
|
||||
]
|
||||
|
||||
const TIME_SLOTS = ['08:00—09:30', '09:30—11:30', '11:30—13:30', '13:30—15:00', '15:00—16:00']
|
||||
const ID_TYPES = ['身份证', '护照', '港澳居民来往内地通行证']
|
||||
|
||||
const createReservation = ({ requiresTimeSlot = true, advanceDays = 0, maxQuantity = 5, timeSlots = TIME_SLOTS } = {}) => ({
|
||||
requiresDate: true,
|
||||
advanceDays,
|
||||
requiresTimeSlot,
|
||||
timeSlots: requiresTimeSlot ? [...timeSlots] : [],
|
||||
realNameRequired: true,
|
||||
idTypes: [...ID_TYPES],
|
||||
maxQuantity,
|
||||
contactPhoneRequired: true,
|
||||
})
|
||||
|
||||
const createProduct = ({
|
||||
id,
|
||||
name,
|
||||
shortTitle,
|
||||
category,
|
||||
description,
|
||||
salePrice,
|
||||
marketPrice = salePrice,
|
||||
stock,
|
||||
sold,
|
||||
highlights,
|
||||
ageRule = null,
|
||||
included,
|
||||
excluded,
|
||||
detail,
|
||||
purchaseNotice,
|
||||
reservation,
|
||||
entitlements,
|
||||
}) => ({
|
||||
id,
|
||||
name,
|
||||
shortTitle,
|
||||
category,
|
||||
description,
|
||||
salePrice,
|
||||
marketPrice,
|
||||
stock,
|
||||
sold,
|
||||
status: 'on_sale',
|
||||
highlights: [...highlights],
|
||||
ageRule: ageRule ? { ...ageRule } : null,
|
||||
included: [...included],
|
||||
excluded: [...excluded],
|
||||
detail,
|
||||
purchaseNotice,
|
||||
images: [PLACEHOLDER_IMAGE],
|
||||
reservation: createReservation(reservation),
|
||||
entitlements: entitlements.map((entitlement) => ({ ...entitlement })),
|
||||
})
|
||||
|
||||
const ticketEntitlement = (id, name, fulfillment, refundAllocation) => ({ id, name, fulfillment, quantity: 1, unit: '张', refundAllocation })
|
||||
|
||||
export const PRODUCTS = [
|
||||
createProduct({
|
||||
id: 'adult', name: '成人票', shortTitle: '成人门票', category: '景点门票', description: '荔波小七孔景区成人入园门票。', salePrice: 160, marketPrice: 170, stock: 120, sold: 2386,
|
||||
highlights: ['实名预约入园', '覆盖核心景区'], ageRule: { min: 18, max: 59 }, included: ['成人景区门票', '景区观光车'], excluded: ['餐饮及个人消费'], detail: '凭本人有效证件在预约时段入园。', purchaseNotice: '请至少提前一天预约,订单当天不可退改。',
|
||||
reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('adult-ticket', '成人门票', 'third_party', 120), ticketEntitlement('shuttle', '观光车票', 'third_party', 40)],
|
||||
}),
|
||||
createProduct({
|
||||
id: 'elderly', name: '老年票', shortTitle: '老年优惠票', category: '景点门票', description: '适用于60周岁及以上游客的优惠门票。', salePrice: 100, marketPrice: 160, stock: 80, sold: 816,
|
||||
highlights: ['60周岁及以上', '实名检票'], ageRule: { min: 60, max: 120 }, included: ['老年优惠门票'], excluded: ['观光车及其他二次消费'], detail: '入园时须出示本人身份证核验年龄。', purchaseNotice: '年龄不符将无法使用,请按实际年龄购票。',
|
||||
reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('elderly-ticket', '老年优惠门票', 'third_party', 100)],
|
||||
}),
|
||||
createProduct({
|
||||
id: 'minor', name: '儿童票', shortTitle: '儿童优惠票', category: '景点门票', description: '适用于6至17周岁未成年人。', salePrice: 100, marketPrice: 160, stock: 75, sold: 629,
|
||||
highlights: ['6至17周岁适用', '实名预约'], ageRule: { min: 6, max: 17 }, included: ['儿童优惠门票'], excluded: ['观光车及其他二次消费'], detail: '未成年人须使用本人有效身份证件预约。', purchaseNotice: '请按使用日年龄选择票种。',
|
||||
reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('minor-ticket', '儿童优惠门票', 'third_party', 100)],
|
||||
}),
|
||||
createProduct({
|
||||
id: 'free', name: '优待票', shortTitle: '优待入园票', category: '景点门票', description: '适用于符合景区优待政策的游客。', salePrice: 40, marketPrice: 80, stock: 60, sold: 346,
|
||||
highlights: ['优待人群适用', '凭证核验'], included: ['优待入园资格'], excluded: ['观光车及其他二次消费'], detail: '入园时须携带可证明优待资格的有效证件。', purchaseNotice: '不符合优待政策者需现场补差价。',
|
||||
reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('preferential-ticket', '优待入园票', 'third_party', 40)],
|
||||
}),
|
||||
createProduct({
|
||||
id: 'vip-fast', name: 'VIP速通票', shortTitle: 'VIP快速通行', category: 'VIP速通', description: '专属通道快速入园,节省排队时间。', salePrice: 98, marketPrice: 128, stock: 50, sold: 126,
|
||||
highlights: ['专属快速通道', '即买即用'], included: ['VIP速通服务'], excluded: ['景区基础门票'], detail: '需与有效景区门票配合使用。', purchaseNotice: '服务一经核销不可退款。',
|
||||
reservation: { advanceDays: 0, timeSlots: ['08:00—10:00', '10:00—12:00', '12:00—14:00', '14:00—16:00'] }, entitlements: [ticketEntitlement('vip-fast-pass', 'VIP速通服务', 'self', 98)],
|
||||
}),
|
||||
createProduct({
|
||||
id: 'direct-bus', name: '景区直通车', shortTitle: '往返直通车', category: '直通车', description: '市区往返景区的便捷直通车服务。', salePrice: 68, marketPrice: 88, stock: 40, sold: 92,
|
||||
highlights: ['往返直达', '固定班次'], included: ['景区直通车单程服务'], excluded: ['景区门票'], detail: '请按预约班次提前15分钟候车。', purchaseNotice: '发车前2小时可免费改期一次。',
|
||||
reservation: { advanceDays: 0, timeSlots: ['06:30—07:00 去程', '18:00—18:30 返程'] }, entitlements: [ticketEntitlement('direct-bus-seat', '景区直通车', 'third_party', 68)],
|
||||
}),
|
||||
createProduct({
|
||||
id: 'classic', name: '经典联票', shortTitle: '门票+观光车', category: '门票+观光车', description: '景区成人门票与观光车组合套餐。', salePrice: 188, marketPrice: 210, stock: 90, sold: 184,
|
||||
highlights: ['门票观光车联票', '比单买更优惠'], ageRule: { min: 18, max: 59 }, included: ['成人景区门票', '景区观光车', '竹筏体验'], excluded: ['餐饮及个人消费'], detail: '一次预约即可享受门票、观光车与竹筏体验。', purchaseNotice: '各权益可分别核销,未使用部分可按规则申请退款。',
|
||||
reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('classic-ticket-shuttle', '成人门票+观光车', 'third_party', 160), ticketEntitlement('classic-boat', '竹筏体验', 'self', 28)],
|
||||
}),
|
||||
createProduct({
|
||||
id: 'day-tour', name: '小七孔一日游', shortTitle: '精品一日游', category: '一日游', description: '包含行程组织服务的小七孔一日游。', salePrice: 188, marketPrice: 228, stock: 30, sold: 68,
|
||||
highlights: ['行程服务', '省心游玩'], included: ['一日游行程服务', '行程讲解'], excluded: ['景区门票及个人消费'], detail: '集合地点与行程安排将于出行前发送。', purchaseNotice: '请在出行前一天18:00前完成预约。',
|
||||
reservation: { advanceDays: 1, maxQuantity: 10, timeSlots: ['08:00—08:30 集合', '09:00—09:30 集合'] }, entitlements: [ticketEntitlement('day-tour-service', '一日游服务', 'self', 188)],
|
||||
}),
|
||||
createProduct({
|
||||
id: 'boat', name: '卧龙潭竹筏体验', shortTitle: '竹筏体验', category: '玩乐体验', description: '卧龙潭水域竹筏游览体验。', salePrice: 50, marketPrice: 60, stock: 45, sold: 326,
|
||||
highlights: ['山水竹筏', '随到随玩'], included: ['竹筏体验一次'], excluded: ['景区门票'], detail: '请在营业时间内前往卧龙潭竹筏码头核销。', purchaseNotice: '受天气影响时,景区可调整运营安排。',
|
||||
reservation: { requiresTimeSlot: false, advanceDays: 0 }, entitlements: [ticketEntitlement('boat-experience', '卧龙潭竹筏体验', 'self', 50)],
|
||||
}),
|
||||
createProduct({
|
||||
id: 'guide', name: '人工讲解服务', shortTitle: '专业人工讲解', category: '玩乐体验', description: '专业讲解员陪同讲解景区自然与人文风光。', salePrice: 68, marketPrice: 88, stock: 20, sold: 40,
|
||||
highlights: ['专业讲解员', '沉浸式游览'], included: ['60分钟人工讲解服务'], excluded: ['景区门票及交通'], detail: '讲解员将按预约时段在景区入口等候。', purchaseNotice: '请提前10分钟到达指定集合点。',
|
||||
reservation: { advanceDays: 1, maxQuantity: 10, timeSlots: ['09:00—10:30', '13:30—15:00'] }, entitlements: [ticketEntitlement('guide-service', '人工讲解服务', 'self', 68)],
|
||||
}),
|
||||
]
|
||||
|
||||
export const AVAILABLE_DATES = [
|
||||
{ date: '2026-08-01', week: '周六', label: '8月1日', stock: 120 },
|
||||
{ date: '2026-08-02', week: '周日', label: '8月2日', stock: 96 },
|
||||
{ date: '2026-08-03', week: '周一', label: '8月3日', stock: 68 },
|
||||
{ date: '2026-08-04', week: '周二', label: '8月4日', stock: 0 },
|
||||
{ date: '2026-08-05', week: '周三', label: '8月5日', stock: 42 },
|
||||
]
|
||||
|
||||
export const INSURANCE = { name: '出行意外险', price: 5, description: '最高10万元意外保障' }
|
||||
|
||||
export const REFUND_REASONS = ['行程有变,无法按计划出行', '重复购买', '选错日期或票种', '其他原因']
|
||||
|
||||
export const INITIAL_TRAVELERS = [
|
||||
{ id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' },
|
||||
{ id: 'traveler-zhou-ning', name: '周宁', idType: '身份证', idNumber: '110101196001020010', phone: '13900139000' },
|
||||
]
|
||||
|
||||
const createDemoOrder = (productId, draft, now) => createPaidOrder({
|
||||
product: PRODUCTS.find((product) => product.id === productId),
|
||||
draft,
|
||||
travelers: INITIAL_TRAVELERS.map((traveler) => ({ ...traveler })),
|
||||
now,
|
||||
})
|
||||
|
||||
export const INITIAL_ORDERS = [
|
||||
createDemoOrder('adult', {
|
||||
visitDate: '2026-08-01', timeSlot: '08:00—09:30', quantity: 1, travelerIds: ['traveler-lin-xiao'], contactName: '林晓', phone: '13800138000', insuranceSelected: true, insurancePrice: INSURANCE.price, maxQuantity: 5,
|
||||
}, new Date('2026-07-30T09:00:00+08:00')),
|
||||
createDemoOrder('classic', {
|
||||
visitDate: '2026-08-02', timeSlot: '09:30—11:30', quantity: 1, travelerIds: ['traveler-lin-xiao'], contactName: '林晓', phone: '13800138000', insuranceSelected: false, insurancePrice: INSURANCE.price, maxQuantity: 5,
|
||||
}, new Date('2026-07-30T09:05:00+08:00')),
|
||||
]
|
||||
@@ -14,6 +14,38 @@ import {
|
||||
validateBooking,
|
||||
} from './model.mjs'
|
||||
|
||||
test('provides coherent default visitor ticket data', async () => {
|
||||
const {
|
||||
AVAILABLE_DATES,
|
||||
GROUPS,
|
||||
INITIAL_ORDERS,
|
||||
INITIAL_TRAVELERS,
|
||||
INSURANCE,
|
||||
PLACEHOLDER_IMAGE,
|
||||
PRODUCTS,
|
||||
REFUND_REASONS,
|
||||
} = await import('./data.js')
|
||||
|
||||
assert.equal(PLACEHOLDER_IMAGE, 'https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/oneFeel/2082673896938569729.jpg')
|
||||
assert.deepEqual(GROUPS.map(({ id }) => id), ['ticket', 'vip', 'bus', 'package', 'tour', 'play'])
|
||||
assert.deepEqual(GROUPS.map(({ sortOrder, enabled }) => [sortOrder, enabled]), [[1, true], [2, true], [3, true], [4, true], [5, true], [6, true]])
|
||||
assert.deepEqual(PRODUCTS.map(({ id }) => id), ['adult', 'elderly', 'minor', 'free', 'vip-fast', 'direct-bus', 'classic', 'day-tour', 'boat', 'guide'])
|
||||
const productsById = new Map(PRODUCTS.map((product) => [product.id, product]))
|
||||
assert.ok(GROUPS.every((group) => group.productIds.every((productId) => productsById.has(productId))))
|
||||
assert.deepEqual(GROUPS.flatMap((group) => group.productIds).sort(), PRODUCTS.map(({ id }) => id).sort())
|
||||
assert.ok(PRODUCTS.every((product) => product.images[0] === PLACEHOLDER_IMAGE && product.reservation && product.entitlements.length && Number.isFinite(product.salePrice) && Number.isFinite(product.stock) && Number.isFinite(product.sold)))
|
||||
assert.deepEqual(PRODUCTS.slice(0, 3).map(({ id, salePrice, sold }) => [id, salePrice, sold]), [['adult', 160, 2386], ['elderly', 100, 816], ['minor', 100, 629]])
|
||||
assert.ok(INITIAL_TRAVELERS.length > 0)
|
||||
assert.ok(INITIAL_TRAVELERS.every((traveler) => traveler.id && traveler.name && traveler.idType && traveler.idNumber && traveler.phone))
|
||||
assert.equal(INITIAL_ORDERS.length, 2)
|
||||
assert.ok(INITIAL_ORDERS.some((order) => order.entitlements.some((entitlement) => entitlement.fulfillment === 'third_party') && order.gateCredential))
|
||||
assert.ok(INITIAL_ORDERS.some((order) => order.entitlements.some((entitlement) => entitlement.fulfillment === 'third_party') && order.entitlements.some((entitlement) => entitlement.fulfillment === 'self') && order.gateCredential && order.selfCredential))
|
||||
assert.equal(AVAILABLE_DATES.length, 5)
|
||||
assert.ok(AVAILABLE_DATES.some((date) => date.stock === 0))
|
||||
assert.equal(REFUND_REASONS.length, 4)
|
||||
assert.equal(INSURANCE.price, 5)
|
||||
})
|
||||
|
||||
const adultProduct = {
|
||||
id: 'adult',
|
||||
name: '成人票',
|
||||
|
||||
Reference in New Issue
Block a user