基于springboot+mybatis plus开发核心技术的Java项目,包括系统管理后台和移动端应用两个部分,其中管理后台 部分提供给内部人员使用,可以对菜品、套餐、订单等进行管理以及维护;移动端主要提供给消费者使用,实现了 在线浏览商品、添加购物车、下单等业务。用户层面采用的技术栈为H5、Vue等,网关层采用Nginx,应用层采用 SpringBoot、SpringMVC等技术栈,数据层使用MySQL以及Redis。
数据库表结构
![image.png](https://img-blog.csdnimg.cn/img_convert/d2a533f219b731f56551adf05f67f4eb.png#averageHue=#cdd4ea&clientId=ufa6d96db-c165-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=360&id=u3c962d2b&margin=[object Object]&name=image.png&originHeight=538&originWidth=727&originalType=binary&ratio=1&rotation=0&showTitle=false&size=113184&status=done&style=none&taskId=udd65a482-fe60-44e3-8659-1d76a1f6d46&title=&width=486)
功能实现
登录模块
登录功能
前端页面展示
前端要求:后端需要返回code、data、msg三个参数;
![image.png](https://img-blog.csdnimg.cn/img_convert/def628e546bdff4bbad95eb6c0f76c69.png#averageHue=#fdfbf9&clientId=u381863fc-a71a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=370&id=uf92e3558&margin=[object Object]&name=image.png&originHeight=449&originWidth=575&originalType=binary&ratio=1&rotation=0&showTitle=false&size=37727&status=done&style=none&taskId=uff66c8ce-5bae-480f-91e6-9ef309301b4&title=&width=474)
前端发起的请求:
![image.png](https://img-blog.csdnimg.cn/img_convert/8f01b468957f10a9b69728887cc0e504.png#averageHue=#f6f3f2&clientId=u26a3a264-56b5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=262&id=ude292321&margin=[object Object]&name=image.png&originHeight=262&originWidth=1123&originalType=binary&ratio=1&rotation=0&showTitle=false&size=31668&status=done&style=none&taskId=u96730a67-8493-425d-a8b9-1b217fb5b55&title=&width=1123)
前端发起请求后携带的参数:
![image.png](https://img-blog.csdnimg.cn/img_convert/ed8fff453327128eba43b9c3b66e316d.png#averageHue=#f7f4f4&clientId=u26a3a264-56b5-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=295&id=u3cfe5201&margin=[object Object]&name=image.png&originHeight=295&originWidth=951&originalType=binary&ratio=1&rotation=0&showTitle=false&size=24142&status=done&style=none&taskId=u31da2c22-24fd-4766-ae1e-f43c8923968&title=&width=951)
后端业务实现
登录功能对应的数据库表中的员工表,所以需要针对员工表进行一系列架构,例如实体类,mapper,service,controller:
Mapper:
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}
Service:
public interface IEmployeeService extends IService<Employee> {
}
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements IEmployeeService {
}
Controller:
@RestController
@RequestMapping("/emploee")
public class EmployeeController {
@Autowired
public IEmployeeService employeeService;
}
并且在经过上面步骤之后,我们需要一个通过结果类,因为我们会编写非常多的Controller,我们应该将返回的结果进行统一!也就是将服务端响应后返回到页面的数据都应该被封装为一个统一的对象!代码如下:
@Data
public class ResultBean<T> {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
public static <T> ResultBean<T> success(T object) {
ResultBean<T> result = new ResultBean<>();
result.data = object;
result.code = 1;
return result;
}
public static <T> ResultBean<T> error(String msg) {
ResultBean result = new ResultBean();
result.msg = msg;
result.code = 0;
return result;
}
public ResultBean<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
具体业务实现代码:
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
public IEmployeeService employeeService;
/**
* 员工登录
* @param request 用于获取用户的session
* @param employee
* @return
*/
@PostMapping("/login")
public ResultBean login(HttpServletRequest request, @RequestBody Employee employee) {
// 将页面传来的数据,也就是账号与密码,将密码进行md5加密
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
// 根据用户名查询用户
LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper<>();
lqw.eq(Employee::getUsername, employee.getUsername()); // eq:等值查询
Employee emp = employeeService.getOne(lqw);
// 判断是否查询到用户
if (emp == null) {
return ResultBean.error("用户名错误");
}
// 判断密码是否匹配
if ( ! password.equals(emp.getPassword())) {
return ResultBean.error("密码错误");
}
// 查询账号是否处于封禁状态
// 在数据库中,员工表具有一个status字段,代表着员工的封禁状态,如果status=1,则代表可登录状态,如果为0,则代表该用户不可登录
if (emp.getStatus().equals(0)) {
return ResultBean.error("账号已禁用");
}
// 登录成功,将用户id放入session中
request.getSession().setAttribute("employee", emp.getId());
return ResultBean.success(emp);
}
}
阻止直接访问页面
当前情况下,用户是可以直接通过访问主页面来跳过登录页面的,所以我们需要对该问题进行一些优化,让未登录的用户必须登录之后,才能访问主页面!
实现方案:过滤器或拦截器,这里使用过滤器
实现步骤:
1、创建一个自定义的过滤器
2、为过滤器增加@WebFilter注解,并在注解中配置过滤器的名称以及需要拦截的路径(这里选择拦截所有路径/*)
3、过滤器继承Filter类
4、在启动类上增加注解@ServletComponentScan
5、完善过滤器逻辑
![image.png](https://img-blog.csdnimg.cn/img_convert/1e0b61fb5d6f6708958b2e3dbf6108a6.png#averageHue=#fcfafc&clientId=ua16b1084-d533-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=333&id=u469875f3&margin=[object Object]&name=image.png&originHeight=549&originWidth=1152&originalType=binary&ratio=1&rotation=0&showTitle=false&size=230517&status=done&style=none&taskId=u9ac6e115-6d21-44fa-a92a-a097a49caff&title=&width=699)
@Slf4j
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 获得本次请求的uri
String uri = request.getRequestURI();
// 定义并不需要拦截的路径(登录、退出、静态资源)
String[] urls = new String[] {
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
// 判断本次路径是否需要进行处理(需要用到AntPathMatcher对象)
boolean checkUrl = checkUrl(urls, uri);
if (checkUrl) {
log.info("本次请求{}不需要处理" + uri);
filterChain.doFilter(request, response);
return;
}
// 如果已经是登录状态,则放行
if (request.getSession().getAttribute("employee") != null) {
log.info("用户{}已登录" + request.getSession().getAttribute("employee"));
return;
}
// 如果是未登录状态,则返回未登录的结果,并通过输出流的方式向客户端响应数据
// 在前端界面中,响应数据中含msg=NOTLOGIN会进行页面的跳转
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(ResultBean.error("NOTLOGIN")));
return;
}
private boolean checkUrl(String[] urls, String requestUri) {
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestUri);
if (match) {
return true;
}
}
return false;
}
}
前端跳转的拦截器reques.js:当响应返回一个“NOTLOGIN”字符串时,进行页面跳转
![image.png](https://img-blog.csdnimg.cn/img_convert/75a163506dcd93d0dfd8dc0adf83fbf6.png#averageHue=#fdfcfb&clientId=ua16b1084-d533-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=255&id=u572b5dc4&margin=[object Object]&name=image.png&originHeight=255&originWidth=606&originalType=binary&ratio=1&rotation=0&showTitle=false&size=25476&status=done&style=none&taskId=uebafa3c5-87d9-42c0-88ee-3e94017c955&title=&width=606)
退出功能
点击退出按钮,将会发起一个退出登录的请求logout:
![image.png](https://img-blog.csdnimg.cn/img_convert/9d172aa2ba4376747c49f067075f5a58.png#clientId=ua16b1084-d533-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=709&id=u74bf06fe&margin=[object Object]&name=image.png&originHeight=709&originWidth=1919&originalType=binary&ratio=1&rotation=0&showTitle=false&size=85323&status=done&style=none&taskId=uce15f47b-aef5-4686-8804-54d8918ffb2&title=&width=1919)
/**
* 员工退出登录
* @param request
* @return
*/
@PostMapping("logout")
public ResultBean logout(HttpServletRequest request) {
request.getSession().removeAttribute("employee");
return ResultBean.success("退出成功");
}
员工页面的CURD
新增员工
/**
* 新增员工
* @param employee
* @return
*/
@PostMapping
public ResultBean<String> save(HttpServletRequest request,@RequestBody Employee employee){
log.info("新增员工,员工信息:{}",employee.toString());
//设置初始密码123456,需要进行md5加密处理
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
//获得当前登录用户的id
Long empId = (Long) request.getSession().getAttribute("employee");
employee.setCreateUser(empId);
employee.setUpdateUser(empId);
employeeService.save(employee);
return ResultBean.success("新增员工成功");
}
由于数据库中,员工的账号是具有唯一约束的,所以当新增的员工账号与数据库中已有的数据冲突时,会报异常(SQLIntegrityConstraintViolationException),异常信息为:
:::success
Duplicate entry ‘xxx’ for key ‘idx_username’
:::
意思为username该字段具有唯一约束,不可以存在有重复的值!
我们需要对异常进行处理。
创建一个全局异常类,用于捕获异常:
@ControllerAdvice:参数为需要处理异常的类,例如此时参数为RestController.class,那么加了@RestController注解的类抛出异常时会被捕捉。
@ExceptionHandler:捕获指定的异常
/**
* 全局异常处理
*/
@ControllerAdvice(annotations = {
RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalException {
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public ResultBean exceptionHandle(SQLIntegrityConstraintViolationException e) {
log.info("捕获到该异常" + e.getMessage());
// 账号名重复:Duplicate entry 'test' for key 'employee.idx_username'
if (e.getMessage().contains("Duplicate entry")) {
// 空格分割异常信息,并放入字符串数组中
String[] split = e.getMessage().split(" ");
String msg = split[2] + "已存在!";
return ResultBean.error(msg);
}
return ResultBean.error("未知错误!");
}
}
分页功能
![image.png](https://img-blog.csdnimg.cn/img_convert/1789650b4d2be7fe88e951e10fb2c68d.png#averageHue=#fbf8fb&clientId=ua16b1084-d533-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=486&id=u2fe832d2&margin=[object Object]&name=image.png&originHeight=762&originWidth=1612&originalType=binary&ratio=1&rotation=0&showTitle=false&size=146155&status=done&style=none&taskId=ud3f71da5-60f9-4798-beb2-c6c10e53592&title=&width=1029)
当进入该页面,也就是员工管理页面时,会自动发起一个请求,而我们则需要对该请求进行处理,编写对应的Controller,不过在此之前,我们需要引用MybatisPlus的分页插件,该分页插件可以很好地帮我们对数据进行分页,这个请求在前端中是一个getMemberList方法发起的,可以看到在该方法中,后端提交给前端的数据应该要有records、total这些属性,**正好在MP中,就有一个具有这些属性的分页对象Page!**所以我们的Controller可以将Page对象作为返回的数据!
![image.png](https://img-blog.csdnimg.cn/img_convert/7345bf760bbd185b8e8a094d5c3805bc.png#averageHue=#fdfcfc&clientId=u49d84423-cb9a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=451&id=haGtp&margin=[object Object]&name=image.png&originHeight=451&originWidth=607&originalType=binary&ratio=1&rotation=0&showTitle=false&size=31237&status=done&style=none&taskId=u94463d79-d2f5-4ac5-821b-995ed485324&title=&width=607)![image.png](https://img-blog.csdnimg.cn/img_convert/8fa4af2043b1a770814f09e30a8abf55.png#averageHue=#f6f2f1&clientId=ua16b1084-d533-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=273&id=u090c2ef7&margin=[object Object]&name=image.png&originHeight=322&originWidth=653&originalType=binary&ratio=1&rotation=0&showTitle=false&size=22236&status=done&style=none&taskId=u49ef4c5d-c4d0-4be5-87ca-8e8f3d4b4ef&title=&width=554)
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 创建拦截器
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
对于Controller的编写,也颇有讲究,我们需要接受前端发过来的参数,那么前端发过来哪些参数呢?
当我们在页面的搜索框内输入内容,例如“123”,页面会发起一个请求,这个请求一共携带了三个参数,分别是page(当前页数),pageSize(一页所展示的数据量),name(搜索的关键字),所以我们在Controller也需要接收这三个参数!
![image.png](https://img-blog.csdnimg.cn/img_convert/9c12dc902fabb4df312f61cd6a0f14ce.png#averageHue=#f2f1f0&clientId=ua16b1084-d533-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=144&id=u55005bb3&margin=[object Object]&name=image.png&originHeight=144&originWidth=685&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10124&status=done&style=none&taskId=ub40b0d3a-4500-4432-b943-d3f98db4283&title=&width=685)
/**
* 分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public ResultBean<Page> page(int page, int pageSize, String name) {
log.info("page={},pageSize={},name={}" + page, pageSize, name);
// 构造分页选择器
Page pageInfo = new Page(page, pageSize);
// 构造条件选择器
LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper();
// 模糊查询
lqw.like(StringUtils.isNotEmpty(name), Employee::getName, name);
// 排序
lqw.orderByDesc(Employee::getUpdateTime);
// 执行查询
employeeService.page(pageInfo, lqw);
return ResultBean.success(pageInfo);
}
启用/禁用员工
管理员admin登录后台系统之后,可以对员工账号的状态进行更改,也就是启用账号或者是禁用账号!
![image.png](https://img-blog.csdnimg.cn/img_convert/fcb01bcb11eb03c428a2594f21398cc9.png#averageHue=#fdfafe&clientId=u49d84423-cb9a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=469&id=ueb844599&margin=[object Object]&name=image.png&originHeight=469&originWidth=1722&originalType=binary&ratio=1&rotation=0&showTitle=false&size=83465&status=done&style=none&taskId=u221398e9-b18d-4d8d-a259-7ee53f4c013&title=&width=1722)
普通员工登录后台系统之后,并不能对账号的状态进行更改:
![image.png](https://img-blog.csdnimg.cn/img_convert/1c89a444ed12e9366a005d3620b1654a.png#averageHue=#fdfbfe&clientId=u49d84423-cb9a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=479&id=u27296937&margin=[object Object]&name=image.png&originHeight=479&originWidth=1752&originalType=binary&ratio=1&rotation=0&showTitle=false&size=74046&status=done&style=none&taskId=u9456638a-e34b-483b-a6fb-50b7c6dd1c5&title=&width=1752)
首先,分析一下为什么管理员admin会有员工状态更改选项:
1、在前段页面中,有这样一段代码,这是一个生命周期函数,在页面启动时就会执行,这个init方法会拿到本地存储中的userInfo,也就是当前登录用户,并且拿到user的username属性!
![image.png](https://img-blog.csdnimg.cn/img_convert/8be98aeb3c6385f806610be86cb87a5c.png#averageHue=#fef9f8&clientId=u49d84423-cb9a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=197&id=uc9308996&margin=[object Object]&name=image.png&originHeight=197&originWidth=871&originalType=binary&ratio=1&rotation=0&showTitle=false&size=17748&status=done&style=none&taskId=u48eed1f7-2117-4944-979b-30928d975b1&title=&width=871)
2、在下面所示的代码中,这里是用于展示操作选项的,可以看到v-if对user进行了值的判断,如果user为admin,才会显示状态更改的操作栏,而且在这里也对状态码进行了动态判断,如果用户已经被封禁了,那么在状态更改的操作选项中显示的应该是“启用”!
![image.png](https://img-blog.csdnimg.cn/img_convert/d8d23074ab1ef6d837b9bb46017c798c.png#averageHue=#fdfaf9&clientId=u49d84423-cb9a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=320&id=u07e189a9&margin=[object Object]&name=image.png&originHeight=320&originWidth=819&originalType=binary&ratio=1&rotation=0&showTitle=false&size=20458&status=done&style=none&taskId=ue385126e-e7ba-4b98-9d28-a909e3ee204&title=&width=819)
分析完毕,接下来分析页面请求,当我们点击操作,也就是启用/禁用时,页面会发起一个请求,需要注意的是,这个请求方式是PUT:
![image.png](https://img-blog.csdnimg.cn/img_convert/e7ef42e93a92587eee67579003b980fd.png#averageHue=#f3f2f1&clientId=u49d84423-cb9a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=144&id=u819c1116&margin=[object Object]&name=image.png&originHeight=144&originWidth=731&originalType=binary&ratio=1&rotation=0&showTitle=false&size=9421&status=done&style=none&taskId=ub8f9cbe6-682a-4562-ac1e-dc1dd9c989f&title=&width=731)
对象转换器与消息转换器(基于Jackson进行java对象到json数据的转换)
这个时候你可能已经自信满满写好了Controller,但是运行之后你会发现,程序可以正常运行,但是数据库并没有更新,前后分析一通,发现我们在page方法中传给页面的数据是正确的,但是我们点击更改用户状态,也就是“禁用”时,页面回传给我们的参数(用户id)却发生了差错!这是因为js对long类型的数据进行处理时会丢失精度,导致提交的用户id和数据库中的id并不一致!所以我们需要进行优化,也就是给页面响应json数据时进行处理,将long类型的数据转为String字符串!
具体实现步骤:
1、提供对象转换器JacksonObjectMapper,基于Jackson进行java对象到json数据的转换
2、在WebMvcConfig中配置类中扩展SpringMVC的转换器,在此消息转换器中使用提供的对象转换器来进行java对象到json数据的转换
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON
* 这个对象转换器不仅提供了Long类型到String类型的转换,也提供了一些日期类型的转换
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 扩展Mvc的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 创建一个新的消息转化器
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
// 设置消息转换器
messageConverter.setObjectMapper(new JacksonObjectMapper());
// 将消息转换器追加到Mvc的转换器集合中,index表示优先级
converters.add(0, messageConverter);
}
}
/**
* 更改用户状态
* @param request
* @param employee 网页已经传入的参数中已经包含用户id和状态码了
* @return
*/
@PutMapping
public ResultBean<String> update(HttpServletRequest request, @RequestBody Employee employee) {
log.info("id=" + employee.getId());
// 获取当前登录用户
Long userID = (Long) request.getSession().getAttribute("employee");
// 更改参数
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(userID);
// 更新用户
employeeService.updateById(employee);
return ResultBean.success("修改成功");
}
修改员工
前端请求的路径如下所示,可以看到这里是路径携带的参数,想要获得该情况下的参数,Controller也会有所不同!不过这里的请求仅仅是前端点击修改选项时,可以展现修改用户的当前信息,并不是直接的修改操作,需要注意!
![image.png](https://img-blog.csdnimg.cn/img_convert/07c08e0f92d0e7ad66d215016ecf24a0.png#averageHue=#fcfbfb&clientId=uf4f27582-c77c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=192&id=u403e099d&margin=[object Object]&name=image.png&originHeight=192&originWidth=353&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10980&status=done&style=none&taskId=u1c710c03-3a35-47b7-b5a0-0dae0e45160&title=&width=353)![image.png](https://img-blog.csdnimg.cn/img_convert/6b9d08841c32f57407f0b9e6323dc5a5.png#averageHue=#f4f3f2&clientId=uf4f27582-c77c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=199&id=u4d35e43b&margin=[object Object]&name=image.png&originHeight=218&originWidth=411&originalType=binary&ratio=1&rotation=0&showTitle=false&