unionj-generator快速上手-后端篇

Photo by Hack Capital on Unsplash

unionj-generator是开源的一套基于openAPI 3.0规范的一套代码生成器,核心组件包括unionj-generator-openapi、unionj-generator-backend、unionj-generator-service等。

  • unionj-generator-openapi包含一套dsl语言,用于定义接口和数据模型。

  • unionj-generator-backend可以基于编写的dsl表达式生成spring boot的controller层接口(proto类)和入参出参vo类。

  • unionj-generator-service可以基于编写的dsl表达式或者openapi3规范的json文档生成typescript的http客户端代码。

我们已经将这套工具应用于公司项目的开发中,节省了前后端开发的工作量,也提高了前后端同事沟通协作的效率。

我们做这套工具出于以下几条核心理念:

  • 设计优先:产品经理给到原型图后,由开发组长负责分析需求和分解任务。后端同学根据各自领到的任务,对照原型图理解任务要求,设计接口的路径、入参和出参
  • 契约精神:前后端同学协作时,以openapi 3.0规范的接口文档为契约,通过代码生成方式实现对接口请求的封装,采用typescript有类型约束的Javascript超集,实现前端工程对契约的遵守;同时生成后端的接口类和vo类代码,实现后端服务对契约的遵守

下面以一个用户管理服务的demo项目为例,通过介绍四种常用的接口:

  • 入参为multipart/form-data格式POST接口

  • 入参为json格式POST接口

  • 查询字符串传参GET接口

  • 下载文件接口

来说明如何快速集成到项目开发中。

准备


  • IDE:Intellij idea

  • Java: java version "1.8.0_281"

  • Maven: Apache Maven 3.6.0

  • postman:推荐用最新版

  • 安装unionj-generator到本地repository:

git clone git@github.com:unionj-cloud/unionj-generator.git
mvn clean install -Dmaven.test.skip=true
  • 安装unionj-generator-maven-plugin到本地repository:
git clone git@github.com:unionj-cloud/unionj-generator-maven-plugin.git
mvn clean install -Dmaven.test.skip=true
  • 安装unionj-java-archetype到本地repository
git clone git@github.com:unionj-cloud/unionj-java-archetype.git
mvn clean install

初始化工程


用准备的unionj-java-archetype初始化一个项目guide。执行命令:

mvn archetype:generate \
-DarchetypeGroupId=cloud.unionj \
-DarchetypeArtifactId=unionj-java-archetype \
-DarchetypeVersion=0.0.1-SNAPSHOT \
-DinteractiveMode=false \
\
-DgroupId=cloud.unionj \
-DartifactId=guide \
-Dversion=0.0.1-SNAPSHOT \
-Dpackage=cloud.unionj.guide

然后用idea打开

cd guide
idea .

项目结构如图:

image
  • guide-api:服务接口模块,启动类是cloud.unionj.guide.api.ApiApplication类
  • guide-gen:接口设计模块,入口类是cloud.unionj.guide.gen.Openapi3Designer类

需求说明


只有五个接口:

  • 注册用户接口

  • 用户信息编辑接口

  • 用户信息查询接口

  • 用户头像图片下载接口

  • 用户分页列表接口

开始设计


首先要修改guide-gen模块的入口文件,通过dsl包里的静态方法定义接口的基本信息,比如标题,版本,已经服务地址,最后返回Openapi3对象。最终是用这个对象来生成代码和json接口文档的。

需要注意的点:

  • 用到表单的场景中,暂时不支持Content-Type是x-www-form-urlencoded的表单入参,只支持multipart/form-data,因为multipart/form-data既可以上传文件,也可以传递键值对,比较通用,作者就偷了个懒。

  • 接口入参的request body那里,如果不指定SchemaType(下文会介绍),默认Content-Type为application/json

  • 总的来说,推荐大家设计入参请求体是formdata或者json的接口

  • 接口设计的流程一般是先设计schema,也就是入参请求体(如果需要的话)和出参的vo类,再设计接口

package cloud.unionj.guide.gen;

import cloud.unionj.generator.openapi3.PathConfig;
import cloud.unionj.generator.openapi3.expression.paths.ParameterBuilder;
import cloud.unionj.generator.openapi3.model.Openapi3;
import cloud.unionj.generator.openapi3.model.Schema;
import cloud.unionj.generator.openapi3.model.paths.Parameter;

import static cloud.unionj.generator.openapi3.PathHelper.get;
import static cloud.unionj.generator.openapi3.PathHelper.post;
import static cloud.unionj.generator.openapi3.dsl.Generic.generic;
import static cloud.unionj.generator.openapi3.dsl.Openapi3.openapi3;
import static cloud.unionj.generator.openapi3.dsl.Schema.schema;
import static cloud.unionj.generator.openapi3.dsl.SchemaHelper.*;
import static cloud.unionj.generator.openapi3.dsl.info.Info.info;
import static cloud.unionj.generator.openapi3.dsl.servers.Server.server;

public class Openapi3Designer {

  public static Openapi3 design() {
    Openapi3 openAPI3 = openapi3(ob -> {
      info(ib -> {
        ib.title("用户管理模块");
        ib.version("v1.0.0");
      });

      server(sb -> {
        sb.url("http://unionj.cloud");
      });

    });
    return openAPI3;
  }

}

注册用户接口

此接口为了说明multipart/form-data入参的POST接口如何设计。入参是注册表单数据,出参是返回用户id。

先设计schema

private static Schema ResultVO = schema(sb -> {
    sb.type("object");
    sb.title("ResultVO");
    sb.properties("code", int32);
    sb.properties("msg", string);
    sb.properties("data", T);
});

public static Schema UserRegisterFormVO = schema(sb -> {
    sb.type("object");
    sb.title("UserRegisterFormVO");
    sb.description("用户注册表单");
    sb.properties("username", string("用户名"));
    sb.properties("password", string("密码"));
});

public static Schema UserRegisterRespVO = schema(sb -> {
    sb.type("object");
    sb.title("UserRegisterRespVO");
    sb.description("用户注册结果");
    sb.properties("id", int64("用户ID"));
});

public static Schema ResultVOUserRegisterRespVO = generic(gb -> {
    gb.generic(ResultVO, ref(UserRegisterRespVO.getTitle()));
});

需要注意的点:

  • title必填,title是后续生成的代码的类名和json文档里的schema名

  • vo类的schema定义里type必须是"object"

  • properties方法里定义属性名和属性类型

  • 属性类型有helper类,java里的基本且常用的类型的schema已经封装好了,开箱即用,只要静态导入即可:

import static cloud.unionj.generator.openapi3.dsl.SchemaHelper.*;
  • generic方法用于生成泛型,最后生成的代码是ResultVO<UserRegisterRespVO>
  • ResultVO类的data属性是泛型属性。一个schema最多只能有一个T类型的属性

具体有哪些请查看源码或者代码库的readme

再设计接口

post("/api/user/register", PathConfig.builder()
     .summary("用户注册接口")
     .tags(new String[]{"用户管理模块", "User"})
     .reqSchema(UserRegisterFormVO)
     .reqSchemaType(PathConfig.SchemaType.FORMDATA)
     .respSchema(ResultVOUserRegisterRespVO)
     .build());

需要注意的点:

  • summary里写接口说明,方便调用方理解接口是做什么用的

  • tags传字符串数组,第一个元素传语义化的标签,一般用于其他UI版本的接口文档用来做锚点或者是菜单分组,第二个元素如果有,则会用于生成代码,做接口名称。tags非必填,不需要刻意忽略不写。代码的接口名称默认是取请求地址用"/"分割后的第一个非空字符串元素

  • reqSchema传入参的schema

  • reqSchemaType传上文提到的schema的类型,是作者定义的。暂时只有三种

public enum SchemaType {
    JSON("json"),
    FORMDATA("formdata"),
    STREAM("stream");

    //省略
}
  • respSchema传出参的schema

到这里,有些同学可能已经迫不及待想看效果。莫急。全部设计完再看生成效果.

用户信息编辑接口

此接口为了说明multipart/form-data入参,同时包含文件上传的POST接口如何设计。入参是用户信息编辑的表单数据,用户ID放进查询字符串里,出参只返回字符串(比如"ok")。

先设计schema

public static Schema UserEditFormVO = schema(sb -> {
    sb.type("object");
    sb.title("UserEditFormVO");
    sb.description("用户信息编辑表单");
    sb.properties("name", string("真实姓名"));
    sb.properties("age", int32("年龄"));
    sb.properties("sex", enums("性别", new String[]{"BOY", "GIRL"}));
    sb.properties("avatar", file("用户头像"));
});

public static Schema ResultVOstring = generic(gb -> {
    gb.generic(ResultVO, string);
});

需要注意的点:

  • file:文件类型的schema
  • enums:枚举类型的schema。枚举类型的属性会作为内部enum类生成

再设计接口

post("/api/user/edit", PathConfig.builder()
          .summary("用户信息编辑接口")
          .tags(new String[]{"用户管理模块", "User"})
          .parameters(new Parameter[]{
              ParameterBuilder.builder().name("id").description("用户ID").in(Parameter.InEnum.QUERY).required(true).schema(int64).build(),
          })
          .reqSchema(UserEditFormVO)
          .reqSchemaType(PathConfig.SchemaType.FORMDATA)
          .respSchema(ResultVOstring)
          .build());

需要注意的点:

  • Parameter.InEnum枚举类表示参数放在哪里,暂时只支持放在请求链接的查询字符串里

例如:?name=jack&id=1

用户信息查询接口

此接口为了说明GET请求查询字符串传参的接口如何设计。入参是用户ID,出参返回用户详情。

先设计schema

public static Schema UserDetailVO = schema(sb -> {
    sb.type("object");
    sb.title("UserDetailVO");
    sb.description("用户详情");
    sb.properties("id", int64("用户ID"));
    sb.properties("name", string("真实姓名"));
    sb.properties("age", int32("年龄"));
    sb.properties("sex", enums("性别", new String[]{"BOY", "GIRL"}));
    sb.properties("avatar", string("用户头像下载地址"));
});

public static Schema ResultVOUserDetailVO = generic(gb -> {
    gb.generic(ResultVO, ref(UserDetailVO.getTitle()));
});

再设计接口

get("/api/user/detail", PathConfig.builder()
    .summary("用户信息查询接口")
    .tags(new String[]{"用户管理模块", "User"})
    .parameters(new Parameter[]{
        ParameterBuilder.builder().name("id").description("用户ID").in(Parameter.InEnum.QUERY).required(true).schema(int64).build(),
    })
    .respSchema(ResultVOUserDetailVO)
    .build());

用户头像图片下载接口

此接口为了说明文件下载的接口如何设计。入参是用户ID,出参返回文件二进制流,这里是GET请求,实际用GET请求还是POST请求都无所谓。

先设计schema

再设计接口

get("/api/user/avatar", PathConfig.builder()
    .summary("用户头像下载接口")
    .tags(new String[]{"用户管理模块", "User"})
    .parameters(new Parameter[]{
        ParameterBuilder.builder().name("id").description("用户ID").in(Parameter.InEnum.QUERY).required(true).schema(int64).build(),
    })
    .respSchema(file("用户头像"))
    .build());

用户分页列表接口

此接口为了说明json格式的请求体传参的接口如何设计。入参是分页和查询条件,出参返回用户列表。

先设计schema

public static Schema UserPageReqVO = schema(sb -> {
    sb.type("object");
    sb.title("UserPageReqVO");
    sb.description("用户列表分页查询条件");
    sb.properties("size", int32("每页多少条数据"));
    sb.properties("current", int32("第几页"));
    sb.properties("sort", string("排序条件字符串:排序字段前使用'-'(降序)和'+'(升序)号表示排序规则,多个排序字段用','隔开",
                                 "+age,-create_at"));
    sb.properties("sex", string("筛选条件:用户性别"));
});

public static Schema PageResultVO = schema(sb -> {
    sb.type("object");
    sb.title("PageResultVO");
    sb.properties("items", ListT);
    sb.properties("total", int64("总数"));
    sb.properties("size", int32("每页多少条数据"));
    sb.properties("current", int32("当前页码"));
    sb.properties("pages", int32("总页数"));
});

public static Schema PageResultVOUserDetailVO = generic(gb -> {
    gb.generic(PageResultVO, ref(UserDetailVO.getTitle()));
});

public static Schema ResultVOPageResultVOUserDetailVO = generic(gb -> {
    gb.generic(ResultVO, ref(PageResultVOUserDetailVO.getTitle()));
});

需要注意的点:

  • 泛型支持嵌套,例如:ResultVOPageResultVOUserDetailVO生成的代码是ResultVO<PageResultVO<UserDetailVO>>

再设计接口

post("/api/user/page", PathConfig.builder()
     .summary("用户分页列表接口")
     .tags(new String[]{"用户管理模块", "User"})
     .reqSchema(UserPageReqVO)
     .respSchema(ResultVOPageResultVOUserDetailVO)
     .build());

需要注意的点:

  • reqSchemaType不设置的情况下,默认是json格式的请求体

至此,我们接口和vo类全部设计完成,完整代码如下:

package cloud.unionj.guide.gen;

import cloud.unionj.generator.openapi3.PathConfig;
import cloud.unionj.generator.openapi3.expression.paths.ParameterBuilder;
import cloud.unionj.generator.openapi3.model.Openapi3;
import cloud.unionj.generator.openapi3.model.Schema;
import cloud.unionj.generator.openapi3.model.paths.Parameter;

import static cloud.unionj.generator.openapi3.PathHelper.get;
import static cloud.unionj.generator.openapi3.PathHelper.post;
import static cloud.unionj.generator.openapi3.dsl.Generic.generic;
import static cloud.unionj.generator.openapi3.dsl.Openapi3.openapi3;
import static cloud.unionj.generator.openapi3.dsl.Schema.schema;
import static cloud.unionj.generator.openapi3.dsl.SchemaHelper.*;
import static cloud.unionj.generator.openapi3.dsl.info.Info.info;
import static cloud.unionj.generator.openapi3.dsl.servers.Server.server;

public class Openapi3Designer {

  private static Schema ResultVO = schema(sb -> {
    sb.type("object");
    sb.title("ResultVO");
    sb.properties("code", int32);
    sb.properties("msg", string);
    sb.properties("data", T);
  });

  public static Schema UserRegisterFormVO = schema(sb -> {
    sb.type("object");
    sb.title("UserRegisterFormVO");
    sb.description("用户注册表单");
    sb.properties("username", string("用户名"));
    sb.properties("password", string("密码"));
  });

  public static Schema UserRegisterRespVO = schema(sb -> {
    sb.type("object");
    sb.title("UserRegisterRespVO");
    sb.description("用户注册结果");
    sb.properties("id", int64("用户ID"));
  });

  public static Schema ResultVOUserRegisterRespVO = generic(gb -> {
    gb.generic(ResultVO, ref(UserRegisterRespVO.getTitle()));
  });

  public static Schema UserEditFormVO = schema(sb -> {
    sb.type("object");
    sb.title("UserEditFormVO");
    sb.description("用户信息编辑表单");
    sb.properties("name", string("真实姓名"));
    sb.properties("age", int32("年龄"));
    sb.properties("sex", enums("性别", new String[]{"BOY", "GIRL"}));
    sb.properties("avatar", file("用户头像"));
  });

  public static Schema ResultVOstring = generic(gb -> {
    gb.generic(ResultVO, string);
  });

  public static Schema UserDetailVO = schema(sb -> {
    sb.type("object");
    sb.title("UserDetailVO");
    sb.description("用户详情");
    sb.properties("id", int64("用户ID"));
    sb.properties("name", string("真实姓名"));
    sb.properties("age", int32("年龄"));
    sb.properties("sex", enums("性别", new String[]{"BOY", "GIRL"}));
    sb.properties("avatar", string("用户头像下载地址"));
  });

  public static Schema ResultVOUserDetailVO = generic(gb -> {
    gb.generic(ResultVO, ref(UserDetailVO.getTitle()));
  });

  public static Schema UserPageReqVO = schema(sb -> {
    sb.type("object");
    sb.title("UserPageReqVO");
    sb.description("用户列表分页查询条件");
    sb.properties("size", int32("每页多少条数据"));
    sb.properties("current", int32("第几页"));
    sb.properties("sort", string("排序条件字符串:排序字段前使用'-'(降序)和'+'(升序)号表示排序规则,多个排序字段用','隔开",
        "+age,-create_at"));
    sb.properties("sex", string("筛选条件:用户性别"));
  });

  public static Schema PageResultVO = schema(sb -> {
    sb.type("object");
    sb.title("PageResultVO");
    sb.properties("items", ListT);
    sb.properties("total", int64("总数"));
    sb.properties("size", int32("每页多少条数据"));
    sb.properties("current", int32("当前页码"));
    sb.properties("pages", int32("总页数"));
  });

  public static Schema PageResultVOUserDetailVO = generic(gb -> {
    gb.generic(PageResultVO, ref(UserDetailVO.getTitle()));
  });

  public static Schema ResultVOPageResultVOUserDetailVO = generic(gb -> {
    gb.generic(ResultVO, ref(PageResultVOUserDetailVO.getTitle()));
  });

  public static Openapi3 design() {
    Openapi3 openAPI3 = openapi3(ob -> {
      info(ib -> {
        ib.title("用户管理模块");
        ib.version("v1.0.0");
      });

      server(sb -> {
        sb.url("http://unionj.cloud");
      });

      post("/api/user/register", PathConfig.builder()
          .summary("用户注册接口")
          .tags(new String[]{"用户管理模块", "User"})
          .reqSchema(UserRegisterFormVO)
          .reqSchemaType(PathConfig.SchemaType.FORMDATA)
          .respSchema(ResultVOUserRegisterRespVO)
          .build());

      post("/api/user/edit", PathConfig.builder()
          .summary("用户信息编辑接口")
          .tags(new String[]{"用户管理模块", "User"})
          .parameters(new Parameter[]{
              ParameterBuilder.builder().name("id").description("用户ID").in(Parameter.InEnum.QUERY).required(true).schema(int64).build(),
          })
          .reqSchema(UserEditFormVO)
          .reqSchemaType(PathConfig.SchemaType.FORMDATA)
          .respSchema(ResultVOstring)
          .build());

      get("/api/user/detail", PathConfig.builder()
          .summary("用户信息查询接口")
          .tags(new String[]{"用户管理模块", "User"})
          .parameters(new Parameter[]{
              ParameterBuilder.builder().name("id").description("用户ID").in(Parameter.InEnum.QUERY).required(true).schema(int64).build(),
          })
          .respSchema(ResultVOUserDetailVO)
          .build());

      get("/api/user/avatar", PathConfig.builder()
          .summary("用户头像下载接口")
          .tags(new String[]{"用户管理模块", "User"})
          .parameters(new Parameter[]{
              ParameterBuilder.builder().name("id").description("用户ID").in(Parameter.InEnum.QUERY).required(true).schema(int64).build(),
          })
          .respSchema(file("用户头像"))
          .build());

      post("/api/user/page", PathConfig.builder()
          .summary("用户分页列表接口")
          .tags(new String[]{"用户管理模块", "User"})
          .reqSchema(UserPageReqVO)
          .respSchema(ResultVOPageResultVOUserDetailVO)
          .build());
    });
    return openAPI3;
  }

}

生成代码和json文档

image

在你的idea右上角有如图所示的maven按钮,点击打开窗口

image

双击图中画红勾的compile,执行代码生成命令,可以看到下方控制台有如下输出:

image

看到Code generated和BUILD SUCCESS就说明执行成功了。生成的代码在这里:

image

还没有导入工程中。打开pom.xml文件,格式化一下,在modules标签里加入箭头所示的两行代码。

image

再点击下图所示按钮即可导入工程:

image
image

生成的proto代码如下:

package cloud.unionj.guide.proto;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.*;
import cloud.unionj.guide.vo.*;

public interface UserProto {

    @GetMapping("/api/user/avatar")
    ResponseEntity<byte[]> getApiUserAvatar(
        @RequestParam("id") Long id
    );

    @GetMapping("/api/user/detail")
    ResultVO<UserDetailVO> getApiUserDetail(
        @RequestParam("id") Long id
    );

    @PostMapping("/api/user/edit")
    ResultVO<String> postApiUserEdit(
        @RequestParam(value="name", required=false) String name,
        @RequestParam(value="age", required=false) Integer age,
        @RequestParam(value="sex", required=false) String sex,
        @RequestParam("id") Long id,
        @RequestPart(value="avatar", required=false) MultipartFile avatar
    );

    @PostMapping("/api/user/page")
    ResultVO<PageResultVO<UserDetailVO>> postApiUserPage(
        @RequestBody UserPageReqVO body
    );

    @PostMapping("/api/user/register")
    ResultVO<UserRegisterRespVO> postApiUserRegister(
        @RequestParam(value="username", required=false) String username,
        @RequestParam(value="password", required=false) String password
    );

}

生成的json文档在这里:

image

如何使用

代码如何使用

还是以文本的guide项目为例,在guide-api模块的pom.xml文件里加入上文生成的guide-proto和guide-vo模块的依赖:

image

在guide-api模块里创建controller包和UserController类,实现UserProto接口,点implement methods按钮,可以自动生成打桩代码。

image

image

然后就可以实现业务需求了!

json文档如何使用

导入postman

image

点击import按钮,弹出模态框,将生成的openapi3.json文件拖入上传区域:

image

点击import按钮导入,效果是这样的:

image.png

User文件夹是多余的,因为postman默认是用json文档里的每个接口的tags值来做分组的。因为咱们设计的时候tags里传了两个值,所以生成了多余的User文件夹,可以删掉或者忽略。

生成前端http请求客户端

《unionj-generator快速上手-前端篇》已发布。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容

  • 这里主要推荐一下自己平时常用,提高效率的一些库和软件。 前端常用 sweetalert2[https://gith...
    安之烟波客阅读 2,027评论 4 4
  • 写在前面 总体而言,python作为一门胶水语言,由于学习成本相对较低,主要用途还是在原型验证、模型探索等方面,而...
    wanzhouyi阅读 4,446评论 0 2
  • https://juejin.im/post/5d3a7134f265da1b5d57f1ed# 一个人走的更快,...
    videring阅读 634评论 0 0
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,523评论 28 53
  • 信任包括信任自己和信任他人 很多时候,很多事情,失败、遗憾、错过,源于不自信,不信任他人 觉得自己做不成,别人做不...
    吴氵晃阅读 6,181评论 4 8