shinnosuke
2026-08-28 7149b556e77d2514f0ed1f25adb16e83c472205c
fix: show selected parking lot in plate management
2个文件已添加
5个文件已修改
302 ■■■■■ 已修改文件
talent-card-server/src/main/java/com/talentcard/server/business/portal/ParkingMerchantPortalService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/entity/TransportParkingPlateRegistration.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/mapper/TransportParkingPlateRegistrationMapper.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/service/ParkingPlateMerchantService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/service/ParkingPlateSubmissionService.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/db/migration/V125__parking_lot_registration_snapshot.java 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/db/migration/V125ParkingLotRegistrationSnapshotTests.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/portal/ParkingMerchantPortalService.java
@@ -118,11 +118,13 @@
        if (!"free".equalsIgnoreCase(text(right.get("parkingType"), "paid"))) {
            throw new BusinessException(ErrorCode.CONFLICT, "付费停车权益无需登记车牌");
        }
        Map<String, Object> selectedParkingLot = null;
        if ("withParking".equals(benefit.get("plateFormMode"))) {
            if (request.parkingLotId() == null) throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择停车场");
            boolean matchedLot = parkingStores(merchant.get("usageInstructions")).stream()
                    .anyMatch(item -> String.valueOf(request.parkingLotId()).equals(String.valueOf(item.get("id"))));
            if (!matchedLot) throw new BusinessException(ErrorCode.BAD_REQUEST, "停车场不可用");
            selectedParkingLot = parkingStores(merchant.get("usageInstructions")).stream()
                    .filter(item -> String.valueOf(request.parkingLotId()).equals(String.valueOf(item.get("id"))))
                    .findFirst()
                    .orElseThrow(() -> new BusinessException(ErrorCode.BAD_REQUEST, "停车场不可用"));
        }
        Integer duplicateCount = jdbc.queryForObject("""
                select count(*) from transport_parking_plate_registration
@@ -133,9 +135,16 @@
        String registrationNo = BusinessIdGenerator.next("PR");
        jdbc.update("""
                insert into transport_parking_plate_registration(registration_no, merchant_id, benefit_id, parking_lot_id, talent_id, plate_number, status, idempotency_key)
                values (?, ?, ?, ?, ?, ?, 'SUBMITTED', ?)
                """, registrationNo, merchantId, request.benefitId(), request.parkingLotId(), request.talentId(), request.plateNumber(), idempotencyKey);
                insert into transport_parking_plate_registration(
                    registration_no, merchant_id, benefit_id, parking_lot_id,
                    parking_lot_name_snapshot, parking_lot_address_snapshot,
                    talent_id, plate_number, status, idempotency_key
                )
                values (?, ?, ?, ?, ?, ?, ?, ?, 'SUBMITTED', ?)
                """, registrationNo, merchantId, request.benefitId(), request.parkingLotId(),
                selectedParkingLot == null ? null : parkingLotName(selectedParkingLot),
                selectedParkingLot == null ? null : text(selectedParkingLot.get("address"), ""),
                request.talentId(), request.plateNumber(), idempotencyKey);
        return registrationView(one("""
                select id, registration_no, status
                from transport_parking_plate_registration
@@ -232,6 +241,11 @@
        return stores;
    }
    private String parkingLotName(Map<String, Object> parkingLot) {
        return text(parkingLot.get("parkingLotName"),
                text(parkingLot.get("storeName"), text(parkingLot.get("name"), "")));
    }
    private Map<String, Object> benefitView(Map<String, Object> benefit) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("benefitId", benefit.get("id"));
talent-card-server/src/main/java/com/talentcard/server/entity/TransportParkingPlateRegistration.java
@@ -16,6 +16,8 @@
    private Long merchantId;
    private Long benefitId;
    private Long parkingLotId;
    private String parkingLotNameSnapshot;
    private String parkingLotAddressSnapshot;
    private Long talentId;
    private Long cardId;
    private String talentName;
@@ -33,4 +35,4 @@
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private Integer deleted;
}
}
talent-card-server/src/main/java/com/talentcard/server/mapper/TransportParkingPlateRegistrationMapper.java
@@ -60,6 +60,7 @@
            from transport_parking_plate_registration r
            left join merchant m on m.id = r.merchant_id
            left join transport_parking_lot l on l.id = r.parking_lot_id
                and l.merchant_id = r.merchant_id and l.deleted = 0
            left join talent_user u on u.id = r.talent_id
            left join talent_card c on c.id = coalesce(r.card_id, (
                select c2.id
@@ -89,7 +90,7 @@
              and (#{plateNumber} is null or #{plateNumber} = '' or r.plate_number like concat('%', #{plateNumber}, '%'))
              and (#{serviceKeyword} is null or #{serviceKeyword} = ''
                   or m.merchant_name like concat('%', #{serviceKeyword}, '%')
                   or l.parking_lot_name like concat('%', #{serviceKeyword}, '%'))
                   or coalesce(r.parking_lot_name_snapshot, l.parking_lot_name) like concat('%', #{serviceKeyword}, '%'))
            """)
    Long countAdminPage(@Param("merchantId") Long merchantId,
                        @Param("parkingLotId") Long parkingLotId,
@@ -111,7 +112,7 @@
                   m.merchant_name as merchantName,
                   r.benefit_id as benefitId,
                   r.parking_lot_id as parkingLotId,
                   l.parking_lot_name as parkingLotName,
                   coalesce(r.parking_lot_name_snapshot, l.parking_lot_name) as parkingLotName,
                   r.talent_id as talentId,
                   r.card_id as cardId,
                   coalesce(r.talent_name, u.name) as talentName,
@@ -131,6 +132,7 @@
            from transport_parking_plate_registration r
            left join merchant m on m.id = r.merchant_id
            left join transport_parking_lot l on l.id = r.parking_lot_id
                and l.merchant_id = r.merchant_id and l.deleted = 0
            left join talent_user u on u.id = r.talent_id
            left join talent_card c on c.id = coalesce(r.card_id, (
                select c2.id
@@ -162,7 +164,7 @@
              and (#{plateNumber} is null or #{plateNumber} = '' or r.plate_number like concat('%', #{plateNumber}, '%'))
              and (#{serviceKeyword} is null or #{serviceKeyword} = ''
                   or m.merchant_name like concat('%', #{serviceKeyword}, '%')
                   or l.parking_lot_name like concat('%', #{serviceKeyword}, '%'))
                   or coalesce(r.parking_lot_name_snapshot, l.parking_lot_name) like concat('%', #{serviceKeyword}, '%'))
            order by r.submitted_at desc, r.id desc
            limit #{size} offset #{offset}
            """)
@@ -190,8 +192,8 @@
                   m.address as merchantAddress,
                   r.benefit_id as benefitId,
                   r.parking_lot_id as parkingLotId,
                   l.parking_lot_name as parkingLotName,
                   l.address as parkingLotAddress,
                   coalesce(r.parking_lot_name_snapshot, l.parking_lot_name) as parkingLotName,
                   coalesce(r.parking_lot_address_snapshot, l.address) as parkingLotAddress,
                   r.talent_id as talentId,
                   r.card_id as cardId,
                   coalesce(r.talent_name, u.name) as talentName,
@@ -211,6 +213,7 @@
            from transport_parking_plate_registration r
            left join merchant m on m.id = r.merchant_id
            left join transport_parking_lot l on l.id = r.parking_lot_id
                and l.merchant_id = r.merchant_id and l.deleted = 0
            left join talent_user u on u.id = r.talent_id
            left join talent_card c on c.id = coalesce(r.card_id, (
                select c2.id
@@ -248,6 +251,7 @@
            select count(*)
            from transport_parking_plate_registration r
            left join transport_parking_lot l on l.id = r.parking_lot_id
                and l.merchant_id = r.merchant_id and l.deleted = 0
            left join talent_user u on u.id = r.talent_id
            left join talent_card c on c.id = coalesce(r.card_id, (
                select c2.id
@@ -272,7 +276,7 @@
                   or coalesce(r.talent_name, u.name) like concat('%', #{keyword}, '%')
                   or coalesce(r.talent_mobile, u.mobile) like concat('%', #{keyword}, '%')
                   or coalesce(r.card_no, c.card_no) like concat('%', #{keyword}, '%')
                   or l.parking_lot_name like concat('%', #{keyword}, '%'))
                   or coalesce(r.parking_lot_name_snapshot, l.parking_lot_name) like concat('%', #{keyword}, '%'))
            """)
    Long countMerchantPage(@Param("merchantId") Long merchantId,
                           @Param("parkingLotId") Long parkingLotId,
@@ -290,7 +294,7 @@
                   m.merchant_name as merchantName,
                   r.benefit_id as benefitId,
                   r.parking_lot_id as parkingLotId,
                   l.parking_lot_name as parkingLotName,
                   coalesce(r.parking_lot_name_snapshot, l.parking_lot_name) as parkingLotName,
                   r.talent_id as talentId,
                   r.card_id as cardId,
                   coalesce(r.talent_name, u.name) as talentName,
@@ -310,6 +314,7 @@
            from transport_parking_plate_registration r
            left join merchant m on m.id = r.merchant_id
            left join transport_parking_lot l on l.id = r.parking_lot_id
                and l.merchant_id = r.merchant_id and l.deleted = 0
            left join talent_user u on u.id = r.talent_id
            left join talent_card c on c.id = coalesce(r.card_id, (
                select c2.id
@@ -335,7 +340,7 @@
                   or coalesce(r.talent_name, u.name) like concat('%', #{keyword}, '%')
                   or coalesce(r.talent_mobile, u.mobile) like concat('%', #{keyword}, '%')
                   or coalesce(r.card_no, c.card_no) like concat('%', #{keyword}, '%')
                   or l.parking_lot_name like concat('%', #{keyword}, '%'))
                   or coalesce(r.parking_lot_name_snapshot, l.parking_lot_name) like concat('%', #{keyword}, '%'))
            order by r.submitted_at desc, r.id desc
            limit #{size} offset #{offset}
            """)
@@ -359,8 +364,8 @@
                   m.address as merchantAddress,
                   r.benefit_id as benefitId,
                   r.parking_lot_id as parkingLotId,
                   l.parking_lot_name as parkingLotName,
                   l.address as parkingLotAddress,
                   coalesce(r.parking_lot_name_snapshot, l.parking_lot_name) as parkingLotName,
                   coalesce(r.parking_lot_address_snapshot, l.address) as parkingLotAddress,
                   r.talent_id as talentId,
                   r.card_id as cardId,
                   coalesce(r.talent_name, u.name) as talentName,
@@ -380,6 +385,7 @@
            from transport_parking_plate_registration r
            left join merchant m on m.id = r.merchant_id
            left join transport_parking_lot l on l.id = r.parking_lot_id
                and l.merchant_id = r.merchant_id and l.deleted = 0
            left join talent_user u on u.id = r.talent_id
            left join talent_card c on c.id = coalesce(r.card_id, (
                select c2.id
talent-card-server/src/main/java/com/talentcard/server/service/ParkingPlateMerchantService.java
@@ -192,6 +192,8 @@
        registration.setMerchantId(principal.merchantId());
        registration.setBenefitId(benefitId);
        registration.setParkingLotId(lot.getId());
        registration.setParkingLotNameSnapshot(lot.getParkingLotName());
        registration.setParkingLotAddressSnapshot(lot.getAddress());
        registration.setTalentId(talentId);
        registration.setCardId(cardId);
        registration.setTalentName(text(card, "talentName", "talent_name"));
@@ -460,4 +462,4 @@
    private String clean(String value) {
        return value == null ? "" : value.trim();
    }
}
}
talent-card-server/src/main/java/com/talentcard/server/service/ParkingPlateSubmissionService.java
@@ -103,6 +103,8 @@
        registration.setMerchantId(merchantId);
        registration.setBenefitId(benefitId);
        registration.setParkingLotId(lot.getId());
        registration.setParkingLotNameSnapshot(lot.getParkingLotName());
        registration.setParkingLotAddressSnapshot(lot.getAddress());
        registration.setTalentId(talentId);
        registration.setCardId(cardId);
        registration.setCardNo(text(card, "cardNo", "card_no"));
@@ -285,4 +287,4 @@
    private String first(String first, String second) {
        return clean(first).isBlank() ? clean(second) : clean(first);
    }
}
}
talent-card-server/src/main/java/db/migration/V125__parking_lot_registration_snapshot.java
New file
@@ -0,0 +1,144 @@
package db.migration;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
public class V125__parking_lot_registration_snapshot extends BaseJavaMigration {
    @Override
    public boolean canExecuteInTransaction() {
        return false;
    }
    @Override
    public void migrate(Context context) throws Exception {
        Connection connection = context.getConnection();
        addColumnIfMissing(connection, "parking_lot_name_snapshot", "varchar(128) default null");
        addColumnIfMissing(connection, "parking_lot_address_snapshot", "varchar(255) default null");
        try (PreparedStatement query = connection.prepareStatement("""
                select r.id, r.card_id, r.parking_lot_id, l.parking_lot_name, l.address, m.usage_instructions
                from transport_parking_plate_registration r
                join merchant m on m.id = r.merchant_id and m.deleted = 0
                left join transport_parking_lot l on l.id = r.parking_lot_id
                    and l.merchant_id = r.merchant_id and l.deleted = 0
                where r.parking_lot_id is not null
                  and (r.parking_lot_name_snapshot is null or r.parking_lot_name_snapshot = '')
                """);
             PreparedStatement update = connection.prepareStatement("""
                     update transport_parking_plate_registration
                     set parking_lot_name_snapshot = ?, parking_lot_address_snapshot = ?
                     where id = ?
                     """)) {
            try (ResultSet rows = query.executeQuery()) {
                while (rows.next()) {
                    Long parkingLotId = rows.getLong("parking_lot_id");
                    String name = rows.getString("parking_lot_name");
                    String address = rows.getString("address");
                    Map<String, Object> configuredLot = configuredLot(rows.getString("usage_instructions"), parkingLotId);
                    if (rows.getObject("card_id") == null && !configuredLot.isEmpty()) {
                        name = parkingLotName(configuredLot);
                        address = text(configuredLot.get("address"));
                    } else if (name == null || name.isBlank()) {
                        name = parkingLotName(configuredLot);
                        address = text(configuredLot.get("address"));
                    }
                    if (name == null || name.isBlank()) {
                        continue;
                    }
                    update.setString(1, name);
                    update.setString(2, address == null || address.isBlank() ? null : address);
                    update.setLong(3, rows.getLong("id"));
                    update.addBatch();
                }
            }
            update.executeBatch();
        }
    }
    private void addColumnIfMissing(Connection connection, String columnName, String definition) throws Exception {
        if (columnExists(connection, columnName)) {
            return;
        }
        try (Statement statement = connection.createStatement()) {
            statement.execute("alter table transport_parking_plate_registration add column "
                    + columnName + " " + definition);
        }
    }
    private boolean columnExists(Connection connection, String columnName) throws Exception {
        DatabaseMetaData metadata = connection.getMetaData();
        String catalog = connection.getCatalog();
        for (String tableName : List.of("transport_parking_plate_registration", "TRANSPORT_PARKING_PLATE_REGISTRATION")) {
            for (String candidate : List.of(columnName, columnName.toUpperCase())) {
                try (ResultSet columns = metadata.getColumns(catalog, null, tableName, candidate)) {
                    if (columns.next()) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    private Map<String, Object> configuredLot(String usageInstructions, Long parkingLotId) {
        if (usageInstructions == null || usageInstructions.isBlank() || parkingLotId == null) {
            return Map.of();
        }
        try {
            List<Map<String, Object>> lots = JSON.parseObject(
                    usageInstructions,
                    new TypeReference<List<Map<String, Object>>>() {}
            );
            if (lots == null) {
                return Map.of();
            }
            for (int index = 0; index < lots.size(); index++) {
                Map<String, Object> lot = lots.get(index);
                Long configuredId = longValue(lot.get("id"));
                if (configuredId == null) {
                    configuredId = (long) index + 1;
                }
                if (parkingLotId.equals(configuredId)) {
                    return new LinkedHashMap<>(lot);
                }
            }
        } catch (Exception ignored) {
            // Some older merchants store plain usage text instead of a parking-lot JSON array.
        }
        return Map.of();
    }
    private String parkingLotName(Map<String, Object> lot) {
        String value = text(lot.get("parkingLotName"));
        if (!value.isBlank()) return value;
        value = text(lot.get("storeName"));
        return value.isBlank() ? text(lot.get("name")) : value;
    }
    private Long longValue(Object value) {
        if (value instanceof Number number) {
            return number.longValue();
        }
        try {
            String text = text(value);
            return text.isBlank() ? null : Long.valueOf(text);
        } catch (NumberFormatException ignored) {
            return null;
        }
    }
    private String text(Object value) {
        return value == null ? "" : String.valueOf(value).trim();
    }
}
talent-card-server/src/test/java/db/migration/V125ParkingLotRegistrationSnapshotTests.java
New file
@@ -0,0 +1,94 @@
package db.migration;
import static org.assertj.core.api.Assertions.assertThat;
import java.sql.Connection;
import javax.sql.DataSource;
import org.flywaydb.core.api.configuration.Configuration;
import org.flywaydb.core.api.migration.Context;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
class V125ParkingLotRegistrationSnapshotTests {
    @Test
    void backfillsLegacyMerchantJsonWithoutUsingAnotherMerchantsGlobalLot() throws Exception {
        DataSource dataSource = new DriverManagerDataSource(
                "jdbc:h2:mem:parking-lot-snapshot-" + System.nanoTime()
                        + ";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1", "sa", "");
        JdbcTemplate jdbc = new JdbcTemplate(dataSource);
        createSchema(jdbc);
        insertFixture(jdbc);
        try (Connection connection = dataSource.getConnection()) {
            V125__parking_lot_registration_snapshot migration = new V125__parking_lot_registration_snapshot();
            Context context = new Context() {
                @Override
                public Configuration getConfiguration() {
                    return null;
                }
                @Override
                public Connection getConnection() {
                    return connection;
                }
            };
            migration.migrate(context);
            migration.migrate(context);
        }
        assertThat(snapshot(jdbc, 100L)).containsExactly("P1停车场", "P1地址");
        assertThat(snapshot(jdbc, 101L)).containsExactly("P2停车场", "P2地址");
        assertThat(snapshot(jdbc, 102L)).containsExactly("数据库停车场", "数据库地址");
    }
    private void createSchema(JdbcTemplate jdbc) {
        jdbc.execute("""
                create table merchant (
                    id bigint primary key,
                    usage_instructions text,
                    deleted tinyint not null default 0
                )
                """);
        jdbc.execute("""
                create table transport_parking_lot (
                    id bigint primary key,
                    merchant_id bigint not null,
                    parking_lot_name varchar(128),
                    address varchar(255),
                    deleted tinyint not null default 0
                )
                """);
        jdbc.execute("""
                create table transport_parking_plate_registration (
                    id bigint primary key,
                    merchant_id bigint not null,
                    parking_lot_id bigint,
                    card_id bigint
                )
                """);
    }
    private void insertFixture(JdbcTemplate jdbc) {
        jdbc.update("""
                insert into merchant(id, usage_instructions, deleted)
                values (99, '[{"id":1,"name":"P1停车场","address":"P1地址"},{"name":"P2停车场","address":"P2地址"}]', 0)
                """);
        jdbc.update("insert into merchant(id, usage_instructions, deleted) values (33, null, 0)");
        jdbc.update("insert into transport_parking_lot values (1, 33, '其他商家停车场', '错误地址', 0)");
        jdbc.update("insert into transport_parking_lot values (5, 99, '数据库停车场', '数据库地址', 0)");
        jdbc.update("insert into transport_parking_plate_registration values (100, 99, 1, null)");
        jdbc.update("insert into transport_parking_plate_registration values (101, 99, 2, null)");
        jdbc.update("insert into transport_parking_plate_registration values (102, 99, 5, 88)");
    }
    private String[] snapshot(JdbcTemplate jdbc, Long id) {
        return jdbc.queryForObject("""
                select parking_lot_name_snapshot, parking_lot_address_snapshot
                from transport_parking_plate_registration
                where id = ?
                """, (row, rowNumber) -> new String[]{row.getString(1), row.getString(2)}, id);
    }
}