diff --git a/src/pages-service/tickets/components/TicketHome.vue b/src/pages-service/tickets/components/TicketHome.vue index 293b64a..47d4b14 100644 --- a/src/pages-service/tickets/components/TicketHome.vue +++ b/src/pages-service/tickets/components/TicketHome.vue @@ -45,7 +45,7 @@ - {{ product.shortTitle || product.name }} + + {{ product.shortTitle || product.name }} + + + + - {{ ageRuleText(product.ageRule) }} - {{ highlight }} + {{ ageRuleText(product.ageRule) }} + {{ highlight }} 已售 {{ product.sold || 0 }} - ¥{{ product.salePrice }} + ¥{{ product.salePrice }} - - 选购 + + 选购 @@ -132,9 +137,13 @@ const productsById = computed(() => new Map(props.products.map((product) => [pro const productsForGroup = (group) => (group.productIds || []) .map((productId) => productsById.value.get(productId)) - .filter(Boolean) + .filter((product) => product?.status === 'on_sale' && product.clientVisible !== false) -const ageRuleText = (rule) => `${rule.min}—${rule.max}周岁` +const ageRuleText = (rule) => { + if (rule.min > 0 && rule.max >= 999) return `${rule.min}周岁及以上` + if (rule.min <= 0 && rule.max < 999) return `${rule.max}周岁及以下` + return `${rule.min}—${rule.max}周岁` +} const selectGroup = (groupId) => { activeGroupId.value = groupId diff --git a/src/pages-service/tickets/data.js b/src/pages-service/tickets/data.js index 552c612..664b208 100644 --- a/src/pages-service/tickets/data.js +++ b/src/pages-service/tickets/data.js @@ -15,16 +15,16 @@ export const SCENIC_SPOT = { } 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 }, + { id: 'ticket', name: '景点门票', subtitle: '累计销量 5000+', description: '一票通玩,核销后门票7日内有效', 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: '累计销量 1.5万+', 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 ID_TYPES = ['身份证', '护照'] const createReservation = ({ requiresDate = true, @@ -64,6 +64,8 @@ const createProduct = ({ purchaseNotice, reservation, entitlements, + status = 'on_sale', + clientVisible = true, }) => ({ id, name, @@ -74,7 +76,8 @@ const createProduct = ({ marketPrice, stock, sold, - status: 'on_sale', + status, + clientVisible, highlights: [...highlights], ageRule: ageRule ? { ...ageRule } : null, included: [...included], @@ -86,58 +89,58 @@ const createProduct = ({ entitlements: entitlements.map((entitlement) => ({ ...entitlement })), }) -const ticketEntitlement = (id, name, fulfillment, refundAllocation) => ({ id, name, fulfillment, quantity: 1, unit: '张', refundAllocation }) +const ticketEntitlement = (id, name, fulfillment, refundAllocation, unit = '张') => ({ 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: ['实名预约入园', '覆盖核心景区'], included: ['成人景区门票', '景区观光车'], excluded: ['餐饮及个人消费'], detail: '凭本人有效证件在预约时段入园。', purchaseNotice: '请至少提前一天预约,订单当天不可退改。', - reservation: { advanceDays: 1 }, entitlements: [ticketEntitlement('adult-ticket', '成人门票', 'third_party', 120), ticketEntitlement('shuttle', '观光车票', 'third_party', 40)], + id: 'adult', name: '荔波小七孔成人票+观光车', shortTitle: '成人门票+观光车', category: '景区门票', description: '成人门票与观光车固定组合,指定日期和场次使用,由第三方票务系统统一出票。', salePrice: 160, marketPrice: 160, stock: 500, sold: 2386, + highlights: ['官方', '可订明日', '随时退', '无需取票'], included: ['荔波小七孔景区成人门票 1 张、观光车票 1 张。'], excluded: ['不含景区保险及其他园内自费项目。'], detail: '门票与观光车为固定销售组合,由第三方票务系统统一出票。出票成功后,可凭本人身份证或入园二维码通过景区闸机。', purchaseNotice: '实名预约,一人一证;请按照预约日期和入园时段使用。', + reservation: { advanceDays: 30 }, 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, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('elderly-ticket', '老年优惠门票', 'third_party', 60), ticketEntitlement('shuttle', '观光车票', 'third_party', 40)], + id: 'elderly', name: '荔波小七孔老年人票+观光车', shortTitle: '老年人门票+观光车', category: '景区门票', description: '老年人门票与观光车固定组合,系统按使用日期和身份证出生日期校验年龄。', salePrice: 100, marketPrice: 100, stock: 1800, sold: 816, + highlights: ['官方', '实名预约', '年龄校验', '第三方出票'], ageRule: { min: 60, max: 999 }, included: ['荔波小七孔景区老年人票 1 张、观光车票 1 张。'], excluded: ['不含景区保险及其他园内项目。'], detail: '系统会根据使用日期和身份证中的出生日期计算年龄,符合条件后提交第三方票务系统出票。', purchaseNotice: '限使用日年满60周岁的游客购买;一人一证,证件信息须与使用人一致。', + reservation: { advanceDays: 30, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('elderly-ticket', '小七孔老年人票', 'third_party', 60), ticketEntitlement('shuttle', '小七孔观光车票', 'third_party', 40)], }), 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, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('minor-ticket', '儿童优惠门票', 'third_party', 60), ticketEntitlement('shuttle', '观光车票', 'third_party', 40)], + id: 'minor', name: '荔波小七孔未成年票+观光车', shortTitle: '未成年门票+观光车', category: '景区门票', description: '未成年人门票与观光车固定组合,系统按使用日期和身份证出生日期校验年龄。', salePrice: 100, marketPrice: 100, stock: 1800, sold: 629, + highlights: ['官方', '实名预约', '年龄校验', '第三方出票'], ageRule: { min: 6, max: 17 }, included: ['荔波小七孔景区未成年票 1 张、观光车票 1 张。'], excluded: ['不含景区保险及其他园内项目。'], detail: '系统会根据使用日期和身份证中的出生日期计算年龄,符合6—17周岁条件后提交第三方票务系统出票。', purchaseNotice: '限使用日6—17周岁的游客购买;一人一证,证件信息须与使用人一致。', + reservation: { advanceDays: 30, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('minor-ticket', '小七孔未成年票', 'third_party', 60), ticketEntitlement('shuttle', '小七孔观光车票', 'third_party', 40)], }), 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('free-ticket', '免门票', 'third_party', 0), ticketEntitlement('shuttle', '观光车票', 'third_party', 40)], + id: 'free', name: '荔波小七孔免门票+观光车', shortTitle: '免门票+观光车实名预约', category: '景区门票', description: '特殊政策期门票免费、观光车正常计费,仍需实名校验并由第三方票务系统出票。', salePrice: 40, marketPrice: 40, stock: 1200, sold: 346, + highlights: ['政策免票', '实名预约', '限期开放', '第三方出票'], ageRule: { min: 0, max: 120 }, included: ['特殊政策期小七孔景区入园预约资格 1 份、观光车票 1 张。'], excluded: ['不含景区保险及所有园内自费项目。'], detail: '免票商品仍需选择日期与场次并填写实名信息。系统校验预约日期处于政策期后,向第三方票务系统请求出票。', purchaseNotice: '仅在后台配置的政策日期内开放;游客还须符合当期景区公告的适用条件。', + reservation: { advanceDays: 30, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('free-ticket', '小七孔特殊时期免门票', 'third_party', 0), ticketEntitlement('shuttle', '小七孔观光车票', '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, realNameRequired: false, 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)], + id: 'vip-fast', name: '小七孔景区 VIP 快速通行服务', shortTitle: 'VIP 快速通行', category: '园内项目', description: '专属通道与现场引导服务,减少排队等待时间。', salePrice: 98, marketPrice: 98, stock: 200, sold: 126, + highlights: ['专属通道', '快速通行', '无需取票'], included: ['小七孔景区 VIP 快速通行服务 1 次。'], excluded: ['不含景区门票、观光车及其他园内项目。'], detail: '购买后生成园内权益码,请按预约时间到指定服务区域联系工作人员。', purchaseNotice: '请提前完成预约并按时到场,具体通行安排以现场工作人员指引为准。', + reservation: { advanceDays: 15, realNameRequired: false, idTypes: ['身份证'], 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: '06:30—07:00 为固定去程、18:00—18:30 为固定返程,两段均属于同一往返权益,不可按单程理解;发车前2小时可免费改期一次。', - reservation: { advanceDays: 0, timeSlots: ['06:30—07:00 去程', '18:00—18:30 返程'] }, entitlements: [ticketEntitlement('direct-bus-seat', '景区直通车', 'third_party', 68)], + id: 'direct-bus', name: '贵阳往返小七孔景区直通车', shortTitle: '贵阳往返小七孔直通车', category: '交通票', description: '贵阳市区集合,往返直达小七孔景区,省去中转换乘。', salePrice: 68, marketPrice: 68, stock: 180, sold: 92, + highlights: ['往返直达', '定点发车', '第三方出票'], included: ['贵阳至小七孔景区往返直通车乘车权益 1 份。'], excluded: ['不含景区门票、餐饮及其他个人消费。'], detail: '下单时选择出行日期和班次,出票成功后凭乘车凭证按时到达集合地点。', purchaseNotice: '请至少提前20分钟到达集合地点,迟到或错过班次按商品退改规则处理。', + reservation: { advanceDays: 30, 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, idTypes: ['身份证'] }, entitlements: [ticketEntitlement('classic-ticket-shuttle', '成人门票+观光车', 'third_party', 160), ticketEntitlement('classic-boat', '鸳鸯湖游船', 'self', 28)], + id: 'classic', name: '小七孔经典畅游套餐', shortTitle: '门票 + 观光车 + 鸳鸯湖游船(成人套餐)', category: '组合套餐', description: '门票 + 观光车 + 鸳鸯湖游船,一次购买,两类凭证独立履约。', salePrice: 188, marketPrice: 210, stock: 480, sold: 184, + highlights: ['官方', '实名预约', '整包退', '双凭证'], included: ['成人门票 1 张、观光车票 1 张、鸳鸯湖游船 1 次。'], excluded: ['不含景区保险、餐饮及套餐外个人消费。'], detail: '一个订单包含两类凭证:第三方票务凭证包含景区门票和观光车票;自营权益码包含鸳鸯湖游船,由工作人员扫码核销。', purchaseNotice: '套餐统一选择使用日期和入园时段。全部权益未使用可整单全额退;任一权益使用后整单不可退,不支持拆项退款。', + reservation: { advanceDays: 30 }, entitlements: [ticketEntitlement('classic-ticket', '小七孔成人门票', 'third_party', 120), ticketEntitlement('classic-shuttle', '观光车票', 'third_party', 40), 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)], + id: 'day-tour', name: '荔波小七孔精品一日游', shortTitle: '小七孔精品一日游', category: '园内项目', description: '精选游览路线与随行服务,一天轻松游览小七孔代表性景观。', salePrice: 188, marketPrice: 188, stock: 120, sold: 68, + highlights: ['精品小团', '行程讲解', '可订明日'], included: ['小七孔精品一日游随行服务 1 份。'], excluded: ['不含未在商品说明中列明的个人消费。'], detail: '覆盖小七孔古桥、拉雅瀑布、水上森林等代表性游览区域,具体线路以当天安排为准。', purchaseNotice: '下单时请选择出行日期和集合场次,并准确填写联系人与出行人信息。', + reservation: { advanceDays: 20, 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, realNameRequired: false, advanceDays: 0 }, entitlements: [ticketEntitlement('boat-experience', '鸳鸯湖游船票', 'self', 50)], + id: 'boat', name: '鸳鸯湖游船票', shortTitle: '鸳鸯湖游船票', category: '园内项目', description: '鸳鸯湖游船体验,凭园内权益码由工作人员扫码核销。', salePrice: 50, marketPrice: 60, stock: 480, sold: 326, + highlights: ['官方自营', '当日可订', '未用可退'], included: ['鸳鸯湖游船体验 1 次。'], excluded: ['不含景区门票、观光车及个人消费。'], detail: '乘船游览鸳鸯湖水上森林景观,凭园内权益码由工作人员核销。', purchaseNotice: '受天气和现场运营情况影响,具体以景区通知为准。', + reservation: { requiresTimeSlot: false, realNameRequired: false, advanceDays: 15, idTypes: ['身份证'] }, 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, realNameRequired: false, maxQuantity: 10, timeSlots: ['09:00—10:30', '13:30—15:00'] }, entitlements: [ticketEntitlement('guide-service', '人工讲解服务', 'self', 68)], + id: 'guide', name: '小七孔精品讲解服务', shortTitle: '小七孔精品讲解服务', category: '园内项目', description: '官方讲解员小团服务,园内权益码核销。', salePrice: 68, marketPrice: 88, stock: 30, sold: 0, + highlights: ['官方讲解', '预约服务'], included: ['精品讲解服务 1 份。'], excluded: ['不含门票、观光车及耳麦以外设备。'], detail: '由官方讲解员提供小团讲解服务,具体集合信息将在订单内展示。', purchaseNotice: '请在预约场次开始前10分钟到达集合点。', + reservation: { advanceDays: 7, realNameRequired: false, idTypes: ['身份证'], maxQuantity: 3, timeSlots: ['09:00—10:30', '13:30—15:00'] }, entitlements: [ticketEntitlement('guide-service', '精品讲解服务', 'self', 68, '份')], status: 'draft', clientVisible: false, }), ] diff --git a/src/pages-service/tickets/model.test.mjs b/src/pages-service/tickets/model.test.mjs index 073892d..b38a074 100644 --- a/src/pages-service/tickets/model.test.mjs +++ b/src/pages-service/tickets/model.test.mjs @@ -35,17 +35,19 @@ test('provides coherent default visitor ticket data', async () => { 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()) - const productKeys = ['id', 'name', 'shortTitle', 'category', 'description', 'salePrice', 'marketPrice', 'stock', 'sold', 'status', 'highlights', 'ageRule', 'included', 'excluded', 'detail', 'purchaseNotice', 'images', 'reservation', 'entitlements'] + const productKeys = ['id', 'name', 'shortTitle', 'category', 'description', 'salePrice', 'marketPrice', 'stock', 'sold', 'status', 'clientVisible', 'highlights', 'ageRule', 'included', 'excluded', 'detail', 'purchaseNotice', 'images', 'reservation', 'entitlements'] const reservationKeys = ['requiresDate', 'advanceDays', 'requiresTimeSlot', 'timeSlots', 'realNameRequired', 'idTypes', 'maxQuantity', 'contactPhoneRequired'] const entitlementKeys = ['id', 'name', 'fulfillment', 'quantity', 'unit', 'refundAllocation'] - assert.ok(PRODUCTS.every((product) => productKeys.every((key) => key in product) && product.status === 'on_sale' && Number.isFinite(product.salePrice) && Number.isFinite(product.stock) && Number.isFinite(product.sold) && product.images.length === 1 && product.images[0] === PLACEHOLDER_IMAGE && reservationKeys.every((key) => key in product.reservation) && product.entitlements.length && product.entitlements.every((entitlement) => entitlementKeys.every((key) => key in entitlement)))) + assert.ok(PRODUCTS.every((product) => productKeys.every((key) => key in product) && ['on_sale', 'draft'].includes(product.status) && Number.isFinite(product.salePrice) && Number.isFinite(product.stock) && Number.isFinite(product.sold) && product.images.length === 1 && product.images[0] === PLACEHOLDER_IMAGE && reservationKeys.every((key) => key in product.reservation) && product.entitlements.length && product.entitlements.every((entitlement) => entitlementKeys.every((key) => key in entitlement)))) + assert.deepEqual(PRODUCTS.filter(({ status, clientVisible }) => status !== 'on_sale' || clientVisible === false).map(({ id }) => id), ['guide']) assert.deepEqual(PRODUCTS.map(({ id, salePrice, marketPrice, sold }) => [id, salePrice, marketPrice, sold]), [ - ['adult', 160, 170, 2386], ['elderly', 100, 160, 816], ['minor', 100, 160, 629], ['free', 40, 80, 346], ['vip-fast', 98, 128, 126], ['direct-bus', 68, 88, 92], ['classic', 188, 210, 184], ['day-tour', 188, 228, 68], ['boat', 50, 60, 326], ['guide', 68, 88, 40], + ['adult', 160, 160, 2386], ['elderly', 100, 100, 816], ['minor', 100, 100, 629], ['free', 40, 40, 346], ['vip-fast', 98, 98, 126], ['direct-bus', 68, 68, 92], ['classic', 188, 210, 184], ['day-tour', 188, 188, 68], ['boat', 50, 60, 326], ['guide', 68, 88, 0], ]) - assert.deepEqual(productsById.get('elderly').ageRule, { min: 60, max: 120 }) + assert.deepEqual(productsById.get('elderly').ageRule, { min: 60, max: 999 }) assert.deepEqual(productsById.get('minor').ageRule, { min: 6, max: 17 }) + assert.deepEqual(productsById.get('free').ageRule, { min: 0, max: 120 }) assert.deepEqual(productsById.get('adult').entitlements.map(({ fulfillment, refundAllocation }) => [fulfillment, refundAllocation]), [['third_party', 120], ['third_party', 40]]) - assert.deepEqual(productsById.get('classic').entitlements.map(({ fulfillment, refundAllocation }) => [fulfillment, refundAllocation]), [['third_party', 160], ['self', 28]]) + assert.deepEqual(productsById.get('classic').entitlements.map(({ fulfillment, refundAllocation }) => [fulfillment, refundAllocation]), [['third_party', 120], ['third_party', 40], ['self', 28]]) for (const productId of ['vip-fast', 'day-tour', 'boat', 'guide']) assert.ok(productsById.get(productId).entitlements.every(({ fulfillment }) => fulfillment === 'self')) for (const productId of ['direct-bus', 'free', 'elderly', 'minor']) assert.ok(productsById.get(productId).entitlements.every(({ fulfillment }) => fulfillment === 'third_party')) const standardSlots = ['08:00—09:30', '09:30—11:30', '11:30—13:30', '13:30—15:00', '15:00—16:00'] @@ -64,17 +66,15 @@ test('provides coherent default visitor ticket data', async () => { assert.deepEqual(INITIAL_TRAVELERS, [ { id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' }, { id: 'traveler-zhou-ning', name: '周宁', idType: '身份证', idNumber: '110101196001020017', phone: '13900139000' }, ]) - assert.deepEqual(productsById.get('adult'), { - id: 'adult', name: '成人门票+观光车', shortTitle: '成人门票+观光车', category: '景点门票', description: '荔波小七孔景区成人入园门票。', salePrice: 160, marketPrice: 170, stock: 120, sold: 2386, status: 'on_sale', highlights: ['实名预约入园', '覆盖核心景区'], ageRule: null, included: ['成人景区门票', '景区观光车'], excluded: ['餐饮及个人消费'], detail: '凭本人有效证件在预约时段入园。', purchaseNotice: '请至少提前一天预约,订单当天不可退改。', images: [PLACEHOLDER_IMAGE], reservation: { requiresDate: true, advanceDays: 1, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证', '护照', '港澳居民来往内地通行证'], maxQuantity: 5, contactPhoneRequired: true }, entitlements: [{ id: 'adult-ticket', name: '成人门票', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 120 }, { id: 'shuttle', name: '观光车票', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 40 }], - }) - assert.deepEqual(productsById.get('classic'), { - id: 'classic', name: '门票+观光车+鸳鸯湖游船', shortTitle: '门票+观光车+鸳鸯湖游船', category: '门票+观光车', description: '景区成人门票与观光车组合套餐。', salePrice: 188, marketPrice: 210, stock: 90, sold: 184, status: 'on_sale', highlights: ['门票观光车联票', '比单买更优惠'], ageRule: { min: 18, max: 59 }, included: ['成人景区门票', '景区观光车', '鸳鸯湖游船'], excluded: ['餐饮及个人消费'], detail: '一次预约即可享受门票、观光车与鸳鸯湖游船。', purchaseNotice: '各权益可分别核销,未使用部分可按规则申请退款。', images: [PLACEHOLDER_IMAGE], reservation: { requiresDate: true, advanceDays: 1, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证'], maxQuantity: 5, contactPhoneRequired: true }, entitlements: [{ id: 'classic-ticket-shuttle', name: '成人门票+观光车', fulfillment: 'third_party', quantity: 1, unit: '张', refundAllocation: 160 }, { id: 'classic-boat', name: '鸳鸯湖游船', fulfillment: 'self', quantity: 1, unit: '张', refundAllocation: 28 }], - }) + assert.deepEqual(productsById.get('adult').reservation, { requiresDate: true, advanceDays: 30, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证', '护照'], maxQuantity: 5, contactPhoneRequired: true }) + assert.deepEqual(productsById.get('adult').highlights, ['官方', '可订明日', '随时退', '无需取票']) + assert.deepEqual(productsById.get('classic').reservation, { requiresDate: true, advanceDays: 30, requiresTimeSlot: true, timeSlots: standardSlots, realNameRequired: true, idTypes: ['身份证', '护照'], maxQuantity: 5, contactPhoneRequired: true }) + assert.deepEqual(productsById.get('classic').highlights, ['官方', '实名预约', '整包退', '双凭证']) assert.deepEqual(INITIAL_TRAVELERS[0], { id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' }) assert.equal(INITIAL_ORDERS.length, 2) assert.deepEqual(INITIAL_ORDERS.map(({ productId, visitDate, timeSlot, quantity, travelers, insurance, gateCredential, selfCredential, entitlements }) => ({ productId, visitDate, timeSlot, quantity, travelers, insurance, hasGateCredential: Boolean(gateCredential), hasSelfCredential: Boolean(selfCredential), fulfillment: entitlements.map(({ fulfillment }) => fulfillment) })), [ { productId: 'adult', visitDate: '2026-08-01', timeSlot: '08:00—09:30', quantity: 1, travelers: [{ id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' }], insurance: { name: '出行意外险', price: 5, quantity: 1, status: 'active' }, hasGateCredential: true, hasSelfCredential: false, fulfillment: ['third_party', 'third_party'] }, - { productId: 'classic', visitDate: '2026-08-02', timeSlot: '09:30—11:30', quantity: 1, travelers: [{ id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' }], insurance: null, hasGateCredential: true, hasSelfCredential: true, fulfillment: ['third_party', 'self'] }, + { productId: 'classic', visitDate: '2026-08-02', timeSlot: '09:30—11:30', quantity: 1, travelers: [{ id: 'traveler-lin-xiao', name: '林晓', idType: '身份证', idNumber: '110101199001020010', phone: '13800138000' }], insurance: null, hasGateCredential: true, hasSelfCredential: true, fulfillment: ['third_party', 'third_party', 'self'] }, ]) for (const order of INITIAL_ORDERS) { assert.notEqual(order.travelers[0], INITIAL_TRAVELERS[0]) @@ -118,26 +118,26 @@ test('keeps ticket source prototypes, valid traveler IDs, and entitlement alloca const productsById = new Map(PRODUCTS.map((product) => [product.id, product])) assert.equal(productsById.get('adult').ageRule, null) - assert.deepEqual(productsById.get('adult').name, '成人门票+观光车') - assert.deepEqual(productsById.get('elderly').name, '老年人门票+观光车') - assert.deepEqual(productsById.get('minor').name, '未成年门票+观光车') - assert.deepEqual(productsById.get('free').name, '免门票+观光车实名预约') - assert.deepEqual(productsById.get('classic').name, '门票+观光车+鸳鸯湖游船') + assert.deepEqual(productsById.get('adult').name, '荔波小七孔成人票+观光车') + assert.deepEqual(productsById.get('elderly').name, '荔波小七孔老年人票+观光车') + assert.deepEqual(productsById.get('minor').name, '荔波小七孔未成年票+观光车') + assert.deepEqual(productsById.get('free').name, '荔波小七孔免门票+观光车') + assert.deepEqual(productsById.get('classic').name, '小七孔经典畅游套餐') assert.deepEqual(productsById.get('boat').name, '鸳鸯湖游船票') assert.deepEqual(productsById.get('day-tour').name, '荔波小七孔精品一日游') assert.deepEqual(productsById.get('guide').name, '小七孔精品讲解服务') - assert.deepEqual(productsById.get('direct-bus').name, '贵阳往返小七孔直通车') + assert.deepEqual(productsById.get('direct-bus').name, '贵阳往返小七孔景区直通车') assert.deepEqual(productsById.get('elderly').reservation.idTypes, ['身份证']) assert.deepEqual(productsById.get('minor').reservation.idTypes, ['身份证']) assert.ok(PRODUCTS.filter((product) => product.ageRule).every((product) => product.reservation.idTypes.length === 1 && product.reservation.idTypes[0] === '身份证')) assert.ok(productsById.get('direct-bus').included.every((item) => item.includes('往返'))) - assert.match(productsById.get('direct-bus').detail, /往返/) - assert.match(productsById.get('direct-bus').purchaseNotice, /往返/) - assert.deepEqual(productsById.get('adult').entitlements.map(({ name, fulfillment, refundAllocation }) => [name, fulfillment, refundAllocation]), [['成人门票', 'third_party', 120], ['观光车票', 'third_party', 40]]) + assert.match(productsById.get('direct-bus').detail, /乘车凭证/) + assert.match(productsById.get('direct-bus').purchaseNotice, /提前20分钟/) + assert.deepEqual(productsById.get('adult').entitlements.map(({ name, fulfillment, refundAllocation }) => [name, fulfillment, refundAllocation]), [['小七孔成人门票', 'third_party', 120], ['小七孔观光车票', 'third_party', 40]]) assert.deepEqual(productsById.get('elderly').entitlements.map(({ refundAllocation }) => refundAllocation), [60, 40]) assert.deepEqual(productsById.get('minor').entitlements.map(({ refundAllocation }) => refundAllocation), [60, 40]) assert.deepEqual(productsById.get('free').entitlements.map(({ refundAllocation }) => refundAllocation), [0, 40]) - assert.deepEqual(productsById.get('classic').entitlements.map(({ name, fulfillment, refundAllocation }) => [name, fulfillment, refundAllocation]), [['成人门票+观光车', 'third_party', 160], ['鸳鸯湖游船', 'self', 28]]) + assert.deepEqual(productsById.get('classic').entitlements.map(({ name, fulfillment, refundAllocation }) => [name, fulfillment, refundAllocation]), [['小七孔成人门票', 'third_party', 120], ['观光车票', 'third_party', 40], ['鸳鸯湖游船', 'self', 28]]) assert.ok(PRODUCTS.every((product) => product.entitlements.every((entitlement) => Number.isFinite(entitlement.quantity) && entitlement.quantity > 0 && Number.isFinite(entitlement.refundAllocation)) && product.entitlements.reduce((total, entitlement) => total + entitlement.refundAllocation, 0) === product.salePrice)) assert.deepEqual(INITIAL_TRAVELERS[1], { id: 'traveler-zhou-ning', name: '周宁', idType: '身份证', idNumber: '110101196001020017', phone: '13900139000' }) })