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