MerchantMapSyncService.java 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. package com.ruoyi.app.map;
  2. import com.alibaba.fastjson2.JSONArray;
  3. import com.alibaba.fastjson2.JSONObject;
  4. import com.ruoyi.common.exception.ServiceException;
  5. import com.ruoyi.common.utils.MessageUtils;
  6. import com.ruoyi.system.domain.PosStore;
  7. import com.ruoyi.system.mapper.PosStoreMapper;
  8. import org.springframework.stereotype.Service;
  9. import java.math.BigDecimal;
  10. import java.util.ArrayList;
  11. import java.util.HashSet;
  12. import java.util.List;
  13. import java.util.Set;
  14. /** 只读所选业务门店、手动同步人工地点源表;搜索索引另行发布。 */
  15. @Service
  16. public class MerchantMapSyncService {
  17. private final PosStoreMapper stores;
  18. private final SelfHostedMapAdminClient map;
  19. public MerchantMapSyncService(PosStoreMapper stores, SelfHostedMapAdminClient map) {
  20. this.stores = stores;
  21. this.map = map;
  22. }
  23. public SyncResult sync(MerchantMapSyncRequest request) {
  24. if (request == null) throw invalidRequest();
  25. List<Long> ids = request.getStoreIds() == null ? List.of() : request.getStoreIds();
  26. List<Long> removals = request.getRemoveStoreIds() == null ? List.of() : request.getRemoveStoreIds();
  27. Set<Long> seen = new HashSet<>();
  28. if (ids.size() + removals.size() < 1 || ids.size() + removals.size() > 100) throw invalidRequest();
  29. for (List<Long> group : List.of(ids, removals)) {
  30. for (Long id : group) if (id == null || id <= 0 || !seen.add(id)) throw invalidRequest();
  31. }
  32. List<RowResult> rows = new ArrayList<>();
  33. List<JSONObject> places = new ArrayList<>();
  34. List<RowResult> submitted = new ArrayList<>();
  35. for (Long id : ids) {
  36. RowResult row = new RowResult(id, "UPSERT");
  37. rows.add(row);
  38. PosStore store = stores.selectPosStoreById(id);
  39. if (store == null) {
  40. row.fail("STORE_NOT_FOUND", "map.merchantSync.storeMissing");
  41. continue;
  42. }
  43. if (!validStore(store)) {
  44. row.fail("INVALID_STORE", "map.merchantSync.invalidStore");
  45. continue;
  46. }
  47. JSONObject place = new JSONObject();
  48. place.put("external_id", externalId(id));
  49. place.put("name", store.getPosName().trim());
  50. place.put("address", store.getAddress() == null ? "" : store.getAddress().trim());
  51. place.put("lat", store.getLatitude());
  52. place.put("lng", store.getLongitude());
  53. place.put("type", "business");
  54. places.add(place);
  55. submitted.add(row);
  56. }
  57. // 已尝试的写入遇到超时/响应异常也可能部分成功,仍提示索引发布和幂等重试。
  58. boolean rebuild = !places.isEmpty() || !removals.isEmpty();
  59. if (!places.isEmpty()) {
  60. try {
  61. Set<Integer> rejected = rejectedIndexes(map.importPlaces(places), places.size());
  62. for (int index = 0; index < submitted.size(); index++) {
  63. if (rejected.contains(index)) submitted.get(index).fail("INVALID_STORE", "map.merchantSync.invalidStore");
  64. else submitted.get(index).succeed();
  65. }
  66. } catch (RuntimeException failure) {
  67. for (RowResult row : submitted) row.fail("MAP_UNAVAILABLE", "map.merchantSync.unavailable");
  68. }
  69. }
  70. for (Long id : removals) {
  71. RowResult row = new RowResult(id, "REMOVE");
  72. rows.add(row);
  73. try {
  74. JSONObject result = map.deletePlace(externalId(id));
  75. Object removed = result == null ? null : result.get("removed");
  76. if (result == null || !"removed".equals(result.getString("status"))
  77. || !(removed instanceof Integer count) || count < 0 || count > 1
  78. || !Boolean.TRUE.equals(result.get("index_rebuild_required"))) throw malformed();
  79. row.succeed();
  80. } catch (RuntimeException failure) {
  81. row.fail("MAP_UNAVAILABLE", "map.merchantSync.unavailable");
  82. }
  83. }
  84. return new SyncResult(rows, rebuild);
  85. }
  86. private Set<Integer> rejectedIndexes(JSONObject response, int total) {
  87. if (response == null || !(response.get("imported") instanceof Integer imported)
  88. || !(response.get("rejected") instanceof Integer rejected)
  89. || imported < 0 || rejected < 0 || imported + rejected != total
  90. || !(response.get("rejections") instanceof JSONArray rejections) || rejections.size() != rejected)
  91. throw malformed();
  92. Set<Integer> indexes = new HashSet<>();
  93. for (Object value : rejections) {
  94. if (!(value instanceof JSONObject row) || !(row.get("index") instanceof Integer index)
  95. || index < 0 || index >= total || !indexes.add(index)) throw malformed();
  96. }
  97. if (imported > 0 && !Boolean.TRUE.equals(response.get("index_rebuild_required"))) throw malformed();
  98. return indexes;
  99. }
  100. private boolean validStore(PosStore store) {
  101. // state=1 是休息/打烊,不作为永久停用依据;移除只能来自显式 removeStoreIds。
  102. return store.getPosName() != null && !store.getPosName().isBlank() && store.getPosName().trim().length() <= 200
  103. && (store.getAddress() == null || store.getAddress().trim().length() <= 500)
  104. && (store.getDelFlag() == null || "0".equals(store.getDelFlag()))
  105. && (store.getOffShelf() == null || "0".equals(store.getOffShelf()))
  106. && store.getLatitude() != null && store.getLatitude().abs().compareTo(BigDecimal.valueOf(90)) <= 0
  107. && store.getLongitude() != null && store.getLongitude().abs().compareTo(BigDecimal.valueOf(180)) <= 0;
  108. }
  109. private String externalId(Long id) { return "foodie-store-" + id; }
  110. private IllegalStateException malformed() { return new IllegalStateException("invalid merchant sync response"); }
  111. private ServiceException invalidRequest() { return new ServiceException(MessageUtils.message("map.merchantSync.invalid")); }
  112. public static class SyncResult {
  113. private final List<RowResult> rows;
  114. private final boolean indexRebuildRequired;
  115. public SyncResult(List<RowResult> rows, boolean indexRebuildRequired) {
  116. this.rows = rows;
  117. this.indexRebuildRequired = indexRebuildRequired;
  118. }
  119. public List<RowResult> getRows() { return rows; }
  120. public boolean isIndexRebuildRequired() { return indexRebuildRequired; }
  121. }
  122. public static class RowResult {
  123. private final Long storeId;
  124. private final String action;
  125. private boolean success;
  126. private String code;
  127. private String message;
  128. public RowResult(Long storeId, String action) { this.storeId = storeId; this.action = action; }
  129. private void succeed() { success = true; code = "OK"; message = null; }
  130. private void fail(String code, String key) { success = false; this.code = code; message = MessageUtils.message(key); }
  131. public Long getStoreId() { return storeId; }
  132. public String getAction() { return action; }
  133. public boolean isSuccess() { return success; }
  134. public String getCode() { return code; }
  135. public String getMessage() { return message; }
  136. }
  137. }