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

---
 talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantWorkService.java |  166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 162 insertions(+), 4 deletions(-)

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 "待核销";

--
Gitblit v1.7.1