fix: show selected parking lot in plate management
| | |
| | | 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 |
| | |
| | | |
| | | 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 |
| | |
| | | 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")); |
| | |
| | | private Long merchantId; |
| | | private Long benefitId; |
| | | private Long parkingLotId; |
| | | private String parkingLotNameSnapshot; |
| | | private String parkingLotAddressSnapshot; |
| | | private Long talentId; |
| | | private Long cardId; |
| | | private String talentName; |
| | |
| | | private LocalDateTime createdAt; |
| | | private LocalDateTime updatedAt; |
| | | private Integer deleted; |
| | | } |
| | | } |
| | |
| | | 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 |
| | |
| | | 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, |
| | |
| | | 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, |
| | |
| | | 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 |
| | |
| | | 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} |
| | | """) |
| | |
| | | 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, |
| | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | 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, |
| | |
| | | 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, |
| | |
| | | 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 |
| | |
| | | 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} |
| | | """) |
| | |
| | | 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, |
| | |
| | | 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 |
| | |
| | | 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")); |
| | |
| | | private String clean(String value) { |
| | | return value == null ? "" : value.trim(); |
| | | } |
| | | } |
| | | } |
| | |
| | | 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")); |
| | |
| | | private String first(String first, String second) { |
| | | return clean(first).isBlank() ? clean(second) : clean(first); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |