记录同步数据日志功能
This commit is contained in:
+53
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+29
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<BillSyncSnapshot> rows = new ArrayList<BillSyncSnapshot>();
|
||||
|
||||
public long getTotal()
|
||||
{
|
||||
return total;
|
||||
}
|
||||
|
||||
public void setTotal(long total)
|
||||
{
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public List<BillSyncSnapshot> getRows()
|
||||
{
|
||||
return rows;
|
||||
}
|
||||
|
||||
public void setRows(List<BillSyncSnapshot> rows)
|
||||
{
|
||||
this.rows = rows == null ? new ArrayList<BillSyncSnapshot>() : rows;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+73
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String> entity = new HttpEntity<String>(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<String> 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<String> entity = new HttpEntity<String>(body.toJSONString(), headers);
|
||||
ResponseEntity<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+119
-5
@@ -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<PrcBillFee> 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))
|
||||
|
||||
+33
@@ -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<PrcQuoteFee> 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();
|
||||
|
||||
@@ -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);
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<basic-table
|
||||
ref="tableRef"
|
||||
:columns="columns"
|
||||
:search-columns="searchColumns"
|
||||
:query="queryList"
|
||||
row-key="docId"
|
||||
>
|
||||
<template #col-logType="{ row }">
|
||||
<el-tag size="small" :type="typeTag(row.logType)">{{ typeLabel(row.logType) }}</el-tag>
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="handleView(row)" v-hasPermi="['monitor:eslog:list']">查看</el-button>
|
||||
</template>
|
||||
</basic-table>
|
||||
|
||||
<el-drawer v-model="open" size="720px" :title="drawerTitle" append-to-body destroy-on-close>
|
||||
<div v-loading="loading">
|
||||
<el-empty v-if="!detail" description="暂无详情" />
|
||||
<div v-else>
|
||||
<el-descriptions :column="2" border size="small" class="mb12">
|
||||
<el-descriptions-item label="类型">
|
||||
<el-tag size="small" :type="typeTag(detail.logType)">{{ typeLabel(detail.logType) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="业务ID">{{ detail.bizId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="写入时间" :span="2">{{ detail.updatedAt || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="json-panel">
|
||||
<div class="json-panel__head">
|
||||
<span>请求报文</span>
|
||||
<el-button size="small" icon="CopyDocument" @click="copyJson(detail.requestJson)">复制</el-button>
|
||||
</div>
|
||||
<pre class="code-pre">{{ formatJson(detail.requestJson) }}</pre>
|
||||
</div>
|
||||
<div class="json-panel">
|
||||
<div class="json-panel__head">
|
||||
<span>返回报文</span>
|
||||
<el-button size="small" icon="CopyDocument" @click="copyJson(detail.responseJson)">复制</el-button>
|
||||
</div>
|
||||
<pre class="code-pre">{{ formatJson(detail.responseJson) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="EsLog">
|
||||
import { listEsLog, getEsLog } from '@/api/monitor/eslog'
|
||||
|
||||
const tableRef = ref()
|
||||
const open = ref(false)
|
||||
const loading = ref(false)
|
||||
const detail = ref(null)
|
||||
const drawerTitle = ref('ES日志详情')
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '计费', value: 'BILL' },
|
||||
{ label: '试算', value: 'QUOTE' }
|
||||
]
|
||||
|
||||
const searchColumns = [
|
||||
{ label: '类型', prop: 'logType', component: 'select', options: typeOptions, placeholder: '请选择类型', width: '160px', props: { clearable: true } },
|
||||
{ label: '业务ID', prop: 'bizId', component: 'input', placeholder: '业务ID', width: '220px' }
|
||||
]
|
||||
|
||||
const columns = [
|
||||
{ label: '类型', prop: 'logType', align: 'center', width: 110, slot: 'logType' },
|
||||
{ label: '业务ID', prop: 'bizId', minWidth: 180, showOverflowTooltip: true },
|
||||
{ label: '写入时间', prop: 'updatedAt', align: 'center', width: 180 },
|
||||
{ label: '操作', prop: 'action', align: 'center', width: 90, fixed: 'right', slot: 'action', showInToolbar: false }
|
||||
]
|
||||
|
||||
function queryList(params) {
|
||||
return listEsLog(params)
|
||||
}
|
||||
|
||||
function typeLabel(type) {
|
||||
const found = typeOptions.find(item => item.value === type)
|
||||
return found ? found.label : (type || '-')
|
||||
}
|
||||
|
||||
function typeTag(type) {
|
||||
if (type === 'BILL') return 'success'
|
||||
if (type === 'QUOTE') return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function handleView(row) {
|
||||
drawerTitle.value = 'ES日志详情 #' + (row.bizId || row.docId)
|
||||
open.value = true
|
||||
loading.value = true
|
||||
detail.value = null
|
||||
getEsLog(row.docId).then(res => {
|
||||
detail.value = res.data || row
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.json-panel {
|
||||
margin-bottom: 12px;
|
||||
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: 280px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -101,6 +101,23 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
<section v-if="detail.requestJson || detail.responseJson" class="detail-block">
|
||||
<div class="sec-title">同步报文</div>
|
||||
<div class="json-panel">
|
||||
<div class="json-panel__head">
|
||||
<span>同步请求参数</span>
|
||||
<el-button size="small" icon="CopyDocument" @click="copyJson(detail.requestJson)">复制</el-button>
|
||||
</div>
|
||||
<pre class="code-pre">{{ formatJson(detail.requestJson) }}</pre>
|
||||
</div>
|
||||
<div class="json-panel">
|
||||
<div class="json-panel__head">
|
||||
<span>计费返回数据</span>
|
||||
<el-button size="small" icon="CopyDocument" @click="copyJson(detail.responseJson)">复制</el-button>
|
||||
</div>
|
||||
<pre class="code-pre">{{ formatJson(detail.responseJson) }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user