| | |
| | | request: "请求内容", |
| | | response: "响应内容", |
| | | err: "异常", |
| | | result: "请求结果", |
| | | result: "结果", |
| | | costMs: "耗时(毫秒)", |
| | | }, |
| | | |
| New file |
| | |
| | | import * as React from 'react'; |
| | | import { Labeled } from 'react-admin'; |
| | | import { Box, Grid, Typography, Card, CardContent, TextField } from '@mui/material'; |
| | | import { format } from 'date-fns'; |
| | | |
| | | const IntegrationRecordDetail = (props) => { |
| | | const { integration } = props; |
| | | if (!integration) return null; |
| | | |
| | | const formatTimestamp = (timestamp) => { |
| | | if (!timestamp) return ''; |
| | | try { |
| | | return format(new Date(Number(timestamp)), 'yyyy-MM-dd HH:mm:ss'); |
| | | } catch (e) { |
| | | return timestamp; |
| | | } |
| | | }; |
| | | |
| | | return ( |
| | | <Box width={{ xs: '100vw', sm: 400 }} mt={{ xs: 2, sm: 1 }}> |
| | | <Card> |
| | | <CardContent> |
| | | <Grid container rowSpacing={1} mb={1}> |
| | | <Grid item xs={6}> |
| | | <Labeled label="table.field.integrationRecord.uuid"> |
| | | <Typography variant="body2" flexWrap="nowrap"> |
| | | {integration.uuid || ''} |
| | | </Typography> |
| | | </Labeled> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Labeled label="table.field.integrationRecord.namespace"> |
| | | <Typography variant="body2" flexWrap="nowrap"> |
| | | {integration.namespace || ''} |
| | | </Typography> |
| | | </Labeled> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Labeled label="table.field.integrationRecord.url"> |
| | | <Typography variant="body2" flexWrap="nowrap"> |
| | | {integration.url || ''} |
| | | </Typography> |
| | | </Labeled> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Labeled label="table.field.integrationRecord.appkey"> |
| | | <Typography variant="body2" flexWrap="nowrap"> |
| | | {integration.appkey || ''} |
| | | </Typography> |
| | | </Labeled> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Labeled label="table.field.integrationRecord.caller"> |
| | | <Typography variant="body2" flexWrap="nowrap"> |
| | | {integration.caller || ''} |
| | | </Typography> |
| | | </Labeled> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Labeled label="table.field.integrationRecord.direction"> |
| | | <Typography variant="body2" flexWrap="nowrap"> |
| | | {integration.direction$ || ''} |
| | | </Typography> |
| | | </Labeled> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Labeled label="table.field.integrationRecord.timestamp"> |
| | | <Typography variant="body2" flexWrap="nowrap"> |
| | | {formatTimestamp(integration.timestamp)} |
| | | </Typography> |
| | | </Labeled> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Labeled label="table.field.integrationRecord.clientIp"> |
| | | <Typography variant="body2" flexWrap="nowrap"> |
| | | {integration.clientIp || ''} |
| | | </Typography> |
| | | </Labeled> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Labeled label="table.field.integrationRecord.result"> |
| | | <Typography variant="body2" flexWrap="nowrap"> |
| | | {integration.result$ || ''} |
| | | </Typography> |
| | | </Labeled> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Labeled label="table.field.integrationRecord.costMs"> |
| | | <Typography variant="body2" flexWrap="nowrap"> |
| | | {integration.costMs ?? ''} |
| | | </Typography> |
| | | </Labeled> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Labeled label="table.field.integrationRecord.err"> |
| | | <Typography variant="body2" flexWrap="nowrap"> |
| | | {integration.err || ''} |
| | | </Typography> |
| | | </Labeled> |
| | | </Grid> |
| | | </Grid> |
| | | <Grid container rowSpacing={2}> |
| | | <Grid item xs={12}> |
| | | <TextField |
| | | label="Request" |
| | | value={integration.request || ''} |
| | | maxRows={15} |
| | | multiline |
| | | fullWidth |
| | | InputProps={{ readOnly: true }} |
| | | /> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextField |
| | | label="Response" |
| | | value={integration.response || ''} |
| | | maxRows={15} |
| | | multiline |
| | | fullWidth |
| | | InputProps={{ readOnly: true }} |
| | | /> |
| | | </Grid> |
| | | </Grid> |
| | | </CardContent> |
| | | </Card> |
| | | </Box> |
| | | ); |
| | | }; |
| | | |
| | | export default IntegrationRecordDetail; |
| | |
| | | import React, { useState, useRef, useEffect, useMemo, useCallback } from "react"; |
| | | import { useNavigate } from 'react-router-dom'; |
| | | import React, { useState } from "react"; |
| | | import { |
| | | List, |
| | | DatagridConfigurable, |
| | | SearchInput, |
| | | TopToolbar, |
| | | SelectColumnsButton, |
| | | EditButton, |
| | | FilterButton, |
| | | CreateButton, |
| | | ExportButton, |
| | | BulkDeleteButton, |
| | | WrapperField, |
| | | useRecordContext, |
| | | useTranslate, |
| | | useNotify, |
| | | useListContext, |
| | | FunctionField, |
| | | TextField, |
| | | NumberField, |
| | | DateField, |
| | | BooleanField, |
| | | ReferenceField, |
| | | TextInput, |
| | | DateTimeInput, |
| | | DateInput, |
| | | SelectInput, |
| | | NumberInput, |
| | | ReferenceInput, |
| | | ReferenceArrayInput, |
| | | AutocompleteInput, |
| | | DeleteButton, |
| | | } from 'react-admin'; |
| | | import { Box, Typography, Card, Stack } from '@mui/material'; |
| | | import { Box, Chip } from '@mui/material'; |
| | | import { styled } from '@mui/material/styles'; |
| | | import IntegrationRecordCreate from "./IntegrationRecordCreate"; |
| | | import IntegrationRecordPanel from "./IntegrationRecordPanel"; |
| | | import IntegrationRecordDetail from "./IntegrationRecordDetail"; |
| | | import EmptyDataLoader from "../components/EmptyDataLoader"; |
| | | import MyCreateButton from "../components/MyCreateButton"; |
| | | import MyExportButton from '../components/MyExportButton'; |
| | | import PageDrawer from "../components/PageDrawer"; |
| | | import MyField from "../components/MyField"; |
| | | import { PAGE_DRAWER_WIDTH, OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting'; |
| | | import * as Common from '@/utils/common'; |
| | | import { PAGE_DRAWER_WIDTH, DEFAULT_PAGE_SIZE } from '@/config/setting'; |
| | | import { format } from 'date-fns'; |
| | | import rowSx from './rowSx'; |
| | | |
| | | const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({ |
| | | '& .css-1vooibu-MuiSvgIcon-root': { |
| | |
| | | '& .RaDatagrid-row': { |
| | | cursor: 'auto' |
| | | }, |
| | | '& .column-name': { |
| | | '& .column-url': { |
| | | maxWidth: '16em', |
| | | overflow: 'hidden', |
| | | textOverflow: 'ellipsis', |
| | | whiteSpace: 'nowrap', |
| | | }, |
| | | '& .column-request': { |
| | | maxWidth: '18em', |
| | | overflow: 'hidden', |
| | | textOverflow: 'ellipsis', |
| | | whiteSpace: 'nowrap', |
| | | }, |
| | | '& .column-response': { |
| | | maxWidth: '18em', |
| | | overflow: 'hidden', |
| | | textOverflow: 'ellipsis', |
| | | whiteSpace: 'nowrap', |
| | | }, |
| | | '& .column-costMs': { |
| | | maxWidth: '9em', |
| | | }, |
| | | '& .opt': { |
| | | width: 200 |
| | | }, |
| | | '& .RaDatagrid-thead': { |
| | | borderLeftColor: 'transparent', |
| | | borderLeftWidth: 5, |
| | | borderLeftStyle: 'solid', |
| | | }, |
| | | })); |
| | | |
| | |
| | | ] |
| | | |
| | | const IntegrationRecordList = () => { |
| | | const translate = useTranslate(); |
| | | |
| | | const [createDialog, setCreateDialog] = useState(false); |
| | | const [drawerVal, setDrawerVal] = useState(false); |
| | | |
| | |
| | | actions={( |
| | | <TopToolbar> |
| | | <FilterButton /> |
| | | <MyCreateButton onClick={() => { setCreateDialog(true) }} /> |
| | | <SelectColumnsButton preferenceKey='integrationRecord' /> |
| | | <MyExportButton /> |
| | | </TopToolbar> |
| | |
| | | > |
| | | <StyledDatagrid |
| | | preferenceKey='integrationRecord' |
| | | bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />} |
| | | rowClick={(id, resource, record) => false} |
| | | expand={() => <IntegrationRecordPanel />} |
| | | expandSingle={true} |
| | | omit={['id', 'createTime', 'createBy', 'memo']} |
| | | bulkActionButtons={false} |
| | | rowClick={(id, resource, record) => { |
| | | setDrawerVal(!!drawerVal && drawerVal === record ? null : record); |
| | | return false; |
| | | }} |
| | | omit={['id', 'uuid', 'appkey', 'direction', 'timestamp', 'updateTime', 'memo']} |
| | | rowSx={rowSx(drawerVal || null)} |
| | | > |
| | | <NumberField source="id" /> |
| | | <TextField source="uuid" label="table.field.integrationRecord.uuid" /> |
| | |
| | | <TextField source="url" label="table.field.integrationRecord.url" /> |
| | | <TextField source="appkey" label="table.field.integrationRecord.appkey" /> |
| | | <TextField source="caller" label="table.field.integrationRecord.caller" /> |
| | | <TextField source="direction$" label="table.field.integrationRecord.direction" sortable={false} /> |
| | | <TextField source="timestamp" label="table.field.integrationRecord.timestamp" /> |
| | | <TextField source="direction" label="table.field.integrationRecord.direction" sortable={false} /> |
| | | <FormattedTimestampField source="timestamp" label="table.field.integrationRecord.timestamp" /> |
| | | <TextField source="clientIp" label="table.field.integrationRecord.clientIp" /> |
| | | <TextField source="request" label="table.field.integrationRecord.request" /> |
| | | <TextField source="response" label="table.field.integrationRecord.response" /> |
| | | <TextField source="request" label="table.field.integrationRecord.request" sortable={false} hidden={!!drawerVal} /> |
| | | <TextField source="response" label="table.field.integrationRecord.response" sortable={false} hidden={!!drawerVal} /> |
| | | <ResultField source="result" label="table.field.integrationRecord.result" /> |
| | | <TextField source="err" label="table.field.integrationRecord.err" /> |
| | | <TextField source="result$" label="table.field.integrationRecord.result" sortable={false} /> |
| | | <NumberField source="costMs" label="table.field.integrationRecord.costMs" /> |
| | | <NumberField source="costMs" label="table.field.integrationRecord.costMs" sx={{ fontWeight: 'bold' }} /> |
| | | |
| | | <ReferenceField source="updateBy" label="common.field.updateBy" reference="user" link={false} sortable={false}> |
| | | <TextField source="nickname" /> |
| | | </ReferenceField> |
| | | <DateField source="updateTime" label="common.field.updateTime" showTime /> |
| | | <ReferenceField source="createBy" label="common.field.createBy" reference="user" link={false} sortable={false}> |
| | | <TextField source="nickname" /> |
| | | </ReferenceField> |
| | | <DateField source="createTime" label="common.field.createTime" showTime /> |
| | | <BooleanField source="statusBool" label="common.field.status" sortable={false} /> |
| | | {/* <BooleanField source="statusBool" label="common.field.status" sortable={false} /> */} |
| | | <TextField source="memo" label="common.field.memo" sortable={false} /> |
| | | <WrapperField cellClassName="opt" label="common.field.opt"> |
| | | <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} /> |
| | | <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} /> |
| | | </WrapperField> |
| | | </StyledDatagrid> |
| | | </List> |
| | | <IntegrationRecordCreate |
| | |
| | | drawerVal={drawerVal} |
| | | setDrawerVal={setDrawerVal} |
| | | > |
| | | <IntegrationRecordDetail integration={drawerVal} /> |
| | | </PageDrawer> |
| | | </Box> |
| | | ) |
| | | } |
| | | |
| | | const FormattedTimestampField = ({ source }) => { |
| | | const record = useRecordContext(); |
| | | if (!record) return null; |
| | | const val = record[source]; |
| | | if (!val) return null; |
| | | const formattedDate = format(new Date(Number(val)), 'yyyy-MM-dd HH:mm:ss'); |
| | | return <span>{formattedDate}</span>; |
| | | }; |
| | | |
| | | const ResultField = ({ source }) => { |
| | | const record = useRecordContext(); |
| | | const val = record?.[source]; |
| | | return ( |
| | | <> |
| | | {val === 1 ? ( |
| | | <Chip label="success" color="success" variant="outlined" size="small" /> |
| | | ) : ( |
| | | <Chip label="error" color="error" variant="outlined" size="small" /> |
| | | )} |
| | | </> |
| | | ); |
| | | }; |
| | | |
| | | export default IntegrationRecordList; |
| New file |
| | |
| | | import green from '@mui/material/colors/green'; |
| | | import orange from '@mui/material/colors/orange'; |
| | | import red from '@mui/material/colors/red'; |
| | | |
| | | const rowSx = (selectedRow) => (record) => { |
| | | let style = {}; |
| | | if (!record) { |
| | | return style; |
| | | } |
| | | if (selectedRow && selectedRow.id === record.id) { |
| | | style = { |
| | | ...style, |
| | | backgroundColor: 'action.selected', |
| | | }; |
| | | } |
| | | if (record.result === 1) |
| | | return { |
| | | ...style, |
| | | borderLeftColor: green[500], |
| | | borderLeftWidth: 5, |
| | | borderLeftStyle: 'solid', |
| | | }; |
| | | if (record.result === 2) |
| | | return { |
| | | ...style, |
| | | borderLeftColor: orange[500], |
| | | borderLeftWidth: 5, |
| | | borderLeftStyle: 'solid', |
| | | }; |
| | | if (record.result === 0) |
| | | return { |
| | | ...style, |
| | | borderLeftColor: red[500], |
| | | borderLeftWidth: 5, |
| | | borderLeftStyle: 'solid', |
| | | }; |
| | | return style; |
| | | }; |
| | | |
| | | export default rowSx; |
| | |
| | | import com.zy.acs.manager.manager.enums.IntegrationDirectionType; |
| | | import com.zy.acs.manager.manager.enums.StatusType; |
| | | import com.zy.acs.manager.manager.service.IntegrationRecordService; |
| | | import lombok.Data; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.jetbrains.annotations.NotNull; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | |
| | | Exception failure) { |
| | | Date now = new Date(); |
| | | RequestSnapshot payload = buildSnapshot(request); |
| | | String reqContent = !Cools.isEmpty(payload.getParameters()) |
| | | ? JSON.toJSONString(payload.getParameters()) |
| | | : payload.getJson(); |
| | | |
| | | IntegrationRecord record = new IntegrationRecord(); |
| | | record.setUuid(String.valueOf(snowflakeIdWorker.nextId()).substring(3)); |
| | | record.setNamespace(context.getNamespaceType().name()); |
| | | record.setUrl(payload.getUri()); |
| | | record.setAppkey(request.getHeader(HEADER_APP_KEY)); |
| | | record.setCaller(resolveCaller(request)); |
| | | record.setDirection(IntegrationDirectionType.INBOUND.value); |
| | | record.setTimestamp(String.valueOf(context.getStartAt())); |
| | | record.setClientIp(IpTools.gainRealIp(request)); |
| | | record.setRequest(safeToJson(payload)); |
| | | record.setResponse(safeToJson(responseBody)); |
| | | IntegrationRecord record = new IntegrationRecord( |
| | | String.valueOf(snowflakeIdWorker.nextId()).substring(3), // 编号 |
| | | context.getNamespaceType().name(), // 名称空间 |
| | | payload.getUri(), // 接口地址 |
| | | request.getHeader(HEADER_APP_KEY), // 平台密钥 |
| | | context.getNamespaceType().caller, // 调用方标识 |
| | | IntegrationDirectionType.INBOUND.value, // 方向[非空] |
| | | String.valueOf(context.getStartAt()), // 时间戳 |
| | | IpTools.gainRealIp(request), // 客户端IP |
| | | reqContent, // 请求内容 |
| | | JSON.toJSONString(responseBody), // 响应内容 |
| | | null, // 异常内容 |
| | | 0, // 结果 |
| | | (int) (System.currentTimeMillis() - context.getStartAt()), // 耗时 |
| | | StatusType.ENABLE.val, // 状态 |
| | | now, // 添加时间[非空] |
| | | now, // 修改时间[非空] |
| | | context.getHandler() // 备注 |
| | | ); |
| | | |
| | | applyResult(record, responseBody, failure); |
| | | record.setCostMs(cost(context.getStartAt())); |
| | | record.setStatus(StatusType.ENABLE.val); |
| | | record.setCreateTime(now); |
| | | record.setUpdateTime(now); |
| | | record.setMemo(context.getHandler()); |
| | | return record; |
| | | } |
| | | |
| | |
| | | } |
| | | boolean success = code == 200; |
| | | record.setResult(success ? 1 : 0); |
| | | record.setErr(success ? null : safeToString(response.get("msg"))); |
| | | record.setErr(success ? null : String.valueOf(response.get("msg"))); |
| | | } |
| | | |
| | | private Integer parseInteger(Object codeObj) { |
| | |
| | | } |
| | | } |
| | | |
| | | private String safeToJson(Object value) { |
| | | if (value == null) { |
| | | return null; |
| | | } |
| | | try { |
| | | return JSON.toJSONString(value); |
| | | } catch (Exception e) { |
| | | log.warn("Failed to serialize value for integration log: {}", value.getClass().getName(), e); |
| | | return String.valueOf(value); |
| | | } |
| | | } |
| | | |
| | | private String resolveCaller(HttpServletRequest request) { |
| | | String caller = request.getHeader(HEADER_CALLER); |
| | | if (Cools.isEmpty(caller)) { |
| | | caller = request.getHeader(HEADER_APP_KEY); |
| | | } |
| | | return caller; |
| | | } |
| | | |
| | | private int cost(long startAt) { |
| | | long duration = System.currentTimeMillis() - startAt; |
| | | if (duration < 0) { |
| | | return 0; |
| | | } |
| | | return (int) duration; |
| | | } |
| | | |
| | | private String safeToString(Object value) { |
| | | return value == null ? null : String.valueOf(value); |
| | | } |
| | | |
| | | private RequestSnapshot buildSnapshot(HttpServletRequest request) { |
| | | Map<String, Object> params = flattenParameters(request.getParameterMap()); |
| | | String body = normalizeBody(readBody(request), request.getContentType()); |
| | | return new RequestSnapshot( |
| | | request.getMethod(), |
| | | request.getRequestURI(), |
| | | request.getQueryString(), |
| | | request.getContentType(), |
| | | params.isEmpty() ? null : params, |
| | | body |
| | | normalizeBody(readBody(request), request.getContentType()) |
| | | ); |
| | | } |
| | | |
| | |
| | | Object parsed = JSON.parse(body); |
| | | return JSON.toJSONString(parsed, false); |
| | | } catch (Exception ignore) { |
| | | // fall through to compacting whitespace |
| | | } |
| | | } |
| | | return body.replaceAll("[\\n\\r\\t]", "").trim(); |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | @Data |
| | | private static class RequestSnapshot { |
| | | private final String method; |
| | | private final String uri; |
| | | private final String query; |
| | | private final String contentType; |
| | | private final Map<String, Object> parameters; |
| | | private final String body; |
| | | private final String json; |
| | | |
| | | RequestSnapshot(String method, String uri, String query, String contentType, |
| | | Map<String, Object> parameters, String body) { |
| | | Map<String, Object> parameters, String json) { |
| | | this.method = method; |
| | | this.uri = uri; |
| | | this.query = query; |
| | | this.contentType = contentType; |
| | | this.parameters = parameters; |
| | | this.body = body; |
| | | this.json = json; |
| | | } |
| | | |
| | | public String getUri() { |
| | | return uri; |
| | | } |
| | | } |
| | | } |
| | |
| | | package com.zy.acs.manager.core.domain.type; |
| | | |
| | | import com.zy.acs.manager.common.constant.Constants; |
| | | |
| | | public enum NamespaceType { |
| | | |
| | | NONE("未知"), |
| | | RCS_TASK_REPORT("任务完成上报"), |
| | | |
| | | NONE("未知", "NONE"), |
| | | RCS_TASK_REPORT("任务完成上报", Constants.RCS), |
| | | RCS_BUS_RECEIVE("接收批次任务", Constants.UPLINK), |
| | | ; |
| | | |
| | | public String name; |
| | | |
| | | NamespaceType(String name) { |
| | | public String caller; |
| | | |
| | | NamespaceType(String name, String caller) { |
| | | this.name = name; |
| | | this.caller = caller; |
| | | } |
| | | |
| | | } |
| | |
| | | |
| | | @PostMapping("/bus/submit") |
| | | @OperationLog("generate task from open api") |
| | | @IntegrationAuth(name = NamespaceType.RCS_TASK_REPORT) |
| | | @IntegrationAuth(name = NamespaceType.RCS_BUS_RECEIVE) |
| | | public R submit(@RequestBody OpenBusSubmitParam param, HttpServletRequest request) { |
| | | IntegrationRecord integrationRecord = new IntegrationRecord( |
| | | null, // 编号 |