Controller 层如何写才能简洁又优雅!

更多内容关注微信公众号:fullstack888

一个优秀的 Controller 层逻辑

说到 Controller,相信大家都不陌生,它可以很方便地对外提供数据接口。它的定位,我认为是不可或缺的配角。

说它不可或缺是因为无论是传统的三层架构还是现在的 COLA 架构,Controller 层依旧有一席之地,说明他的必要性。

说它是配角是因为 Controller 层的代码一般是不负责具体的逻辑业务逻辑实现,但是它负责接收和响应请求。

从现状看问题

Controller 主要的工作有以下几项:

  • 接收请求并解析参数

  • 调用 Service 执行具体的业务代码(可能包含参数校验)

  • 捕获业务逻辑异常做出反馈

  • 业务逻辑执行成功做出响应

//DTO
@Data
public class TestDTO {private Integer num;private String type;
}//Service
@Service
public class TestService {public Double service(TestDTO testDTO) throws Exception {if (testDTO.getNum() <= 0) {throw new Exception("输入的数字需要大于0");}if (testDTO.getType().equals("square")) {return Math.pow(testDTO.getNum(), 2);}if (testDTO.getType().equals("factorial")) {double result = 1;int num = testDTO.getNum();while (num > 1) {result = result * num;num -= 1;}return result;}throw new Exception("未识别的算法");}
}//Controller
@RestController
public class TestController {private TestService testService;@PostMapping("/test")public Double test(@RequestBody TestDTO testDTO) {try {Double result = this.testService.service(testDTO);return result;} catch (Exception e) {throw new RuntimeException(e);}}@Autowiredpublic DTOid setTestService(TestService testService) {this.testService = testService;}
}

如果真的按照上面所列的工作项来开发 Controller 代码会有几个问题:

  • 参数校验过多地耦合了业务代码,违背单一职责原则

  • 可能在多个业务中都抛出同一个异常,导致代码重复

  • 各种异常反馈和成功响应格式不统一,接口对接不友好


改造 Controller 层逻辑

统一返回结构

统一返回值类型无论项目前后端是否分离都是非常必要的,方便对接接口的开发人员更加清晰地知道这个接口的调用是否成功(不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此)。

使用一个状态码、状态信息就能清楚地了解接口调用情况:

//定义返回数据结构
public interface IResult {Integer getCode();String getMessage();
}//常用结果的枚举
public enum ResultEnum implements IResult {SUCCESS(2001, "接口调用成功"),VALIDATE_FAILED(2002, "参数校验失败"),COMMON_FAILED(2003, "接口调用失败"),FORBIDDEN(2004, "没有权限访问资源");private Integer code;private String message;//省略get、set方法和构造方法
}//统一返回数据结构
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {private Integer code;private String message;private T data;public static <T> Result<T> success(T data) {return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);}public static <T> Result<T> success(String message, T data) {return new Result<>(ResultEnum.SUCCESS.getCode(), message, data);}public static Result<?> failed() {return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);}public static Result<?> failed(String message) {return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null);}public static Result<?> failed(IResult errorResult) {return new Result<>(errorResult.getCode(), errorResult.getMessage(), null);}public static <T> Result<T> instance(Integer code, String message, T data) {Result<T> result = new Result<>();result.setCode(code);result.setMessage(message);result.setData(data);return result;}
}

统一返回结构后,在 Controller 中就可以使用了,但是每一个 Controller 都写这么一段最终封装的逻辑,这些都是很重复的工作,所以还要继续想办法进一步处理统一返回结构。

统一包装处理

Spring 中提供了一个类 ResponseBodyAdvice ,能帮助我们实现上述需求:

public interface ResponseBodyAdvice<T> {boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);@NullableT beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);
}

ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。

那这样就可以把统一包装的工作放到这个类里面:

  • supports: 判断是否要交给 beforeBodyWrite 方法执行,ture:需要;false:不需要

  • beforeBodyWrite: 对 response 进行具体的处理

// 如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {@Overridepublic boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {// 如果不需要进行封装的,可以添加一些校验手段,比如添加标记排除的注解return true;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 提供一定的灵活度,如果body已经被包装了,就不进行包装if (body instanceof Result) {return body;}return Result.success(body);}
}

经过这样改造,既能实现对 Controller 返回的数据进行统一包装,又不需要对原有代码进行大量的改动。

参数校验

Java API 的规范 JSR303 定义了校验的标准 validation-api ,其中一个比较出名的实现是 hibernate validation。

spring validation 是对其的二次封装,常用于 SpringMVC 的参数自动校验,参数校验的代码就不需要再与业务逻辑代码进行耦合了。

①@PathVariable 和 @RequestParam 参数校验

Get 请求的参数接收一般依赖这两个注解,但是处于 url 有长度限制和代码的可维护性,超过 5 个参数尽量用实体来传参。

对 @PathVariable 和 @RequestParam 参数进行校验需要在入参声明约束的注解。

如果校验失败,会抛出 MethodArgumentNotValidException 异常。

@RestController(value = "prettyTestController")
@RequestMapping("/pretty")
public class TestController {private TestService testService;@GetMapping("/{num}")public Integer detail(@PathVariable("num") @Min(1) @Max(20) Integer num) {return num * num;}@GetMapping("/getByEmail")public TestDTO getByAccount(@RequestParam @NotBlank @Email String email) {TestDTO testDTO = new TestDTO();testDTO.setEmail(email);return testDTO;}@Autowiredpublic void setTestService(TestService prettyTestService) {this.testService = prettyTestService;}
}
校验原理

在 SpringMVC 中,有一个类是 RequestResponseBodyMethodProcessor,这个类有两个作用(实际上可以从名字上得到一点启发)

  • 用于解析 @RequestBody 标注的参数

  • 处理 @ResponseBody 标注方法的返回值

解析 @RequestBoyd 标注参数的方法是 resolveArgument。

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {/*** Throws MethodArgumentNotValidException if validation fails.* @throws HttpMessageNotReadableException if {@link RequestBody#required()}* is {@code true} and there is no body content or if there is no suitable* converter to read the content with.*/@Overridepublic Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {parameter = parameter.nestedIfOptional();//把请求数据封装成标注的DTO对象Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());String name = Conventions.getVariableNameForParameter(parameter);if (binderFactory != null) {WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);if (arg != null) {//执行数据校验validateIfApplicable(binder, parameter);//如果校验不通过,就抛出MethodArgumentNotValidException异常//如果我们不自己捕获,那么最终会由DefaultHandlerExceptionResolver捕获处理if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());}}if (mavContainer != null) {mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());}}return adaptArgumentIfNecessary(arg, parameter);}
}public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {/*** Validate the binding target if applicable.* <p>The default implementation checks for {@code @javax.validation.Valid},* Spring's {@link org.springframework.validation.annotation.Validated},* and custom annotations whose name starts with "Valid".* @param binder the DataBinder to be used* @param parameter the method parameter descriptor* @since 4.1.5* @see #isBindExceptionRequired*/protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {//获取参数上的所有注解Annotation[] annotations = parameter.getParameterAnnotations();for (Annotation ann : annotations) {//如果注解中包含了@Valid、@Validated或者是名字以Valid开头的注解就进行参数校验Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);if (validationHints != null) {//实际校验逻辑,最终会调用Hibernate Validator执行真正的校验//所以Spring Validation是对Hibernate Validation的二次封装binder.validate(validationHints);break;}}}
}

②@RequestBody 参数校验

Post、Put 请求的参数推荐使用 @RequestBody 请求体参数。

对 @RequestBody 参数进行校验需要在 DTO 对象中加入校验条件后,再搭配 @Validated 即可完成自动校验。

如果校验失败,会抛出 ConstraintViolationException 异常。

//DTO
@Data
public class TestDTO {@NotBlankprivate String userName;@NotBlank@Length(min = 6, max = 20)private String password;@NotNull@Emailprivate String email;
}//Controller
@RestController(value = "prettyTestController")
@RequestMapping("/pretty")
public class TestController {private TestService testService;@PostMapping("/test-validation")public void testValidation(@RequestBody @Validated TestDTO testDTO) {this.testService.save(testDTO);}@Autowiredpublic void setTestService(TestService testService) {this.testService = testService;}
}
校验原理

声明约束的方式,注解加到了参数上面,可以比较容易猜测到是使用了 AOP 对方法进行增强。

而实际上 Spring 也是通过 MethodValidationPostProcessor 动态注册 AOP 切面,然后使用 MethodValidationInterceptor 对切点方法进行织入增强。

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {//指定了创建切面的Bean的注解private Class<? extends Annotation> validatedAnnotationType = Validated.class;@Overridepublic void afterPropertiesSet() {//为所有@Validated标注的Bean创建切面Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);//创建Advisor进行增强this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));}//创建Advice,本质就是一个方法拦截器protected Advice createMethodValidationAdvice(@Nullable Validator validator) {return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());}
}public class MethodValidationInterceptor implements MethodInterceptor {@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {//无需增强的方法,直接跳过if (isFactoryBeanMetadataMethod(invocation.getMethod())) {return invocation.proceed();}Class<?>[] groups = determineValidationGroups(invocation);ExecutableValidator execVal = this.validator.forExecutables();Method methodToValidate = invocation.getMethod();Set<ConstraintViolation<Object>> result;try {//方法入参校验,最终还是委托给Hibernate Validator来校验//所以Spring Validation是对Hibernate Validation的二次封装result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups);}catch (IllegalArgumentException ex) {...}//校验不通过抛出ConstraintViolationException异常if (!result.isEmpty()) {throw new ConstraintViolationException(result);}//Controller方法调用Object returnValue = invocation.proceed();//下面是对返回值做校验,流程和上面大概一样result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);if (!result.isEmpty()) {throw new ConstraintViolationException(result);}return returnValue;}
}

③自定义校验规则

有些时候 JSR303 标准中提供的校验规则不满足复杂的业务需求,也可以自定义校验规则。

自定义校验规则需要做两件事情:

  • 自定义注解类,定义错误信息和一些其他需要的内容

  • 注解校验器,定义判定规则

//自定义注解类
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {/*** 是否允许为空*/boolean required() default true;/*** 校验不通过返回的提示信息*/String message() default "不是一个手机号码格式";/*** Constraint要求的属性,用于分组校验和扩展,留空就好*/Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};
}//注解校验器
public class MobileValidator implements ConstraintValidator<Mobile, CharSequence> {private boolean required = false;private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$"); // 验证手机号/*** 在验证开始前调用注解里的方法,从而获取到一些注解里的参数** @param constraintAnnotation annotation instance for a given constraint declaration*/@Overridepublic void initialize(Mobile constraintAnnotation) {this.required = constraintAnnotation.required();}/*** 判断参数是否合法** @param value   object to validate* @param context context in which the constraint is evaluated*/@Overridepublic boolean isValid(CharSequence value, ConstraintValidatorContext context) {if (this.required) {// 验证return isMobile(value);}if (StringUtils.hasText(value)) {// 验证return isMobile(value);}return true;}private boolean isMobile(final CharSequence str) {Matcher m = pattern.matcher(str);return m.matches();}
}

自动校验参数真的是一项非常必要、非常有意义的工作。JSR303 提供了丰富的参数校验规则,再加上复杂业务的自定义校验规则,完全把参数校验和业务逻辑解耦开,代码更加简洁,符合单一职责原则。

自定义异常与统一拦截异常

原来的代码中可以看到有几个问题:

  • 抛出的异常不够具体,只是简单地把错误信息放到了 Exception 中

  • 抛出异常后,Controller 不能具体地根据异常做出反馈

  • 虽然做了参数自动校验,但是异常返回结构和正常返回结构不一致

自定义异常是为了后面统一拦截异常时,对业务中的异常有更加细颗粒度的区分,拦截时针对不同的异常作出不同的响应。

而统一拦截异常的目的一个是为了可以与前面定义下来的统一包装返回结构能对应上,另一个是我们希望无论系统发生什么异常,Http 的状态码都要是 200 ,尽可能由业务来区分系统的异常。

//自定义异常
public class ForbiddenException extends RuntimeException {public ForbiddenException(String message) {super(message);}
}//自定义异常
public class BusinessException extends RuntimeException {public BusinessException(String message) {super(message);}
}//统一拦截异常
@RestControllerAdvice(basePackages = "com.example.demo")
public class ExceptionAdvice {/*** 捕获 {@code BusinessException} 异常*/@ExceptionHandler({BusinessException.class})public Result<?> handleBusinessException(BusinessException ex) {return Result.failed(ex.getMessage());}/*** 捕获 {@code ForbiddenException} 异常*/@ExceptionHandler({ForbiddenException.class})public Result<?> handleForbiddenException(ForbiddenException ex) {return Result.failed(ResultEnum.FORBIDDEN);}/*** {@code @RequestBody} 参数校验不通过时抛出的异常处理*/@ExceptionHandler({MethodArgumentNotValidException.class})public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {BindingResult bindingResult = ex.getBindingResult();StringBuilder sb = new StringBuilder("校验失败:");for (FieldError fieldError : bindingResult.getFieldErrors()) {sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");}String msg = sb.toString();if (StringUtils.hasText(msg)) {return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);}return Result.failed(ResultEnum.VALIDATE_FAILED);}/*** {@code @PathVariable} 和 {@code @RequestParam} 参数校验不通过时抛出的异常处理*/@ExceptionHandler({ConstraintViolationException.class})public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {if (StringUtils.hasText(ex.getMessage())) {return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());}return Result.failed(ResultEnum.VALIDATE_FAILED);}/*** 顶级异常捕获并统一处理,当其他异常无法处理时候选择使用*/@ExceptionHandler({Exception.class})public Result<?> handle(Exception ex) {return Result.failed(ex.getMessage());}
}

总结

做好了这一切改动后,可以发现 Controller 的代码变得非常简洁,可以很清楚地知道每一个参数、每一个 DTO 的校验规则,可以很明确地看到每一个 Controller 方法返回的是什么数据,也可以方便每一个异常应该如何进行反馈。

这一套操作下来后,我们能更加专注于业务逻辑的开发,代码简介、功能完善,何乐而不为呢?

- END -

往期回顾

◆产线环境故障排查常用套路

◆京东基于 Seata 探寻分布式事务的实现方案

◆真正的缓存之王,Google Guava 只是弟弟

◆好分期热线客服系统的架构演进

◆Prometheus这些基础概念你应该了解

◆万字追溯ChatGPT各项能力的起源

◆60 个神级 VS Code 插件,总有一个是你需要的

◆聊聊微服务架构中的用户认证方案!

54d97222566ebb7541d7abaa5e7d713e.png

技术交流,请加微信: jiagou6688 ,备注:Java,拉你进架构群

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/48495.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI对话交互场景使用WebSocket建立H5客户端和服务端的信息实时双向通信

WebSocket使得客户端和服务器之间的数据交换变得更加简单&#xff0c;允许服务端主动向客户端推送数据。在WebSocket API中&#xff0c;浏览器和服务器只需要完成一次握手&#xff0c;两者之间就可以创建持久性的连接&#xff0c;并进行双向数据传输。 一、为什么需要 WebSock…

WorkPlus AI助理 | 将企业业务场景与ChatGPT结合

近年来&#xff0c;人工智能成为了企业数字化转型的热门话题&#xff0c;作为被训练的语言模型&#xff0c;ChatGPT具备模拟对话、回答问题、写代码、写小说、进行线上内容创作的能力&#xff0c;还能根据聊天的上下文进行互动。作为一款新兴的人工智能应用程序&#xff0c;对于…

用Python比较图片的不同

准备两张不同的图片 原图 修改后&#xff08;在左下角增加了文字&#xff09; 比较不同 使用PIL&#xff08;Pillow library&#xff09;库 安装 pip install pillow&#xff0c;然后直接用其中的ImageChops函数 from PIL import Image from PIL import ImageChops def comp…

对比两张图片的相似度

&#x1f468;‍&#x1f4bb;个人简介&#xff1a; 深度学习图像领域工作者 &#x1f389;总结链接&#xff1a; 链接中主要是个人工作的总结&#xff0c;每个链接都是一些常用demo&#xff0c;代码直接复制运行即可。包括&#xff1a; &am…

[274]用python对比两张图片的不同

from PIL import Image from PIL import ImageChops def compare_images(path_one, path_two, diff_save_location):"""比较图片&#xff0c;如果有不同则生成展示不同的图片参数一: path_one: 第一张图片的路径参数二: path_two: 第二张图片的路径参数三: diff…

怎么判断两张图片是否完全相同,通过读取图片内容进行对比

ep1&#xff1a; ep2&#xff1a; 实现原理&#xff1a; 通过读取图片&#xff0c;把图片转为base64后进行对比即可达到目的。 以下是图片转base64的方法&#xff1a; public string GetBase64StringByImage(Image img){string base64buffer string.Empty;try{if (img ! nul…

Python如何比较两张图片的相似度

前言 本文是该专栏的第21篇,后面会持续分享python的各种干货知识,值得关注。 工作上,可能会需要你对两张图片进行相似度比较。比如现在的图片验证码,需要你对两张图片进行比较,找出图中存在相似特征的地方或动作;再或是在做电商项目的时候,需要你对商品主图进行相似度比…

chatgpt赋能python:Python图片找不同的SEO文章

Python 图片找不同的SEO文章 在网上&#xff0c;图片找不同游戏是一种非常受欢迎的娱乐方式。但是&#xff0c;这些游戏经常需要手动比对两张图片&#xff0c;这是一项费时费力的任务。那么&#xff0c;有没有一种自动化的方法来找到这些不同之处呢&#xff1f; 答案是肯定的…

如何判断两张图片是否类似

如何判断两张图是否相似&#xff1f; 查到了很多算法&#xff0c;流程都是“特征提取”&#xff0c;“特征对比”。以下列出了三个常见算法的浅显的介绍&#xff0c; 平均哈希算法 平均哈希算法是三种Hash算法中最简单的一种&#xff0c;它通过下面几个步骤来获得图片的Hash值…

【图像】搜索相同,或者相似照片

目录 1. 查找完全相同的一对张照片 2. 查找相似照片&#xff0c; 1. 查找完全相同的一对张照片 利用MD5&#xff0c;变换找到两张一模一样的图片。 import cv2 import numpy as np import osimport json import os from hashlib import md5def getmd5(image_path, md5_path):…

go 图片相似 查找两张图片不同的部分 找出两幅图片中的不同处

golang Image similarity comparison 目前网上找了很多的 图片相似 查找两张图片不同的部分&#xff0c;实现图像比较和标记以显示不同 &#xff0c;很多都是python写的&#xff0c;没有找到go语言写的&#xff0c;所以想自己写一个 图片A 为参照物&#xff0c;去寻找图片B 的…

元宇宙是个什么样的概念?

什么是元宇宙&#xff1f; 百度百科上提到&#xff1a; 元宇宙&#xff08;Metaverse&#xff09;&#xff0c;是人类运用数字技术构建的&#xff0c;由现实世界映射或超越现实世界&#xff0c;可与现实世界交互的虚拟世界&#xff0c;具备新型社会体系的数字生活空间。 元宇…

最全元宇宙概念分析!元宇宙为何发展于区块链?

元宇宙&#xff0c;Web3 时代最新热词&#xff0c;和 NFT、DAO 等新晋热门概念一起在 2021 年横空出世。这一概念最早诞生于 1992 年的科幻小说《雪崩》&#xff0c;小说中描绘了一个庞大的虚拟现实世界&#xff0c;人们用数字化身来控制&#xff0c;并相互竞争以提高自己的地位…

chatgpt赋能python:Python期末考:如何顺利通过?

Python期末考&#xff1a;如何顺利通过&#xff1f; Python是一门广受欢迎的编程语言&#xff0c;无论是初学者还是有经验的工程师&#xff0c;都会在其职业生涯中使用Python。在学术领域&#xff0c;Python也被广泛应用于数据分析、人工智能和机器学习等方面。但是&#xff0…

【电商系列】shopee的数据获取

在Amazon&#xff0c;Aliexpress之后&#xff0c;又一个海外电商出现在我的视野里——shopee&#xff0c;在东南亚很火的电商平台。 这战略布局都到南美跟欧洲了 这网站有意思的是啊&#xff0c;每个国家的商品虽然大同小异&#xff0c;但是也能凸显各个国家的风格的&#xff0…

分享4点选品思路,电商大牛都在用

Tiktok选品数据分析是很多跨境电商商家都需要解决的首要问题。如何选品才能提高TikTok变现率&#xff1f;商家选品时需要结合实际数据进行分析&#xff0c;不能一概而论。本文将和大家谈论三个问题。 选品思路 选品方法 选品数据哪里找&#xff1f; 一、选品思路 选品是tiktok小…

电商平台OnBuy选品技巧分享一二

OnBuy是这两年发展较快的蓝海电商平台&#xff0c;是跨境电商人可以选择的一个优质电商平台。今天我们小编就给大家分享一下OnBuy选品技巧以及方法&#xff0c;希望对大家有用。 OnBuy热销类目 1、 健康(防护用品) 2、 美妆护肤 3、 多媒体 4、 玩具 5、 宠物 6、 婴儿用品 …

利用Tushare获取A股所有股票代码

Tusahre注册链接 https://tushare.pro/register?reg365850 import os import tushare as ts import pandas import datetimetoken 自己的 #可以登录文章首的链接注册获取 pro ts.pro_api(token) dateToday datetime.datetime.today().strftime(%Y%m%d)def GetList():try:d…

chatgpt赋能python:Python语言中的语句输入方法

Python语言中的语句输入方法 作为一门广泛应用于科学计算和计算机编程领域的编程语言&#xff0c;Python以其易读易写、简洁明了的语法特点&#xff0c;广受程序员和科学家的喜爱。本文将重点介绍Python中的语句输入方法&#xff0c;包括基本输入方法、文件读取、网络输入和交…

10款爆火且实用的AIGC工具大盘点

大家好。我是不知名设计师 l1m0_&#xff0c;今天分享内容为&#xff1a;10款爆火且实用的AIGC工具。文中我会跟大家针对10款不同功能优势的AI工具向各位朋友进行介绍&#xff0c;对AI创作感兴趣的朋友一定不能错过&#xff0c;一起来看看吧。 人工智能&#xff08;AI&#xff…