From e52934f6527027bd6008c2ade61c3083159efd4a Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期五, 09 十月 2026 18:14:28 +0800
Subject: [PATCH] 更新....

---
 rencaika/config/api.uts                                                                                 |    3 
 rencaika/docs/pages/05-暖心食享.md                                                                          |    6 
 rencaika/pages/stay-benefits/stay-benefits.uvue                                                         |    2 
 rencaika_merchant/pages/orders/orders.uvue                                                              |   19 +
 rencaika/pages/benefit-search/benefit-search.uvue                                                       |   28 +-
 talent-card-server/src/main/java/com/talentcard/server/service/TicketPackageAdminService.java           |   40 +++
 rencaika/pages/merchant/merchant.uvue                                                                   |   40 +-
 rencaika_merchant/services/orders.uts                                                                   |   53 ++++
 talent-card-admin/src/views/MovieTicketView.vue                                                         |  101 +++++++
 talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java       |  166 ++++++++++++
 rencaika/docs/pages/59-首页.md                                                                            |    6 
 rencaika/pages/spouse-employment/spouse-employment.uvue                                                 |    2 
 rencaika/docs/开发进度与注意事项.md                                                                              |    2 
 talent-card-server/src/main/java/com/talentcard/server/service/TicketPackagePortalService.java          |    2 
 rencaika/pages/order-completed/order-completed.uvue                                                     |    4 
 rencaika/pages/order/order.uvue                                                                         |    8 
 rencaika_merchant/docs/pages/23-观影权益核销.md                                                               |    4 
 rencaika/pages/food-benefits/food-benefits.uvue                                                         |    2 
 rencaika/pages/index/index.uvue                                                                         |   23 +
 talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java      |    6 
 rencaika/pages/payment-confirm/payment-confirm.uvue                                                     |   10 
 rencaika/pages/benefits/benefits.uvue                                                                   |   24 
 rencaika_merchant/config/api.uts                                                                        |    3 
 rencaika_merchant/pages.json                                                                            |    8 
 rencaika_merchant/docs/pages/README.md                                                                  |    2 
 rencaika/pages.json                                                                                     |    8 
 talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkController.java    |   12 
 rencaika/pages/pending-use/pending-use.uvue                                                             |    6 
 /dev/null                                                                                               |    0 
 rencaika/pages/product-detail/product-detail.uvue                                                       |   14 
 talent-card-admin/src/api/admin.ts                                                                      |    4 
 rencaika/pages/policy-detail/policy-detail.uvue                                                         |    2 
 rencaika/docs/pages/README.md                                                                           |    2 
 rencaika/App.uvue                                                                                       |   29 ++
 talent-card-server/src/main/java/com/talentcard/server/business/admin/TicketPackageAdminController.java |   17 +
 talent-card-server/src/main/java/com/talentcard/server/mapper/TicketPackageClaimMapper.java             |   79 ++++++
 rencaika/pages/entertainment-benefits/entertainment-benefits.uvue                                       |    2 
 rencaika_merchant/docs/pages/02-订单管理.md                                                                 |    5 
 38 files changed, 645 insertions(+), 99 deletions(-)

diff --git a/rencaika/App.uvue b/rencaika/App.uvue
index 4675afb..07ea739 100644
--- a/rencaika/App.uvue
+++ b/rencaika/App.uvue
@@ -1,7 +1,24 @@
 <script setup lang="uts">
-	import { developmentLoginBypassEnabled } from './config/api.uts'
+	import { developmentLoginBypassEnabled, isApiConfigured } from './config/api.uts'
 	import { initializeDevelopmentApiBaseUrl } from './config/development-server.uts'
 	import { loginWithDevelopmentSession } from './services/auth.uts'
+	import { resolveApiUrl } from './services/http.uts'
+
+	/**
+	 * 行楷字体(古典艺术行楷简,5.1MB)放在服务器 `/uploads/fonts/gudian-xingkai.ttf`:
+	 * 包体主包上限 2MB,完整字体不能进包,所以启动时动态加载;加载失败时页面仍用包内子集/系统字体。
+	 * `global: true` 只在 app.uvue 里调用才全局生效(微信小程序基础库 2.10+)。
+	 */
+	const loadCalligraphyFont = () => {
+		if (!isApiConfigured()) return
+		uni.loadFontFace({
+			family: 'FoodCalligraphyFull',
+			source: 'url("' + resolveApiUrl('/uploads/fonts/gudian-xingkai.ttf') + '")',
+			global: true,
+			success: () => { console.log('Calligraphy font loaded') },
+			fail: (error: any) => { console.warn('Calligraphy font load failed: ' + JSON.stringify(error)) }
+		})
+	}
 
 	// #ifdef APP-ANDROID || APP-HARMONY
 	let firstBackTime = 0
@@ -11,6 +28,7 @@
 		console.log('App Launch')
 		// 先把服务端地址解析好:开发环境探测本机局域网 IP,换网后不必改代码重新编译。
 		initializeDevelopmentApiBaseUrl()
+		loadCalligraphyFont()
 		if (developmentLoginBypassEnabled) {
 			loginWithDevelopmentSession(() => {}, (error) => {
 				console.warn('Development session initialization failed: ' + error.code)
@@ -98,6 +116,15 @@
 		background-color: transparent !important;
 	}
 
+	/*
+	 * 专题页的大标题/菜名用行楷:优先用 App.uvue 动态加载的服务器字体(FoodCalligraphyFull),
+	 * 没加载出来或缺少字符时回退到包内子集(FoodCalligraphy)与系统字体。
+	 */
+	.dish-name,
+	.scene-name {
+		font-family: 'FoodCalligraphyFull', 'FoodCalligraphy' !important;
+	}
+
 	.nav-row .back-button-active {
 		opacity: .55;
 	}
diff --git a/rencaika/config/api.uts b/rencaika/config/api.uts
index 47c377f..2066371 100644
--- a/rencaika/config/api.uts
+++ b/rencaika/config/api.uts
@@ -4,7 +4,8 @@
 import { developmentApiBaseUrl } from './development-server.uts'
 
 // 2026-09-29:体验版指向攀枝花测试环境。本机联调改回 'development';生产上线改 'production'。
-export const apiEnvironment: ApiEnvironment = 'staging'
+// 2026-10-09:本地联调电影票,临时切 'development'(开发者工具走 127.0.0.1:8080,见 config/development-server.uts)。测完改回 'staging'。
+export const apiEnvironment: ApiEnvironment = 'development'
 // Separate from demo-data controls; only development builds request a test session.
 export const developmentLoginBypassEnabled: boolean = false 
 
diff --git "a/rencaika/docs/pages/05-\346\232\226\345\277\203\351\243\237\344\272\253.md" "b/rencaika/docs/pages/05-\346\232\226\345\277\203\351\243\237\344\272\253.md"
index 0328926..0909562 100644
--- "a/rencaika/docs/pages/05-\346\232\226\345\277\203\351\243\237\344\272\253.md"
+++ "b/rencaika/docs/pages/05-\346\232\226\345\277\203\351\243\237\344\272\253.md"
@@ -43,8 +43,8 @@
 
 ## 资源与包体说明
 
-- 体验版仅保留 `static/food-assets/food-1.jpg`、`food-2.jpg` 两张唯一餐饮图,图册、商户、商品、支付及订单相关展示均循环复用,合计约 210KB。
+- 餐饮图不再单独打一份兜底图:接口没配图、图片加载失败、以及本地演示数据统一用 `static/common/image-placeholder.png`(全站唯一兜底图,24KB),图册/商户/商品/支付/订单相关展示都用它。
 - 完整餐饮素材继续保留在项目外 `icon/吃` 素材库,正式版由后台/CDN配置,不直接打入小程序代码包。
-- 完整“古典艺术行楷简”字体约 5.35MB,开发包未直接引入。
+- 完整“古典艺术行楷简”字体约 5.35MB,**不放包内**(主包上限 2MB),已放服务器 `/data/talent-card/uploads/fonts/gudian-xingkai.ttf`(`/uploads/fonts/gudian-xingkai.ttf`),由 `App.uvue` 启动时用 `uni.loadFontFace({ family:'FoodCalligraphyFull', global:true })` 动态加载;专题页的 `.scene-name/.dish-name` 全局优先用它,加载失败或缺字回退到包内子集与系统字体。
 - 当前菜名使用约 12KB 的 `food-calligraphy-subset.ttf` 子集,只用于设计稿演示字符。
-- 后台动态名称正式上线前,必须完成完整字体的分包、远程加载或正式 CDN 方案,并验证字体授权与失败回退。
+- 后台动态名称的完整字体已走"服务器 + 动态加载"方案;上线前仍需验证字体授权、首次加载耗时(5.35MB)与失败回退,必要时换 WOFF/子集。
diff --git "a/rencaika/docs/pages/59-\351\246\226\351\241\265.md" "b/rencaika/docs/pages/59-\351\246\226\351\241\265.md"
index 60f9c07..a208002 100644
--- "a/rencaika/docs/pages/59-\351\246\226\351\241\265.md"
+++ "b/rencaika/docs/pages/59-\351\246\226\351\241\265.md"
@@ -12,9 +12,9 @@
 
 | 功能 | 当前实现 | 后续接口目标 |
 | --- | --- | --- |
-| 顶部导航 | 按微信胶囊动态适配;标题实际使用 `/static/home-assets/home-title-left-v3.png`;下滑超过阈值后橙色实体背景渐显,标题位置保持固定。 | 后台无需下发导航资源;发版时按字体与包体策略确认标题资源。 |
-| 东区主图 | 顶图叠加橙色透明渐变,提供“向东看”入口。 | 后台返回宣传视频或内容链接及封面。 |
-| 城市介绍 | 展示东区简介、潜力城、兴业城、黄金城、宜居城入口。 | 后台配置专题类型、标题、图片和详情内容。 |
+| 顶部导航 | 按微信胶囊动态适配;标题是文本「攀枝花市东区人才服务平台」,用行楷(优先服务器字体 `FoodCalligraphyFull`,加载失败或缺字回退系统字体);下滑超过阈值后橙色实体背景渐显,标题位置保持固定。 | 后台无需下发导航资源。 |
+| 东区主图 | 主图取服务器 `/uploads/home/home-top.png`(`homeImage()` 拼当前接口域名),上半部压橙色透明渐变、底部圆角,提供“向东看”入口。 | 后台返回宣传视频或内容链接及封面。 |
+| 城市介绍 | 展示东区简介、潜力城、兴业城、黄金城、宜居城入口,配图取服务器 `/uploads/home/city-*.jpg\|png`;换图只换服务器文件,不用发版。 | 后台配置专题类型、标题、图片和详情内容。 |
 | 人才资讯 | 配置服务地址后调用 `GET /api/app/home` 获取置顶及普通资讯摘要,按稳定 ID 进入富文本资讯详情;查看更多进入资讯列表。 | 后台发布管理、封面图和附件待补充。 |
 | 底部导航 | 首页、人才权益、人才卡、人才政策、个人中心为平级页面,使用 `reLaunch` 切换。 | 登录态失效时统一进入登录页。 |
 
diff --git a/rencaika/docs/pages/README.md b/rencaika/docs/pages/README.md
index 00e79cb..15bcef7 100644
--- a/rencaika/docs/pages/README.md
+++ b/rencaika/docs/pages/README.md
@@ -17,7 +17,7 @@
 | 14~28 | 个人中心、人才卡升级、使用记录与订单状态、家庭附属卡、设置与协议 |
 | 29~46 | 游、住、酒店预约与免费权益、娱、行、购、享及相关商户/结果页面 |
 | 47~58 | 配偶推荐就业说明、体检、行政审批、金融服务、学生托付、子女信息、学校详情、人才码与体检医院详情 |
-| 60~62 | 配偶信息填写、配偶信息提交成功、外部服务网页 |
+| 60~63 | 配偶信息填写、配偶信息提交成功、外部服务网页、观影券详情 |
 
 - 文档编号必须唯一;当前编号 `00~62` 中保留历史业务顺序,不要求与 `pages.json` 顺序一致。
 - 一份文档可覆盖同一路由的一种明确业务模式,但必须写明入口参数、状态差异和后台字段。
diff --git "a/rencaika/docs/\345\274\200\345\217\221\350\277\233\345\272\246\344\270\216\346\263\250\346\204\217\344\272\213\351\241\271.md" "b/rencaika/docs/\345\274\200\345\217\221\350\277\233\345\272\246\344\270\216\346\263\250\346\204\217\344\272\213\351\241\271.md"
index d012348..8da4d8c 100644
--- "a/rencaika/docs/\345\274\200\345\217\221\350\277\233\345\272\246\344\270\216\346\263\250\346\204\217\344\272\213\351\241\271.md"
+++ "b/rencaika/docs/\345\274\200\345\217\221\350\277\233\345\272\246\344\270\216\346\263\250\346\204\217\344\272\213\351\241\271.md"
@@ -92,7 +92,7 @@
 - 人才权益原始顶图约 9.1MB,项目内使用压缩后的 `static/benefits-assets/benefits-hero.jpg`(1080×607,约 140KB),禁止将原图直接加入小程序包。
 - 小程序预览主包上限为 4MB。娱页面图片和首页城市小卡图已使用面向显示尺寸的 WebP 资源;新增或替换资源后必须检查编译目录总大小并预留至少 200KB 余量,原始大图保留在项目外的 `icon/` 素材库中,不得直接放入 `static/`。
 - **体验版专题素材规则**:吃/游/住/娱/行/购/享每个专题最多保留 2 张唯一演示图片,图册可保留多个滑动项但必须循环复用这两张图;商户、商品、支付、预约和订单详情优先复用所属专题图片,不再单独打包重复封面。完整素材保留在项目外 `icon/`,正式全量版由后台/CDN加载;每次新增图片前必须先检查编译包体并保留安全余量。
-- 暖心食享演示菜名使用 `static/fonts/food-calligraphy-subset.ttf` 字体子集,编译后以内联字体使用;该子集只覆盖当前演示菜名,后台配置任意名称时需加载完整字体包或正式 CDN 字体。
+- 暖心食享演示菜名使用 `static/fonts/food-calligraphy-subset.ttf` 字体子集,编译后以内联字体使用;完整“古典艺术行楷简”(5.35MB)不进包,放服务器 `/uploads/fonts/gudian-xingkai.ttf`,由 `App.uvue` 启动时 `uni.loadFontFace` 动态加载(`family: 'FoodCalligraphyFull'`,`global: true`),专题页 `.scene-name/.dish-name` 优先用它、失败回退子集与系统字体。**覆盖范围**:7 个专题页顶部大图的那行名称(吃/游/住/娱/行/购/享)——吃用 `.dish-name`,其余用 `.scene-name`;住—舒心安居与娱—怡乐生活原先没有(或只有子集)声明,已按同一套字体链补上。
 - 所有“顶部背景全图/沉浸式头图”页面统一使用固定自定义导航栏:导航栏必须放在纵向滚动容器外,返回按钮和标题不得随内容滚走;初始状态透明叠加在头图上,页面上滑超过 `64px` 后渐显橙色实体渐变背景,背景使用 `linear-gradient(180deg, #F26A38 0%, #EE542B 100%)` 和 `280ms ease-out` 透明度过渡;导航高度、底部留白和微信胶囊适配保持与首页一致。除非设计图明确要求不同,后续同类页面必须按此方式交付。当前已应用:首页、人才权益、暖心食享、商家详情、商品详情、人才政策。
 - 吃/游/住/娱等“顶图 + 图册 + 介绍文案 + 列表导航”专题页,介绍卡必须置于正常文档流并由内容自然撑高,同时通过统一的 `-66rpx` 负外边距向上覆盖顶图,保留悬浮效果;列表导航区固定使用介绍卡底部后的 `48rpx` 间距。不得通过固定介绍卡高度或固定 `padding-top` 猜测文案高度,后台文案变更为多行时,服务列表必须自动向下顺延。
 - **全量发版要求**:正式全量发布时,需按发布包策略注入完整的“古典艺术行楷简”字体包,并复核微信小程序分包/总体积限制、字体授权及真机渲染效果;开发预览阶段继续使用标题图片,避免超过预览包限制。
diff --git a/rencaika/pages.json b/rencaika/pages.json
index dd6b21f..66b9450 100644
--- a/rencaika/pages.json
+++ b/rencaika/pages.json
@@ -241,6 +241,14 @@
 			}
 		},
 		{
+			"path": "pages/ticket-package-detail/ticket-package-detail",
+			"style": {
+				"navigationStyle": "custom",
+				"backgroundColor": "#FFFFFF",
+				"disableScroll": true
+			}
+		},
+		{
 			"path": "pages/ticket-package/ticket-package",
 			"style": {
 				"navigationStyle": "custom",
diff --git a/rencaika/pages/benefit-search/benefit-search.uvue b/rencaika/pages/benefit-search/benefit-search.uvue
index 045ce38..28d191e 100644
--- a/rencaika/pages/benefit-search/benefit-search.uvue
+++ b/rencaika/pages/benefit-search/benefit-search.uvue
@@ -92,20 +92,20 @@
 	const histories = ref<string[]>([])
 	// 演示数据已按 sort 从小到大排列;后端接入后由接口返回同序分页结果。
 	const allMerchants = [
-		{ id: 101, name: '乡菜郎·攀枝花特色延边菜', category: '暖心食享', merchantType: 'food', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/food-assets/food-1.jpg', sort: 10 },
-		{ id: 102, name: '攀枝花希尔顿欢朋酒店', category: '舒心安居', merchantType: 'hotel', discount: '6折', address: '东区机场路10号', image: '/static/stay-assets/hotel-hampton.jpg', sort: 20 },
-		{ id: 103, name: '攀枝花万达影城', category: '怡乐生活', merchantType: 'cinema', discount: '免费券', address: '东区攀枝花大道东段699号', image: '/static/entertainment-assets/wanda-cinema.webp', sort: 30 },
-		{ id: 104, name: '攀枝花中国三线建设博物馆', category: '悦览花城', merchantType: 'scenic', discount: '优惠票', address: '仁和区花城新区干坝塘村', image: '/static/common/image-fallback.svg', sort: 40 },
-		{ id: 105, name: '金府酒楼(西区店)', category: '暖心食享', merchantType: 'food', discount: '6折', address: '西区清香坪北街51号', image: '/static/food-assets/food-2.jpg', sort: 50 },
-		{ id: 106, name: '中国石油东区新福路加油站', category: '畅行无忧', merchantType: 'fuel', discount: '9.4折', address: '东区新福路20号附3-21号', image: '/static/home-assets/top.jpg', sort: 60 },
-		{ id: 107, name: '渡口公司管辖停车场', category: '畅行无忧', merchantType: 'parking', discount: '免费', address: '攀枝花市东区指定停车场', image: '/static/common/image-fallback.svg', sort: 70 },
-		{ id: 108, name: '攀商未来城', category: '惠享好物', merchantType: 'shopping', discount: '购房优惠3%', address: '东区榕树街55号', image: '/static/home-assets/top.jpg', sort: 80 },
-		{ id: 109, name: '攀枝花市中心医院', category: '臻享礼遇', merchantType: 'medical', discount: '免费体检', address: '东区攀枝花大道中段益康街34号', image: '/static/stay-assets/hotel-vienna.jpg', sort: 90 },
-		{ id: 110, name: '人才金融服务中心', category: '臻享礼遇', merchantType: 'finance', discount: '专属金融方案', address: '东区人民街中心广场', image: '/static/stay-assets/hotel-hampton.jpg', sort: 100 },
-		{ id: 111, name: '东区人才自助洗车服务点', category: '畅行无忧', merchantType: 'carWash', discount: '专属优惠', address: '东区机场路人才服务站', image: '/static/home-assets/top.jpg', sort: 110 },
-		{ id: 112, name: '攀枝花维也纳国际酒店', category: '舒心安居', merchantType: 'hotel', discount: '免费5天', address: '东区攀枝花大道中段', image: '/static/stay-assets/hotel-vienna.jpg', sort: 120 },
-		{ id: 113, name: '百杏厨房(中环天地店)', category: '暖心食享', merchantType: 'food', discount: '8.8折', address: '东区炳三区新福路20号', image: '/static/food-assets/food-1.jpg', sort: 130 },
-		{ id: 114, name: '攀枝花大剧院', category: '怡乐生活', merchantType: 'cinema', discount: '演出优惠', address: '东区机场路文化艺术中心', image: '/static/entertainment-assets/entertainment-hero.webp', sort: 140 }
+		{ id: 101, name: '乡菜郎·攀枝花特色延边菜', category: '暖心食享', merchantType: 'food', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/common/image-placeholder.png', sort: 10 },
+		{ id: 102, name: '攀枝花希尔顿欢朋酒店', category: '舒心安居', merchantType: 'hotel', discount: '6折', address: '东区机场路10号', image: '/static/common/image-placeholder.png', sort: 20 },
+		{ id: 103, name: '攀枝花万达影城', category: '怡乐生活', merchantType: 'cinema', discount: '免费券', address: '东区攀枝花大道东段699号', image: '/static/common/image-placeholder.png', sort: 30 },
+		{ id: 104, name: '攀枝花中国三线建设博物馆', category: '悦览花城', merchantType: 'scenic', discount: '优惠票', address: '仁和区花城新区干坝塘村', image: '/static/common/image-placeholder.png', sort: 40 },
+		{ id: 105, name: '金府酒楼(西区店)', category: '暖心食享', merchantType: 'food', discount: '6折', address: '西区清香坪北街51号', image: '/static/common/image-placeholder.png', sort: 50 },
+		{ id: 106, name: '中国石油东区新福路加油站', category: '畅行无忧', merchantType: 'fuel', discount: '9.4折', address: '东区新福路20号附3-21号', image: '/static/common/image-placeholder.png', sort: 60 },
+		{ id: 107, name: '渡口公司管辖停车场', category: '畅行无忧', merchantType: 'parking', discount: '免费', address: '攀枝花市东区指定停车场', image: '/static/common/image-placeholder.png', sort: 70 },
+		{ id: 108, name: '攀商未来城', category: '惠享好物', merchantType: 'shopping', discount: '购房优惠3%', address: '东区榕树街55号', image: '/static/common/image-placeholder.png', sort: 80 },
+		{ id: 109, name: '攀枝花市中心医院', category: '臻享礼遇', merchantType: 'medical', discount: '免费体检', address: '东区攀枝花大道中段益康街34号', image: '/static/common/image-placeholder.png', sort: 90 },
+		{ id: 110, name: '人才金融服务中心', category: '臻享礼遇', merchantType: 'finance', discount: '专属金融方案', address: '东区人民街中心广场', image: '/static/common/image-placeholder.png', sort: 100 },
+		{ id: 111, name: '东区人才自助洗车服务点', category: '畅行无忧', merchantType: 'carWash', discount: '专属优惠', address: '东区机场路人才服务站', image: '/static/common/image-placeholder.png', sort: 110 },
+		{ id: 112, name: '攀枝花维也纳国际酒店', category: '舒心安居', merchantType: 'hotel', discount: '免费5天', address: '东区攀枝花大道中段', image: '/static/common/image-placeholder.png', sort: 120 },
+		{ id: 113, name: '百杏厨房(中环天地店)', category: '暖心食享', merchantType: 'food', discount: '8.8折', address: '东区炳三区新福路20号', image: '/static/common/image-placeholder.png', sort: 130 },
+		{ id: 114, name: '攀枝花大剧院', category: '怡乐生活', merchantType: 'cinema', discount: '演出优惠', address: '东区机场路文化艺术中心', image: '/static/common/image-placeholder.png', sort: 140 }
 	]
 	const formatRemoteDiscount = (item: any): string => {
 		const sale = item.salePrice != null ? Number(item.salePrice) : 0
diff --git a/rencaika/pages/benefits/benefits.uvue b/rencaika/pages/benefits/benefits.uvue
index 6e5d92b..d91f3e9 100644
--- a/rencaika/pages/benefits/benefits.uvue
+++ b/rencaika/pages/benefits/benefits.uvue
@@ -110,18 +110,18 @@
 	]
 
 	const allBenefits = [
-		{ id: 1, name: '乡菜郎·攀枝花特色延边菜', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/food-assets/food-1.jpg' },
-		{ id: 2, name: '金府酒楼(西区店)', discount: '6折', address: '西区清香坪北街51号', image: '/static/food-assets/food-2.jpg' },
-		{ id: 3, name: '乡菜郎·攀枝花特色延边菜', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/food-assets/food-1.jpg' },
-		{ id: 4, name: '金府酒楼(西区店)', discount: '6折', address: '西区清香坪北街51号', image: '/static/food-assets/food-2.jpg' },
-		{ id: 5, name: '百杏厨房(中环天地店)', discount: '8.8折', address: '东区炳三区新福路20号中环天地负一层', image: '/static/food-assets/food-1.jpg' },
-		{ id: 6, name: '香佰里鲜货火锅(万象城店)', discount: '8.8折', address: '东区攀枝花大道东段298号', image: '/static/food-assets/food-2.jpg' },
-		{ id: 7, name: '乡菜郎·攀枝花特色延边菜', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/food-assets/food-1.jpg' },
-		{ id: 8, name: '金府酒楼(西区店)', discount: '6折', address: '西区清香坪北街51号', image: '/static/food-assets/food-2.jpg' },
-		{ id: 9, name: '百杏厨房(中环天地店)', discount: '8.8折', address: '东区炳三区新福路20号中环天地负一层', image: '/static/food-assets/food-1.jpg' },
-		{ id: 10, name: '香佰里鲜货火锅(万象城店)', discount: '8.8折', address: '东区攀枝花大道东段298号', image: '/static/food-assets/food-2.jpg' },
-		{ id: 11, name: '乡菜郎·攀枝花特色延边菜', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/food-assets/food-1.jpg' },
-		{ id: 12, name: '金府酒楼(西区店)', discount: '6折', address: '西区清香坪北街51号', image: '/static/food-assets/food-2.jpg' }
+		{ id: 1, name: '乡菜郎·攀枝花特色延边菜', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/common/image-placeholder.png' },
+		{ id: 2, name: '金府酒楼(西区店)', discount: '6折', address: '西区清香坪北街51号', image: '/static/common/image-placeholder.png' },
+		{ id: 3, name: '乡菜郎·攀枝花特色延边菜', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/common/image-placeholder.png' },
+		{ id: 4, name: '金府酒楼(西区店)', discount: '6折', address: '西区清香坪北街51号', image: '/static/common/image-placeholder.png' },
+		{ id: 5, name: '百杏厨房(中环天地店)', discount: '8.8折', address: '东区炳三区新福路20号中环天地负一层', image: '/static/common/image-placeholder.png' },
+		{ id: 6, name: '香佰里鲜货火锅(万象城店)', discount: '8.8折', address: '东区攀枝花大道东段298号', image: '/static/common/image-placeholder.png' },
+		{ id: 7, name: '乡菜郎·攀枝花特色延边菜', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/common/image-placeholder.png' },
+		{ id: 8, name: '金府酒楼(西区店)', discount: '6折', address: '西区清香坪北街51号', image: '/static/common/image-placeholder.png' },
+		{ id: 9, name: '百杏厨房(中环天地店)', discount: '8.8折', address: '东区炳三区新福路20号中环天地负一层', image: '/static/common/image-placeholder.png' },
+		{ id: 10, name: '香佰里鲜货火锅(万象城店)', discount: '8.8折', address: '东区攀枝花大道东段298号', image: '/static/common/image-placeholder.png' },
+		{ id: 11, name: '乡菜郎·攀枝花特色延边菜', discount: '6折', address: '西区格里坪镇清香坪东街78号', image: '/static/common/image-placeholder.png' },
+		{ id: 12, name: '金府酒楼(西区店)', discount: '6折', address: '西区清香坪北街51号', image: '/static/common/image-placeholder.png' }
 	]
 	const visibleBenefits = ref<any[]>([])
 	const loadedBenefitCount = ref(0)
diff --git a/rencaika/pages/entertainment-benefits/entertainment-benefits.uvue b/rencaika/pages/entertainment-benefits/entertainment-benefits.uvue
index fbe93aa..22813c3 100644
--- a/rencaika/pages/entertainment-benefits/entertainment-benefits.uvue
+++ b/rencaika/pages/entertainment-benefits/entertainment-benefits.uvue
@@ -185,6 +185,8 @@
 </script>
 
 <style>
+	/* 顶部大图标题用行楷:优先 App.uvue 动态加载的服务器完整字体,回退包内子集与系统字体。 */
+	.scene-name { font-family: 'FoodCalligraphyFull', 'FoodCalligraphy'; }
 	.page, .page-scroll { width: 100%; height: 100vh; background-color: #FFFFFF; }
 	.custom-nav { position: fixed; z-index: 20; left: 0; right: 0; top: 0; padding-bottom: 20rpx; }
 	.nav-bg { position: absolute; left: 0; right: 0; top: 0; bottom: 0; opacity: 0; background-image: linear-gradient(180deg, #F26A38 0%, #EE542B 100%); transition: opacity 280ms ease-out; }
diff --git a/rencaika/pages/food-benefits/food-benefits.uvue b/rencaika/pages/food-benefits/food-benefits.uvue
index 8491310..537c9f8 100644
--- a/rencaika/pages/food-benefits/food-benefits.uvue
+++ b/rencaika/pages/food-benefits/food-benefits.uvue
@@ -76,7 +76,7 @@
 	const activeIndex = ref(-1)
 	const windowWidth = ref(375)
 	const thumbScrollLeft = ref(0)
-	const fallbackFoodImage = '/static/food-assets/food-1.jpg'
+	const fallbackFoodImage = '/static/common/image-placeholder.png'
 	const topicDescription = ref('')
 
 	const gallery = ref<any[]>([])
diff --git a/rencaika/pages/index/index.uvue b/rencaika/pages/index/index.uvue
index 11b07f5..d55b75e 100644
--- a/rencaika/pages/index/index.uvue
+++ b/rencaika/pages/index/index.uvue
@@ -2,6 +2,7 @@
 	<view class="page">
 		<scroll-view class="scroll-page" scroll-y @scroll="handleScroll">
 			<view class="hero">
+				<image class="hero-image" :src="homeImage('home-top.png')" mode="aspectFill"></image>
 				<view class="hero-gradient"></view>
 				<view v-if="homeVideoUrl != ''" class="play-button" @tap="playVideo"><text>向东看</text></view>
 			</view>
@@ -9,9 +10,9 @@
 			<view class="city-panel">
 				<view class="city-heading"><text>才聚东区,共富未来</text><view class="city-label" @tap="openIntro"><text>幸福东区</text><image src="/static/home-assets/arrow-right.svg"></image></view></view>
 				<view class="city-grid">
-					<view class="city-card city-intro" @tap="openIntro"><text class="intro-name">东区简介</text><view class="intro-button"><text>查看更多</text><image src="/static/home-assets/arrow-right.svg"></image></view></view>
-					<view class="city-side"><view class="city-card city-potential"><text>潜力城</text></view><view class="city-card city-golden"><text>黄金城</text></view></view>
-					<view class="city-side"><view class="city-card city-industry"><text>兴业城</text></view><view class="city-card city-livable"><text>宜居城</text></view></view>
+					<view class="city-card city-intro" @tap="openIntro"><image class="city-bg" :src="homeImage('city-intro.jpg')" mode="aspectFill"></image><text class="intro-name">东区简介</text><view class="intro-button"><text>查看更多</text><image src="/static/home-assets/arrow-right.svg"></image></view></view>
+					<view class="city-side"><view class="city-card city-potential"><image class="city-bg" :src="homeImage('city-potential.jpg')" mode="aspectFill"></image><text>潜力城</text></view><view class="city-card city-golden"><image class="city-bg" :src="homeImage('city-golden.jpg')" mode="aspectFill"></image><text>黄金城</text></view></view>
+					<view class="city-side"><view class="city-card city-industry"><image class="city-bg" :src="homeImage('city-industry.jpg')" mode="aspectFill"></image><text>兴业城</text></view><view class="city-card city-livable"><image class="city-bg" :src="homeImage('city-livable.png')" mode="aspectFill"></image><text>宜居城</text></view></view>
 				</view>
 			</view>
 
@@ -46,6 +47,12 @@
 	import { isApiConfigured } from '../../config/api.uts'
 	import { getAppHome } from '../../services/portal.uts'
 	import { resolveApiUrl } from '../../services/http.uts'
+
+	/**
+	 * 首页图片放在服务器上(`/uploads/home/…`),代码里只存相对路径:
+	 * 换图不用改代码、也不用重新上传体验版,和后台域名一起走环境配置。
+	 */
+	const homeImage = (name: string): string => resolveApiUrl('/uploads/home/' + name)
 
 	const navTop = ref(28)
 	const navHeight = ref(32)
@@ -155,15 +162,16 @@
 <style>
 	.page { height: 100%; background-color: #FFFFFF; }
 	.scroll-page { height: calc(100% - 126rpx); height: calc(100% - 126rpx - env(safe-area-inset-bottom)); background-color: #FFFFFF; }
-	.hero { height: 468rpx; background-color: #FFFFFF; position: relative; overflow: hidden; }
-	.hero-gradient { position: absolute; z-index: 1; top: 0; left: 0; right: 0; bottom: 0; border-bottom-left-radius: 48rpx; border-bottom-right-radius: 48rpx; background-image: linear-gradient(145deg, #F45A2A 0%, #FF9E4B 55%, #FFE0AC 100%); }
+	.hero { height: 468rpx; background-color: #FFFFFF; position: relative; overflow: hidden; border-bottom-left-radius: 48rpx; border-bottom-right-radius: 48rpx; }
+	.hero-image { position: absolute; left: 0; top: 0; width: 100%; height: 468rpx; }
+	.hero-gradient { position: absolute; z-index: 1; top: 0; left: 0; right: 0; height: 300rpx; background-image: linear-gradient(180deg, rgba(244,90,42,.88) 0%, rgba(255,130,60,.42) 62%, rgba(255,166,80,0) 100%); }
 	.safe-space { height: 28px; position: relative; z-index: 2; }
 	.nav-row { height: 32px; padding: 0 32rpx; position: relative; z-index: 2; flex-direction: row; align-items: center; justify-content: space-between; }
 	.sticky-nav { position: absolute; z-index: 30; top: 0; left: 0; right: 0; padding-bottom: 20rpx; }
 	.sticky-nav-bg { position: absolute; z-index: 0; top: 0; left: 0; right: 0; bottom: 0; opacity: 0; background-image: linear-gradient(180deg, #F26A38 0%, #EE542B 100%); transition: opacity 280ms ease-out; }
 	.sticky-nav.is-solid .sticky-nav-bg { opacity: 1; }
 	.nav-title { position: absolute; z-index: 1; left: 30rpx; top: 10rpx; height: 100%; flex-direction: row; align-items: center; justify-content: flex-start; }
-	.nav-title text { color: #FFFFFF; font-size: 28rpx; font-weight: 700; opacity: .95; }
+	.nav-title text { color: #FFFFFF; font-family: 'FoodCalligraphyFull', 'FoodCalligraphy'; font-size: 38rpx; font-weight: 700; opacity: .95; }
 	.capsule-reserve { width: 88px; height: 100%; flex-shrink: 0; }
 	.play-button { position: absolute; z-index: 2; bottom: 20rpx; left: 50%; margin-left: -78rpx; width: 156rpx; height: 56rpx; border-radius: 28rpx; background-image: linear-gradient(180deg, #FFB75A 0%, #FF6A24 100%); border-width: 2rpx; border-color: rgba(255,255,255,.82); box-shadow: 0 7rpx 14rpx rgba(234,92,26,.26); flex-direction: row; justify-content: center; align-items: center; }
 	.play-button text { color: #FFFFFF; font-size: 23rpx; }
@@ -173,8 +181,9 @@
 	.city-heading > text { color: #F16624; font-size: 35rpx; font-weight: 700; letter-spacing: 1rpx; background-image: linear-gradient(180deg, #F45A2A 0%, #FFB257 100%); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
 	.city-label { margin-right: 48rpx; padding: 10rpx 13rpx; border-radius: 18rpx; background-image: linear-gradient(180deg, #FFB85E 0%, #FF6726 100%); box-shadow: 0 5rpx 10rpx rgba(239,101,34,.22); flex-direction: row; align-items: center; } .city-label text { color: #FFFFFF; font-size: 20rpx; } .city-label image { width: 20rpx; height: 20rpx; margin-left: 7rpx; }
 	.city-grid { box-sizing: border-box; height: 292rpx; margin-top: 18rpx; padding: 22rpx; overflow: visible; background-image: linear-gradient(180deg, #FF5F2D 0%, #FFB36B 100%); border-bottom-left-radius: 26rpx; border-bottom-right-radius: 26rpx; flex-direction: row; } .city-card { overflow: hidden; border-radius: 17rpx; position: relative; justify-content: flex-end; background-image: linear-gradient(135deg, #E96C3D 0%, #F8AA56 100%); }
+	.city-bg { position: absolute; left: 0; top: 0; width: 100%; height: 100%; }
 	.city-intro { width: 242rpx; height: 308rpx; box-sizing: border-box; margin-right: 14rpx; padding: 22rpx; } .intro-name { position: absolute; z-index: 1; top: 50%; left: 0; right: 0; transform: translateY(-50%); color: #FFFFFF; font-size: 37rpx; font-weight: 700; text-align: center; } .intro-button { position: relative; z-index: 1; align-self: center; box-sizing: border-box; width: 146rpx; height: 42rpx; margin-top: 13rpx; padding: 0 12rpx; border-radius: 21rpx; background-image: linear-gradient(90deg, rgba(255,207,138,.92) 0%, rgba(255,101,36,.68) 100%); border-width: 1rpx; border-color: rgba(255,255,255,.42); flex-direction: row; align-items: center; justify-content: space-between; } .intro-button text { color: #FFFFFF; font-size: 19rpx; } .intro-button image { width: 19rpx; height: 19rpx; margin-left: 8rpx; }
-	.city-side { height: 308rpx; flex: 1; margin-right: 14rpx; justify-content: space-between; } .city-side:last-child { margin-right: 0; } .city-side .city-card { height: 147rpx; padding: 0; } .city-side .city-card > text { position: absolute; z-index: 1; left: 0; right: 0; bottom: 0; padding: 10rpx 12rpx; color: #FFFFFF; font-size: 22rpx; font-weight: 700; }
+	.city-side { height: 308rpx; flex: 1; margin-right: 14rpx; justify-content: space-between; } .city-side:last-child { margin-right: 0; } .city-side .city-card { height: 147rpx; padding: 0; } .city-side .city-card > text { position: absolute; z-index: 1; left: 0; right: 0; bottom: 0; padding: 10rpx 12rpx; color: #FFFFFF; font-size: 22rpx; font-weight: 700; text-shadow: 0 2rpx 8rpx rgba(0,0,0,.5); }
 
 	.section-head { height: 93rpx; padding: 0 31rpx; flex-direction: row; align-items: center; justify-content: space-between; } .section-title { flex-direction: row; align-items: center; } .section-title text { color: #353535; font-size: 31rpx; font-weight: 700; } .more { color: #5C5C5C; font-size: 22rpx; }
 	.news-state { min-height: 150rpx; margin: 0 31rpx; justify-content: center; align-items: center; color: #8B8B8B; font-size: 24rpx; } .news-retry { margin-top: 12rpx; color: #F16624; }
diff --git a/rencaika/pages/merchant/merchant.uvue b/rencaika/pages/merchant/merchant.uvue
index 7338141..07856c9 100644
--- a/rencaika/pages/merchant/merchant.uvue
+++ b/rencaika/pages/merchant/merchant.uvue
@@ -227,30 +227,30 @@
 	const applicableStores = ref<any[]>([])
 
 	const packages = ref([
-		{ id: 'food-set-001', name: '【川味盛宴】川菜12人餐', discount: '6折', price: '138', originalPrice: '266', image: '/static/food-assets/food-1.jpg' },
-		{ id: 'food-set-002', name: '【川香四溢】川菜4人餐', discount: '6折', price: '118', originalPrice: '198', image: '/static/food-assets/food-2.jpg' },
-		{ id: 'food-set-003', name: '【欢聚时光】招牌菜6人餐', discount: '6.8折', price: '168', originalPrice: '248', image: '/static/food-assets/food-1.jpg' },
-		{ id: 'food-set-004', name: '【暖心小聚】特色菜双人餐', discount: '7折', price: '88', originalPrice: '126', image: '/static/food-assets/food-2.jpg' },
-		{ id: 'food-set-005', name: '【家庭聚餐】经典川菜8人餐', discount: '6.5折', price: '238', originalPrice: '366', image: '/static/food-assets/food-1.jpg' },
-		{ id: 'food-set-006', name: '【人气必点】招牌菜4人餐', discount: '6.8折', price: '128', originalPrice: '188', image: '/static/food-assets/food-2.jpg' },
-		{ id: 'food-set-007', name: '【周末欢聚】特色菜10人餐', discount: '6折', price: '298', originalPrice: '498', image: '/static/food-assets/food-1.jpg' },
-		{ id: 'food-set-008', name: '【一人食】暖心工作餐', discount: '8折', price: '39', originalPrice: '49', image: '/static/food-assets/food-2.jpg' }
+		{ id: 'food-set-001', name: '【川味盛宴】川菜12人餐', discount: '6折', price: '138', originalPrice: '266', image: '/static/common/image-placeholder.png' },
+		{ id: 'food-set-002', name: '【川香四溢】川菜4人餐', discount: '6折', price: '118', originalPrice: '198', image: '/static/common/image-placeholder.png' },
+		{ id: 'food-set-003', name: '【欢聚时光】招牌菜6人餐', discount: '6.8折', price: '168', originalPrice: '248', image: '/static/common/image-placeholder.png' },
+		{ id: 'food-set-004', name: '【暖心小聚】特色菜双人餐', discount: '7折', price: '88', originalPrice: '126', image: '/static/common/image-placeholder.png' },
+		{ id: 'food-set-005', name: '【家庭聚餐】经典川菜8人餐', discount: '6.5折', price: '238', originalPrice: '366', image: '/static/common/image-placeholder.png' },
+		{ id: 'food-set-006', name: '【人气必点】招牌菜4人餐', discount: '6.8折', price: '128', originalPrice: '188', image: '/static/common/image-placeholder.png' },
+		{ id: 'food-set-007', name: '【周末欢聚】特色菜10人餐', discount: '6折', price: '298', originalPrice: '498', image: '/static/common/image-placeholder.png' },
+		{ id: 'food-set-008', name: '【一人食】暖心工作餐', discount: '8折', price: '39', originalPrice: '49', image: '/static/common/image-placeholder.png' }
 	])
 	const hotelPackages = [
-		{ id: 101, name: '都市视野大床房', discount: '6折', price: '588-800', originalPrice: '', image: '/static/stay-assets/hotel-hampton.jpg' },
-		{ id: 102, name: '山景云窗商务双床房', discount: '6折', price: '388-600', originalPrice: '', image: '/static/stay-assets/hotel-vienna.jpg' },
-		{ id: 103, name: '都市视野大床房', discount: '6折', price: '588-800', originalPrice: '', image: '/static/stay-assets/hotel-hampton.jpg' },
-		{ id: 104, name: '行政景观大床房', discount: '6折', price: '688-920', originalPrice: '', image: '/static/stay-assets/hotel-vienna.jpg' },
-		{ id: 105, name: '亲子主题家庭房', discount: '6.5折', price: '628-860', originalPrice: '', image: '/static/stay-assets/hotel-hampton.jpg' },
-		{ id: 106, name: '高层城景双床房', discount: '6折', price: '468-680', originalPrice: '', image: '/static/stay-assets/hotel-vienna.jpg' },
-		{ id: 107, name: '豪华行政套房', discount: '6.8折', price: '988-1280', originalPrice: '', image: '/static/stay-assets/hotel-hampton.jpg' },
-		{ id: 108, name: '舒适大床房', discount: '6折', price: '428-620', originalPrice: '', image: '/static/stay-assets/hotel-vienna.jpg' }
+		{ id: 101, name: '都市视野大床房', discount: '6折', price: '588-800', originalPrice: '', image: '/static/common/image-placeholder.png' },
+		{ id: 102, name: '山景云窗商务双床房', discount: '6折', price: '388-600', originalPrice: '', image: '/static/common/image-placeholder.png' },
+		{ id: 103, name: '都市视野大床房', discount: '6折', price: '588-800', originalPrice: '', image: '/static/common/image-placeholder.png' },
+		{ id: 104, name: '行政景观大床房', discount: '6折', price: '688-920', originalPrice: '', image: '/static/common/image-placeholder.png' },
+		{ id: 105, name: '亲子主题家庭房', discount: '6.5折', price: '628-860', originalPrice: '', image: '/static/common/image-placeholder.png' },
+		{ id: 106, name: '高层城景双床房', discount: '6折', price: '468-680', originalPrice: '', image: '/static/common/image-placeholder.png' },
+		{ id: 107, name: '豪华行政套房', discount: '6.8折', price: '988-1280', originalPrice: '', image: '/static/common/image-placeholder.png' },
+		{ id: 108, name: '舒适大床房', discount: '6折', price: '428-620', originalPrice: '', image: '/static/common/image-placeholder.png' }
 	]
-	// 体检医院演示套餐:图片按体验版素材规则复用“享”专题两张演示图。
+	// 体检医院演示套餐:本地演示数据,图片统一用兜底占位图(后台没配图时也是这张)。
 	const medicalPackages = [
-		{ id: 1, name: 'A套餐(脑血管系统筛查)', openTime: '5-6月', image: '/static/stay-assets/hotel-hampton.jpg' },
-		{ id: 2, name: 'B套餐(心血管系统筛查)', openTime: '5-6月', image: '/static/stay-assets/hotel-vienna.jpg' },
-		{ id: 3, name: 'C套餐(消化系统筛查)', openTime: '5-6月', image: '/static/stay-assets/hotel-hampton.jpg' }
+		{ id: 1, name: 'A套餐(脑血管系统筛查)', openTime: '5-6月', image: '/static/common/image-placeholder.png' },
+		{ id: 2, name: 'B套餐(心血管系统筛查)', openTime: '5-6月', image: '/static/common/image-placeholder.png' },
+		{ id: 3, name: 'C套餐(消化系统筛查)', openTime: '5-6月', image: '/static/common/image-placeholder.png' }
 	]
 	const visiblePackages = ref<any[]>([])
 	const visibleMedicalPackages = ref<any[]>([])
diff --git a/rencaika/pages/order-completed/order-completed.uvue b/rencaika/pages/order-completed/order-completed.uvue
index 92a751d..983666c 100644
--- a/rencaika/pages/order-completed/order-completed.uvue
+++ b/rencaika/pages/order-completed/order-completed.uvue
@@ -58,7 +58,7 @@
 	const talentId = ref('')
 	const loading = ref(true)
 	const loadError = ref(false)
-	const productCoverUrl = ref('/static/home-assets/top.jpg')
+	const productCoverUrl = ref('/static/common/image-placeholder.png')
 	const productName = ref('')
 	const originalAmount = ref('0.00')
 	const payAmount = ref('0.00')
@@ -93,7 +93,7 @@
 		loading.value = true
 		loadError.value = false
 		getCompletedOrder(orderId.value, talentId.value, (data: any) => {
-			productCoverUrl.value = resolveApiUrl(data.productCoverUrlSnapshot || '/static/home-assets/top.jpg')
+			productCoverUrl.value = resolveApiUrl(data.productCoverUrlSnapshot || '/static/common/image-placeholder.png')
 			productName.value = data.productNameSnapshot || '人才权益服务'
 			originalAmount.value = Number(data.originalAmount).toFixed(2)
 			payAmount.value = Number(data.payAmount).toFixed(2)
diff --git a/rencaika/pages/order/order.uvue b/rencaika/pages/order/order.uvue
index 69e1525..5ac88ed 100644
--- a/rencaika/pages/order/order.uvue
+++ b/rencaika/pages/order/order.uvue
@@ -374,8 +374,12 @@
 		if (item.statusCode == 'received') {
 			const detailName = item.detailName != null ? item.detailName as string : item.name as string
 			const claimId = item.targetDetailId != null ? item.targetDetailId as string : ''
-			const typeParam = item.targetDetailType == 'TICKET_PACKAGE_DETAIL' ? '&type=ticketPackage' : ''
-			uni.navigateTo({ url: '/pages/coupon-received/coupon-received?recordId=' + encodeURIComponent(item.id) + '&claimId=' + encodeURIComponent(claimId) + '&name=' + encodeURIComponent(detailName) + typeParam + '&talentId=' + encodeURIComponent(talentId.value) + '&merchantId=' + encodeURIComponent(item.merchantId) })
+			// 观影券(票包):详情页按「观影券」原型展示二维码、适用门店和领取/核销时间。
+			if (item.targetDetailType == 'TICKET_PACKAGE_DETAIL') {
+				uni.navigateTo({ url: '/pages/ticket-package-detail/ticket-package-detail?claimId=' + encodeURIComponent(claimId) + '&talentId=' + encodeURIComponent(talentId.value) + '&merchantId=' + encodeURIComponent(item.merchantId) })
+				return
+			}
+			uni.navigateTo({ url: '/pages/coupon-received/coupon-received?recordId=' + encodeURIComponent(item.id) + '&claimId=' + encodeURIComponent(claimId) + '&name=' + encodeURIComponent(detailName) + '&talentId=' + encodeURIComponent(talentId.value) + '&merchantId=' + encodeURIComponent(item.merchantId) })
 			return
 		}
 		uni.showToast({ title: '记录详情后续接入', icon: 'none' })
diff --git a/rencaika/pages/payment-confirm/payment-confirm.uvue b/rencaika/pages/payment-confirm/payment-confirm.uvue
index c8557a6..87404a7 100644
--- a/rencaika/pages/payment-confirm/payment-confirm.uvue
+++ b/rencaika/pages/payment-confirm/payment-confirm.uvue
@@ -67,7 +67,7 @@
 	const couponText = ref('无')
 	const couponAmount = ref(0.0)
 	const productName = ref('【川味盛宴】川菜12人餐')
-	const productImage = ref('/static/food-assets/food-1.jpg')
+	const productImage = ref('/static/common/image-placeholder.png')
 	const salePrice = ref(138.0)
 	const originalAmount = ref(266.0)
 	const productDiscount = ref('6折')
@@ -80,13 +80,13 @@
 	let quoteSequence = 0
 
 	const applyProductById = (id: string) => {
-		if (id == 'food-set-002') { productName.value = '【川香四溢】川菜4人餐'; productImage.value = '/static/food-assets/food-2.jpg'; salePrice.value = 118.0; originalAmount.value = 198.0; productDiscount.value = '6折'; return }
+		if (id == 'food-set-002') { productName.value = '【川香四溢】川菜4人餐'; productImage.value = '/static/common/image-placeholder.png'; salePrice.value = 118.0; originalAmount.value = 198.0; productDiscount.value = '6折'; return }
 		if (id == 'food-set-003') { productName.value = '【欢聚时光】招牌菜6人餐'; salePrice.value = 168.0; originalAmount.value = 248.0; productDiscount.value = '6.8折'; return }
-		if (id == 'food-set-004') { productName.value = '【暖心小聚】特色菜双人餐'; productImage.value = '/static/food-assets/food-2.jpg'; salePrice.value = 88.0; originalAmount.value = 126.0; productDiscount.value = '7折'; return }
+		if (id == 'food-set-004') { productName.value = '【暖心小聚】特色菜双人餐'; productImage.value = '/static/common/image-placeholder.png'; salePrice.value = 88.0; originalAmount.value = 126.0; productDiscount.value = '7折'; return }
 		if (id == 'food-set-005') { productName.value = '【家庭聚餐】经典川菜8人餐'; salePrice.value = 238.0; originalAmount.value = 366.0; productDiscount.value = '6.5折'; return }
-		if (id == 'food-set-006') { productName.value = '【人气必点】招牌菜4人餐'; productImage.value = '/static/food-assets/food-2.jpg'; salePrice.value = 128.0; originalAmount.value = 188.0; productDiscount.value = '6.8折'; return }
+		if (id == 'food-set-006') { productName.value = '【人气必点】招牌菜4人餐'; productImage.value = '/static/common/image-placeholder.png'; salePrice.value = 128.0; originalAmount.value = 188.0; productDiscount.value = '6.8折'; return }
 		if (id == 'food-set-007') { productName.value = '【周末欢聚】特色菜10人餐'; salePrice.value = 298.0; originalAmount.value = 498.0; productDiscount.value = '6折'; return }
-		if (id == 'food-set-008') { productName.value = '【一人食】暖心工作餐'; productImage.value = '/static/food-assets/food-2.jpg'; salePrice.value = 39.0; originalAmount.value = 49.0; productDiscount.value = '8折'; return }
+		if (id == 'food-set-008') { productName.value = '【一人食】暖心工作餐'; productImage.value = '/static/common/image-placeholder.png'; salePrice.value = 39.0; originalAmount.value = 49.0; productDiscount.value = '8折'; return }
 	}
 
 	onLoad((options: any) => {
diff --git a/rencaika/pages/pending-use/pending-use.uvue b/rencaika/pages/pending-use/pending-use.uvue
index 050bef6..bd1436a 100644
--- a/rencaika/pages/pending-use/pending-use.uvue
+++ b/rencaika/pages/pending-use/pending-use.uvue
@@ -55,7 +55,7 @@
 	const navWidth = ref(88)
 	const navRight = ref(10)
 	const isSubmittingRefund = ref(false)
-	const productCoverUrl = ref('/static/home-assets/top.jpg')
+	const productCoverUrl = ref('/static/common/image-placeholder.png')
 	const recordId = ref('')
 	const orderId = ref(''); const talentId = ref(''); const codeImage = ref(''); const codeText = ref('正在生成动态核销码'); const codeError = ref(false); const productName = ref('人才权益服务'); const originalAmount = ref('0.00'); const payAmount = ref('0.00'); const discountAmount = ref('0.00'); const merchantName = ref(''); const merchantAddress = ref(''); const merchantMobile = ref(''); const merchantLatitude = ref(0.0); const merchantLongitude = ref(0.0); const createdAt = ref(''); const verifyTime = ref('-'); const canRefund = ref(false); let refreshTimer = 0
 
@@ -84,13 +84,13 @@
 			uni.redirectTo({ url: '/pages/order-cancelled/order-cancelled?recordId=' + encodeURIComponent(recordId.value) + '&orderId=' + encodeURIComponent(orderId.value) + '&talentId=' + encodeURIComponent(talentId.value) })
 			return
 		}
-		productName.value=data.productNameSnapshot||productName.value; productCoverUrl.value=resolveApiUrl(data.productCoverUrlSnapshot||'/static/home-assets/top.jpg'); originalAmount.value=Number(data.originalAmount).toFixed(2); payAmount.value=Number(data.payAmount).toFixed(2); discountAmount.value=Number(data.discountAmount).toFixed(2); merchantName.value=data.merchantNameSnapshot||''; merchantAddress.value=data.merchantAddressSnapshot||''; merchantMobile.value=data.merchantMobileSnapshot||''; merchantLatitude.value=Number(data.merchantLatitudeSnapshot); merchantLongitude.value=Number(data.merchantLongitudeSnapshot); createdAt.value=(''+data.createdAt).replace('T',' ').substring(0,19); verifyTime.value=data.verifyTime?(''+data.verifyTime).replace('T',' ').substring(0,19):'-'; canRefund.value=data.canRefund===true }, () => {})
+		productName.value=data.productNameSnapshot||productName.value; productCoverUrl.value=resolveApiUrl(data.productCoverUrlSnapshot||'/static/common/image-placeholder.png'); originalAmount.value=Number(data.originalAmount).toFixed(2); payAmount.value=Number(data.payAmount).toFixed(2); discountAmount.value=Number(data.discountAmount).toFixed(2); merchantName.value=data.merchantNameSnapshot||''; merchantAddress.value=data.merchantAddressSnapshot||''; merchantMobile.value=data.merchantMobileSnapshot||''; merchantLatitude.value=Number(data.merchantLatitudeSnapshot); merchantLongitude.value=Number(data.merchantLongitudeSnapshot); createdAt.value=(''+data.createdAt).replace('T',' ').substring(0,19); verifyTime.value=data.verifyTime?(''+data.verifyTime).replace('T',' ').substring(0,19):'-'; canRefund.value=data.canRefund===true }, () => {})
 	const stopRefresh=()=>{if(refreshTimer!=0){clearTimeout(refreshTimer);refreshTimer=0}}
 	const refreshCode=()=>{stopRefresh(); if(orderId.value=='')return; codeError.value=false; issueOrderVerificationCode(orderId.value,talentId.value,(data:any)=>{codeImage.value=resolveApiUrl(data.imagePath as string);codeText.value='核销码 '+(data.displayCode as string)+',请向商家出示';refreshTimer=setTimeout(()=>refreshCode(),Math.max(1,Number(data.refreshAfterSeconds)-3)*1000)},()=>{codeImage.value='';codeError.value=true;codeText.value='点击重新生成核销码'})}
 	// @error 必须收回图片地址:二维码取不回来时页面只会剩一片空白,用户分不清是没生成、没网还是码失效。
 	// 这是 7.22 遗留问题 P1-006(远程图片失败会留白)的同一类缺陷,人才码和免费券码页都已按此处理。
 	const handleCodeImageError=()=>{codeImage.value='';codeError.value=true;codeText.value='核销码加载失败,点击重新生成'}
-	const handleCoverImageError=()=>{productCoverUrl.value='/static/home-assets/top.jpg'}
+	const handleCoverImageError=()=>{productCoverUrl.value='/static/common/image-placeholder.png'}
 	const openNavigation = () => { if(merchantLatitude.value==0||merchantLongitude.value==0){uni.showToast({title:'商户暂未配置导航地址',icon:'none'});return};uni.openLocation({latitude:merchantLatitude.value,longitude:merchantLongitude.value,name:merchantName.value,address:merchantAddress.value,scale:16}) }
 	const callMerchant = () => { if(merchantMobile.value==''){uni.showToast({title:'商户暂未配置联系电话',icon:'none'});return};uni.makePhoneCall({phoneNumber:merchantMobile.value}) }
 	const requestRefund = () => {
diff --git a/rencaika/pages/policy-detail/policy-detail.uvue b/rencaika/pages/policy-detail/policy-detail.uvue
index 778b483..84e88a2 100644
--- a/rencaika/pages/policy-detail/policy-detail.uvue
+++ b/rencaika/pages/policy-detail/policy-detail.uvue
@@ -85,7 +85,7 @@
 	const useHeroFallback = () => { policyCover.value = '' }
 	const useContentImageFallback = (index: number) => {
 		const next = policyContentImages.value.slice()
-		next[index] = '/static/policy-assets/policy-explanation.jpg'
+		next[index] = '/static/common/image-placeholder.png'
 		policyContentImages.value = next
 	}
 	const goBack = () => safeBack('/pages/policy/policy')
diff --git a/rencaika/pages/product-detail/product-detail.uvue b/rencaika/pages/product-detail/product-detail.uvue
index 44bdc77..4ee0e83 100644
--- a/rencaika/pages/product-detail/product-detail.uvue
+++ b/rencaika/pages/product-detail/product-detail.uvue
@@ -84,13 +84,13 @@
 	const purchaseNotices = ref<any[]>(defaultPurchaseNotices)
 
 	const loadProduct = (id: string) => {
-		if (id == 'food-set-002') { productName.value = '【川香四溢】川菜4人餐'; productImage.value = '/static/food-assets/food-2.jpg'; productPrice.value = '118'; productOriginalPrice.value = '198'; productDiscount.value = '6折'; return }
-		if (id == 'food-set-003') { productName.value = '【欢聚时光】招牌菜6人餐'; productImage.value = '/static/food-assets/food-1.jpg'; productPrice.value = '168'; productOriginalPrice.value = '248'; productDiscount.value = '6.8折'; return }
-		if (id == 'food-set-004') { productName.value = '【暖心小聚】特色菜双人餐'; productImage.value = '/static/food-assets/food-2.jpg'; productPrice.value = '88'; productOriginalPrice.value = '126'; productDiscount.value = '7折'; return }
-		if (id == 'food-set-005') { productName.value = '【家庭聚餐】经典川菜8人餐'; productImage.value = '/static/food-assets/food-1.jpg'; productPrice.value = '238'; productOriginalPrice.value = '366'; productDiscount.value = '6.5折'; return }
-		if (id == 'food-set-006') { productName.value = '【人气必点】招牌菜4人餐'; productImage.value = '/static/food-assets/food-2.jpg'; productPrice.value = '128'; productOriginalPrice.value = '188'; productDiscount.value = '6.8折'; return }
-		if (id == 'food-set-007') { productName.value = '【周末欢聚】特色菜10人餐'; productImage.value = '/static/food-assets/food-1.jpg'; productPrice.value = '298'; productOriginalPrice.value = '498'; productDiscount.value = '6折'; return }
-		if (id == 'food-set-008') { productName.value = '【一人食】暖心工作餐'; productImage.value = '/static/food-assets/food-2.jpg'; productPrice.value = '39'; productOriginalPrice.value = '49'; productDiscount.value = '8折'; return }
+		if (id == 'food-set-002') { productName.value = '【川香四溢】川菜4人餐'; productImage.value = '/static/common/image-placeholder.png'; productPrice.value = '118'; productOriginalPrice.value = '198'; productDiscount.value = '6折'; return }
+		if (id == 'food-set-003') { productName.value = '【欢聚时光】招牌菜6人餐'; productImage.value = '/static/common/image-placeholder.png'; productPrice.value = '168'; productOriginalPrice.value = '248'; productDiscount.value = '6.8折'; return }
+		if (id == 'food-set-004') { productName.value = '【暖心小聚】特色菜双人餐'; productImage.value = '/static/common/image-placeholder.png'; productPrice.value = '88'; productOriginalPrice.value = '126'; productDiscount.value = '7折'; return }
+		if (id == 'food-set-005') { productName.value = '【家庭聚餐】经典川菜8人餐'; productImage.value = '/static/common/image-placeholder.png'; productPrice.value = '238'; productOriginalPrice.value = '366'; productDiscount.value = '6.5折'; return }
+		if (id == 'food-set-006') { productName.value = '【人气必点】招牌菜4人餐'; productImage.value = '/static/common/image-placeholder.png'; productPrice.value = '128'; productOriginalPrice.value = '188'; productDiscount.value = '6.8折'; return }
+		if (id == 'food-set-007') { productName.value = '【周末欢聚】特色菜10人餐'; productImage.value = '/static/common/image-placeholder.png'; productPrice.value = '298'; productOriginalPrice.value = '498'; productDiscount.value = '6折'; return }
+		if (id == 'food-set-008') { productName.value = '【一人食】暖心工作餐'; productImage.value = '/static/common/image-placeholder.png'; productPrice.value = '39'; productOriginalPrice.value = '49'; productDiscount.value = '8折'; return }
 	}
 
 	onLoad((options: any) => {
diff --git a/rencaika/pages/spouse-employment/spouse-employment.uvue b/rencaika/pages/spouse-employment/spouse-employment.uvue
index 65d00cb..0b0b087 100644
--- a/rencaika/pages/spouse-employment/spouse-employment.uvue
+++ b/rencaika/pages/spouse-employment/spouse-employment.uvue
@@ -27,7 +27,7 @@
 
 	const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10)
 	const serviceId = ref(''); const talentId = ref(''); const loading = ref(true); const loadError = ref(false); const available = ref(false)
-	const fallbackHeroImage = '/static/stay-assets/hotel-vienna.jpg'; const heroImage = ref(fallbackHeroImage); const title = ref('配偶推荐就业'); const applicableCards = ref(''); const rulesText = ref(''); const unavailableReason = ref('该权益暂未开放')
+	const fallbackHeroImage = '/static/common/image-placeholder.png'; const heroImage = ref(fallbackHeroImage); const title = ref('配偶推荐就业'); const applicableCards = ref(''); const rulesText = ref(''); const unavailableReason = ref('该权益暂未开放')
 	onLoad((options: any) => {
 		// #ifdef MP-WEIXIN
 		const capsule = uni.getMenuButtonBoundingClientRect(); const info = uni.getSystemInfoSync(); navTop.value = capsule.top; navHeight.value = capsule.height; navWidth.value = capsule.width; navRight.value = info.windowWidth - capsule.right
diff --git a/rencaika/pages/stay-benefits/stay-benefits.uvue b/rencaika/pages/stay-benefits/stay-benefits.uvue
index faf7259..5ea6c0a 100644
--- a/rencaika/pages/stay-benefits/stay-benefits.uvue
+++ b/rencaika/pages/stay-benefits/stay-benefits.uvue
@@ -326,7 +326,7 @@
 		top: 245rpx;
 		max-width: 650rpx;
 		color: #FFFFFF;
-		font-family: 'StayCalligraphy', 'STXingkai', 'KaiTi', cursive;
+		font-family: 'FoodCalligraphyFull', 'StayCalligraphy', 'STXingkai', 'KaiTi', cursive;
 		font-size: 48rpx;
 		font-weight: normal;
 		font-style: normal;
diff --git a/rencaika/static/common/image-fallback.svg b/rencaika/static/common/image-fallback.svg
deleted file mode 100644
index 379ef0e..0000000
--- a/rencaika/static/common/image-fallback.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="320" height="200" viewBox="0 0 320 200">
-  <rect width="320" height="200" fill="#eef1f5"/>
-  <path d="M92 132l38-42 28 29 21-24 49 37H92z" fill="#c7d0dc"/>
-  <circle cx="119" cy="69" r="15" fill="#c7d0dc"/>
-  <text x="160" y="174" text-anchor="middle" fill="#8d98a8" font-family="sans-serif" font-size="14">图片加载失败</text>
-</svg>
diff --git a/rencaika/static/entertainment-assets/entertainment-hero.webp b/rencaika/static/entertainment-assets/entertainment-hero.webp
deleted file mode 100644
index 00163de..0000000
--- a/rencaika/static/entertainment-assets/entertainment-hero.webp
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/entertainment-assets/wanda-cinema.webp b/rencaika/static/entertainment-assets/wanda-cinema.webp
deleted file mode 100644
index d7fc0b9..0000000
--- a/rencaika/static/entertainment-assets/wanda-cinema.webp
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/food-assets/food-1.jpg b/rencaika/static/food-assets/food-1.jpg
deleted file mode 100644
index fd5c44b..0000000
--- a/rencaika/static/food-assets/food-1.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/food-assets/food-2.jpg b/rencaika/static/food-assets/food-2.jpg
deleted file mode 100644
index 77ff201..0000000
--- a/rencaika/static/food-assets/food-2.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/home-assets/top.jpg b/rencaika/static/home-assets/top.jpg
deleted file mode 100644
index bee2ee6..0000000
--- a/rencaika/static/home-assets/top.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/news-assets/meeting-thumb.jpg b/rencaika/static/news-assets/meeting-thumb.jpg
deleted file mode 100644
index 47ba123..0000000
--- a/rencaika/static/news-assets/meeting-thumb.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/policy-assets/policy-explanation.jpg b/rencaika/static/policy-assets/policy-explanation.jpg
deleted file mode 100644
index 93d9b78..0000000
--- a/rencaika/static/policy-assets/policy-explanation.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/stay-assets/hotel-hampton.jpg b/rencaika/static/stay-assets/hotel-hampton.jpg
deleted file mode 100644
index b08c8dc..0000000
--- a/rencaika/static/stay-assets/hotel-hampton.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika/static/stay-assets/hotel-vienna.jpg b/rencaika/static/stay-assets/hotel-vienna.jpg
deleted file mode 100644
index 1df04c6..0000000
--- a/rencaika/static/stay-assets/hotel-vienna.jpg
+++ /dev/null
Binary files differ
diff --git a/rencaika_merchant/config/api.uts b/rencaika_merchant/config/api.uts
index 3645a91..29c2a46 100644
--- a/rencaika_merchant/config/api.uts
+++ b/rencaika_merchant/config/api.uts
@@ -5,7 +5,8 @@
 
 // 切换环境时只修改这里;页面和业务服务不得自行拼接后台域名。
 // 2026-09-29:体验版指向攀枝花测试环境。本机联调改回 'development';生产上线改 'production'。
-export const apiEnvironment: ApiEnvironment = 'staging'
+// 2026-10-09:本地联调电影票,临时切 'development'(开发者工具走 127.0.0.1:8080,见 config/development-server.uts)。测完改回 'staging'。
+export const apiEnvironment: ApiEnvironment = 'development'
 
 // Keep the business pages accessible while merchant APIs are still being integrated.
 // This flag is ignored outside the development environment.
diff --git "a/rencaika_merchant/docs/pages/02-\350\256\242\345\215\225\347\256\241\347\220\206.md" "b/rencaika_merchant/docs/pages/02-\350\256\242\345\215\225\347\256\241\347\220\206.md"
index 73dfa39..f59632f 100644
--- "a/rencaika_merchant/docs/pages/02-\350\256\242\345\215\225\347\256\241\347\220\206.md"
+++ "b/rencaika_merchant/docs/pages/02-\350\256\242\345\215\225\347\256\241\347\220\206.md"
@@ -21,6 +21,8 @@
 | F-006 | 详情路由 | 非免费入住订单携带订单 ID、订单号进入已有详情页。 | 已接入路由 |
 | F-007 | 开发演示 | 开发环境标题右侧提供“演示数据”开关,可在本地分页数据与真实接口之间切换。 | 已完成 |
 | F-008 | 底部导航 | 与首页、商户中心同级,使用 `uni.reLaunch` 切换。 | 已完成 |
+| F-009 | 观影券样式 | 「团购」里混排观影券(票包领取记录):标题固定「观影券1张」,与人才卡等级同一行,无金额行;未核销显示「领取时间」、已核销显示「核销时间」,状态标签为「待核销 / 已核销」。 | 已接入 |
+| F-010 | 观影券详情 | 点开观影券进入 `/pages/ticket-package-detail/ticket-package-detail?claimId={领取记录ID}`。 | 已接入路由 |
 
 ## 操作说明
 
@@ -30,12 +32,14 @@
 4. 上滑触底加载下一页;追加失败时保留已展示订单并可点击底部重试。
 5. 无登录会话却关闭演示数据时,页面提示登录后加载真实订单,不将该场景误显示为空列表。
 6. 点击团购待核销订单进入待核销详情;到店待支付和已完成订单进入对应详情;免费入住不提供详情入口。
+7. 团购列表里的观影券来自票包领取记录(`ticket_package_claim`),和普通团购订单按时间倒序混排分页;点开进入观影券核销详情。
 
 ## 接口与数据
 
 | 数据项/动作 | 接口标识 | 参数 | 返回字段 | 状态 |
 | --- | --- | --- | --- | --- |
 | 订单分页列表 | `GET /api/merchant/orders` | `page`、`size`、`category`、`status`、`startDate`、`endDate`;门店来自会话 | 订单 ID/号、分类、稳定状态、脱敏用户、金额、卡种、下单/核销时间、`total` | 已接入 |
+| 观影券领取记录详情 | `GET /api/merchant/ticket-package-claims/{claimId}` | 领取记录 ID;商户来自会话 | 领取编号、券名、持卡人与卡等级、状态、领取/核销时间、当前权益 | 已接入 |
 | 订单详情 | `GET /api/merchant/orders/{orderNo}` | 订单号;门店来自会话 | 商品/权益、用户摘要、金额、状态 | 服务端已有,详情页视觉后续继续深化 |
 | 订单核销 | `POST /api/merchant/orders/{orderNo}/verify` | 核销方式、备注 | 核销结果 | 服务端已有,扫码核销为主流程 |
 
@@ -43,6 +47,7 @@
 
 - 分类只接受 `group`、`store`、`stay`;状态只接受约定枚举,后端不依赖中文文案。
 - 日期采用 `yyyy-MM-dd`,结束日期按次日零点前包含整日;开始日期晚于结束日期时返回明确错误。
+- `category=group` 时列表把普通团购订单与观影券混排,`sourceType=TICKET_PACKAGE` 表示这一条是观影券;分页 `total` 是两类记录之和。
 - 后端按当前商户、门店权限过滤,并只向列表返回脱敏姓名和手机号。
 - 列表响应使用 `records`、`page`、`size`、`total` 判断分页,不以本地长度猜测是否到底。
 
diff --git "a/rencaika_merchant/docs/pages/23-\350\247\202\345\275\261\346\235\203\347\233\212\346\240\270\351\224\200.md" "b/rencaika_merchant/docs/pages/23-\350\247\202\345\275\261\346\235\203\347\233\212\346\240\270\351\224\200.md"
index 7d73141..a564437 100644
--- "a/rencaika_merchant/docs/pages/23-\350\247\202\345\275\261\346\235\203\347\233\212\346\240\270\351\224\200.md"
+++ "b/rencaika_merchant/docs/pages/23-\350\247\202\345\275\261\346\235\203\347\233\212\346\240\270\351\224\200.md"
@@ -29,4 +29,6 @@
 
 ## 后台可见性
 
-- 管理后台「电影票管理 → 领取记录」展示领取状态与核销时间(`ticket_package_claim.status/used_at`),核销后即可看到;无需单独页面。
+- 管理后台「电影票管理 → 领取记录」展示领取状态与核销时间(`ticket_package_claim.status/used_at`)。
+- 管理后台「电影票管理 → 核销记录」(`GET /api/admin/ticket-packages/verifications`)只列已核销记录,可按票包、持卡人、卡片等级、领取时间、核销时间和核销商家筛选与导出。
+- 商家端在「订单管理 → 团购」里能看到这条观影券,点开的核销详情见 `24-观影券核销详情.md`。
diff --git a/rencaika_merchant/docs/pages/README.md b/rencaika_merchant/docs/pages/README.md
index 23f130d..1adf7da 100644
--- a/rencaika_merchant/docs/pages/README.md
+++ b/rencaika_merchant/docs/pages/README.md
@@ -31,6 +31,8 @@
 | 19 | 进件待审核 | `/pages/merchant-onboarding-pending/merchant-onboarding-pending` | 待审核结果与返回流程首版已完成 |
 | 21 | 结算管理 | `/pages/settlement-management/settlement-management` | 结算汇总、列表与导出已接入 |
 | 22 | 核销失败 | `/pages/verification-failure/verification-failure` | 非当前商户二维码独立失败结果页已完成 |
+| 23 | 观影权益核销 | `/pages/ticket-package-confirm/ticket-package-confirm` | 扫码核销观影权益(票包兑换码)已接入 |
+| 24 | 观影券核销详情 | `/pages/ticket-package-detail/ticket-package-detail?claimId={claimId}` | 团购列表观影券的待核销/已核销详情已接入 |
 
 - 文档编号必须唯一,页面目录、路由和文档标题应保持一致。
 - 页面有多种业务状态时,可在同一说明中用“状态差异”说明,或增加明确命名的补充文档。
diff --git a/rencaika_merchant/pages.json b/rencaika_merchant/pages.json
index d2a2924..0ade985 100644
--- a/rencaika_merchant/pages.json
+++ b/rencaika_merchant/pages.json
@@ -203,6 +203,14 @@
 				"backgroundColor": "#F7F8FA",
 				"disableScroll": true
 			}
+		},
+		{
+			"path": "pages/ticket-package-detail/ticket-package-detail",
+			"style": {
+				"navigationStyle": "custom",
+				"backgroundColor": "#FFFFFF",
+				"disableScroll": true
+			}
 		}
 	],
 	"globalStyle": {
diff --git a/rencaika_merchant/pages/orders/orders.uvue b/rencaika_merchant/pages/orders/orders.uvue
index 678d640..7b53714 100644
--- a/rencaika_merchant/pages/orders/orders.uvue
+++ b/rencaika_merchant/pages/orders/orders.uvue
@@ -20,10 +20,10 @@
 			<view v-if="listLoadState == 'success'" class="order-list">
 				<view v-for="order in orders" :key="order.id" class="order-card" :class="order.category == 'stay' ? 'stay-order-card' : (order.category == 'store' ? 'store-order-card' : '')" @tap="openOrderItem(order)">
 					<view class="customer-row"><view class="customer-info"><image class="customer-icon" src="/static/icons/lucide/circle-user-round.svg" mode="aspectFit"></image><text class="customer-name">{{order.customerName}}</text><text class="customer-phone">{{order.customerMobile}}</text></view><text class="status" :class="order.status == 'completed' ? 'verified' : ((order.status == 'cancelled' || order.status == 'refunded') ? 'closed' : 'pending')">{{order.statusText}}</text></view>
-					<text v-if="order.category != 'stay'" class="order-name">{{order.title}}</text>
-					<view v-if="order.category == 'stay'" class="stay-title-row"><text class="stay-order-name">{{order.title}}</text><text class="card-tag">{{order.cardLevel}}</text></view>
+					<text v-if="order.category != 'stay' && !isTicketOrder(order)" class="order-name">{{order.title}}</text>
+					<view v-if="order.category == 'stay' || isTicketOrder(order)" class="stay-title-row"><text class="stay-order-name">{{order.title}}</text><text class="card-tag">{{order.cardLevel}}</text></view>
 					<view v-else class="price-row" :class="order.category == 'store' ? 'store-price-row' : ''"><view class="price-group"><text class="price">¥{{formatAmount(order.payAmount)}}</text><text v-if="order.orderAmount > order.payAmount" class="original-price">¥{{formatAmount(order.orderAmount)}}</text></view><text class="card-tag">{{order.cardLevel}}</text></view>
-					<text class="time-text">{{order.status == 'completed' && order.verifyTime.length > 0 ? '核销时间:' + formatDateTime(order.verifyTime) : '下单时间:' + formatDateTime(order.createdAt)}}</text>
+					<text class="time-text">{{orderTimeText(order)}}</text>
 				</view>
 			</view>
 
@@ -283,8 +283,21 @@
 	const formatAmount = (amount: number): string => (amount / 100).toFixed(2)
 	const formatDateTime = (value: string): string => value.length > 0 ? value.replace('T', ' ') : '—'
 
+	/** 观影券没有金额,列表里按「标题 + 人才卡」一行展示,和免费入住同一套排版。 */
+	const isTicketOrder = (order: MerchantOrder): boolean => order.sourceType == 'TICKET_PACKAGE'
+	const orderTimeText = (order: MerchantOrder): string => {
+		if (order.status == 'completed' && order.verifyTime.length > 0) return '核销时间:' + formatDateTime(order.verifyTime)
+		return (isTicketOrder(order) ? '领取时间:' : '下单时间:') + formatDateTime(order.createdAt)
+	}
+
 	const openOrderItem = (order: MerchantOrder) => {
 		const query = 'id=' + encodeURIComponent(order.id) + '&orderNo=' + encodeURIComponent(order.orderNo)
+		if (isTicketOrder(order)) {
+			const rawClaimId = order.claimId != null ? order.claimId as string : ''
+			const claimId = rawClaimId.length > 0 ? rawClaimId : order.id.replace('TP', '')
+			uni.navigateTo({ url: '/pages/ticket-package-detail/ticket-package-detail?claimId=' + encodeURIComponent(claimId) })
+			return
+		}
 		if (order.status == 'pending_verification') {
 			uni.navigateTo({ url: '/pages/verification-detail/verification-detail?' + query })
 			return
diff --git a/rencaika_merchant/services/orders.uts b/rencaika_merchant/services/orders.uts
index ad79001..7869c9a 100644
--- a/rencaika_merchant/services/orders.uts
+++ b/rencaika_merchant/services/orders.uts
@@ -5,6 +5,10 @@
 	id: string
 	orderNo: string
 	category: string
+	/** 订单来源:普通订单为空,观影券(票包领取记录)为 TICKET_PACKAGE。 */
+	sourceType?: string
+	/** 观影券的领取记录 ID(只有 sourceType = TICKET_PACKAGE 时才有值)。 */
+	claimId?: string
 	status: string
 	statusText: string
 	title: string
@@ -19,6 +23,23 @@
 	subsidyAmount: number
 	createdAt: string
 	verifyTime: string
+}
+
+/** 观影券在商户端没有任何金额,详情页只展示领取/核销时间、持卡人和当前权益。 */
+export type MerchantTicketPackageClaim = {
+	id: string
+	claimNo: string
+	title: string
+	packageName: string
+	merchantName: string
+	customerName: string
+	customerMobile: string
+	cardLevel: string
+	status: string
+	statusText: string
+	createdAt: string
+	usedAt: string
+	benefitText: string
 }
 
 export type MerchantOrderPage = {
@@ -117,6 +138,8 @@
 	id: textValue(value, 'id', 'id'),
 	orderNo: textValue(value, 'orderNo', 'order_no'),
 	category: textValue(value, 'category', 'category', 'group'),
+	sourceType: textValue(value, 'sourceType', 'source_type'),
+	claimId: textValue(value, 'claimId', 'claim_id'),
 	status: textValue(value, 'status', 'status', 'pending_verification'),
 	statusText: textValue(value, 'statusText', 'status_text', '待核销'),
 	title: textValue(value, 'title', 'title', '订单'),
@@ -180,6 +203,36 @@
 }, (data: any, traceId: string) => onSuccess(toOrderDetail(data), traceId), onError)
 
 /**
+ * 观影券领取记录详情:团购列表里的观影券那一条点开后调用。
+ * 观影券没有订单号,用领取记录 ID 定位。
+ */
+export const loadMerchantTicketPackageClaim = (
+	claimId: string,
+	onSuccess: (claim: MerchantTicketPackageClaim, traceId: string) => void,
+	onError: (error: HttpError) => void
+): (() => void) => request<any>({
+	path: getApiEndpoint('merchantTicketPackageClaim') + encodeURIComponent(claimId),
+	method: 'GET'
+}, (data: any, traceId: string) => {
+	const source = data != null ? data : {}
+	onSuccess({
+		id: textValue(source, 'claimId', 'claim_id'),
+		claimNo: textValue(source, 'claimNo', 'claim_no'),
+		title: textValue(source, 'title', 'title', '观影券1张'),
+		packageName: textValue(source, 'packageName', 'package_name'),
+		merchantName: textValue(source, 'merchantName', 'merchant_name'),
+		customerName: textValue(source, 'customerName', 'customer_name'),
+		customerMobile: textValue(source, 'customerMobile', 'customer_mobile'),
+		cardLevel: textValue(source, 'cardLevel', 'card_level'),
+		status: textValue(source, 'status', 'status', 'pending_verification'),
+		statusText: textValue(source, 'statusText', 'status_text', '待核销'),
+		createdAt: textValue(source, 'createdAt', 'created_at'),
+		usedAt: textValue(source, 'usedAt', 'used_at'),
+		benefitText: textValue(source, 'benefitText', 'benefit_text')
+	}, traceId)
+}, onError)
+
+/**
  * 订单核销:核销成功会写核销记录并确认延迟入账的资金。
  *
  * verificationId 是扫码尝试编号;从订单列表直接核销时不传。
diff --git a/talent-card-admin/src/api/admin.ts b/talent-card-admin/src/api/admin.ts
index e1a6974..a00a050 100644
--- a/talent-card-admin/src/api/admin.ts
+++ b/talent-card-admin/src/api/admin.ts
@@ -666,6 +666,10 @@
   return http.get<PageResult<AnyRecord>, PageResult<AnyRecord>>('/api/admin/ticket-packages/claims', { params })
 }
 
+export function getTicketPackageVerifications(params: AnyRecord = {}) {
+  return http.get<PageResult<AnyRecord>, PageResult<AnyRecord>>('/api/admin/ticket-packages/verifications', { params })
+}
+
 export function supplementTicketPackageClaim(payload: AnyRecord) {
   return http.post<AnyRecord, AnyRecord>('/api/admin/ticket-packages/claims/supplement', payload)
 }
diff --git a/talent-card-admin/src/views/MovieTicketView.vue b/talent-card-admin/src/views/MovieTicketView.vue
index 6a3808f..924686b 100644
--- a/talent-card-admin/src/views/MovieTicketView.vue
+++ b/talent-card-admin/src/views/MovieTicketView.vue
@@ -10,6 +10,7 @@
   getTicketPackageCodes,
   getTicketPackageMerchantOptions,
   getTicketPackages,
+  getTicketPackageVerifications,
   importTicketPackageCodes,
   updateTicketPackage,
   updateTicketPackageStatus,
@@ -19,7 +20,7 @@
 import { TALENT_CARD_LEVELS } from '../utils/cardLevels'
 import { downloadExcel } from '../utils/excel'
 
-type ViewMode = 'list' | 'claims' | 'form' | 'codes'
+type ViewMode = 'list' | 'claims' | 'verifications' | 'form' | 'codes'
 
 const loading = ref(false)
 const saving = ref(false)
@@ -58,6 +59,15 @@
   claimedRange: [] as string[],
 })
 
+// 核销记录是「领取记录」的已核销子集,筛选口径和原型一致:持卡人、卡片等级、领取时间、核销时间、核销商家。
+const verificationFilters = reactive({
+  talentKeyword: '',
+  cardLevel: '',
+  claimedRange: [] as string[],
+  verifiedRange: [] as string[],
+  merchantKeyword: '',
+})
+
 const codeFilters = reactive({
   keyword: '',
   packageKeyword: '',
@@ -91,6 +101,7 @@
 
 const title = computed(() => {
   if (mode.value === 'claims') return '票包领取记录'
+  if (mode.value === 'verifications') return '票包核销记录'
   if (mode.value === 'form') return '票包信息'
   if (mode.value === 'codes') return '兑换码码池'
   return '查询表格'
@@ -163,12 +174,28 @@
   }
 }
 
+function verificationParams() {
+  return {
+    page: pagination.page,
+    size: pagination.size,
+    packageId: rowId(selectedPackage.value || {}) || undefined,
+    talentKeyword: verificationFilters.talentKeyword || undefined,
+    cardLevel: verificationFilters.cardLevel || undefined,
+    claimedStart: verificationFilters.claimedRange?.[0],
+    claimedEnd: verificationFilters.claimedRange?.[1],
+    verifiedStart: verificationFilters.verifiedRange?.[0],
+    verifiedEnd: verificationFilters.verifiedRange?.[1],
+    merchantKeyword: verificationFilters.merchantKeyword || undefined,
+  }
+}
+
 async function loadData() {
   if (mode.value === 'form') return
   loading.value = true
   try {
     let result
     if (mode.value === 'claims') result = await getTicketPackageClaims(claimParams())
+    else if (mode.value === 'verifications') result = await getTicketPackageVerifications(verificationParams())
     else if (mode.value === 'codes') result = await getTicketPackageCodes(codeParams())
     else result = await getTicketPackages(packageParams())
     rows.value = result.records || []
@@ -199,6 +226,11 @@
 
 function resetClaimFilters() {
   Object.assign(claimFilters, { talentKeyword: '', cardLevel: '', claimedRange: [] })
+  search()
+}
+
+function resetVerificationFilters() {
+  Object.assign(verificationFilters, { talentKeyword: '', cardLevel: '', claimedRange: [], verifiedRange: [], merchantKeyword: '' })
   search()
 }
 
@@ -395,6 +427,21 @@
     ], result.records || [])
     return
   }
+  if (mode.value === 'verifications') {
+    const result = await getTicketPackageVerifications({ ...verificationParams(), page: 1, size: 1000 })
+    downloadExcel('票包核销记录', [
+      { title: '领取编号', value: row => text(row.claimNo || row.id) },
+      { title: '票包名称', value: row => text(row.packageName) },
+      { title: '适用商家', value: row => text(row.merchantNames || row.merchantName) },
+      { title: '持卡人名称', value: row => text(row.talentName) },
+      { title: '持卡人电话', value: row => text(row.talentMobile) },
+      { title: '人才等级', value: row => text(row.cardLevel) },
+      { title: '领取时间', value: row => formatTime(row.claimedAt || row.createdAt) },
+      { title: '核销时间', value: row => formatTime(row.usedAt) },
+      { title: '核销商家', value: row => text(row.verifyMerchantName) },
+    ], result.records || [])
+    return
+  }
   const result = await getTicketPackages({ ...packageParams(), page: 1, size: 1000 })
   downloadExcel('电影票票包列表', [
     { title: '票包编号', value: row => text(row.packageNo || row.id) },
@@ -459,12 +506,13 @@
           <el-table-column label="状态" min-width="90" align="center">
             <template #default="{ row }">{{ statusText(row.status) }}</template>
           </el-table-column>
-          <el-table-column label="操作" width="220" fixed="right" align="center">
+          <el-table-column label="操作" width="280" fixed="right" align="center">
             <template #default="{ row }">
               <el-button link type="primary" @click="toggleStatus(row)">{{ Number(row.status) === 1 ? '禁用' : '启用' }}</el-button>
               <el-button link type="primary" @click="openEdit(row)">查看</el-button>
               <el-button link type="primary" @click="setMode('codes', row)">码池</el-button>
               <el-button link type="primary" @click="setMode('claims', row)">领取记录</el-button>
+              <el-button link type="primary" @click="setMode('verifications', row)">核销记录</el-button>
             </template>
           </el-table-column>
         </el-table>
@@ -509,6 +557,55 @@
         </el-table>
       </template>
 
+      <template v-else-if="mode === 'verifications'">
+        <el-form class="movie-ticket-record-filter-form" :model="verificationFilters" label-width="72px">
+          <el-form-item label="持卡人">
+            <el-input v-model="verificationFilters.talentKeyword" placeholder="持卡人姓名、手机号、卡号" clearable />
+          </el-form-item>
+          <el-form-item label="卡片等级">
+            <el-select v-model="verificationFilters.cardLevel" placeholder="全部" clearable>
+              <el-option label="全部" value="" />
+              <el-option v-for="level in ticketRuleLevels" :key="level" :label="level" :value="level" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="领取时间">
+            <el-date-picker v-model="verificationFilters.claimedRange" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" value-format="YYYY-MM-DD" />
+          </el-form-item>
+          <el-form-item label="核销时间">
+            <el-date-picker v-model="verificationFilters.verifiedRange" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" value-format="YYYY-MM-DD" />
+          </el-form-item>
+          <el-form-item label="核销商家">
+            <el-input v-model="verificationFilters.merchantKeyword" placeholder="商家名称" clearable />
+          </el-form-item>
+          <div class="movie-ticket-record-filter-actions">
+            <el-button type="primary" :icon="Search" @click="search">查询</el-button>
+            <el-button :icon="Refresh" @click="resetVerificationFilters">重置</el-button>
+          </div>
+        </el-form>
+
+        <div class="movie-ticket-record-tools">
+          <el-button v-permission="'admin.movie-tickets.export'" :icon="Download" @click="download">下载</el-button>
+        </div>
+
+        <el-table v-loading="loading" :data="rows" border class="movie-ticket-record-table" empty-text="暂无核销记录">
+          <el-table-column prop="claimNo" label="领取编号" min-width="130" align="center" />
+          <el-table-column prop="packageName" label="票包名称" min-width="150" align="center" />
+          <el-table-column prop="merchantNames" label="适用商家" min-width="190" align="center" show-overflow-tooltip>
+            <template #default="{ row }">{{ text(row.merchantNames || row.merchantName) }}</template>
+          </el-table-column>
+          <el-table-column prop="talentName" label="持卡人名称" min-width="120" align="center" />
+          <el-table-column prop="talentMobile" label="持卡人电话" min-width="130" align="center" />
+          <el-table-column prop="cardLevel" label="人才等级" min-width="120" align="center" />
+          <el-table-column label="领取时间" min-width="160" align="center">
+            <template #default="{ row }">{{ formatTime(row.claimedAt || row.createdAt) }}</template>
+          </el-table-column>
+          <el-table-column label="核销时间" min-width="160" align="center">
+            <template #default="{ row }">{{ formatTime(row.usedAt) }}</template>
+          </el-table-column>
+          <el-table-column prop="verifyMerchantName" label="核销商家" min-width="160" align="center" show-overflow-tooltip />
+        </el-table>
+      </template>
+
       <template v-else-if="mode === 'codes'">
         <div class="movie-ticket-code-pool-filters">
           <el-form class="movie-ticket-package-filter-form movie-ticket-code-pool-filter-form" :model="codeFilters" label-width="72px">
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/TicketPackageAdminController.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/TicketPackageAdminController.java
index d1fdf2f..f3b6090 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/TicketPackageAdminController.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/TicketPackageAdminController.java
@@ -168,6 +168,23 @@
         return ApiResponse.success(service.claims(packageId, talentId, status, keyword, packageKeyword, talentKeyword, cardLevel, claimedStart, claimedEnd, page, size));
     }
 
+    @Operation(summary = "查询票包核销记录", description = "分页查询已核销的票包领取记录,支持按票包、持卡人、卡片等级、领取时间、核销时间和核销商家筛选")
+    @GetMapping("/verifications")
+    public ApiResponse<PageResult<Map<String, Object>>> verifications(
+            @Parameter(description = "票包ID") @RequestParam(required = false) Long packageId,
+            @Parameter(description = "持卡人姓名、手机号或卡号关键词") @RequestParam(required = false) String talentKeyword,
+            @Parameter(description = "卡片等级") @RequestParam(required = false) String cardLevel,
+            @Parameter(description = "领取开始日期") @RequestParam(required = false) LocalDate claimedStart,
+            @Parameter(description = "领取结束日期") @RequestParam(required = false) LocalDate claimedEnd,
+            @Parameter(description = "核销开始日期") @RequestParam(required = false) LocalDate verifiedStart,
+            @Parameter(description = "核销结束日期") @RequestParam(required = false) LocalDate verifiedEnd,
+            @Parameter(description = "核销商家名称关键词") @RequestParam(required = false) String merchantKeyword,
+            @Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
+            @Parameter(description = "每页条数") @RequestParam(defaultValue = "10") int size
+    ) {
+        return ApiResponse.success(service.verifications(packageId, talentKeyword, cardLevel, claimedStart, claimedEnd, verifiedStart, verifiedEnd, merchantKeyword, page, size));
+    }
+
     @Operation(summary = "领取校验", description = "按兑换码码值校验兑换码是否可领取:是否存在、所属票包是否启用、是否在有效期内、是否适用于指定商户,以及当前状态(可用/锁定/已发放/已使用/已作废)")
     @GetMapping("/codes/validate")
     public ApiResponse<Map<String, Object>> validateCode(
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkController.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkController.java
index e6f2963..8eafd12 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkController.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkController.java
@@ -234,6 +234,18 @@
 		return ApiResponse.success(merchantWorkService.order(orderNo, principal.merchantId(), principal.storeId()));
 	}
 
+	/**
+	 * 观影券领取记录详情:团购列表里的观影券那一条点开后展示领取/核销时间和当前权益。
+	 * 观影券没有 benefit_order,用领取记录 ID 而不是订单号定位。
+	 */
+	@GetMapping("/ticket-package-claims/{claimId}")
+	public ApiResponse<Map<String, Object>> ticketPackageClaim(
+			@PathVariable Long claimId,
+			@RequestAttribute(MerchantAuthService.PRINCIPAL_ATTRIBUTE) MerchantPrincipal principal
+	) {
+		return ApiResponse.success(merchantWorkService.ticketPackageClaim(claimId, principal.merchantId()));
+	}
+
 	@PostMapping("/orders/{orderNo}/verify")
 	public ApiResponse<Map<String, Object>> verify(
 			@PathVariable String orderNo,
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java
index 10258c2..151c81c 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java
@@ -58,6 +58,10 @@
 		if (query != null && "stay".equals(normalize(query.category()))) {
 			return freeStayOrders(merchantId, storeId, page, size, query);
 		}
+		// 观影券(票包领取记录)在商户眼里也是「团购」的一份流水,和 benefit_order 混排分页。
+		if (query != null && "group".equals(normalize(query.category()))) {
+			return groupOrders(merchantId, storeId, page, size, query);
+		}
 		// 退款到不到最终结果只有通道知道,退款通知又会遇到和支付通知一样的问题(本地/内网环境收不到)。
 		// 读列表之前先把这家商户在途的退款收敛一次,免得商户看到的退款状态比人才端晚几分钟。这一步必须
 		// 排在本事务的第一条读语句之前,否则列表读到的是收敛前的旧快照。免费入住没有退款,走上面的分支。
@@ -176,6 +180,159 @@
 		List<Map<String, Object>> records = query(select, recordArguments.toArray()).stream().map(this::toListItem).toList();
 		Long total = jdbcTemplate.queryForObject("select count(*) from verify_record r" + condition, Long.class, arguments.toArray());
 		return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total);
+	}
+
+	/**
+	 * 团购列表:普通团购订单与观影券(票包领取记录)混排。
+	 *
+	 * <p>观影券不是 benefit_order:兑换码由后台票包码池批量发放,领取记录落在 ticket_package_claim,
+	 * 只有核销时才写 benefit_verify_record。但商户在「团购」里看到的应该是一份按时间倒序的流水,
+	 * 所以这里把两类记录并成一张表再统一分页,避免前端拿到两段各自分页的数据拼不齐。
+	 */
+	@Transactional(readOnly = true)
+	private PageResult<Map<String, Object>> groupOrders(Long merchantId, Long storeId, int page, int size, MerchantOrderQuery query) {
+		refundService.confirmPendingRefundsOfMerchant(merchantId, HuifuRefundService.PENDING_CONFIRM_LIMIT);
+		int safePage = Math.max(page, 1);
+		int safeSize = Math.min(Math.max(size, 1), 100);
+		OrderFilters filters = filters(query);
+		// 观影券只有已发放/已使用两种有效状态,退款、待支付这些团购态在它身上不存在,按状态筛成空集。
+		String claimStatusCondition = switch (normalize(query == null ? null : query.status())) {
+			case "", "all" -> "";
+			case "pending_verification" -> " and cl.status = 'ISSUED'";
+			case "completed" -> " and cl.status = 'USED'";
+			case "pending_payment" -> " and 1 = 0";
+			case "closed" -> " and cl.status in ('EXPIRED', 'CANCELLED')";
+			default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "订单状态无效");
+		};
+		LocalDate startDate = date(query == null ? null : query.startDate(), "开始日期");
+		LocalDate endDate = date(query == null ? null : query.endDate(), "结束日期");
+		StringBuilder claimConditions = new StringBuilder(claimStatusCondition);
+		List<Object> claimArguments = new ArrayList<>();
+		claimArguments.add(merchantId);
+		// 时间筛选沿用「下单时间」口径:观影券的下单时间就是领取时间。
+		if (startDate != null) {
+			claimConditions.append(" and cl.created_at >= ?");
+			claimArguments.add(Timestamp.valueOf(startDate.atStartOfDay()));
+		}
+		if (endDate != null) {
+			claimConditions.append(" and cl.created_at < ?");
+			claimArguments.add(Timestamp.valueOf(endDate.plusDays(1).atStartOfDay()));
+		}
+		String orderSource = """
+				select cast(o.id as char) as id, o.order_no as order_no, 'GROUP_BUY' as order_type,
+				       tu.name as talent_name, tu.mobile as talent_mobile, tu.gender,
+				       coalesce(nullif(o.card_level_snapshot, ''), tc.card_level) as card_level,
+				       o.product_name_snapshot, b.benefit_name, o.order_amount, o.subsidy_amount, o.pay_amount,
+				       o.order_status, o.verify_status, o.refund_status, o.created_at,
+				       verified.verify_time, '' as source_type, null as claim_id, o.created_at as sort_time, 0 as source_rank, o.id as sort_seq
+				from benefit_order o
+				left join talent_user tu on tu.id = o.talent_id
+				left join talent_card tc on tc.id = o.card_id
+				left join benefit b on b.id = o.benefit_id
+				left join (select order_id, max(verify_time) verify_time from verify_record where status = 1 group by order_id) verified on verified.order_id = o.id
+				where o.deleted = 0 and o.merchant_id = ? and (o.store_id is null or o.store_id = ?)
+				""" + filters.conditions();
+		String claimSource = """
+				select concat('TP', cl.id) as id, cl.claim_no as order_no, 'GROUP_BUY' as order_type,
+				       cl.talent_name as talent_name, cl.talent_mobile as talent_mobile, tu.gender,
+				       cl.card_level as card_level,
+				       '观影券1张' as product_name_snapshot, '观影券' as benefit_name,
+				       0 as order_amount, 0 as subsidy_amount, 0 as pay_amount,
+				       case when cl.status in ('ISSUED', 'USED') then 1 else 2 end as order_status,
+				       case when cl.status = 'USED' then 1 else 0 end as verify_status,
+				       0 as refund_status, cl.created_at,
+				       case when cl.status = 'USED' then cl.used_at else null end as verify_time,
+				       'TICKET_PACKAGE' as source_type, cl.id as claim_id, cl.created_at as sort_time, 1 as source_rank, cl.id as sort_seq
+				from ticket_package_claim cl
+				left join talent_user tu on tu.id = cl.talent_id
+				where cl.deleted = 0 and cl.merchant_id = ?
+				""" + claimConditions;
+		String recordSql = """
+				select t.id, t.order_no, t.order_type, t.talent_name, t.talent_mobile, t.gender, t.card_level,
+				       t.product_name_snapshot, t.benefit_name, t.order_amount, t.subsidy_amount, t.pay_amount,
+				       t.order_status, t.verify_status, t.refund_status, t.created_at, t.verify_time, t.source_type, t.claim_id
+				from (
+				""" + orderSource + "\n union all \n" + claimSource + """
+				) t
+				order by t.sort_time desc, t.source_rank asc, t.sort_seq desc
+				limit ? offset ?
+				""";
+		List<Object> recordArguments = new ArrayList<>();
+		recordArguments.add(merchantId);
+		recordArguments.add(storeId);
+		recordArguments.addAll(filters.arguments());
+		recordArguments.addAll(claimArguments);
+		recordArguments.add(safeSize);
+		recordArguments.add((safePage - 1) * safeSize);
+		List<Map<String, Object>> records = query(recordSql, recordArguments.toArray()).stream().map(this::toListItem).toList();
+		String countSql = "select (select count(*) from benefit_order o where o.deleted = 0 and o.merchant_id = ? and (o.store_id is null or o.store_id = ?)"
+				+ filters.conditions() + ") + (select count(*) from ticket_package_claim cl where cl.deleted = 0 and cl.merchant_id = ?"
+				+ claimConditions + ")";
+		List<Object> countArguments = new ArrayList<>();
+		countArguments.add(merchantId);
+		countArguments.add(storeId);
+		countArguments.addAll(filters.arguments());
+		countArguments.addAll(claimArguments);
+		Long total = jdbcTemplate.queryForObject(countSql, Long.class, countArguments.toArray());
+		return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total);
+	}
+
+	/**
+	 * 观影券领取记录详情:商户在「团购」里点开观影券那一条时看到的信息。
+	 *
+	 * <p>观影券没有 benefit_order,行项目来自 ticket_package_claim;核销时间取 used_at。
+	 */
+	@Transactional(readOnly = true)
+	public Map<String, Object> ticketPackageClaim(Long claimId, Long merchantId) {
+		if (claimId == null || claimId <= 0) {
+			throw new BusinessException(ErrorCode.BAD_REQUEST, "观影券领取记录ID不能为空");
+		}
+		List<Map<String, Object>> rows = query("""
+				select cl.id, cl.claim_no, cl.package_id, p.package_name, cl.merchant_id, m.merchant_name,
+				       m.merchant_rights_config,
+				       cl.talent_name, cl.talent_mobile, tu.gender, cl.card_level, cl.card_no,
+				       cl.status, cl.valid_end, cl.used_at, cl.created_at
+				from ticket_package_claim cl
+				join ticket_package p on p.id = cl.package_id and p.deleted = 0
+				left join merchant m on m.id = cl.merchant_id and m.deleted = 0
+				left join talent_user tu on tu.id = cl.talent_id
+				where cl.id = ? and cl.deleted = 0 and cl.merchant_id = ?
+				limit 1
+				""", claimId, merchantId);
+		if (rows.isEmpty()) {
+			throw new BusinessException(ErrorCode.NOT_FOUND, "观影券领取记录不存在或不属于当前商户");
+		}
+		Map<String, Object> row = rows.get(0);
+		boolean used = "USED".equalsIgnoreCase(text(row.get("status")));
+		String cardLevel = text(row.get("cardLevel"));
+		Map<String, Object> result = new LinkedHashMap<>();
+		result.put("type", "TICKET_PACKAGE");
+		result.put("claimId", row.get("id"));
+		result.put("claimNo", text(row.get("claimNo")));
+		result.put("title", "观影券1张");
+		result.put("quantity", 1);
+		result.put("packageName", text(row.get("packageName")));
+		result.put("merchantName", text(row.get("merchantName")));
+		result.put("customerName", displayName(row.get("talentName"), number(row.get("gender"))));
+		result.put("customerMobile", maskMobile(String.valueOf(row.get("talentMobile"))));
+		result.put("cardLevel", cardLevel);
+		result.put("status", used ? "completed" : "pending_verification");
+		result.put("statusText", used ? "已核销" : "待核销");
+		result.put("createdAt", row.get("createdAt"));
+		result.put("usedAt", row.get("usedAt"));
+		result.put("benefitText", ticketPackageBenefitText(row.get("merchantRightsConfig"), merchantId, cardLevel));
+		return result;
+	}
+
+	/**
+	 * 观影券详情的「当前权益」:与到店消费同一套口径——先取商家人才权益里该等级配置的描述,
+	 * 没配再回落到商户的到店消费权益文案,避免商户在观影券详情看到一句没有信息量的兜底话术。
+	 */
+	private String ticketPackageBenefitText(Object rightsConfig, Long merchantId, String cardLevel) {
+		if (cardLevel.isBlank()) return "";
+		String configured = merchantBenefitDetail(rightsConfig, cardLevel);
+		if (!configured.isBlank()) return configured;
+		return manualConsumptionService.benefitTextForMerchant(merchantId, cardLevel);
 	}
 
 	@Transactional
@@ -362,7 +519,7 @@
 		else status = verified ? "completed" : "pending_verification";
 		result.put("category", category);
 		result.put("status", status);
-		result.put("statusText", statusText(status, refundStatus));
+		result.put("statusText", statusText(status, refundStatus, category));
 		result.put("customerName", displayName(order.get("talentName"), number(order.get("gender"))));
 		result.put("customerMobile", maskMobile(String.valueOf(order.get("talentMobile"))));
 		result.put("title", orderTitle(order, category));
@@ -449,7 +606,7 @@
 		String status = status(number(order.get("orderStatus")), number(order.get("verifyStatus")), refundStatus);
 		result.put("category", category);
 		result.put("status", status);
-		result.put("statusText", statusText(status, refundStatus));
+		result.put("statusText", statusText(status, refundStatus, category));
 		result.put("customerName", displayName(order.get("talentName"), number(order.get("gender"))));
 		result.put("customerMobile", maskMobile(String.valueOf(order.get("talentMobile"))));
 		result.put("title", orderTitle(order, category));
@@ -552,9 +709,10 @@
 		return "pending_verification";
 	}
 
-	private String statusText(String status, Long refundStatus) {
+	private String statusText(String status, Long refundStatus, String category) {
 		if ("pending_payment".equals(status)) return "待支付";
-		if ("completed".equals(status)) return "已完成";
+		// 团购是到店核销的业务:已完成在商户眼里就是「已核销」,原型与列表口径都这么写。
+		if ("completed".equals(status)) return "group".equals(category) ? "已核销" : "已完成";
 		if ("cancelled".equals(status)) return "已取消";
 		if ("refunded".equals(status)) return refundStatus != null && refundStatus == 1 ? "退款中" : "已退款";
 		return "待核销";
diff --git a/talent-card-server/src/main/java/com/talentcard/server/mapper/TicketPackageClaimMapper.java b/talent-card-server/src/main/java/com/talentcard/server/mapper/TicketPackageClaimMapper.java
index 451d9d0..3d1e1c6 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/mapper/TicketPackageClaimMapper.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/mapper/TicketPackageClaimMapper.java
@@ -372,4 +372,83 @@
                         @Param("cardLevel") String cardLevel,
                         @Param("claimedStart") LocalDate claimedStart,
                         @Param("claimedEnd") LocalDate claimedEnd);
+
+    /**
+     * 后台「核销记录」:只列已核销(status = USED)的领取记录。
+     *
+     * <p>核销商家就是领取记录上的 merchant_id——核销接口要求核销方必须等于该商户,所以不需要再关联
+     * benefit_verify_record。
+     */
+    @Select("""
+            select cl.id,
+                   cl.claim_no as claimNo,
+                   cl.package_id as packageId,
+                   p.package_name as packageName,
+                   coalesce((
+                       select group_concat(m1.merchant_name order by pm1.sort_no asc, pm1.id asc separator '、')
+                       from ticket_package_merchant pm1
+                       join merchant m1 on m1.id = pm1.merchant_id and m1.deleted = 0
+                       where pm1.package_id = p.id and pm1.deleted = 0
+                   ), m.merchant_name) as merchantNames,
+                   m.merchant_name as verifyMerchantName,
+                   cl.talent_id as talentId,
+                   cl.card_id as cardId,
+                   cl.card_no as cardNo,
+                   cl.card_level as cardLevel,
+                   cl.talent_name as talentName,
+                   cl.talent_mobile as talentMobile,
+                   cl.status,
+                   cl.created_at as createdAt,
+                   cl.used_at as usedAt
+            from ticket_package_claim cl
+            join ticket_package p on p.id = cl.package_id and p.deleted = 0
+            left join merchant m on m.id = cl.merchant_id and m.deleted = 0
+            where cl.deleted = 0
+              and cl.status = 'USED'
+              and (#{packageId} is null or cl.package_id = #{packageId})
+              and (#{talentKeyword} is null or #{talentKeyword} = '' or cl.talent_name like concat('%', #{talentKeyword}, '%') or cl.talent_mobile like concat('%', #{talentKeyword}, '%') or cl.card_no like concat('%', #{talentKeyword}, '%'))
+              and (#{cardLevel} is null or #{cardLevel} = '' or cl.card_level = #{cardLevel} or cl.card_level like concat('%', #{cardLevel}, '%'))
+              and (#{claimedStart} is null or cl.created_at >= #{claimedStart})
+              and (#{claimedEnd} is null or cl.created_at < date_add(cast(#{claimedEnd} as date), interval 1 day))
+              and (#{verifiedStart} is null or cl.used_at >= #{verifiedStart})
+              and (#{verifiedEnd} is null or cl.used_at < date_add(cast(#{verifiedEnd} as date), interval 1 day))
+              and (#{merchantKeyword} is null or #{merchantKeyword} = '' or m.merchant_name like concat('%', #{merchantKeyword}, '%'))
+            order by cl.used_at desc, cl.id desc
+            limit #{size} offset #{offset}
+            """)
+    List<Map<String, Object>> selectAdminVerificationPage(@Param("packageId") Long packageId,
+                                                          @Param("talentKeyword") String talentKeyword,
+                                                          @Param("cardLevel") String cardLevel,
+                                                          @Param("claimedStart") LocalDate claimedStart,
+                                                          @Param("claimedEnd") LocalDate claimedEnd,
+                                                          @Param("verifiedStart") LocalDate verifiedStart,
+                                                          @Param("verifiedEnd") LocalDate verifiedEnd,
+                                                          @Param("merchantKeyword") String merchantKeyword,
+                                                          @Param("size") int size,
+                                                          @Param("offset") int offset);
+
+    @Select("""
+            select count(*)
+            from ticket_package_claim cl
+            join ticket_package p on p.id = cl.package_id and p.deleted = 0
+            left join merchant m on m.id = cl.merchant_id and m.deleted = 0
+            where cl.deleted = 0
+              and cl.status = 'USED'
+              and (#{packageId} is null or cl.package_id = #{packageId})
+              and (#{talentKeyword} is null or #{talentKeyword} = '' or cl.talent_name like concat('%', #{talentKeyword}, '%') or cl.talent_mobile like concat('%', #{talentKeyword}, '%') or cl.card_no like concat('%', #{talentKeyword}, '%'))
+              and (#{cardLevel} is null or #{cardLevel} = '' or cl.card_level = #{cardLevel} or cl.card_level like concat('%', #{cardLevel}, '%'))
+              and (#{claimedStart} is null or cl.created_at >= #{claimedStart})
+              and (#{claimedEnd} is null or cl.created_at < date_add(cast(#{claimedEnd} as date), interval 1 day))
+              and (#{verifiedStart} is null or cl.used_at >= #{verifiedStart})
+              and (#{verifiedEnd} is null or cl.used_at < date_add(cast(#{verifiedEnd} as date), interval 1 day))
+              and (#{merchantKeyword} is null or #{merchantKeyword} = '' or m.merchant_name like concat('%', #{merchantKeyword}, '%'))
+            """)
+    Long countAdminVerificationPage(@Param("packageId") Long packageId,
+                                    @Param("talentKeyword") String talentKeyword,
+                                    @Param("cardLevel") String cardLevel,
+                                    @Param("claimedStart") LocalDate claimedStart,
+                                    @Param("claimedEnd") LocalDate claimedEnd,
+                                    @Param("verifiedStart") LocalDate verifiedStart,
+                                    @Param("verifiedEnd") LocalDate verifiedEnd,
+                                    @Param("merchantKeyword") String merchantKeyword);
 }
diff --git a/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java b/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java
index 40272eb..52752d5 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/service/BenefitVerificationCodeService.java
@@ -98,8 +98,10 @@
         if (claim == null || (claim.getDeleted() != null && claim.getDeleted() != 0)) {
             throw new BusinessException(ErrorCode.NOT_FOUND, "观影权益领取记录不存在");
         }
-        if (!"ISSUED".equals(upper(claim.getStatus()))) {
-            throw new BusinessException(ErrorCode.CONFLICT, "观影权益已核销或已失效");
+        // 已核销的观影券仍然返回二维码:使用记录详情要按原型继续展示这张码(核销时服务端会拒掉重复核销),
+        // 只有过期/作废才不给。
+        if (!"ISSUED".equals(upper(claim.getStatus())) && !"USED".equals(upper(claim.getStatus()))) {
+            throw new BusinessException(ErrorCode.CONFLICT, "观影权益已失效");
         }
         if (expiredDate(claim.getValidEnd())) {
             throw new BusinessException(ErrorCode.CONFLICT, "观影权益已过期");
diff --git a/talent-card-server/src/main/java/com/talentcard/server/service/TicketPackageAdminService.java b/talent-card-server/src/main/java/com/talentcard/server/service/TicketPackageAdminService.java
index 081374a..07a8c59 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/service/TicketPackageAdminService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/service/TicketPackageAdminService.java
@@ -341,6 +341,46 @@
         return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total);
     }
 
+    /**
+     * 票包核销记录:只列已核销(status = USED)的领取记录,供后台「电影票管理 → 核销记录」查询与导出。
+     *
+     * <p>「核销商家」取领取记录上的 merchant_id。核销接口要求核销方必须等于该商户,所以这里不需要
+     * 再关联 benefit_verify_record 才能知道是谁核销的。
+     */
+    @Transactional(readOnly = true)
+    public PageResult<Map<String, Object>> verifications(Long packageId, String talentKeyword, String cardLevel,
+                                                         LocalDate claimedStart, LocalDate claimedEnd,
+                                                         LocalDate verifiedStart, LocalDate verifiedEnd,
+                                                         String merchantKeyword, int page, int size) {
+        int safePage = Math.max(page, 1);
+        int safeSize = Math.min(Math.max(size, 1), 100);
+        Long total = claimMapper.countAdminVerificationPage(packageId, clean(talentKeyword), clean(cardLevel),
+                claimedStart, claimedEnd, verifiedStart, verifiedEnd, clean(merchantKeyword));
+        List<Map<String, Object>> records = claimMapper.selectAdminVerificationPage(packageId, clean(talentKeyword),
+                        clean(cardLevel), claimedStart, claimedEnd, verifiedStart, verifiedEnd, clean(merchantKeyword),
+                        safeSize, (safePage - 1) * safeSize)
+                .stream()
+                .map(this::verificationView)
+                .toList();
+        return new PageResult<>(records, safePage, safeSize, total == null ? 0 : total);
+    }
+
+    private Map<String, Object> verificationView(Map<String, Object> row) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("id", value(row, "id"));
+        result.put("claimNo", text(row, "claimNo", "claim_no"));
+        result.put("packageId", value(row, "packageId", "package_id"));
+        result.put("packageName", text(row, "packageName", "package_name"));
+        result.put("merchantNames", text(row, "merchantNames", "merchant_names"));
+        result.put("talentName", text(row, "talentName", "talent_name"));
+        result.put("talentMobile", text(row, "talentMobile", "talent_mobile"));
+        result.put("cardLevel", text(row, "cardLevel", "card_level"));
+        result.put("claimedAt", value(row, "createdAt", "created_at"));
+        result.put("usedAt", value(row, "usedAt", "used_at"));
+        result.put("verifyMerchantName", text(row, "verifyMerchantName", "verify_merchant_name"));
+        return result;
+    }
+
 
     @Transactional(readOnly = true)
     public Map<String, Object> codeDetail(Long codeId) {
diff --git a/talent-card-server/src/main/java/com/talentcard/server/service/TicketPackagePortalService.java b/talent-card-server/src/main/java/com/talentcard/server/service/TicketPackagePortalService.java
index 4dc41ea..ecfcb1d 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/service/TicketPackagePortalService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/service/TicketPackagePortalService.java
@@ -288,6 +288,8 @@
         result.put("exchangeCode", "ACTIVE".equals(codeStatus) ? text(row, "exchangeCode") : "");
         result.put("validEnd", value(row, "validEnd"));
         result.put("usedAt", value(row, "usedAt"));
+        // 使用记录里的观影券详情要显示「领取时间」,也就是领取记录的创建时间。
+        result.put("createdAt", value(row, "createdAt"));
         result.put("totalQuantity", totalQuantity);
         result.put("claimedCount", claimedCount);
         result.put("remainingCount", Math.max(totalQuantity - claimedCount, 0));

--
Gitblit v1.7.1