# api-frontend **Repository Path**: s1eep1022/api-frontend ## Basic Information - **Project Name**: api-frontend - **Description**: api-frontend - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2023-05-21 - **Last Updated**: 2025-03-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 项目流程图 ![img.png](img.png) # Ant Design Pro 官方文档:https://ant-design-pro.gitee.io/zh-CN/docs/getting-started ## 前端项目安装启动 ```js yarn install yarn start ``` ## 移除国际化 - i18n-remove 报错 - 执行 yarn add eslint-config-prettier --dev - 执行 yarn add eslint-plugin-unicorn --dev - 修改node_modules/@umijs/lint/dist/config/eslint/index.js文件:注释 // es2022: true ## open api生成前端接口调用 - package.json ```js "openapi": "max openapi", ``` - 配置 config/config.ts ```tsx export default defineConfig({ openAPI: [ { requestLibPath: "import { request } from '@umijs/max'", // 后端openapi 地址 schemaPath: 'http://localhost:8081/v3/api-docs', projectName: 'api-backend', }, ], }) ``` ## 请求配置 - src/requestConfig.ts ```tsx export const requestConfig: RequestConfig = { // 方式1:跨域访问方式,需要添加配置withCredentials: true, 才能携带cookie // baseURL: 'http://localhost:8081', // 方式2:公共前缀 + proxy 代理方式 config/proxy.ts baseURL: '/api', } ``` - 完整配置 ```tsx export const requestConfig: RequestConfig = { // 方式1:跨域访问方式,需要添加配置withCredentials: true, 才能携带cookie // baseURL: 'http://localhost:8081', // 方式2:公共前缀 + proxy 代理方式 config/proxy.ts baseURL: '/api', // 请求拦截器 requestInterceptors: [ (config: RequestOptions) => { // 拦截请求配置,进行个性化处理。 const url = config?.url; return { ...config, url }; }, ], // 响应拦截器 responseInterceptors: [ (response) => { // 拦截响应数据,进行个性化处理 // data: res 重新分配参数 const { data: res } = response as unknown as ResponseStructure; if (res?.success === false) { notification.error({ description: '请求错误', message: '提示', }); throw Error('请求错误'); } const { code, message, data } = res; if (code !== 0) { notification.error({ description: message, message: '提示', }); throw Error(message); } /** * TODO 这里返回response,实际后面调用获取的是response.data,也就是{ code, message, data } * 官方说通过配置 request.dataField = '' 可以获取原始数据,实际上未生效 */ return response; }, ], // 携带cookie 跨域请求 // withCredentials: true, }; ``` ## 登录 - src/pages/user/Login/index.tsx - 核心方法handleSubmit - 获取用户信息fetchUserInfo,调用src/app.tsx getInitialState() 获取 ```tsx const Login: React.FC = () => { const [userLoginState, setUserLoginState] = useState({}); const [type, setType] = useState('account'); const { initialState, setInitialState } = useModel('@@initialState'); const fetchUserInfo = async () => { // 获取userinfo信息,这里调用的fetchUserInfo是个Promisee函数 const userInfo = await initialState?.fetchUserInfo?.(); if (userInfo) { flushSync(() => { setInitialState((s) => ({ ...s, currentUser: userInfo, })); }); } }; const handleSubmit = async (values: API.UserLoginRequest) => { // 登录 try { // 判断能否正常登录 const res = await userLoginUsingPOST({ ...values, }); if (res) { const defaultLoginSuccessMessage = '登录成功!'; notification.success({ description: defaultLoginSuccessMessage, message: '提示', }); // 可以登录,获取用户信息并设置到全局状态 await fetchUserInfo(); console.log('fetchUserInfo initialState', initialState); const urlParams = new URL(window.location.href).searchParams; // setInitialState(res.data); console.log('routes', urlParams.get('redirect') || '/'); history.push(urlParams.get('redirect') || '/'); return; } } catch (e) { // 如果失败去设置用户错误信息 const state: API.LoginResult = { status: 'error', type: 'account' }; setUserLoginState(state); } }; }; ``` ## 全局用户状态 - src/app.tsx ```tsx /** * @see https://umijs.org/zh-CN/plugins/plugin-initial-state * 全局状态管理,通过useModel('@@initialState') 获取 * */ export async function getInitialState(): Promise<{ settings?: Partial; currentUser?: API.LoginUserVO; loading?: boolean; fetchUserInfo?: () => Promise; }> { // 获取用户信息 const fetchUserInfo = async () => { try { const res = await getLoginUserUsingGET(); console.log('fetchUserInfo res', res); return res.data; } catch (error) { history.push(loginPath); } return undefined; }; // 如果不是登录页面,执行 const { location } = history; if (location.pathname !== loginPath) { const currentUser = await fetchUserInfo(); return { fetchUserInfo, currentUser, settings: defaultSettings as Partial, }; } return { fetchUserInfo, settings: defaultSettings as Partial, }; } export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => { return { // 页面路由改变时回调 onPageChange: () => { const { location } = history; // 如果没有登录,重定向到 login if (!initialState?.currentUser && location.pathname !== loginPath) { history.push(loginPath); } }, }; }; ``` ## 登出 - src/components/RightContent/AvatarDropdown.tsx - userLogoutUsingPOST ```tsx export const AvatarDropdown: React.FC = ({ menu, children }) => { /** * 退出登录,并且将当前的 url 保存 */ const loginOut = async () => { await userLogoutUsingPOST(); } ... ``` ## 路由权限 - src/access.ts ```tsx /** * @see https://umijs.org/zh-CN/plugins/plugin-access * */ export default function access(initialState: { currentUser?: API.LoginUserVO } | undefined) { const { currentUser } = initialState ?? {}; return { canAdmin: currentUser && currentUser.userRole === 'admin', }; } ``` ## User注册 - Mybatis-Plus默认的主键策略是:ASSIGN_ID(使用雪花算法) ```java @TableName(value = "user") @Data public class User implements Serializable { /** * id 如果没有配置策略,则默认为ASSIGN_ID */ @TableId(type = IdType.ASSIGN_ID) private Long id; } ``` ## ProTable 表单 - protable通常用来做查询+ 列表的高级组件,但是也可以用来crud,官方文档提供参数type={'form'},来转换为表单,目前使用有bug - 方式一:request提交:重置表单会重复调用请求,解决方式,onReset函数会先于request调用,onReset设置标志区分是否reset - 方式二:onSubmit提交(建议使用这种)form 表单init数据 - ``` form={{ initialValues: !isAdd ? currentRow : {}, }} ``` - status Integer类型数据回显问题,valueEnum枚举类型,如果key是数字,回显会直接显示数字 - 暂时解决方式 ,提前赋值为字符串,record.status += ''; - Add/Update Form表单组件 - 封装单独组件 ```tsx {/* add or update form */} ``` - modal表单中传入值的问题:ProTable中会初始化一次属性,再次打开时候,组件已经赋值,所以有两种解决方式,要么监听外层属性变化,然后重新为表单赋值,要么销毁modal,下次打开重新加载数据 - 方式一:监听 ```tsx // ProForm ref const ref = useRef(); // 类似Vue watch, 参数:(执行某个函数,监听的属性列表) useEffect(() => { // 当外层传入值发生改变时重新设置表单值 ref?.current?.setFieldsValue(currentRow); }, [currentRow]); ``` - 方式二:直接销毁modal ```tsx ``` # 服务端 ## maven子项目总览 - api-admin 后端管理,为前端项目提供数据支持 - 用户登录,session存储用户状态 - 接口api的增删改查 - api-client-demo 集成starter的测试客户端 - 测试通过给定的ak ,sk以及starter提供的客户端能否成功调用服务端接口 - api-client-sdk ApiClient sdk - 提供访问api接口的功能 - api-client-sdk-autoconfiguration 自动配置ApiClient - api-client-sdk-starter 集成sdk starter - api-common 公共类库 - api-gateway - api网关,主要实现了白名单,api鉴权和日志打印的功能 - api-gateway-mock - 为gateway提供接口模拟测试 - api-interface api提供服务端 - 提供对外访问接口 - 通过拦截器判断是否是合法的api访问请求 ## 代码生成 - 插件 MybatisX - options Actual Column 与库表一致 - mybatis-plus3 ## Knife4j 接口文档配置 ```java /** * Knife4j 接口文档配置 * https://doc.xiaominfo.com/knife4j/documentation/get_start.html */ @Configuration @EnableSwagger2 @Profile({"dev", "test"}) public class Knife4jConfig { @Bean public Docket defaultApi2() { return new Docket(DocumentationType.OAS_30) .apiInfo(new ApiInfoBuilder() .title("接口文档") .description("api-backend") .version("1.0") .build()) .select() // 指定 Controller 扫描包路径 .apis(RequestHandlerSelectors.basePackage("io.web.api.controller")) .paths(PathSelectors.any()) .build(); } } ``` ## BeanValidation简化参数校验 - maven依赖 ```xml javax.validation validation-api 2.0.1.Final org.hibernate.validator hibernate-validator 6.0.11.Final compile ``` - BeanValidation快速失败配置(一个校验失败直接抛出) ```java @Configuration public class BeanValidationConfig { @Bean public Validator validator() { ValidatorFactory factory = Validation.byProvider(HibernateValidator.class) .configure() .failFast(true) .buildValidatorFactory(); return factory.getValidator(); } } ``` ## 接口调用 1. HttpClient 2. RestTemplate 3. Hutools ## API签名认证 本质: 1. 签发签名 2. 校验签名 ### 实现方式 > 为用户分配ak和sk,在进行api调用时,验证用户传递的和数据库分配的值是否一致 - AccessKey - SecretKey ## 参数 - 参数1:nonce 随机数 - 参数2:用户参数 - 参数3:时间戳 - 参数4:ak - 参数5:sign 用户参数 + 密钥(sk) => 签名算法 => 不可解密的加密值 ### 防重放? nonce随机数,只能用一次,服务端要保存随机数 加timestamp时间戳,校验时间戳是否过期 ## SDK 开发 - spring starter开发 ### Spring starter开发流程 - 提供一个用于请求的ApiClient,封装签名加密和请求发送(api-client-sdk module) - ApiClientAutoConfiguration,ApiClient的自动配置,判断是否引入ApiClient,并且在META-INF/spring.factories中引入此配置(api-client-sdk-autoconfiguration module) - 提供一个starter,整合ApiClient和ApiClientAutoConfiguration,方便外部使用(api-client-sdk-starter module) ## 接口发布下线 发布接口(admin) 1. 接口是否存在 2. 是否可以调用 3. 修改状态为1 下线接口(admin) 1. 接口是否存在 2. 状态为0 ## 接口模拟调试调用 ## 网关 - 统一接受和分发请求 - 鉴权 - 跨域 - 缓存 - 流量染色 - 访问控制 - 统一业务处理 - 发布控制 - 负载均衡 - 接口保护 - 限流 - 脱敏 - 降级 - 超时 - 统一日志 - 统一文档 ### 网关分类 1. 全局网关(接入层网关):负载均衡 2. 业务网关(微服务网关):请求转发到不同的项目/业务/接口/服务 ### 实现 1. Nginx(全局)、Kong(API网关) 2. Spring Cloud Gateway(取代了Zuul)、高性能 # Spring Cloud Gateway https://spring.io/projects/spring-cloud-gateway#overview ## maven依赖 ```xml 2021.0.7 org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import org.springframework.cloud spring-cloud-starter-gateway org.springframework.boot spring-boot-starter-test test ``` ## 配置断言和过滤器的两种方式 - Shortcut 快捷方式 ```yaml spring: cloud: gateway: routes: - id: after_route uri: https://example.org predicates: // 满足这个断言,cookie的名称是mycookie,value是mycookievalue - Cookie=mycookie,mycookievalue ``` - Fully Expand Arguments 参数展开 ```yaml spring: cloud: gateway: routes: - id: after_route uri: https://example.org predicates: - name: Cookie args: name: mycookie regexp: mycookievalue ``` ## 断言工厂 - `After` route predicate factory 在什么时间后 - `Before` route predicate factory 什么时间前 - `Between` route predicate factory 给定时间之间 - `Cookie` route predicate factory takes two parameters, the cookie `name` and a `regexp` (which is a Java regular expression). 给定Cookie - `Header` route predicate factory takes two parameters, the `header` and a `regexp` 请求头 - `Host` route predicate factory takes one parameter: a list of host name `patterns` 域名 - `Method` Route Predicate Factory takes a `methods` argument which is one or more parameters: the HTTP methods to match. 给定http请求 - `Path` Route Predicate Factory ```yaml spring: cloud: gateway: routes: - id: path_route uri: https://example.org predicates: - Path=/red/{segment},/blue/{segment} ``` - This route matches if the request path was, for example: `/red/1` or `/red/1/` or `/red/blue` or `/blue/green`. - If `matchTrailingSlash` is set to `false`, then request path `/red/1/` will not be matched. 不匹配行尾斜线 - ServerWebExchange.getAttributes() + ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE 获取 segment的值,或者 ```java Map uriVariables = ServerWebExchangeUtils.getUriTemplateVariables(exchange); String segment = uriVariables.get("segment"); ``` - `Query` route predicate factory - Query=green - Query=red, gree. 参数名称是red,值是gree. ,so `green` and `greet` would match. - `RemoteAddr` route predicate factory - `Weight` route predicate factory takes two arguments: `group` and `weight` (an int). 权重 ```yaml spring: cloud: gateway: routes: - id: weight_high uri: https://weighthigh.org predicates: - Weight=group1, 8 - id: weight_low uri: https://weightlow.org predicates: - Weight=group1, 2 ``` - `XForwarded Remote Addr` route predicate factory takes a list (min size 1) of `sources` ## 过滤器工厂 - `AddRequestHeader` `GatewayFilter` factory takes a `name` and `value` parameter - `AddRequestHeadersIfNotPresent` `GatewayFilter` factory takes a collection of `name` and `value` pairs separated by colon (冒号) - `AddRequestParameter` `GatewayFilter` Factory takes a `name` and `value` parameter. - `AddResponseHeader` `GatewayFilter` Factory takes a `name` and `value` parameter - Spring Cloud CircuitBreaker GatewayFilter - To enable the Spring Cloud CircuitBreaker filter, you need to place `spring-cloud-starter-circuitbreaker-reactor-resilience4j` on the classpath. 需要引入`resilience4j` - trip a circuit breaker based on the status code 根据状态码触发断路器 - Some situations necessitate reading the request body. 有些情况需要读取请求体。 请求只能被读取一次,使用 CacheRequestBody 缓存请求体 - `DedupeResponseHeader` GatewayFilter factory takes a `name` parameter and an optional `strategy` parameter. - FallbackHeaders 允许您在转发到外部应用程序中的fallbackUri的请求头中添加Spring Cloud CircuitBreaker执行异常细节 - The JSONToGRPCFilter GatewayFilter Factory converts a JSON payload to a gRPC request. - LocalResponseCache 响应缓存,规则 - 无主体GET请求。 - 指定响应码 200、206、301 - Cache-Control报头不允许(请求中没有存储或响应中没有存储或私有),则不缓存响应数据。 - 如果响应已经被缓存,并且在新请求Cache-Control头中 no-cache ,它将返回一个带有304(未修改)的无实体响应。 - The `MapRequestHeader` `GatewayFilter` factory takes `fromHeader` and `toHeader` parameters. - You can use the `ModifyRequestBody` filter to modify the request body before it is sent downstream by the gateway. 修改请求体 - You can use the `ModifyResponseBody` filter to modify the response body before it is sent back to the client.修改响应体 - The `PrefixPath` `GatewayFilter` factory takes a single `prefix` parameter. The following example configures a `PrefixPath` `GatewayFilter`: 添加请求前缀 - `PreserveHostHeader` `GatewayFilter` - `RedirectTo` `GatewayFilter` 重定向 - `RemoveJsonAttributesResponseBody` `GatewayFilter` factory takes a collection of `attribute names` to search for, 删除responsebody中的属性,最后一个参数可选(true则递归删除属性) - `RemoveRequestHeader` `GatewayFilter` factory takes a `name` parameter. - `RemoveRequestParameter` `GatewayFilter` factory takes a `name` parameter - `RemoveResponseHeader` `GatewayFilter` factory takes a `name` parameter - `RequestHeaderSize` `GatewayFilter` factory takes `maxSize` and `errorHeaderName` parameters. - `RequestRateLimiter` `GatewayFilter` factory uses a `RateLimiter` implementation to determine if the current request is allowed to proceed - KeyResolver - Redis RateLimiter, 添加`spring-boot-starter-data-redis-reactive` Spring Boot starter. - The `redis-rate-limiter.replenishRate` property defines how many requests per second to allow ,填充速率配置,允许每秒执行的请求 - The `redis-rate-limiter.burstCapacity` property 用户在一秒钟内允许的最大请求数(不丢弃任何请求)。 这是令牌桶可以容纳的令牌数量。将此值设置为零将阻止所有请求。 - The `redis-rate-limiter.requestedTokens` property 请求花费的令牌数量 , 这是为每个请求从桶中获取的令牌数量,默认为1。 - `RewriteLocationResponseHeader` `GatewayFilter` - `RewritePath` `GatewayFilter` factory - `/red/blue`, this sets the path to `/blue` ```yaml spring: cloud: gateway: routes: - id: rewritepath_route uri: https://example.org predicates: - Path=/red/** filters: - RewritePath=/red/?(?.*), /$\{segment} ``` - `RewriteResponseHeader` `GatewayFilter` - `SaveSession` `GatewayFilter` factory forces a `WebSession::save` - `SecureHeaders` `GatewayFilter` factory - `SecureHeaders` `GatewayFilter` factory ```yaml spring: cloud: gateway: routes: - id: setpath_route uri: https://example.org predicates: - Path=/red/{segment} filters: - SetPath=/{segment} ``` - `SetRequestHeader` `GatewayFilter` - `SetResponseHeader` `GatewayFilter` - `SetStatus` `GatewayFilter` factory - `StripPrefix` `GatewayFilter` 删掉前缀 ```yaml spring: cloud: gateway: routes: - id: nameRoot uri: https://nameservice predicates: - Path=/name/** filters: - StripPrefix=2 ``` - `Retry` `GatewayFilter` 重试 - `RequestSize` `GatewayFilter` 请求大小 ```yaml spring: cloud: gateway: routes: - id: request_size_route uri: http://localhost:8080/upload predicates: - Path=/upload filters: - name: RequestSize args: maxSize: 5000000 ``` - `SetRequestHostHeader` `GatewayFilter` - TokenRelayGatewayFilterFactory - Default Filters - To add a filter and apply it to all routes, you can use `spring.cloud.gateway.default-filters` ```yaml spring: cloud: gateway: default-filters: - AddResponseHeader=X-Response-Default-Red, Default-Blue - PrefixPath=/httpbin ``` ## GlobalFilter ```java @Bean public GlobalFilter customFilter() { return new CustomGlobalFilter(); } public class CustomGlobalFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { log.info("custom global filter"); return chain.filter(exchange); } @Override public int getOrder() { return -1; } } ``` ## GlobalFilter 和GatewayFilter Global Filters:全局过滤器,不需要配置路由,系统初始化作用到所有路由上。 GatewayFilter:需要配置某个路由,才能过滤。如果需要使用全局路由,需要配置Default Filters。 # RPC 1. Http、TCP 2. Dubbo 3. Open feign ## Feign ### 使用eureka注册中心方式 官方示例:https://github.com/spring-cloud-samples/feign-eureka.git - 要自行启动eureka注册中心 - 项目依赖的是 3.0.6版本的springboot,jdk要17 - 依赖 pom.xml ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.springframework.cloud spring-cloud-starter-openfeign ``` - application.yml ```yaml spring: application: name: eureka-server server: port: 8761 eureka: instance: prefer-ip-address: true client: register-with-eureka: false fetch-registry: false server: enable-self-preservation: false ``` - 启动类 ```java @EnableEurekaServer //通过注解触发自动配置 @SpringBootApplication public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } } ``` ### 不用注册中心,直接访问服务 feign配置,name是毕传的,url服务提供方地址 ``` @FeignClient(name = "${feign.name}", url = "${feign.url}") ``` ### Feign 配置 ```java /** * FooConfiguration. * 1.不需要被 @Configuration 修饰,在@FeignClient configuration 属性中引入 * 2.被 @Configuration 修饰,成为Feign的默认配置 * * @date 2023-06-02 */ @Configuration public class FooConfiguration { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } /** * 自定义拦截器 * @return */ @Bean public RequestInterceptor requestInterceptor() { return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { template.header("myheader", "value"); } }; } /** * 认证,会在请求头添加Authorization * @return */ @Bean public BasicAuthRequestInterceptor basicAuthRequestInterceptor() { return new BasicAuthRequestInterceptor("user", "user"); } } ``` ## Dubbo springboot官方实例: https://github.com/apache/dubbo-samples.git - 1-basic/dubbo-samples-spring-boot - 启动一个zk作为注册中心 ## 对外提供的接口 - 校验用户ak,sk,通过ak查询用户 - 接口是否存在 - 调用次数增加 # 可视化 DataV-React 文档:http://datav-react.jiaminghi.com/guide/#%E7%94%A8%E5%89%8D%E5%BF%85%E7%9C%8B ## 实现功能 接口调用次数前10 轮播 # 部署脚本 ```shell #!/bin/bash nohup java -jar -Dspring.datasource.password=root -Xmx256m api-admin.jar & nohup java -jar -Xmx256m api-gateway.jar & nohup java -jar -Xmx128m api-interface.jar & ``` # 可能遇到的问题 ## SpringBoot单元测试 - Test文件要在SpringBoot启动类包路径下,否则会报错 ```txt java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test ``` ## Hutool 的SecureUtil获取签名的问题 - 因为签名获取和认证不在一个jvm执行,所以默认构造方法会导致失败,因此提供一个privateKey和publicKey,确保每次获取的Sign对象加解密逻辑一致。 ```java private static final Sign sign = SecureUtil.sign(SignAlgorithm.MD5withRSA, privateHex(), publicHex()); /** * 获取签名 * * @param content * @return */ public static String sign(String content, String secretKey) { return sign.signHex(saltString(content, secretKey).getBytes(StandardCharsets.UTF_8)); } /** * 验证签名 * * @param content * @return */ public static boolean verify(String content, String signStr, String secretKey) { byte[] signed = HexUtil.decodeHex(signStr); return sign.verify(saltString(content, secretKey).getBytes(StandardCharsets.UTF_8), signed); } private static String saltString(String content, String secretKey) { return content + "." + secretKey; } public static void main(String[] args) { KeyPair pair = SecureUtil.generateKeyPair("RSA"); PrivateKey privateKey = pair.getPrivate(); PublicKey publicKey = pair.getPublic(); System.out.println(HexUtil.encodeHexStr(privateKey.getEncoded())); System.out.println(HexUtil.encodeHexStr(publicKey.getEncoded())); } // 使用上面生成的key static String privateHex() { return "xx"; } static String publicHex() { return "xx"; } } ``` ## HttpMediaTypeNotAcceptableException: Could not find acceptable representation - 实体类生成get set方法 ## Feign调用接口HttpMessageConverters异常 ### 错误1 > No qualifying bean of type 'org.springframework.boot.autoconfigure.http.HttpMessageConverters' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)} - 添加HttpMessageConverters bean ```java @Configuration public class FeignConfig { ## Spring Cloud Gateway Global Filter 不生效 - ServerHttpResponseDecorator 配置后没有生效,需要配置order小于-1,让优先级高于NettyWriteResponseFilter,使用实现Ordered接口的方式,使用注解不生效 @Bean @ConditionalOnMissingBean public HttpMessageConverters messageConverters(ObjectProvider> converters) { return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList())); } } ``` ### 错误2 > feign.codec.DecodeException: Type definition error: [simple type, class io.web.utils.BaseResponse]; - BaseResponse提供一个无参构造 ## Maven打jar包运行后显示 没有主清单属性 - 父依赖移除build配置 - 子项目需要编译成可运行jar的添加依赖 ```xml ${project.artifactId} org.springframework.boot spring-boot-maven-plugin ${springboot.version} org.projectlombok lombok repackage org.apache.maven.plugins maven-compiler-plugin 3.6.0 1.8 1.8 ``` ## 服务器部署设置hostname hostnamectl set-hostname name # 项目总结 虽然已经有几年工作经验了,不过工作做的项目比较传统,最近连着做了用户中心和api开发平台,收获还是挺多的,对于微服务架构也有了更多的理解,工作使用Vue多一些,React也在熟悉阶段,无论是开发还是部署都踩了不少坑,本次的项目总结和踩坑问题都放到前端项目Readme了,需要的可以自行下载。 项目技术总结: 1. 后端采用Maven工程搭建,使用了springboot框架,Mybatis Plus做数据层操作,数据库使用MySQL 2. Spring Cloud Gateway做统一网关,主要实现了白名单、api调用鉴权、 日志打印 3. 使用Feign作为系统间RPC调用,因为部署资源有限,采用直连方式,没有使用注册中心 4. 自定义SpringBoot starter,自动配置ApiClient sdk,封装了调用API接口的功能 5. 前端使用Ant Design Pro 、React框架,使用DataV做接口调用统计轮播图 6. 服务部署使用腾讯云,nginx做请求转发及前端容器