package com.ruoyi.app.map; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.system.domain.PosStore; import com.ruoyi.system.mapper.PosStoreMapper; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** 只读所选业务门店、手动同步人工地点源表;搜索索引另行发布。 */ @Service public class MerchantMapSyncService { private final PosStoreMapper stores; private final SelfHostedMapAdminClient map; public MerchantMapSyncService(PosStoreMapper stores, SelfHostedMapAdminClient map) { this.stores = stores; this.map = map; } public SyncResult sync(MerchantMapSyncRequest request) { if (request == null) throw invalidRequest(); List ids = request.getStoreIds() == null ? List.of() : request.getStoreIds(); List removals = request.getRemoveStoreIds() == null ? List.of() : request.getRemoveStoreIds(); Set seen = new HashSet<>(); if (ids.size() + removals.size() < 1 || ids.size() + removals.size() > 100) throw invalidRequest(); for (List group : List.of(ids, removals)) { for (Long id : group) if (id == null || id <= 0 || !seen.add(id)) throw invalidRequest(); } List rows = new ArrayList<>(); List places = new ArrayList<>(); List submitted = new ArrayList<>(); for (Long id : ids) { RowResult row = new RowResult(id, "UPSERT"); rows.add(row); PosStore store = stores.selectPosStoreById(id); if (store == null) { row.fail("STORE_NOT_FOUND", "map.merchantSync.storeMissing"); continue; } if (!validStore(store)) { row.fail("INVALID_STORE", "map.merchantSync.invalidStore"); continue; } JSONObject place = new JSONObject(); place.put("external_id", externalId(id)); place.put("name", store.getPosName().trim()); place.put("address", store.getAddress() == null ? "" : store.getAddress().trim()); place.put("lat", store.getLatitude()); place.put("lng", store.getLongitude()); place.put("type", "business"); places.add(place); submitted.add(row); } // 已尝试的写入遇到超时/响应异常也可能部分成功,仍提示索引发布和幂等重试。 boolean rebuild = !places.isEmpty() || !removals.isEmpty(); if (!places.isEmpty()) { try { Set rejected = rejectedIndexes(map.importPlaces(places), places.size()); for (int index = 0; index < submitted.size(); index++) { if (rejected.contains(index)) submitted.get(index).fail("INVALID_STORE", "map.merchantSync.invalidStore"); else submitted.get(index).succeed(); } } catch (RuntimeException failure) { for (RowResult row : submitted) row.fail("MAP_UNAVAILABLE", "map.merchantSync.unavailable"); } } for (Long id : removals) { RowResult row = new RowResult(id, "REMOVE"); rows.add(row); try { JSONObject result = map.deletePlace(externalId(id)); Object removed = result == null ? null : result.get("removed"); if (result == null || !"removed".equals(result.getString("status")) || !(removed instanceof Integer count) || count < 0 || count > 1 || !Boolean.TRUE.equals(result.get("index_rebuild_required"))) throw malformed(); row.succeed(); } catch (RuntimeException failure) { row.fail("MAP_UNAVAILABLE", "map.merchantSync.unavailable"); } } return new SyncResult(rows, rebuild); } private Set rejectedIndexes(JSONObject response, int total) { if (response == null || !(response.get("imported") instanceof Integer imported) || !(response.get("rejected") instanceof Integer rejected) || imported < 0 || rejected < 0 || imported + rejected != total || !(response.get("rejections") instanceof JSONArray rejections) || rejections.size() != rejected) throw malformed(); Set indexes = new HashSet<>(); for (Object value : rejections) { if (!(value instanceof JSONObject row) || !(row.get("index") instanceof Integer index) || index < 0 || index >= total || !indexes.add(index)) throw malformed(); } if (imported > 0 && !Boolean.TRUE.equals(response.get("index_rebuild_required"))) throw malformed(); return indexes; } private boolean validStore(PosStore store) { // state=1 是休息/打烊,不作为永久停用依据;移除只能来自显式 removeStoreIds。 return store.getPosName() != null && !store.getPosName().isBlank() && store.getPosName().trim().length() <= 200 && (store.getAddress() == null || store.getAddress().trim().length() <= 500) && (store.getDelFlag() == null || "0".equals(store.getDelFlag())) && (store.getOffShelf() == null || "0".equals(store.getOffShelf())) && store.getLatitude() != null && store.getLatitude().abs().compareTo(BigDecimal.valueOf(90)) <= 0 && store.getLongitude() != null && store.getLongitude().abs().compareTo(BigDecimal.valueOf(180)) <= 0; } private String externalId(Long id) { return "foodie-store-" + id; } private IllegalStateException malformed() { return new IllegalStateException("invalid merchant sync response"); } private ServiceException invalidRequest() { return new ServiceException(MessageUtils.message("map.merchantSync.invalid")); } public static class SyncResult { private final List rows; private final boolean indexRebuildRequired; public SyncResult(List rows, boolean indexRebuildRequired) { this.rows = rows; this.indexRebuildRequired = indexRebuildRequired; } public List getRows() { return rows; } public boolean isIndexRebuildRequired() { return indexRebuildRequired; } } public static class RowResult { private final Long storeId; private final String action; private boolean success; private String code; private String message; public RowResult(Long storeId, String action) { this.storeId = storeId; this.action = action; } private void succeed() { success = true; code = "OK"; message = null; } private void fail(String code, String key) { success = false; this.code = code; message = MessageUtils.message(key); } public Long getStoreId() { return storeId; } public String getAction() { return action; } public boolean isSuccess() { return success; } public String getCode() { return code; } public String getMessage() { return message; } } }