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