当前位置:   article > 正文

瑞吉外卖知识点总结_吉瑞外卖实践项目知识点总结

吉瑞外卖实践项目知识点总结

用到的注解:

@RestController:
结合了@Controller和@ResponseBody注解的功能,作用是将一个类标记为控制器(Controller),并且该类中的方法默认返回JSON格式的数据。它常用于构建RESTful风格的Web服务

@RequestMapper
是一个用于处理请求地址映射的注解,表示类中的所有响应请求的方法都以该地址作为父路径。

@RequestBody
它用于处理来自 HTTP 请求体的 JSON 数据,只能用于处理 POST 和 PUT 请求。

@PathVariable
用于从请求的URL中提取参数值并传递给处理程序方法的参数。要是/user/id这种,而不是/user?id

HttpServletRequest
提供了一种处理HTTP请求的方式,可以获取客户端发送过来的请求参数

@Transactional
保证事务的一致性,比如新增菜品时,菜品添加进了数据库,而菜品口味因为异常而没有添加进,处理起来就比较麻烦,加入此注解后,有一个没添加进数据库,全部都不会被添加进数据库

@EnableTransactionManagement
用于启用事务管理功能,需要加在启动类上

@Configuration
用于定义配置类,只要是配置类就需要写它,Spring会自动扫描并加载这个类中的Bean定义。

@WebFilter
用于标识一个类作为Web过滤器(Filter)的组件,其中filterName是过滤器的名称,通常用于标识过滤器的功能或用途。urlPatterns用于指定过滤器应该被应用到哪些URL模式上,例如,"/api/"将匹配以"/api/"开头的所有URL。

@ServletComponentScan
自动扫描和注册应用程序中的servlet、filter和listener组件。这样就不需要手动注册和配置每个servlet组件。

@Component
用于将普通Java类声明为Bean的注解,从而不需要手动注册和配置每个Bean。

@ControllerAdvice
主要用于处理全局异常和全局数据预设。其中的annotations参数是一个数组,代表@ControllerAdvice注解可以应用于带有数组里面注释的类和方法。

@ResponseBody
表示Controller的方法返回的结果直接写入HTTP response body中,通常用来返回JSON数据或XML数据

@ExceptionHandler
当Spring MVC应用程序在处理HTTP请求时出现错误或异常时,可以用我们自己自定义的异常处理行为。()里面填遇到哪种错误时

@Bean
这是Spring的注解,表示这个方法会返回一个Bean,这个Bean会被Spring容器管理。

@Data
可以自动生成get,set,tostring,hashcode,equals方法,简化开发

1.项目配置类

内容:过滤器,公共字段自动填充,全局异常处理器,MP分页插件配置,统一返回类型R,文件上传下载

①过滤器

作用:用来判断用户是否登录,否则可以直接通过网址进入,所以要拦截
所用注解:@WebFilter,@ServletComponentScan
使用方法:
(1).首先接口Filter,重写doFilter方法
(2).把ServletRequest和ServletResponse两个参数强转为HttpServletRequest,并request方法get本次请求到的URL
(3).创建一个不需要过滤的url地址的集合,然后检查这里面的地址是否和本次的URL是一个,是就放行,不是就再判断是否已经登录,已经登录就放行,没有就返回未登录结果
(4).在启动类上加@ServletComponentScan注解
  1. @WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
  2. public class LoginCheckFilter implements Filter {
  3. //路径匹配器,支持通配符
  4. public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
  5. @Override
  6. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  7. HttpServletRequest request = (HttpServletRequest) servletRequest;
  8. HttpServletResponse response = (HttpServletResponse) servletResponse;
  9. //1.获取本次请求到的URI
  10. String requestURI = request.getRequestURI();
  11. //2.判断本次请求是否需要处理,先创一个检查方法
  12. String[] urls = new String[]{
  13. "/backend/**",
  14. "/front/**",
  15. "/employee/login",
  16. "/employee/logout",
  17. "/common/**"
  18. };
  19. boolean check = check(urls, requestURI);
  20. //3.如果本次是不需要过滤的地址,直接放行
  21. if (check){
  22. filterChain.doFilter(request,response);
  23. return;
  24. }
  25. //4.如果已登录,那直接放行
  26. if (request.getSession().getAttribute("employee") != null){
  27. //是为了自动填充所以到这里来获取当前进程的id,并保存
  28. Long id =(Long) request.getSession().getAttribute("employee");
  29. BaseContext.setCurrentId(id);
  30. filterChain.doFilter(request,response);
  31. return;
  32. }
  33. //5.如果未登录则通过输出流来返回未登录结果,""里的内容根据前端来写
  34. response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
  35. return;
  36. }
  37. //自定义检查方法
  38. public boolean check(String[] urls, String requestURI) {
  39. for (String url : urls) {
  40. boolean match = PATH_MATCHER.match(url, requestURI);
  41. if (match) {
  42. return true;
  43. }
  44. }
  45. return false;
  46. }
  47. }

②公共字段自动填充 

作用:可以把一些公共的字段(例如创建时间,修改时间,创建人,修改人等)自动设置好,能简化开发
所用注解:@Component
使用方法:
(1).首先创建一个配置类(填充)来接口MetaObjectHandler,重写insertFill和updateFill方法
(2).再创一个配置类(动态id),基于threadLocal用来保存和获取当前登录的id,在过滤器上获取当前id(注意去看过滤器第4个判断)
(3).最后填充配置类用本类的setValue方法来设置公共字段

填充配置类

  1. @Component
  2. public class MyMetaObjectHandler implements MetaObjectHandler {
  3. //插入时自动填充
  4. @Override
  5. public void insertFill(MetaObject metaObject) {
  6. metaObject.setValue("createTime", LocalDateTime.now());
  7. metaObject.setValue("updateTime", LocalDateTime.now());
  8. metaObject.setValue("createUser", BaseContext.getCurrentId());
  9. metaObject.setValue("updateUser", BaseContext.getCurrentId());
  10. }
  11. //更新时自动填充
  12. @Override
  13. public void updateFill(MetaObject metaObject) {
  14. metaObject.setValue("updateTime", LocalDateTime.now());
  15. metaObject.setValue("updateUser", BaseContext.getCurrentId());
  16. }
  17. }

动态id配置类

  1. public class BaseContext {
  2. private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
  3. public static void setCurrentId(Long id) {
  4. threadLocal.set(id);
  5. }
  6. public static Long getCurrentId() {
  7. return threadLocal.get();
  8. }
  9. }

 ③全局异常处理器

作用:用于对系统中出现的异常进行统一的处理,可以避免在每个业务方法中都使用try-catch语句,简化开发.
所用注解:@ControllerAdvice,@ResponseBody,@ExceptionHandler
使用方法:
  1. @ControllerAdvice(annotations = {RestController.class, Controller.class})
  2. @ResponseBody
  3. public class GlobalExceptionHandler {
  4. //用于新增时用户名存在时报的异常
  5. @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
  6. public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
  7. if (ex.getMessage().contains("Duplicate entry")){
  8. return R.error("已存在");
  9. }
  10. return R.error("不知道哪错了,别试了");
  11. }
  12. //当前分类是否关联菜品或套餐的异常
  13. @ExceptionHandler(CustomException.class)
  14. public R<String> exceptionHandler(CustomException ex){
  15. return R.error(ex.getMessage());
  16. }
  17. }

④MP分页插件 

作用:用于查询页面数据,页面展示时的分页效果
所用注解:@Configuration,@Bean
使用方法:(1).先创建一个MP拦截器,并在拦截器上添加一个分页器
  1. @Configuration
  2. public class MybatisPlusConfig{
  3. @Bean
  4. public MybatisPlusInterceptor interceptor(){
  5. MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  6. interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
  7. return interceptor;
  8. }
  9. }

⑤统一返回类型R

作用:用来统一类型向前端返回数据的
所用注解:@Data
使用方法:(1)定义前端需要的状态码,返回信息,数据等(看前端代码,这三个是基本),并创建成功方法和失败方法
  1. @Data
  2. public class R<T> implements Serializable {
  3. private Integer code;//状态码,看前端可知1是成功,0或其他数是失败
  4. private String msg;//返回信息
  5. private T data;//数据
  6. private Map map = new HashMap(); //动态数据
  7. //成功方法
  8. public static <T> R<T> success(T object) {
  9. R<T> r = new R<T>();
  10. r.data = object;
  11. r.code = 1;
  12. return r;
  13. }
  14. //失败方法
  15. public static <T> R<T> error(String msg) {
  16. R r = new R();
  17. r.msg = msg;
  18. r.code = 0;
  19. return r;
  20. }
  21. public R<T> add(String key, Object value) {
  22. this.map.put(key, value);
  23. return this;
  24. }
  25. }

⑥文件上传下载 

作用:上传和下载本地图片到网页
所用注解:@RestController,@RequestMapping,@Value,MultipartFile,HttpServletResponse
使用方法:
注意:此类方法不适用于nginx,nginx需要用OSS来存储图片
(1).先创建一个新的Controller类,然后分别创建上传的方法和下载的方法
(2).上传方法只需要用file的transferTo方法,把图片转存到我们指定的地方,这个地方最好在yml上配置好,然后用@Value调用这个地址,方便后期随时修改地址.图片名字也需要调用原始文件名,把"."后面的字母取出来,再用UUID随机一个名字加上我们取出来的后缀组成一个新的文件名,可以防止文件名因重复而覆盖,最后返回成功文件名
  1. @Value("${it-sky.path}")
  2. private String basePath;
  3. @PostMapping("/upload")
  4. public Result<String> upload(MultipartFile file){
  5. //这个file一定要看页面负载里面源的name是否是file,要保持一致
  6. //原始文件名
  7. String originalFilename = file.getOriginalFilename();
  8. String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
  9. //使用UUID重新生成文件名,繁殖文件名称重复造成文件覆盖
  10. String fileName = UUID.randomUUID().toString()+suffix;
  11. try {
  12. file.transferTo(new File(basePath+fileName));
  13. } catch (IOException e) {
  14. e.printStackTrace();
  15. }
  16. return Result.success(fileName);
  17. }

(3).下载方法则是通过输入流读取图片内容,再用输出流把图片写回到浏览器上,显示图片.需要用response来设置文件类型,定义一个字节数量的整型变量,再创建一个字节数组,使用循环输入流读取数据,并把数据存储在整型变量中,如果read返回-1,则循环结束.在使用输出流把数据写入另一个地方
(4).关流
  1. public void download(String name , HttpServletResponse response){
  2. try {
  3. //输入流,通过输入流读取文件内容
  4. FileInputStream fileInputStream = new FileInputStream(new File(basePath+name));
  5. //输出流,通过输出流将文件写回浏览器,在浏览器展示图片了
  6. ServletOutputStream outputStream = response.getOutputStream();
  7. //设置文件类型
  8. response.setContentType("image/jpeg");
  9. //初始化一个整型变量len,并赋值为0。这个变量将用于存储从输入流中读取的字节数量。
  10. int len = 0;
  11. //创建一个长度为1024的字节数组bytes。这个数组将用于存储从输入文件中读取的数据。
  12. byte[] bytes = new byte[1024];
  13. /*使用fileInputStream(一个文件输入流)来读取数据,并将读取的字节数量存储
  14. 在len中。如果read方法返回-1,说明已经读取到文件的末尾,此时循环将结束。*/
  15. while ((len = fileInputStream.read(bytes)) != -1){
  16. /*使用outputStream(一个输出流)来将读取的数据写入到另一个地方。
  17. write方法的三个参数分别是:要写入的数据(bytes数组),开始写入的
  18. 起始位置(0,表示从数组的开始位置写入),以及要写入的字节数量(len)。*/
  19. outputStream.write(bytes,0,len);
  20. outputStream.flush();
  21. }
  22. //关闭资源
  23. outputStream.close();
  24. fileInputStream.close();
  25. } catch (Exception e) {
  26. e.printStackTrace();
  27. }

2.登录接口

流程分析:
1.先判断账号数据库中是否存在
2.再判断密码是否正确
3.最后把数据发送到前端,并把id存入Session页面中

用到的注解:HttpServletRequest,@RequestBody

实际操作:
①用构造器查询所需要的数据库数据,记得LambdaQueryWrapper要加<>,<>里面是你要操作的表
②用eq来比较,第一个数据是被比较的表,第二个是你传过来要比较的数据
③用实现类查到的数据封装到实体类,好进行判断账号是否存在

  1. LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
  2. queryWrapper.eq(Employee::getUsername, employee.getUsername());
  3. Employee emp = employeeService.getOne(queryWrapper);
  4. if (emp==null){
  5. return R.error("登陆失败");
  6. }


注意:R是用来统一类型向前端返回数据的,一般有状态码(code),返回信息(msg),数据(data),里面一个成功方法一个失败方法,要继承Serializable,还要加@Data注解,这个R要自己创建.

④因为看数据库可知密码都是用md5加密的,所以在比较密码时需要把获取的密码数据先进行加密再进行比较

  1. String password = employee.getPassword();
  2. password = DigestUtils.md5DigestAsHex(password.getBytes());
  3. if (!emp.getPassword().equals(password)){
  4.     return R.error("登陆失败");
  5. }


⑤这里用到了HttpServletRequest

  1. request.getSession().setAttribute("employee",emp.getId());
  2. return R.success(emp);

3.退出接口 

流程分析:删除页面保存的登录信息

用到的注解:HttpServletRequest

实际操作:
  1. @PostMapping("logout")
  2. public R<String> logout(HttpServletRequest request){
  3. request.getSession().removeAttribute("employee");
  4. return R.success("退出成功");
  5. }

4.新增接口(一般用@PostMapping)

①普通新增:

用到的注解:HttpServletRequest,@Requestbody
流程分析:
1.用实体类设置好 密码(有的需要,有的还需要md5加密),创建时间,修改时间,创建人,修改人,人从Http拿.这四个也可以使用公共字段自动填充来设置.
2.用实现类直接.save就行,返回成功信息
实际操作:
  1. public R<String> add(@RequestBody Employee employee){
  2. employee.setPassword(DigestUtils.md5DigestAsHex("12345".getBytes()));
  3. ...
  4. employeeService.save(employee);
  5. return R.success("添加成功");
  6. }

②多表新增:

用到的注解:HttpServletRequest,@Requestbody,@Transactional,
@EnableTransactionManagement
流程分析:
(1)因为最开始的实体类有一些数据没有,所以需要创建对应DTO来新增那些数据,DTO要继承对应实体类,并且@Requestbody后面就需要改成DTO实体类.
(2)在service层创建新的save方法,并且自动注入要操作的另一个表的实现类
(3)首先用本类方法保存本来实体类有的数据,然后用注入进来的另一张表的实现类来保存要新增get出来的数据.
(4)还可以用stream流来完善DTO,比如要新增的是菜品口味,而菜品口味有了却没有对应的菜品id,就可以先获取菜品id,再获取菜品口味,然后用stream流给每一个菜品口味赋上id,最后返回给菜品口味,再保存.
(5)在实现类上加上事务管理,还有启动类上加上扫描事务管理的注解
(6)最后在Controller层调用我们新创建的save方法
实际操作:
Controller层的代码
  1. @PostMapping
  2. public R<String> save(@RequestBody DishDto dishDto){
  3. dishService.saveWithFlavor(dishDto);
  4. return R.success("新增成功");
  5. }
ServiceImpl层的代码
  1. public void saveWithFlavor(DishDto dishDto) {
  2. this.save(dishDto);
  3. Long dishId = dishDto.getId();
  4. List<DishFlavor> flavors = dishDto.getFlavors();
  5. flavors = flavors.stream().map((item)->{
  6. item.setDishId(dishId);
  7. return item;
  8. }).collect(Collectors.toList());
  9. dishFlavorService.saveBatch(flavors);
  10. }

5.删除接口(一般用DeleteMapping)

①普通删除

用到的注解:无
流程分析:
(1)如果是单独删除,参数直接是Long id,不用@PathVariable是因为前端页面路径是?不是/
(2)是批量删除就用String[] ids,然后用增强for循环,把每一个id都用本实现类删除,并返回成功信息
实际操作:
  1. @DeleteMapping
  2. public R<String> delete(String[] ids){
  3.     for (String id:ids) {
  4.         dishService.removeById(id);
  5.     }
  6. return R.success("删除成功");
  7. }

②有条件的删除

用到的注解:无
流程分析:
(1)如果是关联了其他条件导致不能删除的,我们在Service层新创建一个删除方法,并注入要查询的表
(2)用LambdaQueryWrapper在数据库中查询是否关联了,如果关联了就不能删除,抛异常(这里可以创建一个异常类集中处理项目中的异常,也方便向前端传递异常信息),如果没关联,就可以删除
(3)如果是因为商品的某个状态不能删除的,我们查询到那个状态并进行判断,如果状态不能删就返回错误信息,能删就返回成功信息
实际操作:

例1.

  1. public void remove(Long id) {
  2.   LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
  3. queryWrapper.eq(Dish::getCategoryId, id);
  4.   int count1 = dishService.count(queryWrapper);
  5.   if (count1 > 0) {
  6.      throw new CustomException("当前分类下关联了菜品,兄弟换一个删");
  7. }
  8. super.removeById(id);
  9. }

例2.

  1. @DeleteMapping
  2. public R<String> delete(String[] ids){
  3.    int index=0;
  4.    for(String id:ids) {
  5.        Setmeal setmeal = setmealService.getById(id);
  6.        if(setmeal.getStatus()!=1){
  7.              setmealService.removeById(id);
  8.        }else {
  9.              index++;
  10.        }
  11. }
  12.     if (index>0&&index==ids.length){
  13.     return R.error("选中的套餐均为启售状态,不能删除");
  14.   }else {
  15.      return R.success("删除成功");
  16.   }
  17. }

6.修改接口(一般用PutMapping)

①普通修改

用到的注解:@RequestBody,@PathVariable,HttpServletRequest(有时需要)
流程分析:
(1).简单修改直接用实现类方法updateById,记得用Http参数设置修改时间和修改人(有公共字段自动填充的话就不用写)
(2).如果是修改状态这种,看前端页面路径是否改变,可以用@PathVariable获取路径状态,然后实现类用getById获取要改变的用户,setStatus设置获取的状态,最后实现类updateById修改
实际操作:
  1. @PostMapping("/status/{status}")
  2. public R<String> sale(@PathVariable int status, String[] ids){
  3. for (String id:ids){
  4. Setmeal byId = setmealService.getById(id);
  5. byId.setStatus(status);
  6. setmealService.updateById(byId);
  7. }
  8. return R.success("修改状态成功");
  9. }

②多表修改

用到的注解:@RequestBody,HttpServletRequest(有时需要)
流程分析:
(1)因为最开始的实体类有一些数据没有,所以需要创建对应DTO来修改那些数据,DTO要继承对应实体类,并且@Requestbody后面就需要改成DTO实体类.(前面新增时一般都会创建)
(2)在service层创建新的update方法,并且自动注入要操作的另一个表的实现类
(3)首先用本类方法先修改本来实体类有的数据,然后我们需要删除当前数据实体类没有但已经保存了的数据,就比如新增菜品时我们通过多表新增方法新增了菜品口味,这时我们修改了菜品口味就需要先把菜品口味删除
(4)删除完后我们再调用前端传过来的菜品口味,用stream流一一为修改的菜品口味设置菜品id
(5)最后使用另一张表的实现类保存,并在Controller层调用我们新创建的update方法
实际操作:

ServiceImpl层代码

  1. @Override
  2. public void updateWithFlavor(DishDto dishDto) {
  3. this.updateById(dishDto);
  4. LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
  5. queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
  6. dishFlavorService.remove(queryWrapper);
  7. List<DishFlavor> flavors = dishDto.getFlavors();
  8. flavors = flavors.stream().map((item)->{
  9. item.setDishId(dishDto.getId());
  10. return item;
  11. }).collect(Collectors.toList());
  12. dishFlavorService.saveBatch(flavors);
  13. }
  14. }

7.查询接口(一般用GetMapping)

①普通查询

用到的注解:@Configuration(配置时需要)
流程分析:
(1).首先需要创建一个MP分页插件的配置类
(2).再构造一个分页查询器
(3).调用实现类的page方法把分页查询器传过去,并返回分页查询
实际操作:

配置类代码

  1. @Configuration
  2. public class MybatisPlusConfig{
  3. @Bean
  4. public MybatisPlusInterceptor interceptor(){
  5. MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
  6. interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
  7. return interceptor;
  8. }
  9. }

 Controller层方法

  1. @GetMapping("/page")
  2. public R<Page> list(int page,int pageSize){
  3. //分页构造器
  4. Page pageInfo = new Page(page,pageSize);
  5. employeeService.page(pageInfo);
  6. return R.success(pageInfo);
  7. }

②回显查询

用到的注解:@PathVariable
流程分析:
(1).如果是普通回显,则用@PathVariable获取页面路径传回来的id以及实现类getById来获取回显信息,判断不为空则返回成功信息,为空则返回错误信息
(2).如果是列表回显,则先创建条件构造器,eq查询需要回显的数据,orderByAsc设置排序条件,最后用实现类的list方法返回结果
实际操作:

普通回显代码

  1. @GetMapping("/{id}")
  2. public R<Employee> getById(@PathVariable Long id){
  3. Employee byId = employeeService.getById(id);
  4. if (byId != null){
  5. return R.success(byId);
  6. }else {
  7. return R.error("没有查询到对应数据");
  8. }
  9. }

列表回显代码 

  1. @GetMapping("/list")
  2. public R<List<Dish>> Getlist(Dish dish){
  3. LambdaQueryWrapper<Dish> queryWrapper =new LambdaQueryWrapper<>();
  4. queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
  5. queryWrapper.eq(Dish::getStatus,1);
  6. queryWrapper.orderByAsc(Dish::getSort);
  7. List<Dish> listInfo = dishService.list(queryWrapper);
  8. return R.success(listInfo);
  9. }

③多表查询

用到的注解:@Configuration(配置时需要)
流程分析:
(1).首先需要创建一个MP分页插件的配置类(创建过就不需要再创)
(2).其次创建一个旧分页器(利用Dish表)和一个新分页器(利用DishDTO)
(3).用旧分页器利用实现类page方法把数据都查询出来,并把这些数据拷贝到新分页器上,除了records(这里面实际就是我们的页面展示的那些数据)
(4).用旧分页器把records查出来
(5).records用stream流先把旧分页器的数据拷贝到新分页器上,然后把和另一张表关联的id(例如分类id)取出来,然后把这个id用另一张表的实现类查询出对应名字(分类名字),最后把查询出的数据设置给DTO,并返回DTO.
(6)把stream流toList,最后设置给新分页器的records
实际操作:
  1. @GetMapping("/page")
  2. public R<Page> list(int page,int pageSize,String name){
  3. //构造分页器
  4. Page<Dish> pageInfo = new Page<>(page,pageSize);
  5. //构造一个新的分页器,因为之前那个缺少某个类
  6. Page<DishDto> dishDtoPage = new Page<>(page,pageSize);
  7. dishService.page(pageInfo);
  8. BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
  9. List<Dish> records = pageInfo.getRecords();
  10. List<DishDto> list = records.stream().map((item)->{
  11. DishDto dishDto = new DishDto();
  12. BeanUtils.copyProperties(item,dishDto);
  13. Long categoryId = item.getCategoryId();
  14. Category byId = categoryService.getById(categoryId);
  15. String name = byId.getName();
  16. dishDto.setCategoryName(name);
  17. return dishDto;
  18. }).collect(Collectors.toList());
  19. dishDtoPage.setRecords(list1);
  20. return R.success(dishDtoPage);
  21. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/黑客灵魂/article/detail/817713
推荐阅读
相关标签
  

闽ICP备14008679号