目录

项目依赖管理 (Spring Boot为例)

命名规范

数据库设计字段名,接口返回字段名称请使用小驼峰形式。

1
2
3
4
5
6
7
// 如果数据库名字不是下划线隔开单词 
@TableField("phonenumber")
private String phoneNumber;

// 如果第三方是非常让人生气的格式
@JsonProperty("access_token")
private String accessToken;

依赖管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<dependencyManagement>
  <dependencies>
    <!-- 虽然 jupiter-engine 包含 jupiter-api 但还是指定版本,不然会坑 -->
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>${junit.version}</version>
      <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.30</version>
      <scope>runtime</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api -->
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>6.0.0</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

API 依赖

1
2
3
4
<properties>
  <ynthm.version>1.0.0</ynthm.version>
  <ynthm-api-infra.version>1.0.1-SNAPSHOT</ynthm-api-infra.version>
</properties>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!-- 基础设施接口 提供短信邮件发送-->
<dependency>
   <groupId>com.ynthm</groupId>
   <artifactId>ynthm-api-infra</artifactId>
   <version>${ynthm-api-infra.version}</version>
</dependency>
<!-- 用户服务接口 提供用户租管理-->
<dependency>
  <groupId>com.ynthm</groupId>
  <artifactId>ynthm-api-user</artifactId>
  <version>${ynthm.version}</version>
</dependency>

基础依赖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<dependencies>
    <!--基础核心依赖,提供最小依赖,封装大量 util-->
    <dependency>
        <groupId>com.ynthm</groupId>
        <artifactId>ynthm-common-core</artifactId>
        <version>${ynthm.version}</version>
    </dependency>
    <!-- Web服务基础依赖 提供网络相关基础配置,Mybatis Plus  EasyExcel相关工具-->
    <dependency>
        <groupId>com.ynthm</groupId>
        <artifactId>ynthm-common-web-core</artifactId>
        <version>${ynthm.version}</version>
    </dependency>
    <dependency>
        <groupId>com.ynthm</groupId>
        <artifactId>ynthm-common-redis</artifactId>
        <version>${ynthm.version}</version>
    </dependency>
</dependencies>

结果与异常

  • 接口返回使用 R<T> ,分页结果 R<PageResp>
  • 接口接受参数使用单个实体,不要使用数据库对应实体
  • POST 请求参数一律添加 @Validated @RequestBody
  • 每个平台的错误码枚举请实现 ResultCode 接口,枚举范围已经在 BaseResultCode 中定义
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public interface MessageApi {

  /**
   * 发送短信
   *
   * @param req 请求参数
   * @return 结果
   */
  @PostMapping("/sms/send")
  R<String> send(@Validated @RequestBody SendSmsReq req);
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/**
 * 结果错误码接口
 *
 * @author Ethan Wang
 */
public interface ResultCode {
  /**
   * 业务状态码
   *
   * @return
   */
  int getCode();

  /**
   * 业务状态消息
   *
   * @return
   */
  String getMessage();
}

/**
 * 公共错误码 <p>路径区分业务模块</p>
 *
 * @author Ethan Wang
 */
public enum BaseResultCode implements ResultCode {

  OK(0, "OK"),
  ERROR(-1, "ERROR"),

  SUCCESS(200, "SUCCESS"),

  NULL(998, "系统错误 NULL"),
  UNKNOWN_ERROR(999, "Unknown Error!"),

  /**
   * 权限部分
   */
  AUTHENTICATION_EXCEPTION(1000, "权限异常"),
  SIGN_VERIFY_FAILED(1001, "签名验证失败"),
  UNAUTHORIZED(1002, "未登录,请先登录。"),
  BAD_CREDENTIALS(1003, "用户名或密码错误,请确认。"),
  WRONG_PASSWORD(1004, "密码错误,请确认。"),
  USERNAME_NOT_FOUND(1005, "用户名不存在。"),
  CAPTCHA_MISMATCH(1006, "验证码不匹配"),
  NOT_BINDING_WECHAT_QRCODE(1007, "未绑定微信开放平台网页应用二维码登陆"),

  /**
   * 用户输入
   */
  VALID_ERROR(2000, "参数校验错误"),
  DB_NOT_EXIST(2001, "数据库不存在"),
  DB_EXIST(2002, "数据库已存在"),
  DB_INSERT_FAILED(2002, "数据库插入失败"),

  SERVICE_FALLBACK_SYS(3000, "系统服务 fallback"),
  SERVICE_FALLBACK_SMS(3001, "短信服务 fallback"),

  EXCEL_EXPORT_FAILED(4000, "Excel 导出失败"),

  /**
   * 调用第三方服务发生错误
   */
  THIRD_PART_SERVICE_ERROR(90000, "调用第三方服务发生错误"),
  ALI_SMS_SERVER_ERROR(90001, "阿里短信服务异常"),
  WECHAT_OPEN_PLATFORM_ERROR(90002, "微信开放平台异常"),

  /**
   * 第二方服务调用发生错误
   */
  S_INFRA_ERROR(100000, "INFRA错误"),
  S_SYSTEM_ERROR(110000, "SYS错误"),
  S_MES_ERROR(120000, "MES错误"),
  S_ERP_ERROR(130000, "ERP错误"),
  S_CRM_ERROR(140000, "CRM错误"),
  S_WMS_ERROR(150000, "WMS错误"),
  S_SCM_ERROR(160000, "SCM错误"),
  S_AUTH_ERROR(170000, "AUTH错误"),
  S_CROSS_LOGIN_ERROR(180000, "CROSS LOGIN错误"),
  ;


  private final int code;
  private final String message;

  private BaseResultCode(int code, String message) {
    this.code = code;
    this.message = message;
  }

  @Override
  public int getCode() {
    return code;
  }

  @Override
  public String getMessage() {
    return message;
  }
}

public class BaseException extends RuntimeException {
  protected ResultCode resultCode;

  public BaseException(String message) {
    super(message);
    resultCode = BaseResultCode.ERROR;
  }

  public BaseException(ResultCode resultCode) {
    super(resultCode.getMessage());
    this.resultCode = resultCode;
  }

  public BaseException(ResultCode resultCode, Throwable cause) {
    super(cause);
    this.resultCode = resultCode;
  }

  public BaseException(Throwable cause) {
    super(cause);
    this.resultCode = BaseResultCode.ERROR;
  }

  public ResultCode getResultCode() {
    return resultCode;
  }
}

API 使用说明

mapstruct 实体映射

处理DTO/VO与 PO/Entity 之间转化

注意 Mapper 包与 Mybatis 扫描 Mapper 的包区分开,避免冲突。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Mapper
public interface UserMapper {
  UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

  SysUserEntity userToUserDto(SysUser user);

  @Mapping(target = "phoneNumber", source = "phonenumber")
  SysUserVo userToUserVo(SysUser userEntity);

  List<SysUserVo> userListToUserVoList(List<SysUser> userEntityList);
}

class UserMapperTest {
  @Test
  void carToCarDto() {
    SysUser sysUser = new SysUser();
    String name = "EthanWang";
    sysUser.setUserName(name);
    Assertions.assertEquals(UserMapper.INSTANCE.userToUserDto(sysUser).getUserName(), name);
  }
}

短信邮件服务

OpenFegin 服务提供方范例

接口范例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@FeignClient(value = ServiceNameConstants.SERVICE_INFRA, contextId = ServiceNameConstants.CID_MESSAGE, fallbackFactory = SmsApiFallbackFactory.class)
public interface MessageApi {

  /**
   * 发送短信
   *
   * @param req 请求参数
   * @return
   * @throws Exception
   */
  @PostMapping("/sms/send")
  R<String> send(@Validated @RequestBody SendSmsReq req);

  /**
   * 批量发送短信
   *
   * @param req
   * @return
   */
  @PostMapping("/sms/sendBatch")
  R<String> sendBatch(@Validated @RequestBody SmsSendBatchReq req);

  /**
   * 发送邮件
   *
   * @param req 请求参数
   * @return
   */
  @PostMapping("/mail/send")
  R<String> sendEmail(@Validated @RequestBody SendMailReq req);

  /**
   * 发送验证码
   * @param request 请求参数
   * @return
   */
  @PostMapping("/code/sms")
  R<Void> sendCaptcha(@RequestBody @Valid SendPhoneCaptchaReq request);
}

OpenFegin 接口调用范例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RestController
@RequestMapping("/open")
public class OpenController {

  private CaptchaHandler captchaHandler;

  private MessageApi messageApi;
  
  private UserCombineService userCombineService;

  @Resource
  public void setSmsApi(MessageApi messageApi) {
    this.messageApi = messageApi;
  }
  
  @Resource
  public void setCaptchaHandler(CaptchaHandler captchaHandler) {
    this.captchaHandler = captchaHandler;
  }
  
  @Autowired
  public void setUserService(UserCombineService userCombineService) {
    this.userCombineService = userCombineService;
  }
  
  @PostMapping("/loginByPhone")
  public R<LoginSuccessResp> loginByPhone(@Validated @RequestBody PhoneLoginReq req) {
    if (captchaHandler.match(new VerificationCodeMatchDto()
            .setCaptchaScope(CaptchaScopeEnum.PHONE_LOGIN)
            .setCode(req.getCaptcha()).setUserFlag(req.getPhoneNumber()))) {

      return userCombineService.loginUser(null, req.getPhoneNumber());
    }

    return R.error(BaseResultCode.CAPTCHA_MISMATCH);
  }
}

Excel 导入导出

  • ExcelUtil 基础工具类 封装阿里 easyexcel
  • ExcelHelper Web 项目批量导出,注入到 Controller 接口使用

批量导出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@RestController
@RequestMapping("/user/login/history")
public class LoginHistoryController
{
    private final LoginHistoryService loginHistoryService;
 
    private ExcelHelper excelHelper;
    
    public LoginHistoryController(LoginHistoryService loginHistoryService) {
        this.loginHistoryService = loginHistoryService;
    }
    
    @Autowired
    public void setExcelHelper(ExcelHelper excelHelper) {
        this.excelHelper = excelHelper;
    }
    
    @PostMapping("/export")
    public void export(HttpServletResponse response, @RequestBody PageReq<LoginHistoryReq> req) {
        excelHelper.export(response, LoginHistoryVo.class, "登录日志_", "登录日志", req, pageReq -> loginHistoryService.page(req));
    }
}

@Data
@ExcelIgnoreUnannotated
@ColumnWidth(15)
public class LoginHistoryVo {
  private static final long serialVersionUID = 1L;

  /**
   * ID
   */
  @ExcelProperty("序号")
  private Long id;

  /**
   * 用户账号
   */
  @ExcelProperty("用户账号")
  private String username;

  /**
   * 状态 0成功 1失败
   */
  @ExcelProperty(value = "状态", converter = ExcelEnumConverter.class)
  private LoginStatus status;

  /**
   * 地址
   */
  @ExcelProperty("地址")
  private String ipAddress;

  /**
   * 描述
   */
  @ExcelProperty("描述")
  private String message;

  /**
   * 访问时间
   */
  @ColumnWidth(20)
  @ExcelProperty("访问时间")
  private LocalDateTime accessTime;
}

@AllArgsConstructor
@Getter
public enum LoginStatus implements IExcelEnum {
  /**
   * 成功
   */
  SUCCESS("0", "成功"),
  FAILED("1", "失败"),
  ;
  @JsonValue
  @EnumValue
  private final String value;
  private String label;

  @Override
  public String getLabel() {
    return label;
  }
}

分页处理 兼容老代码 如下是 service 层应该有的分页代码结构

服务层分页

1
2
3
public PageResp<LoginHistoryVo> page(PageReq<LoginHistoryReq> pageReq) {
  return PageUtil.from(logininforMapper.page((PageUtil.pageable(pageReq), pageReq.getParam()));
}