跳至主要內容

开发问题记录册

holic-x...大约 23 分钟

开发问题记录册

1.如何基于模板快速开发页面

开发软件版本

node:20.11.1

npm:10.2.4

全量ant design pro模板引用

​ 有时候官网文档打开卡顿,平时开发项目可能为了轻便,一般选择simple版本(基础的登录页面、一个参考的表格页面TableList)。但是开发过程中有时候发现官网打开太慢了,直接安装一个全量的脚手架参考配置。执行npm install报错,可能是版本兼容问题,调整为npm install --force,重新导入依赖尝试(参考解决方案open in new windownpm下载报错open in new window

​ 全量模板框架启动发现出现下面的问题(版本兼容问题考虑),如果出现路由嵌套问题、界面白屏则需进一步检查routes.ts中的路由配置,启动问题解决:解决方案参考open in new window

image-20240427140333433

​ 路由嵌套、界面白屏出现的情况大部分是因为路由配置出错(例如出现重复path或者重复根路径配置),可通过F12查看原因并处理,上图所示的问题解决方案是routes中的路由配置了/*,需要禁用模板中的404路由

2.如何实现搜索条件组合多个tab页面

​ 参考antd pro的ProTable(ToolBar配置),实现根据不同状态刷新列表数据(此处需考虑一个扩展问题,如果根据搜索框联动刷新不同的标签页面)

image-20240430081722662

image-20240430081725501

构建思路

​ 实现效果:以接口信息管理页面为例,将数据拆分为多个tab展示(每个tab对应不同状态下的数据)

(1)数据交互说明

前端页面交互:根据不同状态请求调用后端接口,然后封装tab数据

​ 例如tab有不同的筛选:所有(筛选所有,status不需要传)、其他状态(待审核、已下线等,需要传status)

​ 一种思路就是前端定义的tabKey和status保持一致,因此在请求接口交互的时候就不需要重复进行转化(例如要把前端的tabKey和后端交互的status进行转化,业务场景复杂的情况下推荐。如果是单一状态的请求则考虑进行转化,这个操作前后端都可以做)

​ 如果是前端做转化:则在发送请求前将tabKey转化为要检索的status

​ 如果是后端做转化:则根据tabKey或者检索类型告诉后端要检索什么内容,然后后端根据约定的值进行转化

​ 考虑到目前场景是比较基础的:查找所有的Tab、单一状态数据记录的Tab,因此最简单的实现方式就是约定status为-1的时候默认查找所有(-1必须是脱离原status业务状态设定的值,不要和业务字段值冲突即可,或者可以考虑多加一个请求字段searchType用于明确是查找所有还是分类查找概念,此处为了简化请求,采用约定值实现交互)

​ 当status为-1,后端校验其为查找所有,因此不会拼接封装status条件

​ 当status为其他业务参数,后端校验其为条件检索,根据status值封装status查找条件

(2)前端实现

  • 定义全局变量activeTabKey绑定当前激活tab的key值
  • 配置ProTable属性toolbar:配置tab页内容已经onChange触发事件(触发数据更新操作)
  • 配置ProTable属性request:请求后端交互将status作为请求参数传递

​ 如果配置完成发现点击标签页点击不动(没反应),检查是否存在重复的key定义

// 1.定义全局变量activeTabKey绑定当前激活tab的key值
const [activeTabKey, setActiveTabKey] = useState('-1');

// 配置ProTable属性
<ProTable
  -- 其他属性定义 --
  // 设置标签页
  		// 2.配置ProTable属性(toolbar:配置标签页参数)
        toolbar={{
          menu: {
            type: "tab", // inline、dropdown
            activeKey: activeTabKey,
            // defaultActiveKey: "-1", // 设置默认激活标签(此处初始化设定了activeTabKey不需要额外配置)
            onChange: (key: any) => {
              // 设置表单激活
              setActiveTabKey(key);
              if(actionRef?.current) {
                // 刷新表单数据
                actionRef?.current.reload();
              }
            },
            items: [
              // 为了避免重复转化,此处tabKey和后端接口status一致,或者在请求前自行转化
              // (此处注意key不能重复否则渲染失败,key相同指向同一个tab无法明确)
              {
                label: '所有',
                key: '-1'
              },
              {
                label: '待审核',
                key: '2'
              },
              {
                label: '已上线',
                key: '5'
              },
              {
                label: '已下线',
                key: '0'
              }
            ]
          }
        }}

        // 3.根据request规则,重新编写请求和响应处理
        request={async (params, sort: Record<string, SortOrder>, filter: Record<string, React.ReactText[] | null>) => {
          const res = await listInterfaceInfoByPageUsingPost({
            ...params,
            status: activeTabKey  // 如果查找全部则不需要执行状态,如果查找指定状态记录则传入指定状态即可
          })
          if (res?.data) {
            return {
              data: res?.data.records || [],
              success: true,
              total: res.total,
            }
          }
        }}
  
  		-- 其他属性定义 --
  
  >
  
</ProTable>  

(3)后端:接收status请求,然后根据status筛选记录(组合条件筛选概念)

​ 此处注意一个问题,status的类型是int,无法进行null判断(如果借助String.valueOf()转化会把null转化为字符串,这种校验毫无意义)。

​ 因此设定一个status约定的值(-1),这个值脱离业务场景字段值(不能和业务字段值重复),用于限定默认查找所有

​ 且后端的条件查找需要调整为按需查找。

​ 最终实现参考效果:

3.数据检索条件拼接的小坑(模糊搜索问题)

​ 原始实现方式考虑到多字段组合查询的情况,为了简化代码实现,默认InterfaceInfoRequest的所有字段赋值给到InterfaceInfo(用于结合QueryWrapper实现封装检索条件),那么此处就会存在一个问题,默认是所有字段都会加入条件检索,需要单独进行过滤配置。

image-20240430105354732

​ 例如此处前端通过设置status传入-1(希望当传入status为-1的时候默认筛选所有,即支持模糊搜索概念),但是如果没有了解其代码逻辑,就会忽略掉上面的步骤2,导致queryWrapper.like(status!=-1, "status", status);并没有达到理想预期,即当status为-1的时候没有做置空操作,则会沿用前面通过BeanUtils.copyProperties(interfaceInfoQueryRequest, interfaceInfoQuery);设定的参数。(一开始以为是MyBatis的锅,其实是代码逻辑没有考虑到多样场景支持)

解决方案

​ 最简单的解决方案就是如果哪些字段需要支持模糊搜索,则自行先置空然后再根据参数校验进行数据拼接(基于原有思路去做,🚨不建议,时间长了绝对会懵)

​ 最优解决方案就是按需引入:需要检索什么参数就设定什么内容,不要被一些默认预设项所影响导致后期代码排查困难,这点可以参考自定义实现一个获取查询条件转化的方法,然后在这个方法中自定义将筛选条件请求参数手动按需封装为相应的QueryWrapper(基于MyBatis-plus框架)

 public QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest) {
        if (userQueryRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
        }
        Long id = userQueryRequest.getId();
        String unionId = userQueryRequest.getUnionId();
        String mpOpenId = userQueryRequest.getMpOpenId();
        String userName = userQueryRequest.getUserName();
        String userProfile = userQueryRequest.getUserProfile();
        String userRole = userQueryRequest.getUserRole();
        String userAccount = userQueryRequest.getUserAccount();
        Integer userStatus = userQueryRequest.getUserStatus();
        String sortField = userQueryRequest.getSortField();
        String sortOrder = userQueryRequest.getSortOrder();
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq(id != null, "id", id);
        queryWrapper.eq(StringUtils.isNotBlank(unionId), "unionId", unionId);
        queryWrapper.eq(StringUtils.isNotBlank(mpOpenId), "mpOpenId", mpOpenId);
        queryWrapper.eq(StringUtils.isNotBlank(userRole), "userRole", userRole);
        queryWrapper.eq(StringUtils.isNotBlank(userAccount), "userAccount", userAccount);
        queryWrapper.eq(userStatus != null, "userStatus", userStatus);
        queryWrapper.like(StringUtils.isNotBlank(userProfile), "userProfile", userProfile);
        queryWrapper.like(StringUtils.isNotBlank(userName), "userName", userName);
        queryWrapper.orderBy(SqlUtils.validSortField(sortField), sortOrder.equals(CommonConstant.SORT_ORDER_ASC),
                sortField);
        return queryWrapper;
    }

​ 还有一种方式就是通过MyBatis的动态SQL构建的思路去做,针对搜索的内容,基于Mapper层构建动态SQL做拼接,这样考虑不需要做QueryRequest和对应实体类的转化,直接把QueryRequest(如果没有其他业务逻辑转化的话)请求作为参数传入mapper(特殊场景特殊分析),但一定要养成自己的编码规范,不然后期项目调试会蒙圈(调整内容参考)

public BaseResponse<Page<InterfaceInfo>> listInterfaceInfoByPage(@RequestBody InterfaceInfoQueryRequest interfaceInfoQueryRequest) {
        if (interfaceInfoQueryRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }

        // 校验请求参数,筛选条件封装
        long current = interfaceInfoQueryRequest.getCurrent();
        long size = interfaceInfoQueryRequest.getPageSize();
        String sortField = interfaceInfoQueryRequest.getSortField();
        String sortOrder = interfaceInfoQueryRequest.getSortOrder();
        String description = interfaceInfoQueryRequest.getDescription();
        // status为-1默认筛选所有数据(为其他状态则进行拼接)
        int status = interfaceInfoQueryRequest.getStatus();

        // 限制爬虫
        if (size > 50) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }

        // 封装筛选条件(按需封装)
        QueryWrapper<InterfaceInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.like(StringUtils.isNotBlank(description), "description", description);
        queryWrapper.orderBy(StringUtils.isNotBlank(sortField),
                sortOrder.equals(CommonConstant.SORT_ORDER_ASC), sortField);
        // 根据状态进行分类检索,如果传入status为-1则默认检索所有内容,如果为其他状态则默认拼接SQL
        queryWrapper.eq(status != -1, "status", status);
        // 获取分页数据
        Page<InterfaceInfo> interfaceInfoPage = interfaceInfoService.page(new Page<>(current, size), queryWrapper);
        return ResultUtils.success(interfaceInfoPage);
    }

搜索模块参考

1)搜索模块实现分析

​ 聚合搜索的内容可以参考模板的搜索模块(pages/list/search模块),可以结合其目录结构和具体实现去分析要实现这么一个功能需要哪些内容(最基础的就是把search整块的内容放到项目里面跑一下,边跑边调试分析),跑起来思路参考如下

1)去对比routes.ts配置,看search路由是如何配置的,在自身项目复刻一份

2)把search模块下的内容copy到自身项目,并配置正确的路由跑起来

3)如果过程中出现一些引用的东西缺失,则在对应的模块里面去找(例如一些额外的依赖引入等)

4)确认核心代码块:即组件定义和事件触发,例如要实现一个搜索功能,其搜索组件在哪里定义、触发什么搜索效果、搜索数据请求怎么封装等,梳理好这些思路再进行开发

5)分析代码目录结构、清理无用的内容:参考search模块,其下有index.tsx(主页面)、applications/articles/peoject三个文件夹分别对应存放了应用、文章、项目检索的实现,其中每个文件夹中定义的结构都是大同小异,无非是对应模块的一些代码实现(组件封装、依赖引用、样式定义等,根据文件后缀和存放模块去理解相应的实现和引用,就不会看昏头)

index.tsx : 主页面
	- applications(Tab组件定义,类似的还有articles、projects对应都是这套规范)
		- compontents (自定义组件定义)
			- A (组件A)
				- index.ts(组件A定义)
				- index.styles.ts(组件A相关样式)
			- B (组件B)
				- index.ts(组件B定义)
				- index.styles.ts(组件B相关样式)
		- utils(工具类抽离)
			- tuils.style.ts
		# 因为模板是静态页面,所以其数据交互都是通过静态数据进行mock,所以这部分会涉及数据类型定义、请求接口服务、模式数据定义等相关内容
		# 这些后续和后台交互都可以基于openapi进行规范生成
		- _mock.ts(模拟数据定义)
		- data.d.ts(数据格式定义:类似数据结构定义)
		- service.ts(模拟接口服务调用:和基于openapi构建的services文件中的内容是一个概念,只不过此处模拟的是假数据)
		- style.style.ts(该tab样式定义)
		- index.tsx(该tab的主页面)

# 参考上述结构,每个模块的结构定义都是大同小异,基于这套规范去梳理代码结构

6)从最简单的实现去做起,不要一步到位。例如想要实现一个什么样的功能,就去找对应组件做增删,不要一次性把自己的页面搞的复杂,后期看前端代码特别懵(做基础功能实现、后续再考虑完善样式)

​ 其实现核心是定义一个搜索模块主页面,然后基于主页面去控制检索的内容实现(通过切换不同的tab页实现检索效果),其下

image-20240428200233488

调整为相关内容,需要引入依赖。例如npm install dayjs、npm install numeral

​ 将原有模块嵌入:注意npm依赖引入、路由配置(不能重复路由)、模拟数据(缺少什么内容就需要补充什么),fake_list(调整为实际请求后台接口,基于这种思路构建即可),按需引入

问题1:检索的双向联动问题

1)搜索条件变更触发URL的变动并更新页面数据

2)URL的变动触发搜索事件并向搜索条件回绑到组件中

核心实现:确保单向处理,即在编写代码的时候选择一个方向考虑单向的逻辑实现,不要想着这个变了又会带动另一个变,只看单向,然后去实现这两个功能即可

1)怎么把search传给每个组件(类似父子组件?或者页面传递?)

​ 例如此处,点击tab页面进行切换触发页面切换效果并联动修改url地址,因此可以通过拼接url的方式传递给指定页面,然后每个对应的组件再从url‘中获取到检索的参数信息

2)如何获取从url拿到的参数

// 获取从Search/index.tsx 传入的参数 http://xxx:8000/search/articles?searchText='搜索文本'&searchType='搜索类型'
const searchParams = new URLSearchParams(location.search);
const searchText = searchParams.get('searchText');
const searchType = searchParams.get('searchType');
// alert(searchText+'--'+searchType)

​ 此处会涉及到一个问题:初步实现在index.tsx中通过设定Input.Search组件的tabList控制不同标签页的显示,其实现逻辑是通过history.push完成,路由跳转到指定的路径页面然后刷新进而实现联动。但是这里存在一个问题,就是如果想要实现url搜索联动到组件的话可能需要借助useRef进行操作。

​ 例如此处的处理逻辑初步是定义一个全局变量保存搜索条件值,当点击按钮触发的时候把搜索条件绑定到搜索组件或者全局变量中,这样就能够获取到对应的searchText,然后tab切换的时候根据当前的searchText做push的时候进行url拼接即可。但是这种情况会出现,点击一次搜索没反应,点击第二次的时候才会实现url跳转联动,但是页面数据并没有刷新。

​ 此处要注意onChange、onSubmit的用法,可能原有的antd pro框架设计初意在于通过这个事件触发表单搜索操作

2)页面定义参考说明

tab页面封装:使用<PageContainer>会把页面路由也封装进来,如果只想保留组件内容,可以用<div></div>标签进行封装

image-20240427220032106

BI模块参考

1)自定义弹窗实现交互(后台图表信息管理查看弹窗图表渲染)

​ 参考此前新增操作的实现,先确认弹窗基本内容(弹窗控制设定的属性),然后思考在页面中如何引入并使用弹窗

ShowChartModal构建:Props设定交互的参数和对应格式

import '@umijs/max';
import {Modal} from 'antd';
import React from 'react';

export type Props = {
  chartData: API.Chart;
  // 当用户点击取消按钮时触发
  onCancel: () => void;
  // 模态框是否可见
  visible: boolean;
};

const ShowChartModal: React.FC<Props> = (props) => {
  // 使用解构赋值获取props中的属性
  const {visible, chartData, onCancel} = props;

  return (
    // 创建一个Modal组件,通过visible属性控制其显示或隐藏,footer设置为null把表单项的'取消'和'确认'按钮去掉
    <Modal visible={visible} footer={null} onCancel={() => onCancel?.()}>
      {/* 创建一个组件,用于渲染图表信息 */}
      展示图表信息
    </Modal>
  );
};
export default ShowChartModal;

index中引入组件

// 1.引入弹窗组件
import ShowChartModal from "@/pages/Admin/Bi/Chart/components/ShowChartModal";
const Index: React.FC = () => {

  // 2.定义查看图表信息窗口的弹窗相关属性(控制弹窗的开启、关闭状态)、定义当前弹窗数据
  const [showChartModalOpen, handleShowChartModalOpen] = useState<boolean>(false);
  const [currentChartDataDetail, setCurrentChartDataDetail] = useState<API.Chart>();

  ------------------------  其他组件定义 ------------------------
 		 // 3.按钮触发事件定义(触发弹窗)
          <a key="showChart"
             onClick={() => {
               handleShowChartModalOpen(true);
             }}>
            查看
          </a>
------------------------  其他组件定义 ------------------------

 /**
   * 查看节点数据
   */
  const handleShowChart = (record:API.Chart)=>{
    // 校验当前选中行
    if (!record) return true;
    // 设置当前选中的弹窗数据
    setCurrentChartDataDetail(record);
    // 打开弹窗
    handleShowChartModalOpen(true);
  }


  return (
	  // 4.在合适的位置定义弹窗组件
      {/* 创建一个ShowChartModal组件,用于在点击查看图表按钮时弹出 */}
      <ShowChartModal
        chartData={currentChartDataDetail}
        // 当取消按钮被点击时,设置更新模态框为false以隐藏模态窗口
        onCancel={() => {
          handleShowChart(record);
        }}
        // 根据更新窗口的值决定模态窗口是否显示
        visible={showChartModalOpen}
      />

    </PageContainer>
  );
};

​ PS:主要关注此处的{chartData},应该传入的是当前选中的行,因此这部分还需再进一步完善。当指定某一行的数据对应按钮点击触发事件执行方法handleShowChart(record)并传入当前的record,然后在这个handleShowChart方法中自定义实现设置弹窗的值(定义一个全局变量存储要传递的弹窗的值),然后再触发打开弹窗

​ 此外,还需关注传入的record是否正常,因为初始化属性页面的时候所有的内容都是为null,要处理空指针异常,因为传入的是一条record记录,要获取里面的字段则需要record?.id,因为一开始刷新页面是没有指定哪行数据的,如果不做null判断就会报错。

image-20240429222801019

实现效果

image-20240429215231945

其他问题

1.WebStorm在编写代码的时候持续性崩溃

没有了解react的机制,自己编写代码 导致webstorm好几次奔溃,原因是钩子函数使用方式不对,导致loop触发内存泄漏,也不知道是不是卡顿问题open in new window参考解决方案open in new window

image-20240427221351557

​ 在编写图片信息展示的时候,发现内存突然飙升导致webstorm卡死,虽然数据可以正常展示(通过cmd窗口启动、vscode编辑),但是并没有从根本解决问题,本质上还是代码编写漏洞导致,例如编写下面这段逻辑展示图片信息(需分析代码可能存在的问题)(排除新引入代码的问题,是自己的编写逻辑不对,没有理解react的机制导致出错)

​ 将fetchResData相关内容放到 Index: React.FC 得以缓解?

import { searchAllByCondAdaptorUsingPost } from '@/services/itc-platform/searchOptimizeController';
import { Card, List } from 'antd';
import React from 'react';

const params = {
  searchText: '小黑子',
  searchType: 'pictures',
};
console.log(params);

// 请求接口获取图片信息
const fetchResData = async () => {
  try {
    const res = await searchAllByCondAdaptorUsingPost({
      ...params,
    });
    return res.data;
  } catch (error) {
    // 提示异常信息
    alert('图片信息请求异常');
  }
  return undefined;
};
// 调用方法获取响应处理后的数据
const resData = await fetchResData();

const Index: React.FC = () => {
  return (
    // 页面信息定义(search)
    <div className="search">
      <div>
        <List
          grid={{ gutter: 16, column: 6 }}
          dataSource={resData?.dataList}
          renderItem={(item) => (
            <List.Item>
              <Card
                hoverable
                cover={
                  <img
                    alt={item.title}
                    src={item.url}
                    style={{ height: '375px', objectFit: 'scaleDown' }}
                  />
                }
              >
                <List.Item.Meta title={<a href={item.url}>{item.title}</a>} />
              </Card>
            </List.Item>
          )}
        />
      </div>
    </div>
  );
};
export default Index;

参考写法:使用钩子函数调用接口,定义全局状态保存响应的数据

import React, { useEffect, useState } from 'react';
import { List, Image } from 'antd';
import axios from 'axios';
 
const PhotoList = () => {
  const [data, setData] = useState([]);
  const [photos, setPhotos] = useState([]);
 
  useEffect(() => {
    axios.get('/api/photos')
      .then(response => {
        setData(response.data);
        setPhotos(response.data.map(photo => photo.url));
      })
      .catch(error => console.error(error));
  }, []);
 
  return (
    <List
      grid={{ gutter: 16, column: 4 }}
      dataSource={photos}
      renderItem={item => <List.Item><Image src={item} alt="photo" /></List.Item>}
    />
  );
};
 
export default PhotoList;

image-20240428081252686

3.Bi模块构建引入(需补充说明迁移内容,避免项目改造导致缺胳膊少腿)

No qualifying bean of type 'org.redisson.api.RedissonClient' available: expected at least 1 bean which qualifies as autowire candidate.

RedissonConfig

需要配置redisson内容,并且需要提供一个配置类,否则无法装配redis连接(例如迁移代码是少了一个RedissonConfig),所以代码迁移一定要注意功能的完整性和关联性(可能部分配置文件或者其他问题导致项目启动异常),这种情况不像是普通业务逻辑代码迁移,而是基础配置项的内容,需要跟踪代码实现的具体细节

类似地还有线程池相关的配置:

A component required a bean of type 'java.util.concurrent.ThreadPoolExecutor' that could not be found.

ThreadPoolExecutorConfig

2.动态SQL常见问题

返回类型定义转化错误:

​ 例如此处NotificationVO没有配置getter、setter(@Data注解)导致MyBatis无法识别转化(SQL中配置的是resultType),且如果是一些特殊类型转化的话还需配置自定义转化器(例如JSON字符串转化),或者在SQL中自定义resultMap

org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class com.noob.module.admin.cms.model.vo.NotificationVO]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.noob.module.admin.cms.model.vo.NotificationVO and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.noob.framework.common.BaseResponse["data"])
	at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:462) ~[spring-web-5.3.22.jar:5.3.22]
	at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:104) ~[spring-web-5.3.22.jar:5.3.22]
	at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:290) ~[spring-webmvc-5.3.22.jar:5.3.22]
	at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:183) ~[spring-webmvc-5.3.22.jar:5.3.22]
	at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:78) ~[spring-web-5.3.22.jar:5.3.22]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:135) ~[spring-webmvc-5.3.22.jar:5.3.22]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.22.jar:5.3.22]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.22.jar:5.3.22]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.22.jar:5.3.22]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1070) ~[spring-webmvc-5.3.22.jar:5.3.22]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.22.jar:5.3.22]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.22.jar:5.3.22]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.22.jar:5.3.22]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[tomcat-embed-core-9.0.65.jar:4.0.FR]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.22.jar:5.3.22]
	

​ 分页:如果是在mapper层使用分页,借助resultMap转化正常。相应的如果使用resultType别忘了确认对应类型要配置正确。还需注意逻辑删除,如果说是自定义SQL接管MyBatisX的逻辑删除,在SQL中需要过滤到已经逻辑删除的数据(这点不要混淆)。否则界面上显示的数据没有过滤掉已经删除的数据,再次对数据进行操作的时候就会提示数据已经被删除。因为查询操作是自定义SQL实现的。

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.executor.ExecutorException: A query was run and no Result Maps were found for the Mapped Statement 'com.noob.module.admin.cms.mapper.NotificationMapper.getVOByPage'.  It's likely that neither a Result Type nor a Result Map was specified.

3.数据表格PorTable分页配置

​ 首先确认pagination配置,然后确认请求返回数据配置是否正确:对应return的参数是data、total、success,需要和后台的响应数据对照,如果对照数据异常则渲染出错,例如total对应的是分页记录条数,如果total不存在则分页按钮无法点击(一直默认只有一页)

import ProTable from '@ant-design/pro-table';
import React from 'react';
 
export default () => {
  return (
    <ProTable
      headerTitle="查询表格数据"
      rowKey="id"
      request={(params, sorter, filter) => queryData(params)} // 请求数据的方法
      pagination={{
        pageSize: 10, // 每页的数据量
      }}
      columns={[
        {
          title: 'Name',
          dataIndex: 'name',
        },
        {
          title: 'Age',
          dataIndex: 'age',
        },
        // 其他列配置...
      ]}
    />
  );
};
 
// 模拟请求数据的函数
async function queryData({ current, pageSize }) {
  // 这里使用 fetch 进行数据请求,实际项目中替换为你的数据请求方式
  const result = await fetch(`/api/data?page=${current}&pageSize=${pageSize}`);
  return {
    data: result.data, // 数据列表
    total: result.total, // 数据总数
    success: true, // 请求成功标识
  };
}

4.ES数据同步&检索问题

​ 项目启动有时候会卡住,项目重新启动可以正常访问后台管理的FetchPost信息检索,随后再访问ES(动静分离,先查ES后查数据库),查询成功返回后再重新查询后台管理的FetchPost却发现一直卡顿在检索记录总条数(没有关联找对应数据),前端响应结果也是记录条数有但是record为null。偶尔出现这个问题,排查数据库连接问题或者其他进程阻塞,但还是偶尔出现这个问题。重启就好了GG

​ 考虑一个问题:日志打印阻塞,突然停住日志打印不出来,也不执行下一步操作,可能是因为日志打印太多?各种因素

5.前端响应处理问题

​ 前端拦截器定义了如果code不为0则抛出异常,所以会跳转页面抛出异常

​ 如果基于这种设定,提示非常不友好,应该改成通过message.error(引入antd的message组件)进行提示

const handleDailySignIn = () => {
      // alert('模拟调用接口进行每日签到领取积分');
       userSignInUsingPost().then(res => {
        alert('🚀🚀🚀签到成功,恭喜获取💰10积分,请再接再厉');
      });
      // 响应成功,重新请求刷新页面数据
      fetchUserInfo();
  }

// 这种方式当code不为0则会交由全局拦截器进行处理(参考requestConfig.ts(原errorConfig.ts))

​ 或者是通过try catch进行额外的异常捕获和处理

const handleDailySignIn = async () => {
    try {
      // alert('模拟调用接口进行每日签到领取积分');
      await userSignInUsingPost().then(res => {
        alert('🚀🚀🚀签到成功,恭喜获取💰10积分,请再接再厉');
      });
      // 响应成功,重新请求刷新页面数据
      fetchUserInfo();
    } catch (error: any) {
      message.error('签到失败,' + error.message);
      return false;
    }
  }

​ 基于这种try catch机制,虽然可以捕获异常,但是拿不到error的message数据(requestConfig做了拦截,抛出了一个new Error(),这是一个默认的Error对象,需要调整为throw new Error(data.message);),然后通过此处try catch获取到的就是拦截器throw出来的Error对象

image-20240502175948233

解决方案思路:约定严格的响应码信息,返回为0默认是成功,返回其他业务异常码(正常提示并进行区分)

配置

项说明

1.Redis连接(本地连接正常远程无法访问)

​ 先排查远程连接是否正常(通过其他redis客户端连接远程redis访问确认),然后再排查项目配置问题

​ 本地Redis连接配置和远程模式不太一样,本地连接配置有些不需要指定密码,而远程模式需要指定密码。在application.yml中进行配置,然后在RedissonConfig确认是否配置正常。

spring:
  # Redis 配置
  redis:
    database: 1
    host: localhost # 如果是远程则填写IP地址
    port: 6379
    timeout: 5000
    password: 123456 # 如果指定了密码需要填充

​ 排查RedissonConfig发现一开始为了方便,只是创建了一个简单的redis客户端连接对象(由于本地没有设定密码,所以没有做额外的处理),需要根据指定的参数进行配置。

@Configuration
@ConfigurationProperties(prefix = "spring.redis")
public class RedissonConfig {

    private Integer database;

    private String host;

    private Integer port;

    // 如果redis没有默认密码则不用写
//    private String password;

    @Bean
    public RedissonClient getRedissonClient() {
        // 1.创建配置对象
        Config config = new Config();
        // 添加单机Redisson配置
        config.useSingleServer()
                .setDatabase(database)
                .setPassword(StringUtils.isNotBlank(password)?password:null)
                .setAddress("redis://"+host+":"+port);
        // 如果没有密码则不需要设定

        // 2.创建Redisson实例
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }

}
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3