diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/EsLogController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/EsLogController.java new file mode 100644 index 00000000..e3f7e244 --- /dev/null +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/EsLogController.java @@ -0,0 +1,53 @@ +package com.ruoyi.web.controller.monitor; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.constant.HttpStatus; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.PageDomain; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.page.TableSupport; +import com.ruoyi.price.search.BillSearchPage; +import com.ruoyi.price.search.IBillSearchStore; + +/** + * Generic Elasticsearch business-log viewer. + */ +@RestController +@RequestMapping("/monitor/eslog") +public class EsLogController extends BaseController +{ + @Autowired + private IBillSearchStore billSearchStore; + + @PreAuthorize("@ss.hasPermi('monitor:eslog:list')") + @GetMapping("/list") + public TableDataInfo list( + @RequestParam(value = "logType", required = false) String logType, + @RequestParam(value = "bizId", required = false) String bizId) + { + PageDomain pageDomain = TableSupport.buildPageRequest(); + int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue(); + int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue(); + BillSearchPage page = billSearchStore.search(logType, bizId, pageNum, pageSize); + TableDataInfo rsp = new TableDataInfo(); + rsp.setCode(HttpStatus.SUCCESS); + rsp.setMsg("\u67e5\u8be2\u6210\u529f"); + rsp.setRows(page.getRows()); + rsp.setTotal(page.getTotal()); + return rsp; + } + + @PreAuthorize("@ss.hasPermi('monitor:eslog:list')") + @GetMapping("/{docId}") + public AjaxResult getInfo(@PathVariable("docId") String docId) + { + return success(billSearchStore.getByDocId(docId)); + } +} diff --git a/bms-back/ruoyi-admin/src/main/resources/application.yml b/bms-back/ruoyi-admin/src/main/resources/application.yml index 1edbe948..ee0b02e5 100644 --- a/bms-back/ruoyi-admin/src/main/resources/application.yml +++ b/bms-back/ruoyi-admin/src/main/resources/application.yml @@ -151,3 +151,9 @@ bms: tms: # Agreed API identification code for TMS callers api-code: BMS.TMS.ZYaOvlQzpcfau8TUaCFfO3Hp8ieHIqGw + elasticsearch: + enabled: true + uris: http://127.0.0.1:9200 + username: + password: + index: bms_biz_log diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java index a696dc59..27ada5f6 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java @@ -74,4 +74,8 @@ public class TmsPriceQuoteResult implements Serializable private String orderRemark; /** true=hit stored quote */ private Boolean fromCache; + /** original TMS sync request JSON from Elasticsearch */ + private String requestJson; + /** original billing response JSON from Elasticsearch */ + private String responseJson; } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/BillSearchConfiguration.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/BillSearchConfiguration.java new file mode 100644 index 00000000..1d397ccc --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/BillSearchConfiguration.java @@ -0,0 +1,29 @@ +package com.ruoyi.price.search; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Bill sync search-store beans. + */ +@Configuration +@EnableConfigurationProperties(ElasticsearchProperties.class) +public class BillSearchConfiguration +{ + @Bean + @ConditionalOnProperty(prefix = "bms.elasticsearch", name = "enabled", havingValue = "true", matchIfMissing = true) + public IBillSearchStore esBillSearchStore(ElasticsearchProperties properties) + { + return new EsBillSearchStore(properties); + } + + @Bean + @ConditionalOnMissingBean(IBillSearchStore.class) + public IBillSearchStore noopBillSearchStore() + { + return new NoopBillSearchStore(); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/BillSearchPage.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/BillSearchPage.java new file mode 100644 index 00000000..b3c210fa --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/BillSearchPage.java @@ -0,0 +1,31 @@ +package com.ruoyi.price.search; + +import java.util.ArrayList; +import java.util.List; + +/** Paginated ES search result. */ +public class BillSearchPage +{ + private long total; + private List rows = new ArrayList(); + + public long getTotal() + { + return total; + } + + public void setTotal(long total) + { + this.total = total; + } + + public List getRows() + { + return rows; + } + + public void setRows(List rows) + { + this.rows = rows == null ? new ArrayList() : rows; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/BillSyncSnapshot.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/BillSyncSnapshot.java new file mode 100644 index 00000000..12360889 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/BillSyncSnapshot.java @@ -0,0 +1,72 @@ +package com.ruoyi.price.search; + +/** Generic Elasticsearch business log. Distinguished by logType + bizId. */ +public class BillSyncSnapshot +{ + private String docId; + private String logType; + private String bizId; + private String requestJson; + private String responseJson; + private String updatedAt; + + public String getDocId() + { + return docId; + } + + public void setDocId(String docId) + { + this.docId = docId; + } + + public String getLogType() + { + return logType; + } + + public void setLogType(String logType) + { + this.logType = logType; + } + + public String getBizId() + { + return bizId; + } + + public void setBizId(String bizId) + { + this.bizId = bizId; + } + + public String getRequestJson() + { + return requestJson; + } + + public void setRequestJson(String requestJson) + { + this.requestJson = requestJson; + } + + public String getResponseJson() + { + return responseJson; + } + + public void setResponseJson(String responseJson) + { + this.responseJson = responseJson; + } + + public String getUpdatedAt() + { + return updatedAt; + } + + public void setUpdatedAt(String updatedAt) + { + this.updatedAt = updatedAt; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/ElasticsearchProperties.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/ElasticsearchProperties.java new file mode 100644 index 00000000..a0eaddd4 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/ElasticsearchProperties.java @@ -0,0 +1,73 @@ +package com.ruoyi.price.search; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Elasticsearch connection for bill sync search store. + */ +@ConfigurationProperties(prefix = "bms.elasticsearch") +public class ElasticsearchProperties +{ + /** false = skip ES write/read */ + private boolean enabled = true; + + /** e.g. http://127.0.0.1:9200 */ + private String uris = "http://127.0.0.1:9200"; + + private String username; + + private String password; + + /** index name */ + private String index = "bms_biz_log"; + + public boolean isEnabled() + { + return enabled; + } + + public void setEnabled(boolean enabled) + { + this.enabled = enabled; + } + + public String getUris() + { + return uris; + } + + public void setUris(String uris) + { + this.uris = uris; + } + + public String getUsername() + { + return username; + } + + public void setUsername(String username) + { + this.username = username; + } + + public String getPassword() + { + return password; + } + + public void setPassword(String password) + { + this.password = password; + } + + public String getIndex() + { + return index; + } + + public void setIndex(String index) + { + this.index = index; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsBillSearchStore.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsBillSearchStore.java new file mode 100644 index 00000000..a6d41082 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsBillSearchStore.java @@ -0,0 +1,330 @@ +package com.ruoyi.price.search; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Date; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.utils.StringUtils; + +/** + * Persist / load generic business logs via Elasticsearch REST API. + */ +public class EsBillSearchStore implements IBillSearchStore +{ + private static final Logger log = LoggerFactory.getLogger(EsBillSearchStore.class); + + private final ElasticsearchProperties properties; + private final RestTemplate restTemplate; + + public EsBillSearchStore(ElasticsearchProperties properties) + { + this.properties = properties; + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(3000); + factory.setReadTimeout(5000); + RestTemplate template = new RestTemplate(factory); + if (StringUtils.isNotEmpty(properties.getUsername())) + { + String user = properties.getUsername(); + String pass = properties.getPassword() == null ? "" : properties.getPassword(); + template.getInterceptors().add((request, body, execution) -> { + request.getHeaders().setBasicAuth(user, pass); + return execution.execute(request, body); + }); + } + this.restTemplate = template; + } + + @Override + public void save(String logType, String bizId, String requestJson, String responseJson) + { + if (StringUtils.isEmpty(logType) || StringUtils.isEmpty(bizId)) + { + return; + } + String docId = EsLogType.docId(logType, bizId); + try + { + JSONObject doc = new JSONObject(); + doc.put("logType", logType.trim()); + doc.put("bizId", bizId.trim()); + doc.put("request", parseOrRaw(requestJson)); + doc.put("response", parseOrRaw(responseJson)); + doc.put("updatedAt", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity(doc.toJSONString(), headers); + restTemplate.exchange(docUrl(docId), HttpMethod.PUT, entity, String.class); + } + catch (Exception e) + { + log.warn("elasticsearch save {} / {} failed: {}", logType, bizId, e.getMessage()); + } + } + + @Override + public BillSyncSnapshot get(String logType, String bizId) + { + if (StringUtils.isEmpty(logType) || StringUtils.isEmpty(bizId)) + { + return null; + } + return getByDocId(EsLogType.docId(logType, bizId)); + } + + @Override + public BillSyncSnapshot getByDocId(String docId) + { + if (StringUtils.isEmpty(docId)) + { + return null; + } + try + { + ResponseEntity resp = restTemplate.exchange(docUrl(docId), HttpMethod.GET, HttpEntity.EMPTY, String.class); + String body = resp.getBody(); + if (StringUtils.isEmpty(body)) + { + return null; + } + JSONObject root = JSON.parseObject(body); + if (root == null || !Boolean.TRUE.equals(root.getBoolean("found"))) + { + return null; + } + return toSnapshot(root.getJSONObject("_source"), root.getString("_id")); + } + catch (HttpClientErrorException.NotFound e) + { + return null; + } + catch (Exception e) + { + log.warn("elasticsearch get {} failed: {}", docId, e.getMessage()); + return null; + } + } + + @Override + public BillSearchPage search(String logType, String bizId, int pageNum, int pageSize) + { + BillSearchPage page = new BillSearchPage(); + if (pageNum < 1) + { + pageNum = 1; + } + if (pageSize < 1) + { + pageSize = 10; + } + try + { + JSONObject body = new JSONObject(); + body.put("from", Integer.valueOf((pageNum - 1) * pageSize)); + body.put("size", Integer.valueOf(pageSize)); + JSONArray sort = new JSONArray(); + JSONObject sortItem = new JSONObject(); + JSONObject sortOpt = new JSONObject(); + sortOpt.put("order", "desc"); + sortOpt.put("unmapped_type", "keyword"); + sortItem.put("updatedAt.keyword", sortOpt); + sort.add(sortItem); + body.put("sort", sort); + body.put("query", buildQuery(logType, bizId)); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity entity = new HttpEntity(body.toJSONString(), headers); + ResponseEntity resp = restTemplate.exchange(searchUrl(), HttpMethod.POST, entity, String.class); + JSONObject root = JSON.parseObject(resp.getBody()); + if (root == null) + { + return page; + } + JSONObject hits = root.getJSONObject("hits"); + if (hits == null) + { + return page; + } + page.setTotal(readTotal(hits.get("total"))); + JSONArray hitList = hits.getJSONArray("hits"); + if (hitList == null) + { + return page; + } + for (int i = 0; i < hitList.size(); i++) + { + JSONObject hit = hitList.getJSONObject(i); + if (hit == null) + { + continue; + } + BillSyncSnapshot snap = toSnapshot(hit.getJSONObject("_source"), hit.getString("_id")); + if (snap != null) + { + page.getRows().add(snap); + } + } + } + catch (HttpClientErrorException.NotFound e) + { + return page; + } + catch (Exception e) + { + log.warn("elasticsearch search failed: {}", e.getMessage()); + } + return page; + } + + private BillSyncSnapshot toSnapshot(JSONObject source, String id) + { + if (source == null) + { + return null; + } + BillSyncSnapshot snap = new BillSyncSnapshot(); + snap.setDocId(id); + snap.setLogType(source.getString("logType")); + snap.setBizId(source.getString("bizId")); + if (StringUtils.isEmpty(snap.getBizId())) + { + snap.setBizId(source.getString("orderNo")); + } + if (StringUtils.isEmpty(snap.getLogType()) && StringUtils.isNotEmpty(source.getString("billId"))) + { + snap.setLogType(EsLogType.BILL); + } + snap.setRequestJson(toJsonString(source.get("request"))); + snap.setResponseJson(toJsonString(source.get("response"))); + snap.setUpdatedAt(source.getString("updatedAt")); + return snap; + } + + private JSONObject buildQuery(String logType, String bizId) + { + JSONArray filters = new JSONArray(); + addTerm(filters, "logType", logType); + addWildcard(filters, "bizId", bizId); + if (filters.isEmpty()) + { + JSONObject matchAll = new JSONObject(); + matchAll.put("match_all", new JSONObject()); + return matchAll; + } + JSONObject bool = new JSONObject(); + bool.put("must", filters); + JSONObject query = new JSONObject(); + query.put("bool", bool); + return query; + } + + private void addTerm(JSONArray filters, String field, String value) + { + if (StringUtils.isEmpty(value)) + { + return; + } + JSONObject term = new JSONObject(); + term.put(field + ".keyword", value.trim()); + JSONObject wrap = new JSONObject(); + wrap.put("term", term); + filters.add(wrap); + } + + private void addWildcard(JSONArray filters, String field, String value) + { + if (StringUtils.isEmpty(value)) + { + return; + } + JSONObject wildcard = new JSONObject(); + JSONObject opts = new JSONObject(); + opts.put("value", "*" + escapeWildcard(value.trim()) + "*"); + opts.put("case_insensitive", Boolean.TRUE); + wildcard.put(field + ".keyword", opts); + JSONObject wrap = new JSONObject(); + wrap.put("wildcard", wildcard); + filters.add(wrap); + } + + private String escapeWildcard(String value) + { + return value.replace("\\", "\\\\").replace("*", "\\*").replace("?", "\\?"); + } + + private long readTotal(Object total) + { + if (total instanceof JSONObject) + { + return ((JSONObject) total).getLongValue("value"); + } + if (total instanceof Number) + { + return ((Number) total).longValue(); + } + return 0L; + } + + private String searchUrl() + { + return baseUrl() + "/" + properties.getIndex() + "/_search"; + } + + private String docUrl(String docId) + { + String encodedId = URLEncoder.encode(docId, StandardCharsets.UTF_8); + return baseUrl() + "/" + properties.getIndex() + "/_doc/" + encodedId; + } + + private String baseUrl() + { + String base = properties.getUris(); + if (base.endsWith("/")) + { + base = base.substring(0, base.length() - 1); + } + return base; + } + + private Object parseOrRaw(String json) + { + if (StringUtils.isEmpty(json)) + { + return null; + } + try + { + return JSON.parse(json); + } + catch (Exception e) + { + return json; + } + } + + private String toJsonString(Object value) + { + if (value == null) + { + return null; + } + if (value instanceof String) + { + return (String) value; + } + return JSON.toJSONString(value); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsLogType.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsLogType.java new file mode 100644 index 00000000..07374890 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/EsLogType.java @@ -0,0 +1,19 @@ +package com.ruoyi.price.search; + +/** Common ES business-log types. */ +public final class EsLogType +{ + public static final String BILL = "BILL"; + public static final String QUOTE = "QUOTE"; + + private EsLogType() + { + } + + public static String docId(String logType, String bizId) + { + String type = logType == null ? "" : logType.trim(); + String id = bizId == null ? "" : bizId.trim(); + return type + "_" + id; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/IBillSearchStore.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/IBillSearchStore.java new file mode 100644 index 00000000..eee2b894 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/IBillSearchStore.java @@ -0,0 +1,15 @@ +package com.ruoyi.price.search; + +/** + * Generic Elasticsearch store for business request/response logs. + */ +public interface IBillSearchStore +{ + void save(String logType, String bizId, String requestJson, String responseJson); + + BillSyncSnapshot get(String logType, String bizId); + + BillSyncSnapshot getByDocId(String docId); + + BillSearchPage search(String logType, String bizId, int pageNum, int pageSize); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/NoopBillSearchStore.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/NoopBillSearchStore.java new file mode 100644 index 00000000..e981d250 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/search/NoopBillSearchStore.java @@ -0,0 +1,30 @@ +package com.ruoyi.price.search; + +/** + * No-op when Elasticsearch is disabled. + */ +public class NoopBillSearchStore implements IBillSearchStore +{ + @Override + public void save(String logType, String bizId, String requestJson, String responseJson) + { + } + + @Override + public BillSyncSnapshot get(String logType, String bizId) + { + return null; + } + + @Override + public BillSyncSnapshot getByDocId(String docId) + { + return null; + } + + @Override + public BillSearchPage search(String logType, String bizId, int pageNum, int pageSize) + { + return new BillSearchPage(); + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcBillServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcBillServiceImpl.java index 55211068..0d479275 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcBillServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcBillServiceImpl.java @@ -10,6 +10,8 @@ import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.common.utils.StringUtils; @@ -21,6 +23,9 @@ import com.ruoyi.price.domain.vo.TmsPriceQuoteFeeLine; import com.ruoyi.price.domain.vo.TmsPriceQuoteResult; import com.ruoyi.price.mapper.PrcBillFeeMapper; import com.ruoyi.price.mapper.PrcBillMapper; +import com.ruoyi.price.search.BillSyncSnapshot; +import com.ruoyi.price.search.EsLogType; +import com.ruoyi.price.search.IBillSearchStore; import com.ruoyi.price.service.IPrcBillService; import com.ruoyi.price.service.ITmsPriceQuoteService; @@ -36,6 +41,9 @@ public class PrcBillServiceImpl implements IPrcBillService @Autowired private PrcBillFeeMapper billFeeMapper; + @Autowired + private IBillSearchStore billSearchStore; + @Override @Transactional(rollbackFor = Exception.class) public TmsPriceQuoteResult bill(TmsPriceBillRequest request) @@ -152,6 +160,11 @@ public class PrcBillServiceImpl implements IPrcBillService bill.setPayableTotal(result.getPayableTotal()); bill.setCurrency(result.getCurrency()); bill.setRoundMode(result.getRoundMode()); + result.setBillId(bill.getBillId()); + result.setOrderNo(orderNo); + result.setCustomerCode(request.getCustomerCode()); + fillResultOrder(result, bill); + result.setFromCache(Boolean.valueOf(existed != null)); if (existed == null) { billMapper.insertPrcBill(bill); @@ -166,11 +179,7 @@ public class PrcBillServiceImpl implements IPrcBillService { billFeeMapper.insertBatch(fees); } - result.setBillId(bill.getBillId()); - result.setOrderNo(orderNo); - result.setCustomerCode(request.getCustomerCode()); - fillResultOrder(result, bill); - result.setFromCache(Boolean.valueOf(existed != null)); + billSearchStore.save(EsLogType.BILL, orderNo, toRequestJson(request), toResponseJson(result)); } private List toFeeRows(String billId, TmsPriceQuoteResult result) @@ -252,9 +261,31 @@ public class PrcBillServiceImpl implements IPrcBillService } } r.setFeeDetails(lines); + fillSnapshotFromSearch(r, bill); return r; } + private void fillSnapshotFromSearch(TmsPriceQuoteResult r, PrcBill bill) + { + BillSyncSnapshot snap = billSearchStore.get(EsLogType.BILL, bill.getOrderNo()); + if (snap != null && StringUtils.isNotEmpty(snap.getRequestJson())) + { + r.setRequestJson(snap.getRequestJson()); + } + else + { + r.setRequestJson(rebuildRequestJson(bill)); + } + if (snap != null && StringUtils.isNotEmpty(snap.getResponseJson())) + { + r.setResponseJson(snap.getResponseJson()); + } + else + { + r.setResponseJson(toResponseJson(r)); + } + } + private void fillResultOrder(TmsPriceQuoteResult r, PrcBill bill) { r.setTrackingNo(bill.getTrackingNo()); @@ -274,6 +305,89 @@ public class PrcBillServiceImpl implements IPrcBillService r.setOrderRemark(bill.getOrderRemark()); } + private String toRequestJson(TmsPriceBillRequest request) + { + JSONObject obj = parseJsonObject(JSON.toJSONString(request)); + if (obj == null) + { + return null; + } + obj.remove("apiCode"); + return obj.toJSONString(); + } + + private String toResponseJson(TmsPriceQuoteResult result) + { + JSONObject obj = parseJsonObject(JSON.toJSONString(result)); + if (obj == null) + { + return null; + } + obj.remove("requestJson"); + obj.remove("responseJson"); + return obj.toJSONString(); + } + + private String rebuildRequestJson(PrcBill bill) + { + JSONObject obj = new JSONObject(); + putIfNotNull(obj, "orderNo", bill.getOrderNo()); + putIfNotNull(obj, "trackingNo", bill.getTrackingNo()); + putIfNotNull(obj, "customerId", bill.getCustomerId()); + putIfNotNull(obj, "customerCode", bill.getCustomerCode()); + putIfNotNull(obj, "customerName", bill.getCustomerName()); + putIfNotNull(obj, "productCode", bill.getProductCode()); + putIfNotNull(obj, "countryCode", bill.getCountryCode()); + putIfNotNull(obj, "countryName", bill.getCountryName()); + putIfNotNull(obj, "weight", bill.getWeight()); + if (bill.getBizDate() != null) + { + obj.put("bizDate", new SimpleDateFormat("yyyy-MM-dd").format(bill.getBizDate())); + } + putIfNotNull(obj, "tmsId", bill.getTmsId()); + putIfNotNull(obj, "pieces", bill.getPieces()); + putIfNotNull(obj, "actualWeight", bill.getActualWeight()); + putIfNotNull(obj, "volume", bill.getVolume()); + putIfNotNull(obj, "volumeWeight", bill.getVolumeWeight()); + putIfNotNull(obj, "pkgLength", bill.getPkgLength()); + putIfNotNull(obj, "pkgWidth", bill.getPkgWidth()); + putIfNotNull(obj, "pkgHeight", bill.getPkgHeight()); + putIfNotNull(obj, "shipperName", bill.getShipperName()); + putIfNotNull(obj, "consigneeName", bill.getConsigneeName()); + putIfNotNull(obj, "destPostcode", bill.getDestPostcode()); + putIfNotNull(obj, "warehouseCode", bill.getWarehouseCode()); + if (bill.getOrderTime() != null) + { + obj.put("orderTime", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(bill.getOrderTime())); + } + putIfNotNull(obj, "orderRemark", bill.getOrderRemark()); + return obj.toJSONString(); + } + + private JSONObject parseJsonObject(String json) + { + if (StringUtils.isEmpty(json)) + { + return null; + } + try + { + return JSON.parseObject(json); + } + catch (Exception e) + { + return null; + } + } + + private void putIfNotNull(JSONObject obj, String key, Object value) + { + if (value != null && !(value instanceof String && ((String) value).isEmpty())) + { + obj.put(key, value); + } + } + private Date resolveBizDate(String bizDate) { if (StringUtils.isEmpty(bizDate)) diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java index ad1ac544..7e2720c6 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java @@ -14,6 +14,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.common.utils.StringUtils; @@ -37,6 +39,8 @@ import com.ruoyi.price.mapper.PrcPriceSetMapper; import com.ruoyi.price.mapper.PrcQuoteFeeMapper; import com.ruoyi.price.mapper.PrcQuoteMapper; import com.ruoyi.price.mapper.PrcSurchargeRelMapper; +import com.ruoyi.price.search.EsLogType; +import com.ruoyi.price.search.IBillSearchStore; import com.ruoyi.price.service.ITmsPriceQuoteService; /** @@ -82,6 +86,9 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService @Autowired private PrcQuoteFeeMapper quoteFeeMapper; + @Autowired + private IBillSearchStore billSearchStore; + @Override @Transactional(rollbackFor = Exception.class) public TmsPriceQuoteResult quote(TmsPriceQuoteRequest request) @@ -104,6 +111,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService replaceQuoteFees(cached.getQuoteId(), refreshed); refreshed.setQuoteId(cached.getQuoteId()); refreshed.setFromCache(false); + billSearchStore.save(EsLogType.QUOTE, cached.getQuoteId(), toEsRequest(request), toEsResponse(refreshed)); return refreshed; } @@ -531,6 +539,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService } result.setQuoteId(quote.getQuoteId()); result.setFromCache(false); + billSearchStore.save(EsLogType.QUOTE, quote.getQuoteId(), toEsRequest(request), toEsResponse(result)); } private boolean isStaleSurchargeFees(List fees) @@ -580,6 +589,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService for (TmsPriceQuoteFeeLine line : result.getFeeDetails()) { PrcQuoteFee fee = new PrcQuoteFee(); + fee.setFeeId(UUID.randomUUID().toString()); fee.setQuoteId(quoteId); fee.setFeeType(line.getFeeType()); fee.setFeeName(line.getFeeName()); @@ -832,6 +842,29 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService return p == null ? null : p.getProductName(); } + private String toEsRequest(TmsPriceQuoteRequest request) + { + JSONObject obj = JSON.parseObject(JSON.toJSONString(request)); + if (obj == null) + { + return null; + } + obj.remove("apiCode"); + return obj.toJSONString(); + } + + private String toEsResponse(TmsPriceQuoteResult result) + { + JSONObject obj = JSON.parseObject(JSON.toJSONString(result)); + if (obj == null) + { + return null; + } + obj.remove("requestJson"); + obj.remove("responseJson"); + return obj.toJSONString(); + } + private static String emptyToNull(String s) { return StringUtils.isEmpty(s) ? null : s.trim(); diff --git a/bms-back/sql/monitor/eslog_menu.sql b/bms-back/sql/monitor/eslog_menu.sql new file mode 100644 index 00000000..2792c0c7 --- /dev/null +++ b/bms-back/sql/monitor/eslog_menu.sql @@ -0,0 +1,14 @@ +-- ES log menu under system monitor (parent_id = 2) +SET NAMES utf8mb4; + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2320, N'ES日志', 2, 7, 'eslog', 'monitor/eslog/index', '', 'EsLog', 1, 0, 'C', '0', '0', 'monitor:eslog:list', 'log', 'admin', sysdate(), N'查看Elasticsearch计费同步报文' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2320); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2321, N'查询', 2320, 1, '', NULL, 1, 0, 'F', '0', '0', 'monitor:eslog:list', '#', 'admin', sysdate() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2321); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, m.menu_id FROM (SELECT 2320 AS menu_id UNION SELECT 2321) m +WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id); diff --git a/bms-front/src/api/monitor/eslog.js b/bms-front/src/api/monitor/eslog.js new file mode 100644 index 00000000..ea69791e --- /dev/null +++ b/bms-front/src/api/monitor/eslog.js @@ -0,0 +1,16 @@ +import request from '@/utils/request' + +export function listEsLog(query) { + return request({ + url: '/monitor/eslog/list', + method: 'get', + params: query + }) +} + +export function getEsLog(docId) { + return request({ + url: '/monitor/eslog/' + encodeURIComponent(docId), + method: 'get' + }) +} diff --git a/bms-front/src/views/monitor/eslog/index.vue b/bms-front/src/views/monitor/eslog/index.vue new file mode 100644 index 00000000..e4707d6b --- /dev/null +++ b/bms-front/src/views/monitor/eslog/index.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/bms-front/src/views/price/billing/components/BillDetail.vue b/bms-front/src/views/price/billing/components/BillDetail.vue index 96765309..a4899220 100644 --- a/bms-front/src/views/price/billing/components/BillDetail.vue +++ b/bms-front/src/views/price/billing/components/BillDetail.vue @@ -101,6 +101,23 @@ +
+
同步报文
+
+
+ 同步请求参数 + 复制 +
+
{{ formatJson(detail.requestJson) }}
+
+
+
+ 计费返回数据 + 复制 +
+
{{ formatJson(detail.responseJson) }}
+
+
@@ -206,6 +223,30 @@ function displayFeeName(row) { return name || typeLabel || row.feeType || '-' } +function formatJson(val) { + if (val === null || val === undefined || val === '') return '无数据' + if (typeof val === 'object') { + try { return JSON.stringify(val, null, 2) } catch (e) { return String(val) } + } + try { return JSON.stringify(JSON.parse(val), null, 2) } catch (e) { return String(val) } +} + +function copyJson(val) { + const text = formatJson(val) + const done = () => ElMessage({ message: '已复制', type: 'success', duration: 1500 }) + if (navigator.clipboard) { + navigator.clipboard.writeText(text).then(done) + return + } + const ta = document.createElement('textarea') + ta.value = text + document.body.appendChild(ta) + ta.select() + document.execCommand('copy') + document.body.removeChild(ta) + done() +} + function sourceTagType(source) { if (source === 'PACKAGE_FIXED' || source === 'FIXED') return 'success' if (source === 'PACKAGE_DISCOUNT' || source === 'DISCOUNT') return 'warning' @@ -301,6 +342,32 @@ function sourceTagType(source) { flex-shrink: 0; color: #909399; } +.json-panel { + margin-top: 10px; + background: #f7f9fb; + border: 1px solid #e8ecf0; + border-radius: 6px; + overflow: hidden; +} +.json-panel__head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + font-size: 13px; + font-weight: 600; + color: #606266; + background: #fafbfc; + border-bottom: 1px solid #ebeef5; +} +.json-panel .code-pre { + margin: 0; + padding: 12px 14px; + max-height: 240px; + overflow: auto; + white-space: pre-wrap; + word-break: break-all; +} .fee-amount { font-variant-numeric: tabular-nums; font-weight: 500;