This commit is contained in:
六如
2024-09-17 23:07:32 +08:00
parent 50e576b9fc
commit 89ce369c8f
41 changed files with 1261 additions and 229 deletions

View File

@@ -0,0 +1,56 @@
import axios from 'axios';
import qs from 'query-string';
import type { DescData } from '@arco-design/web-vue/es/descriptions/interface';
export interface PolicyRecord {
id: string;
number: number;
name: string;
contentType: 'img' | 'horizontalVideo' | 'verticalVideo';
filterType: 'artificial' | 'rules';
count: number;
status: 'online' | 'offline';
createdTime: string;
}
export interface PolicyParams extends Partial<PolicyRecord> {
pageIndex: number;
pageSize: number;
}
export interface PolicyListRes {
list: PolicyRecord[];
total: number;
}
export function queryPolicyList(params: PolicyParams) {
return axios.get<PolicyListRes>('/api/page', {
params,
paramsSerializer: (obj) => {
return qs.stringify(obj);
},
});
}
export interface ServiceRecord {
id: number;
title: string;
description: string;
name?: string;
actionType?: string;
icon?: string;
data?: DescData[];
enable?: boolean;
expires?: boolean;
}
export function queryInspectionList() {
return axios.get('/api/list/quality-inspection');
}
export function queryTheServiceList() {
return axios.get('/api/list/the-service');
}
export function queryRulesPresetList() {
return axios.get('/api/list/rules-preset');
}

View File

@@ -0,0 +1,514 @@
<template>
<div class="container">
<Breadcrumb :items="['menu.list', 'menu.list.searchTable']" />
<a-card class="general-card" :title="$t('menu.list.searchTable')">
<a-row>
<a-col :flex="1">
<a-form
:model="formModel"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 18 }"
label-align="left"
>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item
field="number"
:label="$t('searchTable.form.number')"
>
<a-input
v-model="formModel.number"
:placeholder="$t('searchTable.form.number.placeholder')"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item field="name" :label="$t('searchTable.form.name')">
<a-input
v-model="formModel.name"
:placeholder="$t('searchTable.form.name.placeholder')"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item
field="contentType"
:label="$t('searchTable.form.contentType')"
>
<a-select
v-model="formModel.contentType"
:options="contentTypeOptions"
:placeholder="$t('searchTable.form.selectDefault')"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item
field="filterType"
:label="$t('searchTable.form.filterType')"
>
<a-select
v-model="formModel.filterType"
:options="filterTypeOptions"
:placeholder="$t('searchTable.form.selectDefault')"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item
field="createdTime"
:label="$t('searchTable.form.createdTime')"
>
<a-range-picker
v-model="formModel.createdTime"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item
field="status"
:label="$t('searchTable.form.status')"
>
<a-select
v-model="formModel.status"
:options="statusOptions"
:placeholder="$t('searchTable.form.selectDefault')"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-col>
<a-divider style="height: 84px" direction="vertical" />
<a-col :flex="'86px'" style="text-align: right">
<a-space direction="vertical" :size="18">
<a-button type="primary" @click="search">
<template #icon>
<icon-search />
</template>
{{ $t('searchTable.form.search') }}
</a-button>
<a-button @click="reset">
<template #icon>
<icon-refresh />
</template>
{{ $t('searchTable.form.reset') }}
</a-button>
</a-space>
</a-col>
</a-row>
<a-divider style="margin-top: 0" />
<a-row style="margin-bottom: 16px">
<a-col :span="12">
<a-space>
<a-button type="primary">
<template #icon>
<icon-plus />
</template>
{{ $t('searchTable.operation.create') }}
</a-button>
<a-upload action="/">
<template #upload-button>
<a-button>
{{ $t('searchTable.operation.import') }}
</a-button>
</template>
</a-upload>
</a-space>
</a-col>
<a-col
:span="12"
style="display: flex; align-items: center; justify-content: end"
>
<a-button>
<template #icon>
<icon-download />
</template>
{{ $t('searchTable.operation.download') }}
</a-button>
<a-tooltip :content="$t('searchTable.actions.refresh')">
<div class="action-icon" @click="search"
><icon-refresh size="18"
/></div>
</a-tooltip>
<a-dropdown @select="handleSelectDensity">
<a-tooltip :content="$t('searchTable.actions.density')">
<div class="action-icon"><icon-line-height size="18" /></div>
</a-tooltip>
<template #content>
<a-doption
v-for="item in densityList"
:key="item.value"
:value="item.value"
:class="{ active: item.value === size }"
>
<span>{{ item.name }}</span>
</a-doption>
</template>
</a-dropdown>
<a-tooltip :content="$t('searchTable.actions.columnSetting')">
<a-popover
trigger="click"
position="bl"
@popup-visible-change="popupVisibleChange"
>
<div class="action-icon"><icon-settings size="18" /></div>
<template #content>
<div id="tableSetting">
<div
v-for="(item, index) in showColumns"
:key="item.dataIndex"
class="setting"
>
<div style="margin-right: 4px; cursor: move">
<icon-drag-arrow />
</div>
<div>
<a-checkbox
v-model="item.checked"
@change="
handleChange($event, item as TableColumnData, index)
"
>
</a-checkbox>
</div>
<div class="title">
{{ item.title === '#' ? '序列号' : item.title }}
</div>
</div>
</div>
</template>
</a-popover>
</a-tooltip>
</a-col>
</a-row>
<a-table
row-key="id"
:loading="loading"
:pagination="pagination"
:columns="(cloneColumns as TableColumnData[])"
:data="renderData"
:bordered="false"
:size="size"
@page-change="onPageChange"
>
<template #index="{ rowIndex }">
{{ rowIndex + 1 + (pagination.current - 1) * pagination.pageSize }}
</template>
<template #contentType="{ record }">
<a-space>
<a-avatar
v-if="record.contentType === 'img'"
:size="16"
shape="square"
>
<img
alt="avatar"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/581b17753093199839f2e327e726b157.svg~tplv-49unhts6dw-image.image"
/>
</a-avatar>
<a-avatar
v-else-if="record.contentType === 'horizontalVideo'"
:size="16"
shape="square"
>
<img
alt="avatar"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/77721e365eb2ab786c889682cbc721c1.svg~tplv-49unhts6dw-image.image"
/>
</a-avatar>
<a-avatar v-else :size="16" shape="square">
<img
alt="avatar"
src="//p3-armor.byteimg.com/tos-cn-i-49unhts6dw/ea8b09190046da0ea7e070d83c5d1731.svg~tplv-49unhts6dw-image.image"
/>
</a-avatar>
{{ $t(`searchTable.form.contentType.${record.contentType}`) }}
</a-space>
</template>
<template #filterType="{ record }">
{{ $t(`searchTable.form.filterType.${record.filterType}`) }}
</template>
<template #status="{ record }">
<span v-if="record.status === 'offline'" class="circle"></span>
<span v-else class="circle pass"></span>
{{ $t(`searchTable.form.status.${record.status}`) }}
</template>
<template #operations>
<a-button v-permission="['admin']" type="text" size="small">
{{ $t('searchTable.columns.operations.view') }}
</a-button>
</template>
</a-table>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, reactive, watch, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import useLoading from '@/hooks/loading';
import { queryPolicyList, PolicyRecord, PolicyParams } from '@/api/apiTable';
import { Pagination } from '@/types/global';
import type { SelectOptionData } from '@arco-design/web-vue/es/select/interface';
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
import cloneDeep from 'lodash/cloneDeep';
import Sortable from 'sortablejs';
type SizeProps = 'mini' | 'small' | 'medium' | 'large';
type Column = TableColumnData & { checked?: true };
const generateFormModel = () => {
return {
number: '',
name: '',
contentType: '',
filterType: '',
createdTime: [],
status: '',
};
};
const { loading, setLoading } = useLoading(true);
const { t } = useI18n();
const renderData = ref<PolicyRecord[]>([]);
const formModel = ref(generateFormModel());
const cloneColumns = ref<Column[]>([]);
const showColumns = ref<Column[]>([]);
const size = ref<SizeProps>('medium');
const basePagination: Pagination = {
current: 1,
pageSize: 20,
};
const pagination = reactive({
...basePagination,
});
const densityList = computed(() => [
{
name: t('searchTable.size.mini'),
value: 'mini',
},
{
name: t('searchTable.size.small'),
value: 'small',
},
{
name: t('searchTable.size.medium'),
value: 'medium',
},
{
name: t('searchTable.size.large'),
value: 'large',
},
]);
const columns = computed<TableColumnData[]>(() => [
{
title: t('searchTable.columns.index'),
dataIndex: 'index',
slotName: 'index',
},
{
title: t('searchTable.columns.number'),
dataIndex: 'number',
},
{
title: t('searchTable.columns.name'),
dataIndex: 'name',
},
{
title: t('searchTable.columns.contentType'),
dataIndex: 'contentType',
slotName: 'contentType',
},
{
title: t('searchTable.columns.filterType'),
dataIndex: 'filterType',
},
{
title: t('searchTable.columns.count'),
dataIndex: 'count',
},
{
title: t('searchTable.columns.createdTime'),
dataIndex: 'createdTime',
},
{
title: t('searchTable.columns.status'),
dataIndex: 'status',
slotName: 'status',
},
{
title: t('searchTable.columns.operations'),
dataIndex: 'operations',
slotName: 'operations',
},
]);
const contentTypeOptions = computed<SelectOptionData[]>(() => [
{
label: t('searchTable.form.contentType.img'),
value: 'img',
},
{
label: t('searchTable.form.contentType.horizontalVideo'),
value: 'horizontalVideo',
},
{
label: t('searchTable.form.contentType.verticalVideo'),
value: 'verticalVideo',
},
]);
const filterTypeOptions = computed<SelectOptionData[]>(() => [
{
label: t('searchTable.form.filterType.artificial'),
value: 'artificial',
},
{
label: t('searchTable.form.filterType.rules'),
value: 'rules',
},
]);
const statusOptions = computed<SelectOptionData[]>(() => [
{
label: t('searchTable.form.status.online'),
value: 'online',
},
{
label: t('searchTable.form.status.offline'),
value: 'offline',
},
]);
const fetchData = async (
params: PolicyParams = { current: 1, pageSize: 20 }
) => {
setLoading(true);
try {
const { data } = await queryPolicyList(params);
renderData.value = data.list;
pagination.current = params.current;
pagination.total = data.total;
} catch (err) {
// you can report use errorHandler or other
} finally {
setLoading(false);
}
};
const search = () => {
fetchData({
...basePagination,
...formModel.value,
} as unknown as PolicyParams);
};
const onPageChange = (current: number) => {
fetchData({ ...basePagination, current });
};
fetchData();
const reset = () => {
formModel.value = generateFormModel();
};
const handleSelectDensity = (
val: string | number | Record<string, any> | undefined,
e: Event
) => {
size.value = val as SizeProps;
};
const handleChange = (
checked: boolean | (string | boolean | number)[],
column: Column,
index: number
) => {
if (!checked) {
cloneColumns.value = showColumns.value.filter(
(item) => item.dataIndex !== column.dataIndex
);
} else {
cloneColumns.value.splice(index, 0, column);
}
};
const exchangeArray = <T extends Array<any>>(
array: T,
beforeIdx: number,
newIdx: number,
isDeep = false
): T => {
const newArray = isDeep ? cloneDeep(array) : array;
if (beforeIdx > -1 && newIdx > -1) {
// 先替换后面的,然后拿到替换的结果替换前面的
newArray.splice(
beforeIdx,
1,
newArray.splice(newIdx, 1, newArray[beforeIdx]).pop()
);
}
return newArray;
};
const popupVisibleChange = (val: boolean) => {
if (val) {
nextTick(() => {
const el = document.getElementById('tableSetting') as HTMLElement;
const sortable = new Sortable(el, {
onEnd(e: any) {
const { oldIndex, newIndex } = e;
exchangeArray(cloneColumns.value, oldIndex, newIndex);
exchangeArray(showColumns.value, oldIndex, newIndex);
},
});
});
}
};
watch(
() => columns.value,
(val) => {
cloneColumns.value = cloneDeep(val);
cloneColumns.value.forEach((item, index) => {
item.checked = true;
});
showColumns.value = cloneDeep(cloneColumns.value);
},
{ deep: true, immediate: true }
);
</script>
<script lang="ts">
export default {
name: 'SearchTable',
};
</script>
<style scoped lang="less">
.container {
padding: 0 20px 20px 20px;
}
:deep(.arco-table-th) {
&:last-child {
.arco-table-th-item-title {
margin-left: 16px;
}
}
}
.action-icon {
margin-left: 12px;
cursor: pointer;
}
.active {
color: #0960bd;
background-color: #e3f4fc;
}
.setting {
display: flex;
align-items: center;
width: 200px;
.title {
margin-left: 12px;
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,44 @@
export default {
'menu.list.searchTable': 'Search Table',
'searchTable.form.number': 'Set Number',
'searchTable.form.number.placeholder': 'Please enter Set Number',
'searchTable.form.name': 'Set Name',
'searchTable.form.name.placeholder': 'Please enter Set Name',
'searchTable.form.contentType': 'Content Type',
'searchTable.form.contentType.img': 'image-text',
'searchTable.form.contentType.horizontalVideo': 'Horizontal short video',
'searchTable.form.contentType.verticalVideo': 'Vertical short video',
'searchTable.form.filterType': 'Filter Type',
'searchTable.form.filterType.artificial': 'artificial',
'searchTable.form.filterType.rules': 'Rules',
'searchTable.form.createdTime': 'Create Date',
'searchTable.form.status': 'Status',
'searchTable.form.status.online': 'Online',
'searchTable.form.status.offline': 'Offline',
'searchTable.form.search': 'Search',
'searchTable.form.reset': 'Reset',
'searchTable.form.selectDefault': 'All',
'searchTable.operation.create': 'Create',
'searchTable.operation.import': 'Import',
'searchTable.operation.download': 'Download',
// columns
'searchTable.columns.index': '#',
'searchTable.columns.number': 'Set Number',
'searchTable.columns.name': 'Set Name',
'searchTable.columns.contentType': 'Content Type',
'searchTable.columns.filterType': 'Filter Type',
'searchTable.columns.count': 'Count',
'searchTable.columns.createdTime': 'CreatedTime',
'searchTable.columns.status': 'Status',
'searchTable.columns.operations': 'Operations',
'searchTable.columns.operations.view': 'View',
// size
'searchTable.size.mini': 'mini',
'searchTable.size.small': 'small',
'searchTable.size.medium': 'middle',
'searchTable.size.large': 'large',
// actions
'searchTable.actions.refresh': 'refresh',
'searchTable.actions.density': 'density',
'searchTable.actions.columnSetting': 'columnSetting',
};

View File

@@ -0,0 +1,45 @@
export default {
'menu.list.searchTable': '查询表格',
'searchTable.form.number': '集合编号',
'searchTable.form.number.placeholder': '请输入集合编号',
'searchTable.form.name': '集合名称',
'searchTable.form.name.placeholder': '请输入集合名称',
'searchTable.form.contentType': '内容体裁',
'searchTable.form.contentType.img': '图文',
'searchTable.form.contentType.horizontalVideo': '横版短视频',
'searchTable.form.contentType.verticalVideo': '竖版小视频',
'searchTable.form.filterType': '筛选方式',
'searchTable.form.filterType.artificial': '人工筛选',
'searchTable.form.filterType.rules': '规则筛选',
'searchTable.form.createdTime': '创建时间',
'searchTable.form.status': '状态',
'searchTable.form.status.online': '已上线',
'searchTable.form.status.offline': '已下线',
'searchTable.form.search': '查询',
'searchTable.form.reset': '重置',
'searchTable.form.selectDefault': '全部',
'searchTable.operation.create': '新建',
'searchTable.operation.import': '批量导入',
'searchTable.operation.download': '下载',
// columns
'searchTable.columns.index': '#',
'searchTable.columns.number': '集合编号',
'searchTable.columns.name': '集合名称',
'searchTable.columns.contentType': '内容体裁',
'searchTable.columns.filterType': '筛选方式',
'searchTable.columns.count': '内容量',
'searchTable.columns.createdTime': '创建时间',
'searchTable.columns.status': '状态',
'searchTable.columns.operations': '操作',
'searchTable.columns.operations.view': '查看',
// size
'searchTable.size.mini': '迷你',
'searchTable.size.small': '偏小',
'searchTable.size.medium': '中等',
'searchTable.size.large': '偏大',
// actions
'searchTable.actions.refresh': '刷新',
'searchTable.actions.density': '密度',
'searchTable.actions.columnSetting': '列设置',
};

View File

@@ -36,16 +36,13 @@
<dependency> <dependency>
<groupId>org.apache.dubbo</groupId> <groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId> <artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency> </dependency>
<!-- nacos注册中心 <!-- nacos注册中心 -->
<dependency> <dependency>
<groupId>com.alibaba.boot</groupId> <groupId>org.apache.dubbo</groupId>
<artifactId>nacos-discovery-spring-boot-starter</artifactId> <artifactId>dubbo-nacos-spring-boot-starter</artifactId>
<version>0.2.1</version>
</dependency> </dependency>
-->
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
@@ -80,6 +77,18 @@
</dependency> </dependency>
</dependencies> </dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-bom</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build> <build>
<plugins> <plugins>
<!-- 打包时跳过测试 --> <!-- 打包时跳过测试 -->

View File

@@ -1,7 +1 @@
server.port=8082
spring.application.name=story-service
dubbo.protocol.name=dubbo
dubbo.protocol.port=-1
dubbo.application.qos-enable=false
dubbo.registry.address=zookeeper://localhost:2181 dubbo.registry.address=zookeeper://localhost:2181

View File

@@ -0,0 +1,2 @@
dubbo.registry.address=nacos://localhost:8848

View File

@@ -1 +1,9 @@
spring.profiles.active=dev spring.profiles.active=test
server.port=8082
spring.application.name=story-service
dubbo.protocol.name=dubbo
dubbo.protocol.port=-1
dubbo.application.qos-enable=false
dubbo.registry.address=zookeeper://localhost:2181

View File

@@ -64,10 +64,10 @@
<artifactId>dubbo-spring-boot-starter</artifactId> <artifactId>dubbo-spring-boot-starter</artifactId>
</dependency> </dependency>
<!-- nacos注册中心 -->
<dependency> <dependency>
<groupId>com.alibaba.boot</groupId> <groupId>org.apache.dubbo</groupId>
<artifactId>nacos-discovery-spring-boot-starter</artifactId> <artifactId>dubbo-nacos-spring-boot-starter</artifactId>
<version>0.2.1</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -85,6 +85,12 @@
<artifactId>spring-boot-starter-jdbc</artifactId> <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency> <dependency>
<groupId>commons-codec</groupId> <groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId> <artifactId>commons-codec</artifactId>
@@ -103,6 +109,13 @@
<artifactId>hibernate-validator</artifactId> <artifactId>hibernate-validator</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<!-- test-->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
@@ -153,14 +166,10 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.dubbo</groupId> <groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId> <artifactId>dubbo-bom</artifactId>
<version>${dubbo.version}</version> <version>${dubbo.version}</version>
</dependency> <type>pom</type>
<scope>import</scope>
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>nacos-discovery-spring-boot-starter</artifactId>
<version>0.2.1</version>
</dependency> </dependency>
<dependency> <dependency>

View File

@@ -12,53 +12,5 @@ public class SopConstants {
public static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8; public static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8;
public static final String UTF8 = "UTF-8"; public static final String UTF8 = "UTF-8";
public static final String FORMAT_JSON = "json";
public static final String DEFAULT_SIGN_METHOD = "md5";
public static final String EMPTY_JSON = "{}";
public static final String METADATA_SERVER_CONTEXT_PATH = "server.servlet.context-path";
public static final String METADATA_SERVER_CONTEXT_PATH_COMPATIBILITY = "context-path";
/**
* 在拦截器中调用获取参数:
* String cachedBody = (String)exchange.getAttribute(SopConstants.CACHE_REQUEST_BODY_OBJECT_KEY);
*/
public static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";
/**
* 在拦截器中调用获取参数:
* Map<String, String> params = exchange.getAttribute(SopConstants.CACHE_REQUEST_BODY_FOR_MAP);
*/
public static final String CACHE_REQUEST_BODY_FOR_MAP = "cacheRequestBodyForMap";
public static final String CACHE_API_PARAM = "cacheApiParam";
public static final String CACHE_UPLOAD_REQUEST = "cacheUploadRequest";
public static final String X_SERVICE_ERROR_CODE = "x-service-error-code";
public static final String X_SERVICE_ERROR_MESSAGE = "x-service-error-message";
public static final String X_SERVICE_ERROR_RESPONSE = "x-service-error-response";
public static final int BIZ_ERROR_STATUS = 4000;
public static final int UNKNOWN_ERROR_STATUS = 5050;
public static final String UNKNOWN_SERVICE= "_sop_unknown_service_";
public static final String UNKNOWN_METHOD = "_sop_unknown_method_";
public static final String UNKNOWN_VERSION = "_sop_unknown_version_";
public static final String METADATA_ENV_KEY = "env";
public static final String METADATA_ENV_PRE_VALUE = "pre";
public static final String METADATA_ENV_GRAY_VALUE = "gray";
public static final String CACHE_ROUTE_INTERCEPTOR_CONTEXT = "cacheRouteInterceptorContext";
public static final String TARGET_SERVICE = "sop-target-service";
public static final String RESTFUL_REQUEST = "sop-restful-request";
public static final String METADATA_KEY_TIME_STARTUP = "server.startup-time";
public static final String CACHE_ROUTE_INFO = "cacheRouteInfo";
} }

View File

@@ -0,0 +1,14 @@
package com.gitee.sop.index.config;
import org.springframework.context.annotation.Configuration;
/**
* 自定的扩展组件放这里
*
* @author 六如
*/
@Configuration
public class CustomConfig {
}

View File

@@ -1,11 +1,18 @@
package com.gitee.sop.index.config; package com.gitee.sop.index.config;
import com.gitee.sop.index.message.ErrorFactory; import com.gitee.sop.index.message.ErrorFactory;
import com.gitee.sop.index.service.ParamExecutor;
import com.gitee.sop.index.service.ParamExecutorImpl;
import com.gitee.sop.index.service.RouteService;
import com.gitee.sop.index.service.RouteServiceImpl;
import com.gitee.sop.index.service.interceptor.internal.ResultRouteInterceptor; import com.gitee.sop.index.service.interceptor.internal.ResultRouteInterceptor;
import com.gitee.sop.index.service.manager.impl.LocalApiCacheManagerImpl; import com.gitee.sop.index.service.manager.ApiManager;
import com.gitee.sop.index.service.manager.IsvManager;
import com.gitee.sop.index.service.manager.SecretManager;
import com.gitee.sop.index.service.manager.impl.LocalApiManagerImpl;
import com.gitee.sop.index.service.manager.impl.LocalIsvManagerImpl; import com.gitee.sop.index.service.manager.impl.LocalIsvManagerImpl;
import com.gitee.sop.index.service.manager.impl.LocalSecretManagerImpl; import com.gitee.sop.index.service.manager.impl.LocalSecretManagerImpl;
import com.gitee.sop.index.service.manager.impl.RedisApiCacheManagerImpl; import com.gitee.sop.index.service.manager.impl.RedisApiManagerImpl;
import com.gitee.sop.index.service.manager.impl.RedisIsvManagerImpl; import com.gitee.sop.index.service.manager.impl.RedisIsvManagerImpl;
import com.gitee.sop.index.service.manager.impl.RedisSecretManager; import com.gitee.sop.index.service.manager.impl.RedisSecretManager;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -24,38 +31,38 @@ import javax.annotation.PostConstruct;
public class IndexConfig { public class IndexConfig {
@Bean @Bean
@ConditionalOnProperty(value = "manager.api-cache", havingValue = "local", matchIfMissing = true) @ConditionalOnProperty(value = "manager.api", havingValue = "local", matchIfMissing = true)
public LocalApiCacheManagerImpl localApiCacheManager() { public ApiManager localApiManager() {
return new LocalApiCacheManagerImpl(); return new LocalApiManagerImpl();
} }
@Bean @Bean
@ConditionalOnProperty(value = "manager.api-cache", havingValue = "redis") @ConditionalOnProperty(value = "manager.api", havingValue = "redis")
public RedisApiCacheManagerImpl redisApiCacheManager() { public ApiManager redisApiManager() {
return new RedisApiCacheManagerImpl(); return new RedisApiManagerImpl();
} }
@Bean @Bean
@ConditionalOnProperty(value = "manager.isv", havingValue = "local", matchIfMissing = true) @ConditionalOnProperty(value = "manager.isv", havingValue = "local", matchIfMissing = true)
public LocalIsvManagerImpl localIsvManager() { public IsvManager localIsvManager() {
return new LocalIsvManagerImpl(); return new LocalIsvManagerImpl();
} }
@Bean @Bean
@ConditionalOnProperty(value = "manager.isv", havingValue = "redis") @ConditionalOnProperty(value = "manager.isv", havingValue = "redis")
public RedisIsvManagerImpl redisIsvManager() { public IsvManager redisIsvManager() {
return new RedisIsvManagerImpl(); return new RedisIsvManagerImpl();
} }
@Bean @Bean
@ConditionalOnProperty(value = "manager.secret", havingValue = "local", matchIfMissing = true) @ConditionalOnProperty(value = "manager.secret", havingValue = "local", matchIfMissing = true)
public LocalSecretManagerImpl localSecretManager() { public SecretManager localSecretManager() {
return new LocalSecretManagerImpl(); return new LocalSecretManagerImpl();
} }
@Bean @Bean
@ConditionalOnProperty(value = "manager.secret", havingValue = "redis") @ConditionalOnProperty(value = "manager.secret", havingValue = "redis")
public RedisSecretManager redisSecretManager() { public SecretManager redisSecretManager() {
return new RedisSecretManager(); return new RedisSecretManager();
} }
@@ -67,6 +74,17 @@ public class IndexConfig {
return new ResultRouteInterceptor(); return new ResultRouteInterceptor();
} }
@Bean
@ConditionalOnMissingBean
public ParamExecutor paramExecutor() {
return new ParamExecutorImpl();
}
@Bean
@ConditionalOnMissingBean
public RouteService routeService() {
return new RouteServiceImpl();
}
@PostConstruct @PostConstruct
public void init() { public void init() {

View File

@@ -1,11 +1,10 @@
package com.gitee.sop.index.controller; package com.gitee.sop.index.controller;
import com.gitee.sop.index.response.ApiResponse;
import com.gitee.sop.index.request.ApiRequestContext; import com.gitee.sop.index.request.ApiRequestContext;
import com.gitee.sop.index.request.ApiRequestContextFactory; import com.gitee.sop.index.request.ApiRequestContextFactory;
import com.gitee.sop.index.response.ApiResponse;
import com.gitee.sop.index.service.ParamExecutor;
import com.gitee.sop.index.service.RouteService; import com.gitee.sop.index.service.RouteService;
import com.gitee.sop.index.util.ResponseUtil;
import com.gitee.sop.support.request.FileData;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -27,28 +26,39 @@ public class IndexController {
@Resource @Resource
private RouteService routeService; private RouteService routeService;
@Resource
private ParamExecutor<HttpServletRequest, HttpServletResponse> paramExecutor;
@GetMapping("/") @GetMapping("/")
public void home(HttpServletResponse response) throws IOException { public void home(HttpServletResponse response) throws IOException {
response.getWriter().write("Open Platform"); response.getWriter().write("Open Platform");
// 跳转到网站首页
// response.sendRedirect("https://www.baidu.com");
} }
/** /**
* 请求入口 * 请求入口
* *
* @return 返回响应内容 * @apiNote 参数描述
<pre>
参数 类型 是否必填 最大长度 描述 示例值
app_id String 是 32 支付宝分配给开发者的应用ID 2014072300007148
method String 是 128 接口名称 alipay.trade.fastpay.refund.query
format String 否 40 仅支持JSON JSON
charset String 是 10 请求使用的编码格式如utf-8,gbk,gb2312等 utf-8
sign_type String 是 10 商户生成签名字符串所使用的签名算法类型目前支持RSA2和RSA推荐使用RSA2 RSA2
sign String 是 344 商户请求参数的签名串,详见签名 详见示例
timestamp String 是 19 发送请求的时间,格式"yyyy-MM-dd HH:mm:ss" 2014-07-24 03:07:50
version String 是 3 调用的接口版本固定为1.0 1.0
app_auth_token String 否 40 详见应用授权概述
biz_content String 是 请求参数的集合,最大长度不限,除公共参数外所有请求参数都必须放在这个参数中传递,具体参照各产品快速接入文档
</pre>
*/ */
@RequestMapping(value = "api", method = {RequestMethod.GET, RequestMethod.POST}) @RequestMapping(value = "${index.path:/api}", method = {RequestMethod.GET, RequestMethod.POST})
public void index(HttpServletRequest request, HttpServletResponse response) throws IOException { public void index(HttpServletRequest request, HttpServletResponse response) throws IOException {
ApiRequestContext apiRequestContext = ApiRequestContextFactory.build(request); ApiRequestContext apiRequestContext = paramExecutor.build(request);
ApiResponse apiResponse = routeService.route(apiRequestContext); ApiResponse apiResponse = routeService.route(apiRequestContext);
Object data = apiResponse.getData(); paramExecutor.write(apiRequestContext, apiResponse, response);
if (data instanceof FileData) {
FileData fileData = (FileData) data;
ResponseUtil.writerFile(fileData, response);
} else {
// 此处还可以判断charset,返回xml格式
ResponseUtil.writerJson(apiRequestContext, apiResponse, response);
}
} }
} }

View File

@@ -8,4 +8,8 @@ import com.gitee.sop.index.dao.entity.IsvKeys;
*/ */
public interface IsvKeysMapper extends BaseMapper<IsvKeys> { public interface IsvKeysMapper extends BaseMapper<IsvKeys> {
default IsvKeys getByAppId(String appId) {
return this.get(IsvKeys::getAppId, appId);
}
} }

View File

@@ -0,0 +1,26 @@
package com.gitee.sop.index.request;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author 六如
*/
@Getter
@AllArgsConstructor
public enum RequestFormatEnum {
NONE(""),
JSON("json"),
XML("xml"),
;
private final String value;
public static RequestFormatEnum of(String value) {
for (RequestFormatEnum requestFormatEnum : RequestFormatEnum.values()) {
if (requestFormatEnum.value.equalsIgnoreCase(value)) {
return requestFormatEnum;
}
}
return NONE;
}
}

View File

@@ -1,31 +0,0 @@
package com.gitee.sop.index.service;
import com.gitee.sop.index.common.ApiInfoDTO;
import com.gitee.sop.index.dao.entity.ApiInfo;
import com.gitee.sop.index.dao.mapper.ApiInfoMapper;
import com.gitee.sop.index.service.manager.ApiCacheManager;
import com.gitee.sop.index.util.CopyUtil;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @author 六如
*/
@Service
public class ApiInfoService {
@Resource
private ApiCacheManager apiCacheManager;
@Resource
private ApiInfoMapper apiInfoMapper;
public ApiInfoDTO getApi(String apiName, String apiVersion) {
return apiCacheManager.getOrElse(apiName, apiVersion, () -> {
ApiInfo apiInfo = apiInfoMapper.getByNameVersion(apiName, apiVersion);
return CopyUtil.copyBean(apiInfo, ApiInfoDTO::new);
});
}
}

View File

@@ -0,0 +1,33 @@
package com.gitee.sop.index.service;
import com.gitee.sop.index.request.ApiRequestContext;
import com.gitee.sop.index.response.ApiResponse;
import java.io.IOException;
/**
* 参数处理
*
* @author 六如
*/
public interface ParamExecutor<Req, Resp> {
/**
* 构建请求参数上下文
*
* @param request request
* @return 返回请求参数上下文
*/
ApiRequestContext build(Req request);
/**
* 结果返回写入
*
* @param apiRequestContext 请求参数上下文
* @param apiResponse 最终返回结果
* @param response response
* @throws IOException IOException
*/
void write(ApiRequestContext apiRequestContext, ApiResponse apiResponse, Resp response) throws IOException;
}

View File

@@ -0,0 +1,36 @@
package com.gitee.sop.index.service;
import com.gitee.sop.index.request.ApiRequestContext;
import com.gitee.sop.index.request.ApiRequestContextFactory;
import com.gitee.sop.index.response.ApiResponse;
import com.gitee.sop.index.util.ResponseUtil;
import com.gitee.sop.support.request.FileData;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 请求参数默认实现
*
* @author 六如
*/
public class ParamExecutorImpl implements ParamExecutor<HttpServletRequest, HttpServletResponse> {
@Override
public ApiRequestContext build(HttpServletRequest request) {
return ApiRequestContextFactory.build(request);
}
@Override
public void write(ApiRequestContext apiRequestContext, ApiResponse apiResponse, HttpServletResponse response) throws IOException {
Object data = apiResponse.getData();
if (data instanceof FileData) {
FileData fileData = (FileData) data;
ResponseUtil.writerFile(fileData, response);
} else {
// 此处还可以判断charset,返回xml格式
ResponseUtil.writerText(apiRequestContext, apiResponse, response);
}
}
}

View File

@@ -3,7 +3,6 @@ package com.gitee.sop.index.service;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.gitee.sop.index.common.ApiInfoDTO; import com.gitee.sop.index.common.ApiInfoDTO;
import com.gitee.sop.index.response.ApiResponse;
import com.gitee.sop.index.common.AttachmentNames; import com.gitee.sop.index.common.AttachmentNames;
import com.gitee.sop.index.common.ParamInfoDTO; import com.gitee.sop.index.common.ParamInfoDTO;
import com.gitee.sop.index.exception.ApiException; import com.gitee.sop.index.exception.ApiException;
@@ -12,6 +11,7 @@ import com.gitee.sop.index.message.ErrorEnum;
import com.gitee.sop.index.request.ApiRequest; import com.gitee.sop.index.request.ApiRequest;
import com.gitee.sop.index.request.ApiRequestContext; import com.gitee.sop.index.request.ApiRequestContext;
import com.gitee.sop.index.request.UploadContext; import com.gitee.sop.index.request.UploadContext;
import com.gitee.sop.index.response.ApiResponse;
import com.gitee.sop.index.service.interceptor.RouteInterceptor; import com.gitee.sop.index.service.interceptor.RouteInterceptor;
import com.gitee.sop.index.service.validate.Validator; import com.gitee.sop.index.service.validate.Validator;
import com.gitee.sop.index.util.ClassUtil; import com.gitee.sop.index.util.ClassUtil;
@@ -22,7 +22,6 @@ import org.apache.dubbo.common.utils.ClassUtils;
import org.apache.dubbo.rpc.RpcContext; import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.rpc.RpcContextAttachment; import org.apache.dubbo.rpc.RpcContextAttachment;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -34,7 +33,6 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@@ -44,7 +42,6 @@ import java.util.Optional;
* *
* @author 六如 * @author 六如
*/ */
@Service
@Slf4j @Slf4j
public class RouteServiceImpl implements RouteService { public class RouteServiceImpl implements RouteService {
@@ -204,7 +201,7 @@ public class RouteServiceImpl implements RouteService {
@PostConstruct @PostConstruct
public void init() { public void init() {
if (routeInterceptors == null) { if (routeInterceptors == null) {
routeInterceptors = Collections.emptyList(); routeInterceptors = new ArrayList<>();
} }
routeInterceptors.sort(Comparator.comparing(RouteInterceptor::getOrder)); routeInterceptors.sort(Comparator.comparing(RouteInterceptor::getOrder));
} }

View File

@@ -4,7 +4,7 @@ import com.gitee.sop.index.common.ApiInfoDTO;
import com.gitee.sop.index.common.StatusEnum; import com.gitee.sop.index.common.StatusEnum;
import com.gitee.sop.index.dao.entity.ApiInfo; import com.gitee.sop.index.dao.entity.ApiInfo;
import com.gitee.sop.index.dao.mapper.ApiInfoMapper; import com.gitee.sop.index.dao.mapper.ApiInfoMapper;
import com.gitee.sop.index.service.manager.ApiCacheManager; import com.gitee.sop.index.service.manager.ApiManager;
import com.gitee.sop.index.util.CopyUtil; import com.gitee.sop.index.util.CopyUtil;
import com.gitee.sop.support.service.ApiRegisterService; import com.gitee.sop.support.service.ApiRegisterService;
import com.gitee.sop.support.service.dto.RegisterDTO; import com.gitee.sop.support.service.dto.RegisterDTO;
@@ -21,7 +21,7 @@ import javax.annotation.Resource;
public class ApiRegisterServiceImpl implements ApiRegisterService { public class ApiRegisterServiceImpl implements ApiRegisterService {
@Resource @Resource
private ApiCacheManager apiCacheManager; private ApiManager apiCacheManager;
@Resource @Resource
private ApiInfoMapper apiInfoMapper; private ApiInfoMapper apiInfoMapper;

View File

@@ -5,6 +5,6 @@ package com.gitee.sop.index.service.interceptor;
*/ */
public class RouteInterceptorOrders { public class RouteInterceptorOrders {
public static final int DOWNLOAD = -10; public static final int RESULT_INTERCEPTOR = -1000;
} }

View File

@@ -3,6 +3,7 @@ package com.gitee.sop.index.service.interceptor.internal;
import com.gitee.sop.index.common.ApiInfoDTO; import com.gitee.sop.index.common.ApiInfoDTO;
import com.gitee.sop.index.request.ApiRequestContext; import com.gitee.sop.index.request.ApiRequestContext;
import com.gitee.sop.index.service.interceptor.RouteInterceptor; import com.gitee.sop.index.service.interceptor.RouteInterceptor;
import com.gitee.sop.index.service.interceptor.RouteInterceptorOrders;
import com.gitee.sop.support.request.CommonFileData; import com.gitee.sop.support.request.CommonFileData;
import com.gitee.sop.support.request.FileData; import com.gitee.sop.support.request.FileData;
@@ -19,6 +20,10 @@ import java.util.Objects;
public class ResultRouteInterceptor implements RouteInterceptor { public class ResultRouteInterceptor implements RouteInterceptor {
private static final String CLASS = "class"; private static final String CLASS = "class";
private static final String KEY_NAME = "name";
private static final String KEY_ORIGINAL_FILENAME = "originalFilename";
private static final String KEY_CONTENT_TYPE = "contentType";
private static final String KEY_BYTES = "bytes";
@Override @Override
public Object afterRoute(ApiRequestContext context, ApiInfoDTO apiInfoDTO, Object result) { public Object afterRoute(ApiRequestContext context, ApiInfoDTO apiInfoDTO, Object result) {
@@ -44,10 +49,10 @@ public class ResultRouteInterceptor implements RouteInterceptor {
"empty": false "empty": false
*/ */
CommonFileData commonFileData = new CommonFileData(); CommonFileData commonFileData = new CommonFileData();
commonFileData.setName(String.valueOf(map.get("name"))); commonFileData.setName(String.valueOf(map.get(KEY_NAME)));
commonFileData.setOriginalFilename(String.valueOf(map.get("originalFilename"))); commonFileData.setOriginalFilename(String.valueOf(map.get(KEY_ORIGINAL_FILENAME)));
commonFileData.setContentType(String.valueOf(map.get("contentType"))); commonFileData.setContentType(String.valueOf(map.get(KEY_CONTENT_TYPE)));
commonFileData.setData((byte[]) map.get("bytes")); commonFileData.setData((byte[]) map.get(KEY_BYTES));
return commonFileData; return commonFileData;
} }
} }
@@ -61,6 +66,6 @@ public class ResultRouteInterceptor implements RouteInterceptor {
@Override @Override
public int getOrder() { public int getOrder() {
return RouteInterceptor.super.getOrder(); return RouteInterceptorOrders.RESULT_INTERCEPTOR;
} }
} }

View File

@@ -7,7 +7,7 @@ import java.util.function.Supplier;
/** /**
* @author 六如 * @author 六如
*/ */
public interface ApiCacheManager { public interface ApiManager {
void save(ApiInfoDTO apiInfoDTO); void save(ApiInfoDTO apiInfoDTO);

View File

@@ -7,7 +7,18 @@ import com.gitee.sop.index.service.manager.dto.IsvDTO;
*/ */
public interface IsvManager { public interface IsvManager {
/**
* 获取isv信息
*
* @param appId appId
* @return 返回isv信息, 没有返回null
*/
IsvDTO getIsv(String appId); IsvDTO getIsv(String appId);
/**
* 重新加载isv信息到内存中
*
* @param appId appId
*/
void reload(String appId); void reload(String appId);
} }

View File

@@ -15,5 +15,5 @@ public interface SecretManager {
*/ */
String getIsvPublicKey(String appId); String getIsvPublicKey(String appId);
String reload(String appId);
} }

View File

@@ -1,27 +0,0 @@
package com.gitee.sop.index.service.manager.impl;
import com.gitee.sop.index.common.ApiInfoDTO;
import com.gitee.sop.index.service.manager.ApiCacheManager;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 本地存储接口信息.
* @author 六如
*/
public class LocalApiCacheManagerImpl implements ApiCacheManager {
private static final Map<String, ApiInfoDTO> CACHE = new ConcurrentHashMap<>();
@Override
public void save(ApiInfoDTO apiInfoDTO) {
String key = apiInfoDTO.buildApiNameVersion();
CACHE.put(key, apiInfoDTO);
}
@Override
public ApiInfoDTO get(String apiName, String apiVersion) {
return CACHE.get(apiName + apiVersion);
}
}

View File

@@ -0,0 +1,39 @@
package com.gitee.sop.index.service.manager.impl;
import com.gitee.sop.index.common.ApiInfoDTO;
import com.gitee.sop.index.dao.entity.ApiInfo;
import com.gitee.sop.index.dao.mapper.ApiInfoMapper;
import com.gitee.sop.index.service.manager.ApiManager;
import com.gitee.sop.index.util.CopyUtil;
import javax.annotation.Resource;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* 本地存储接口信息.
* @author 六如
*/
public class LocalApiManagerImpl implements ApiManager {
private static final Map<String, Optional<ApiInfoDTO>> CACHE = new ConcurrentHashMap<>();
@Resource
protected ApiInfoMapper apiInfoMapper;
@Override
public void save(ApiInfoDTO apiInfoDTO) {
String key = apiInfoDTO.buildApiNameVersion();
CACHE.put(key, Optional.of(apiInfoDTO));
}
@Override
public ApiInfoDTO get(String apiName, String apiVersion) {
String key = apiName + apiVersion;
return CACHE.computeIfAbsent(key, k-> {
ApiInfo apiInfo = apiInfoMapper.getByNameVersion(apiName, apiVersion);
return Optional.ofNullable(CopyUtil.copyBean(apiInfo, ApiInfoDTO::new));
}).orElse(null);
}
}

View File

@@ -7,23 +7,34 @@ import com.gitee.sop.index.service.manager.dto.IsvDTO;
import com.gitee.sop.index.util.CopyUtil; import com.gitee.sop.index.util.CopyUtil;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* @author 六如 * @author 六如
*/ */
public class LocalIsvManagerImpl implements IsvManager { public class LocalIsvManagerImpl implements IsvManager {
private static final Map<String, Optional<IsvDTO>> CACHE = new ConcurrentHashMap<>();
@Resource @Resource
private IsvInfoMapper isvInfoMapper; protected IsvInfoMapper isvInfoMapper;
@Override @Override
public IsvDTO getIsv(String appId) { public IsvDTO getIsv(String appId) {
return CACHE.computeIfAbsent(appId, k -> {
IsvInfo isvInfo = isvInfoMapper.getByAppId(appId); IsvInfo isvInfo = isvInfoMapper.getByAppId(appId);
return CopyUtil.copyBean(isvInfo, IsvDTO::new); return Optional.ofNullable(CopyUtil.copyBean(isvInfo, IsvDTO::new));
}).orElse(null);
} }
@Override @Override
public void reload(String appId) { public void reload(String appId) {
IsvInfo isvInfo = isvInfoMapper.getByAppId(appId);
IsvDTO isvDTO = CopyUtil.copyBean(isvInfo, IsvDTO::new);
CACHE.put(appId, Optional.ofNullable(isvDTO));
} }
} }

View File

@@ -5,19 +5,37 @@ import com.gitee.sop.index.dao.mapper.IsvKeysMapper;
import com.gitee.sop.index.service.manager.SecretManager; import com.gitee.sop.index.service.manager.SecretManager;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* @author 六如 * @author 六如
*/ */
public class LocalSecretManagerImpl implements SecretManager { public class LocalSecretManagerImpl implements SecretManager {
private static final Map<String, Optional<String>> CACHE = new ConcurrentHashMap<>();
@Resource @Resource
private IsvKeysMapper isvKeysMapper; protected IsvKeysMapper isvKeysMapper;
@Override @Override
public String getIsvPublicKey(String appId) { public String getIsvPublicKey(String appId) {
return isvKeysMapper.query() return CACHE.computeIfAbsent(appId, k -> {
String publicKey = isvKeysMapper.query()
.eq(IsvKeys::getAppId, appId) .eq(IsvKeys::getAppId, appId)
.getValue(IsvKeys::getPublicKeyIsv); .getValue(IsvKeys::getPublicKeyIsv);
return Optional.ofNullable(publicKey);
})
.orElse(null);
}
@Override
public String reload(String appId) {
String publicKey = isvKeysMapper.query()
.eq(IsvKeys::getAppId, appId)
.getValue(IsvKeys::getPublicKeyIsv);
CACHE.put(appId, Optional.ofNullable(publicKey));
return publicKey;
} }
} }

View File

@@ -1,24 +0,0 @@
package com.gitee.sop.index.service.manager.impl;
import com.gitee.sop.index.common.ApiInfoDTO;
import com.gitee.sop.index.service.manager.ApiCacheManager;
/**
* redis存储接口信息
*
* @author 六如
*/
public class RedisApiCacheManagerImpl implements ApiCacheManager {
@Override
public void save(ApiInfoDTO apiInfoDTO) {
}
@Override
public ApiInfoDTO get(String apiName, String apiVersion) {
return null;
}
}

View File

@@ -0,0 +1,62 @@
package com.gitee.sop.index.service.manager.impl;
import com.gitee.sop.index.common.ApiInfoDTO;
import com.gitee.sop.index.dao.entity.ApiInfo;
import com.gitee.sop.index.util.CopyUtil;
import com.gitee.sop.index.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
/**
* redis存储接口信息
*
* @author 六如
*/
@Slf4j
public class RedisApiManagerImpl extends LocalApiManagerImpl {
private static final String KEY_API = "sop:api";
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void save(ApiInfoDTO apiInfoDTO) {
String key = apiInfoDTO.buildApiNameVersion();
stringRedisTemplate.opsForHash().put(KEY_API, key, JsonUtil.toJSONString(apiInfoDTO));
}
protected void cache(String key, ApiInfo apiInfo) {
ApiInfoDTO apiInfoDTO = CopyUtil.copyBean(apiInfo, ApiInfoDTO::new);
stringRedisTemplate.opsForHash().put(KEY_API, key, JsonUtil.toJSONString(apiInfoDTO));
}
@Override
public ApiInfoDTO get(String apiName, String apiVersion) {
String key = apiName + apiName;
try {
Object value = stringRedisTemplate.opsForHash().get(KEY_API, key);
if (value == null) {
return null;
}
return JsonUtil.parseObject(String.valueOf(value), ApiInfoDTO.class);
} catch (Exception e) {
log.error("redis访问失败", e);
return super.get(apiName, apiVersion);
}
}
@PostConstruct
public void init() {
log.info("load apiInfo to redis");
List<ApiInfo> apiInfos = this.apiInfoMapper.listAll();
for (ApiInfo apiInfo : apiInfos) {
String key = apiInfo.getApiName() + apiInfo.getApiVersion();
this.cache(key, apiInfo);
}
}
}

View File

@@ -1,20 +1,61 @@
package com.gitee.sop.index.service.manager.impl; package com.gitee.sop.index.service.manager.impl;
import com.gitee.sop.index.service.manager.IsvManager; import com.gitee.sop.index.dao.entity.IsvInfo;
import com.gitee.sop.index.service.manager.dto.IsvDTO; import com.gitee.sop.index.service.manager.dto.IsvDTO;
import com.gitee.sop.index.util.CopyUtil;
import com.gitee.sop.index.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
/** /**
* @author 六如 * @author 六如
*/ */
public class RedisIsvManagerImpl implements IsvManager { @Slf4j
public class RedisIsvManagerImpl extends LocalIsvManagerImpl {
private static final String KEY_ISV = "sop:isv";
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override @Override
public IsvDTO getIsv(String appId) { public IsvDTO getIsv(String appId) {
return null; try {
Object value = stringRedisTemplate.opsForHash().get(KEY_ISV, appId);
if (value == null) {
IsvInfo isvInfo = this.isvInfoMapper.getByAppId(appId);
return this.cache(isvInfo);
}
return JsonUtil.parseObject(String.valueOf(value), IsvDTO.class);
} catch (Exception e) {
log.error("操作redis失败", e);
return super.getIsv(appId);
}
} }
@Override @Override
public void reload(String appId) { public void reload(String appId) {
IsvInfo isvInfo = isvInfoMapper.getByAppId(appId);
this.cache(isvInfo);
}
@PostConstruct
public void init() {
log.info("load isvInfo to redis");
List<IsvInfo> isvInfos = this.isvInfoMapper.listAll();
for (IsvInfo isvInfo : isvInfos) {
this.cache(isvInfo);
}
}
protected IsvDTO cache(IsvInfo isvInfo) {
IsvDTO isvDTO = CopyUtil.copyBean(isvInfo, IsvDTO::new);
stringRedisTemplate.opsForHash().put(KEY_ISV, isvInfo.getAppId(), JsonUtil.toJSONString(isvDTO));
return isvDTO;
} }
} }

View File

@@ -1,13 +1,59 @@
package com.gitee.sop.index.service.manager.impl; package com.gitee.sop.index.service.manager.impl;
import com.gitee.sop.index.service.manager.SecretManager; import com.gitee.sop.index.dao.entity.IsvKeys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Optional;
/** /**
* @author 六如 * @author 六如
*/ */
public class RedisSecretManager implements SecretManager { @Slf4j
public class RedisSecretManager extends LocalSecretManagerImpl {
private static final String KEY_ISV = "sop:sec";
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override @Override
public String getIsvPublicKey(String appId) { public String getIsvPublicKey(String appId) {
return ""; try {
Object value = stringRedisTemplate.opsForHash().get(KEY_ISV, appId);
if (value == null) {
return this.reload(appId);
} }
return String.valueOf(value);
} catch (Exception e) {
log.error("操作redis失败", e);
return super.getIsvPublicKey(appId);
}
}
@Override
public String reload(String appId) {
IsvKeys isvKeys = this.isvKeysMapper.getByAppId(appId);
return this.cache(appId, isvKeys);
}
protected String cache(String appId, IsvKeys isvKeys) {
String publicKey = Optional.ofNullable(isvKeys).map(IsvKeys::getPublicKeyIsv).orElse("");
stringRedisTemplate.opsForHash().put(KEY_ISV, appId, publicKey);
return publicKey;
}
@PostConstruct
public void init() {
log.info("load isvKey to redis");
List<IsvKeys> isvKeys = this.isvKeysMapper.listAll();
for (IsvKeys isvKey : isvKeys) {
this.cache(isvKey.getAppId(), isvKey);
}
}
} }

View File

@@ -1,15 +1,16 @@
package com.gitee.sop.index.service.validate; package com.gitee.sop.index.service.validate;
import com.gitee.sop.index.common.ApiInfoDTO; import com.gitee.sop.index.common.ApiInfoDTO;
import com.gitee.sop.index.request.ApiRequest;
import com.gitee.sop.index.request.ApiRequestContext;
import com.gitee.sop.index.common.ParamNames; import com.gitee.sop.index.common.ParamNames;
import com.gitee.sop.index.common.StatusEnum; import com.gitee.sop.index.common.StatusEnum;
import com.gitee.sop.index.config.ApiConfig; import com.gitee.sop.index.config.ApiConfig;
import com.gitee.sop.index.exception.ApiException; import com.gitee.sop.index.exception.ApiException;
import com.gitee.sop.index.message.ErrorEnum; import com.gitee.sop.index.message.ErrorEnum;
import com.gitee.sop.index.request.ApiRequest;
import com.gitee.sop.index.request.ApiRequestContext;
import com.gitee.sop.index.request.RequestFormatEnum;
import com.gitee.sop.index.request.UploadContext; import com.gitee.sop.index.request.UploadContext;
import com.gitee.sop.index.service.ApiInfoService; import com.gitee.sop.index.service.manager.ApiManager;
import com.gitee.sop.index.service.manager.IpBlacklistManager; import com.gitee.sop.index.service.manager.IpBlacklistManager;
import com.gitee.sop.index.service.manager.IsvApiPermissionManager; import com.gitee.sop.index.service.manager.IsvApiPermissionManager;
import com.gitee.sop.index.service.manager.IsvManager; import com.gitee.sop.index.service.manager.IsvManager;
@@ -27,7 +28,6 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@@ -41,9 +41,7 @@ import java.util.Locale;
@Service @Service
public class ApiValidator implements Validator { public class ApiValidator implements Validator {
private static final int MILLISECOND_OF_ONE_SECOND = 1000; private static final long MILLISECOND_OF_ONE_SECOND = 1000;
private static final List<String> FORMAT_LIST = Arrays.asList("json", "xml");
private final Signer signer = new AlipaySigner(); private final Signer signer = new AlipaySigner();
@@ -63,7 +61,7 @@ public class ApiValidator implements Validator {
private ApiConfig apiConfig; private ApiConfig apiConfig;
@Resource @Resource
private ApiInfoService apiInfoService; private ApiManager apiCacheManager;
@Resource @Resource
private IpBlacklistManager ipBlacklistManager; private IpBlacklistManager ipBlacklistManager;
@@ -94,7 +92,7 @@ public class ApiValidator implements Validator {
checkIP(apiRequestContext); checkIP(apiRequestContext);
ApiRequest apiRequest = apiRequestContext.getApiRequest(); ApiRequest apiRequest = apiRequestContext.getApiRequest();
ApiInfoDTO apiInfo = apiInfoService.getApi(apiRequest.getMethod(), apiRequest.getVersion()); ApiInfoDTO apiInfo = apiCacheManager.get(apiRequest.getMethod(), apiRequest.getVersion());
// 检查接口信息 // 检查接口信息
checkApiInfo(apiRequestContext, apiInfo); checkApiInfo(apiRequestContext, apiInfo);
@@ -271,8 +269,10 @@ public class ApiValidator implements Validator {
protected void checkFormat(ApiRequestContext apiRequestContext) { protected void checkFormat(ApiRequestContext apiRequestContext) {
ApiRequest apiRequest = apiRequestContext.getApiRequest(); ApiRequest apiRequest = apiRequestContext.getApiRequest();
String format = apiRequest.getFormat(); String format = apiRequest.getFormat();
boolean contains = FORMAT_LIST.contains(format.toLowerCase()); if (ObjectUtils.isEmpty(format)) {
if (!contains) { return;
}
if (RequestFormatEnum.of(format) == RequestFormatEnum.NONE) {
throw new ApiException(ErrorEnum.ISV_INVALID_FORMAT, apiRequestContext.getLocale(), throw new ApiException(ErrorEnum.ISV_INVALID_FORMAT, apiRequestContext.getLocale(),
apiRequest.takeNameVersion(), format); apiRequest.takeNameVersion(), format);
} }

View File

@@ -0,0 +1,29 @@
package com.gitee.sop.index.util;
import com.alibaba.fastjson2.JSON;
import org.springframework.util.ObjectUtils;
import java.util.Objects;
/**
* json工具类,默认用fastjson2实现
*
* @author 六如
*/
public class JsonUtil {
public static String toJSONString(Object object) {
if (object == null) {
return "null";
}
return JSON.toJSONString(object);
}
public static <T> T parseObject(String value, Class<T> clazz) {
if (Objects.equals(value, "null") || ObjectUtils.isEmpty(value)) {
return null;
}
return JSON.parseObject(value, clazz);
}
}

View File

@@ -1,8 +1,10 @@
package com.gitee.sop.index.util; package com.gitee.sop.index.util;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.gitee.sop.index.request.ApiRequest; import com.gitee.sop.index.request.ApiRequest;
import com.gitee.sop.index.request.ApiRequestContext; import com.gitee.sop.index.request.ApiRequestContext;
import com.gitee.sop.index.request.RequestFormatEnum;
import com.gitee.sop.index.response.ApiResponse; import com.gitee.sop.index.response.ApiResponse;
import com.gitee.sop.support.request.FileData; import com.gitee.sop.support.request.FileData;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
@@ -16,7 +18,7 @@ import java.io.InputStream;
* @author 六如 * @author 六如
*/ */
public class ResponseUtil { public class ResponseUtil {
private static final XmlMapper XML_MAPPER = new XmlMapper();
public static void writerFile(FileData fileData, HttpServletResponse response) throws IOException { public static void writerFile(FileData fileData, HttpServletResponse response) throws IOException {
InputStream inputStream = fileData.getInputStream(); InputStream inputStream = fileData.getInputStream();
@@ -29,12 +31,20 @@ public class ResponseUtil {
IOUtils.copy(inputStream, response.getOutputStream()); IOUtils.copy(inputStream, response.getOutputStream());
} }
public static void writerJson(ApiRequestContext apiRequestContext, ApiResponse apiResponse, HttpServletResponse response) throws IOException { public static void writerText(ApiRequestContext apiRequestContext, ApiResponse apiResponse, HttpServletResponse response) throws IOException {
ApiRequest apiRequest = apiRequestContext.getApiRequest(); ApiRequest apiRequest = apiRequestContext.getApiRequest();
String charset = apiRequest.getCharset(); String charset = apiRequest.getCharset();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(charset); response.setCharacterEncoding(charset);
response.getWriter().write(JSON.toJSONString(apiResponse)); String format = apiRequest.getFormat();
if (RequestFormatEnum.of(format) == RequestFormatEnum.XML) {
response.setContentType(MediaType.APPLICATION_XML_VALUE);
String xml = XML_MAPPER.writeValueAsString(apiResponse);
response.getWriter().write(xml);
} else {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
String json = JSON.toJSONString(apiResponse);
response.getWriter().write(json);
}
} }

View File

@@ -1,5 +1,7 @@
dubbo.registry.address=zookeeper://localhost:2181 dubbo.registry.address=zookeeper://localhost:2181
mybatis.print-sql=true
# mysql config # mysql config
mysql.host=127.0.0.1:3306 mysql.host=127.0.0.1:3306
mysql.username=root mysql.username=root

View File

@@ -0,0 +1,20 @@
dubbo.registry.address=nacos://localhost:8848
mybatis.print-sql=true
# api manager,local/redis
manager.api=redis
# isv manager,local/redis
manager.isv=redis
# secret manger,local/redis
manager.secret=redis
# mysql config
mysql.host=127.0.0.1:3306
mysql.username=root
mysql.password=root
# redis config
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0

View File

@@ -1,7 +1,18 @@
spring.profiles.active=dev spring.profiles.active=test
spring.application.name=sop-index spring.application.name=sop-index
server.port=8081 server.port=8081
####### index config #######
# request entry path
index.path=/api
# api manager,local/redis
manager.api=local
# isv manager,local/redis
manager.isv=local
# secret manger,local/redis
manager.secret=local
####### dubbo config ####### ####### dubbo config #######
dubbo.protocol.name=dubbo dubbo.protocol.name=dubbo
dubbo.protocol.port=-1 dubbo.protocol.port=-1
@@ -40,16 +51,19 @@ spring.datasource.url=jdbc:mysql://${mysql.host}/${mysql.db}?useUnicode=true&cha
spring.datasource.username=${mysql.username} spring.datasource.username=${mysql.username}
spring.datasource.password=${mysql.password} spring.datasource.password=${mysql.password}
####### redis config #######
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.database=0
####### mybatis config ####### ####### mybatis config #######
mybatis.fill.com.gitee.fastmybatis.core.support.LocalDateTimeFillInsert=add_time mybatis.fill.com.gitee.fastmybatis.core.support.LocalDateTimeFillInsert=add_time
mybatis.fill.com.gitee.fastmybatis.core.support.LocalDateTimeFillUpdate=update_time mybatis.fill.com.gitee.fastmybatis.core.support.LocalDateTimeFillUpdate=update_time
# mybatis config file # mybatis config file
mybatis.config-location=classpath:mybatis/mybatisConfig.xml mybatis.config-location=classpath:mybatis/mybatisConfig.xml
# print SQL # print SQL
logging.level.com.gitee.sop.index.dao=error logging.level.com.gitee.sop.index.dao=error
logging.level.com.gitee.fastmybatis=debug logging.level.com.gitee.fastmybatis=info
mybatis.print-sql=true mybatis.print-sql=false
# Need not register api
open.register.enable=false

View File

@@ -68,6 +68,43 @@ public class AlipayClientPostTest extends TestBase {
System.out.println(responseData); System.out.println(responseData);
} }
// 输出返回xml格式
@Test
public void testGetXml() throws Exception {
// 公共请求参数
Map<String, String> params = new HashMap<String, String>();
params.put("app_id", appId);
params.put("method", "story.get");
params.put("format", "xml"); // xml
params.put("charset", "utf-8");
params.put("sign_type", "RSA2");
params.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
params.put("version", "1.0");
// 业务参数
Map<String, Object> bizContent = new HashMap<>();
bizContent.put("id", "1");
bizContent.put("name", "葫芦娃");
params.put("biz_content", JSON.toJSONString(bizContent));
String content = AlipaySignature.getSignContent(params);
String sign = AlipaySignature.rsa256Sign(content, privateKey, "utf-8");
params.put("sign", sign);
System.out.println("----------- 请求信息 -----------");
System.out.println("请求参数:" + buildParamQuery(params));
System.out.println("商户秘钥:" + privateKey);
System.out.println("待签名内容:" + content);
System.out.println("签名(sign)" + sign);
System.out.println("URL参数" + buildUrlQuery(params));
System.out.println("----------- 返回结果 -----------");
String responseData = postJson(url, params);// 发送请求
// <ApiResponse><code>0</code><msg>success</msg><sub_code></sub_code><sub_msg></sub_msg><data><name>乌鸦喝水</name><id>1</id></data></ApiResponse>
System.out.println(responseData);
}
@Test @Test
public void testFind() throws Exception { public void testFind() throws Exception {

View File

@@ -1,12 +1,10 @@
package com.gitee.sop.test; package com.gitee.sop.test;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -101,7 +99,7 @@ public class AllInOneTest extends TestBase {
Assert.assertEquals("isv.route-no-permissions", jsonObject.getString("sub_code")); Assert.assertEquals("isv.route-no-permissions", jsonObject.getString("sub_code"));
}); });
client.execute(requestBuilder); //client.execute(requestBuilder);
} }
/** /**