From 0b064045b0af20040b6ad976a7ce33cdc03d91f7 Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期五, 28 八月 2026 15:57:48 +0800
Subject: [PATCH] fix: allow scanned benefit codes to finish verification
---
talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationOrderCodeTests.java | 136 +++++++++++++++++++++++++++
talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationFreeStayTests.java | 141 ++++++++++++++++++++++++++++
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java | 17 +-
3 files changed, 286 insertions(+), 8 deletions(-)
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java
index 82188d5..8dfdda1 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantVerificationService.java
@@ -69,11 +69,12 @@
if (number(attempt.get("attemptStatus")) == 1) {
return confirmed(attempt.get("verifyNo"), null);
}
- if (number(attempt.get("attemptStatus")) != 0 || expired(attempt.get("expiresAt"))) {
+ String credentialType = String.valueOf(attempt.get("credentialType"));
+ boolean codeExpiryRequired = !ORDER_CODE.equals(credentialType) && !FREE_STAY_CODE.equals(credentialType);
+ if (number(attempt.get("attemptStatus")) != 0 || (codeExpiryRequired && expired(attempt.get("expiresAt")))) {
jdbc.update("update merchant_verification_attempt set status = 2 where id = ? and status = 0", number(attempt.get("attemptId")));
throw new BusinessException(ErrorCode.CONFLICT, "核销凭证已失效,请重新扫码");
}
- String credentialType = String.valueOf(attempt.get("credentialType"));
return switch (credentialType) {
case TALENT_CODE -> throw new BusinessException(ErrorCode.CONFLICT, "人才码需要先录入消费金额并发送订单");
case ORDER_CODE -> confirmOrderCode(principal, attempt);
@@ -159,19 +160,19 @@
from order_dynamic_verification_code c
join benefit_order o on o.id = c.order_id and o.deleted = 0
left join talent_card tc on tc.id = o.card_id and tc.deleted = 0
- where c.id = ? and c.status = 0
+ where c.id = ? and c.status in (0, 2)
limit 1 for update
""", number(attempt.get("orderVerificationCodeId")));
if (codes.isEmpty()) {
throw new BusinessException(ErrorCode.CONFLICT, "订单核销码已被使用或已失效");
}
Map<String, Object> code = codes.get(0);
- if (!belongsToPrincipal(code, principal) || number(code.get("orderStatus")) != 1 || number(code.get("verifyStatus")) != 0 || expired(code.get("expiresAt"))) {
+ if (!belongsToPrincipal(code, principal) || number(code.get("orderStatus")) != 1 || number(code.get("verifyStatus")) != 0) {
throw new BusinessException(ErrorCode.CONFLICT, "订单当前不可核销");
}
int codeUpdated = jdbc.update("""
update order_dynamic_verification_code set status = 1
- where id = ? and status = 0 and expires_at > now()
+ where id = ? and status in (0, 2)
""", number(code.get("orderVerificationCodeId")));
int orderUpdated = jdbc.update("""
update benefit_order set verify_status = 1
@@ -194,7 +195,7 @@
from free_stay_verification_code c
join talent_free_stay_voucher v on v.id = c.voucher_id
left join talent_card tc on tc.id = v.card_id and tc.deleted = 0
- where c.id = ? and c.status = 0
+ where c.id = ? and c.status in (0, 2)
limit 1 for update
""", number(attempt.get("freeStayVerificationCodeId")));
if (codes.isEmpty()) {
@@ -202,12 +203,12 @@
}
Map<String, Object> code = codes.get(0);
if (!principal.merchantId().equals(number(code.get("merchantId"))) || number(code.get("voucherStatus")) != 1
- || expiredDate(code.get("validEnd")) || number(code.get("usedCount")) >= number(code.get("totalCount")) || expired(code.get("expiresAt"))) {
+ || expiredDate(code.get("validEnd")) || number(code.get("usedCount")) >= number(code.get("totalCount"))) {
throw new BusinessException(ErrorCode.CONFLICT, "免费入住券当前不可核销");
}
int codeUpdated = jdbc.update("""
update free_stay_verification_code set status = 1
- where id = ? and status = 0 and expires_at > now()
+ where id = ? and status in (0, 2)
""", number(code.get("freeStayVerificationCodeId")));
int voucherUpdated = jdbc.update("""
update talent_free_stay_voucher set used_count = used_count + 1
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationFreeStayTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationFreeStayTests.java
new file mode 100644
index 0000000..5c340ef
--- /dev/null
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationFreeStayTests.java
@@ -0,0 +1,141 @@
+package com.talentcard.server.business.merchant;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
+import com.talentcard.server.common.BusinessException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+
+class MerchantVerificationFreeStayTests {
+
+ private JdbcTemplate jdbc;
+ private MerchantVerificationService service;
+ private MerchantPrincipal principal;
+
+ @BeforeEach
+ void setUp() {
+ DriverManagerDataSource dataSource = new DriverManagerDataSource(
+ "jdbc:h2:mem:free-stay-verification-" + System.nanoTime()
+ + ";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1", "sa", "");
+ jdbc = new JdbcTemplate(dataSource);
+ jdbc.execute("""
+ create table merchant_verification_attempt (
+ id bigint auto_increment primary key,
+ verification_id varchar(64) not null,
+ credential_type varchar(32) not null,
+ dynamic_code_id bigint,
+ order_verification_code_id bigint,
+ free_stay_verification_code_id bigint,
+ merchant_id bigint not null,
+ store_id bigint not null,
+ operator_id bigint not null,
+ expires_at timestamp,
+ status tinyint not null default 0,
+ verify_no varchar(64),
+ confirmed_at timestamp
+ )
+ """);
+ jdbc.execute("""
+ create table talent_free_stay_voucher (
+ id bigint primary key,
+ talent_id bigint not null,
+ merchant_id bigint not null,
+ card_id bigint not null,
+ total_count int not null,
+ used_count int not null,
+ valid_end date not null,
+ status tinyint not null
+ )
+ """);
+ jdbc.execute("""
+ create table free_stay_verification_code (
+ id bigint primary key,
+ voucher_id bigint not null,
+ expires_at timestamp not null,
+ status tinyint not null
+ )
+ """);
+ jdbc.execute("""
+ create table talent_card (
+ id bigint primary key,
+ card_level varchar(64),
+ deleted tinyint not null default 0
+ )
+ """);
+ jdbc.execute("""
+ create table verify_record (
+ id bigint auto_increment primary key,
+ verify_no varchar(64) not null,
+ order_id bigint,
+ talent_id bigint,
+ card_id bigint,
+ benefit_id bigint,
+ merchant_id bigint,
+ store_id bigint,
+ staff_id bigint,
+ verify_type varchar(32),
+ verify_amount bigint,
+ subsidy_amount bigint,
+ remark varchar(255)
+ )
+ """);
+ service = new MerchantVerificationService(jdbc, new ManualConsumptionService(jdbc));
+ principal = new MerchantPrincipal(9L, 88L, 7L, "operator");
+ }
+
+ @Test
+ void confirmsFreeStayAfterScannedCodeExpiresAndRefreshes() {
+ insertFreeStay("free-stay-expired-code", LocalDate.now().plusDays(1));
+
+ MapResult confirmed = confirm("free-stay-expired-code");
+
+ assertThat(confirmed.verified()).isTrue();
+ assertThat(jdbc.queryForObject("select status from free_stay_verification_code where id = 30", Integer.class)).isEqualTo(1);
+ assertThat(jdbc.queryForObject("select used_count from talent_free_stay_voucher where id = 20", Integer.class)).isEqualTo(1);
+ assertThat(jdbc.queryForObject("select status from merchant_verification_attempt where verification_id = ?", Integer.class,
+ "free-stay-expired-code")).isEqualTo(1);
+ assertThat(jdbc.queryForObject("select count(*) from verify_record where verify_type = 'HOTEL_FREE'", Integer.class)).isEqualTo(1);
+ }
+
+ @Test
+ void stillRejectsExpiredFreeStayVoucher() {
+ insertFreeStay("free-stay-expired-voucher", LocalDate.now().minusDays(1));
+
+ assertThatThrownBy(() -> confirm("free-stay-expired-voucher"))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining("当前不可核销");
+ }
+
+ private void insertFreeStay(String verificationId, LocalDate voucherValidEnd) {
+ jdbc.update("insert into talent_card(id, card_level, deleted) values (10, '卓越人才卡', 0)");
+ jdbc.update("""
+ insert into talent_free_stay_voucher(id, talent_id, merchant_id, card_id, total_count, used_count, valid_end, status)
+ values (20, 1015, 88, 10, 2, 0, ?, 1)
+ """, voucherValidEnd);
+ jdbc.update("""
+ insert into free_stay_verification_code(id, voucher_id, expires_at, status)
+ values (30, 20, ?, 2)
+ """, Timestamp.valueOf(LocalDateTime.now().minusMinutes(1)));
+ jdbc.update("""
+ insert into merchant_verification_attempt(verification_id, credential_type, free_stay_verification_code_id,
+ merchant_id, store_id, operator_id, expires_at, status)
+ values (?, 'FREE_STAY_CODE', 30, 88, 7, 9, ?, 0)
+ """, verificationId, Timestamp.valueOf(LocalDateTime.now().minusMinutes(1)));
+ }
+
+ private MapResult confirm(String verificationId) {
+ java.util.Map<String, Object> result = service.confirm(principal, new VerificationConfirmRequest(verificationId),
+ "free-stay-confirm-" + verificationId);
+ return new MapResult(Boolean.TRUE.equals(result.get("verified")));
+ }
+
+ private record MapResult(boolean verified) {}
+}
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationOrderCodeTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationOrderCodeTests.java
new file mode 100644
index 0000000..bbd70df
--- /dev/null
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationOrderCodeTests.java
@@ -0,0 +1,136 @@
+package com.talentcard.server.business.merchant;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.Map;
+
+import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
+import com.talentcard.server.common.BusinessException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.DriverManagerDataSource;
+
+class MerchantVerificationOrderCodeTests {
+
+ private JdbcTemplate jdbc;
+ private MerchantVerificationService service;
+ private MerchantPrincipal principal;
+
+ @BeforeEach
+ void setUp() {
+ DriverManagerDataSource dataSource = new DriverManagerDataSource(
+ "jdbc:h2:mem:order-code-verification-" + System.nanoTime()
+ + ";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1", "sa", "");
+ jdbc = new JdbcTemplate(dataSource);
+ jdbc.execute("""
+ create table merchant_verification_attempt (
+ id bigint auto_increment primary key,
+ verification_id varchar(64) not null,
+ credential_type varchar(32) not null,
+ dynamic_code_id bigint,
+ order_verification_code_id bigint,
+ free_stay_verification_code_id bigint,
+ merchant_id bigint not null,
+ store_id bigint not null,
+ operator_id bigint not null,
+ expires_at timestamp,
+ status tinyint not null default 0,
+ verify_no varchar(64),
+ confirmed_at timestamp
+ )
+ """);
+ jdbc.execute("""
+ create table benefit_order (
+ id bigint primary key,
+ talent_id bigint not null,
+ card_id bigint,
+ benefit_id bigint,
+ order_no varchar(64) not null,
+ order_amount bigint not null,
+ subsidy_amount bigint not null,
+ pay_amount bigint not null,
+ order_status tinyint not null,
+ verify_status tinyint not null,
+ merchant_id bigint not null,
+ store_id bigint,
+ deleted tinyint not null default 0
+ )
+ """);
+ jdbc.execute("""
+ create table order_dynamic_verification_code (
+ id bigint primary key,
+ order_id bigint not null,
+ expires_at timestamp not null,
+ status tinyint not null
+ )
+ """);
+ jdbc.execute("create table talent_card (id bigint primary key, card_level varchar(64), deleted tinyint not null default 0)");
+ jdbc.execute("create table benefit (id bigint primary key, benefit_name varchar(128))");
+ jdbc.execute("""
+ create table verify_record (
+ id bigint auto_increment primary key,
+ verify_no varchar(64) not null,
+ order_id bigint,
+ talent_id bigint,
+ card_id bigint,
+ benefit_id bigint,
+ merchant_id bigint,
+ store_id bigint,
+ staff_id bigint,
+ verify_type varchar(32),
+ verify_amount bigint,
+ subsidy_amount bigint,
+ remark varchar(255)
+ )
+ """);
+ service = new MerchantVerificationService(jdbc, new ManualConsumptionService(jdbc));
+ principal = new MerchantPrincipal(9L, 88L, 7L, "operator");
+ }
+
+ @Test
+ void confirmsGroupOrderAfterScannedCodeExpiresAndRefreshes() {
+ insertOrder("expired-group-order", 1);
+
+ Map<String, Object> confirmed = service.confirm(principal,
+ new VerificationConfirmRequest("expired-group-order"), "order-confirm-expired-group-order");
+
+ assertThat(confirmed.get("verified")).isEqualTo(true);
+ assertThat(jdbc.queryForObject("select status from order_dynamic_verification_code where id = 30", Integer.class)).isEqualTo(1);
+ assertThat(jdbc.queryForObject("select verify_status from benefit_order where id = 20", Integer.class)).isEqualTo(1);
+ assertThat(jdbc.queryForObject("select status from merchant_verification_attempt where verification_id = ?", Integer.class,
+ "expired-group-order")).isEqualTo(1);
+ assertThat(jdbc.queryForObject("select count(*) from verify_record where verify_type = 'ORDER_CODE'", Integer.class)).isEqualTo(1);
+ }
+
+ @Test
+ void stillRejectsUnpaidGroupOrder() {
+ insertOrder("unpaid-group-order", 0);
+
+ assertThatThrownBy(() -> service.confirm(principal, new VerificationConfirmRequest("unpaid-group-order"),
+ "order-confirm-unpaid-group-order"))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining("当前不可核销");
+ }
+
+ private void insertOrder(String verificationId, int orderStatus) {
+ jdbc.update("insert into talent_card(id, card_level, deleted) values (10, '卓越人才卡', 0)");
+ jdbc.update("""
+ insert into benefit_order(id, talent_id, card_id, benefit_id, order_no, order_amount, subsidy_amount,
+ pay_amount, order_status, verify_status, merchant_id, store_id, deleted)
+ values (20, 1015, 10, null, ?, 10000, 3000, 7000, ?, 0, 88, 7, 0)
+ """, "GROUP-" + verificationId, orderStatus);
+ jdbc.update("""
+ insert into order_dynamic_verification_code(id, order_id, expires_at, status)
+ values (30, 20, ?, 2)
+ """, Timestamp.valueOf(LocalDateTime.now().minusMinutes(1)));
+ jdbc.update("""
+ insert into merchant_verification_attempt(verification_id, credential_type, order_verification_code_id,
+ merchant_id, store_id, operator_id, expires_at, status)
+ values (?, 'ORDER_CODE', 30, 88, 7, 9, ?, 0)
+ """, verificationId, Timestamp.valueOf(LocalDateTime.now().minusMinutes(1)));
+ }
+}
--
Gitblit v1.7.1