实例:sharp-*完成可配置的增删改查

人员信息表

数据库设计

表名:t_user

字段名 描述 字段类型 java 字段 UI 控件
name 姓名 varchar(32) String input[text]
gender 性别 varchar(32) Enum(by code) radio
age 年龄 int Integer input[number]
birthday 出生时间 datetime LocalDate input[date]
mobile 手机号码 varchar String text
email 邮箱 varchar String input[email]
native_place 籍贯 varchar(32) String select
hobby 兴趣爱好 varchar(32) Enum(by name) checkbox
marriage 婚否 bit String checkbox
introduce 自我介绍 text String textarea
attachment 附件 text List file
school_experience 学习经历 text List table
status 用户状态 varchar(32) Enum(by order) select

常规代码

User.java

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "t_user", comment = "人员信息表")
public class User extends BaseEntity {

    @Column(comment = "姓名", nullable = false)
    private String name;

    @Column(comment = "性别", nullable = false)
    private GenderEnum gender;

    @Column(comment = "年龄", nullable = false)
    private Integer age;

    @Column(comment = "出生时间")
    private LocalDate birthday;

    @Column(comment = "手机号码", nullable = false)
    private String mobile;

    @Column(comment = "邮箱")
    private String email;

    @Column(comment = "籍贯", nullable = false)
    private String nativePlace;

    @Column(comment = "兴趣爱好", value = "hobby", nullable = false)
    private List<HobbyEnum> hobbyList;

    @Column(comment = "婚否", nullable = false)
    private Boolean marriage;

    @Column(comment = "自我介绍", columnDefinition = "text", nullable = false)
    private String introduce;

    @Column(comment = "附件", columnDefinition = "text", value = "attachment", nullable = false)
    private List<Map<String, Object>> attachmentList;

    @Column(comment = "学习经历", columnDefinition = "text", value = "school_experience")
    private List<List<String>> schoolExperienceList;

    @Column(comment = "用户状态")
    private StatusEnum status;

    @AllArgsConstructor
    @Getter
    public enum GenderEnum {
        UNKNOWN(0, "Unknown"),
        MALE(1, "Male"),
        FEMALE(2, "Female");
        private static final Map<Integer, GenderEnum> codeMap = new HashMap<>();
        static {
            for (GenderEnum e : values()) {
                codeMap.put(e.code, e);
            }
        }
        private final int code;
        private final String label;
        @JsonValue
        public int getCode() {
            return this.code;
        }
        public static GenderEnum valueOfCode(int code) {
            return codeMap.get(code);
        }
    }

    @Getter
    public enum StatusEnum {
        LOCKED, NORMAL
    }

    @AllArgsConstructor
    @Getter
    public enum HobbyEnum {
        BASKETBALL("篮球"),
        FOOTBALL("羽毛球"),
        BADMINTON("羽毛球"),
        VOLLEYBALL("排球");

        @JsonValue
        public String getCode() {
            return this.name();
        }
        private final String label;
        public static HobbyEnum valueOfCode(String code) {
            return valueOf(code);
        }
    }
}

执行 tableGenerator.createTable(User.class) 生成表。

create table t_user
(
    id bigint not null comment '主键'
        primary key,
    name varchar(32) not null comment '姓名',
    gender varchar(16) not null comment '性别',
    age int not null comment '年龄',
    birthday date null comment '出生时间',
    mobile varchar(32) not null comment '手机号码',
    email varchar(32) null comment '邮箱',
    native_place varchar(32) not null comment '籍贯',
    hobby json not null comment '兴趣爱好',
    marriage bit not null comment '婚否',
    introduce text null,
    attachment text null,
    school_experience text null,
    status varchar(16) null comment '用户状态'
)
comment '人员信息表';

DAO

UserDAO.java

@Repository
public class UserDAO extends EntityDAOImpl<User, Long> {

}

UserFormAdvice

@Component("userFormAdvice")
public class UserFormAdvice implements FormAdvice {

    @Override
    public void beforeInstanceHandle(FormBO form, Long instanceId, Map<String, Object> values) {
        // checkbox List => boolean
        if (CollectionUtils.isEmpty((Collection<?>) values.get("marriage"))) {
            values.put("marriage", false);
        } else {
            values.put("marriage", true);
        }
    }

    @Override
    public void afterInstanceHandle(FormBO form, Long instanceId, Map<String, Object> values) {

    }
}

创建UI控件

@Test
  public void testAddConfigurerToForm() {
      // 设计控件
      List<CpnConfigurer> cpnConfigurerList = createCpnConfigurerList();
      cpnConfigurerService.saveOrUpdate(cpnConfigurerList);

      // 创建表
      Form form = formService.save(Form.builder()
              .code("t_user")
              .tableName("t_user")
              .name("人员信息表")
              .repositoryName("userDAO")
              .formAdviceName("userFormAdvice")
              .storageStrategy(Form.StorageStrategyEnum.CREATE_TABLE)
              .build());

      // 关联关系
      formCpnService.saveOrUpdateByConfigIds(form.getId(), cpnConfigurerList.stream().map(CpnConfigurer::getId).collect(Collectors.toList()));
  }

  public List<CpnConfigurer> createCpnConfigurerList() {
      // 文本校验器
      Set<Validator> textValidatorList = Sets.newHashSetWithExpectedSize(2);
      textValidatorList.add(new Required(true));
      textValidatorList.add(new Length(16));

      Set<Validator> requiredValidatorList = Sets.newHashSetWithExpectedSize(1);
      requiredValidatorList.add(new Required(true));

      CpnConfigurer nameCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.TEXT)
              .name("name")
              .label("姓名")
              .placeholder("请输入姓名")
              .defaultValue("Rick.Xu")
              .validatorList(textValidatorList)
              .build();

      CpnConfigurer genderCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.RADIO)
              .name("gender")
              .label("性别")
              .options(Arrays.asList(new CpnConfigurer.CpnOption("0", "未知"), new CpnConfigurer.CpnOption("1", "男"), new CpnConfigurer.CpnOption("2", "女")))
              .defaultValue("1")
              .validatorList(requiredValidatorList)
              .build();

      Set<Validator> ageValidatorList = Sets.newHashSetWithExpectedSize(1);
      ageValidatorList.add(new Size(18, 100));
      ageValidatorList.add(new Required(true));
      CpnConfigurer ageCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.NUMBER_TEXT)
              .name("age")
              .label("年龄")
              .placeholder("请输入年龄")
              .defaultValue("28")
              .validatorList(ageValidatorList)
              .build();

      CpnConfigurer birthdayCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.DATE)
              .name("birthday")
              .label("出生时间")
              .placeholder("请输入出生时间")
              .defaultValue("1989-12-11")
              .build();

      CpnConfigurer mobileCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.MOBILE)
              .name("mobile")
              .label("手机号码")
              .placeholder("请输入手机号码")
              .defaultValue("18898765543")
              .validatorList(requiredValidatorList)
              .build();

      CpnConfigurer emailCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.EMAIL)
              .name("email")
              .label("邮箱")
              .placeholder("请输入邮箱")
              .defaultValue("jkxyx@163.com")
              .build();

      CpnConfigurer nativePlaceCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.SELECT)
              .name("native_place")
              .label("籍贯")
              .options(Arrays.asList(new CpnConfigurer.CpnOption("陕西"), new CpnConfigurer.CpnOption("江苏"), new CpnConfigurer.CpnOption("四川"), new CpnConfigurer.CpnOption("上海")))
              .defaultValue("江苏")
              .validatorList(requiredValidatorList)
              .build();

      CpnConfigurer hobbyCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.CHECKBOX)
              .name("hobby")
              .label("兴趣爱好")
              .options(Arrays.asList(new CpnConfigurer.CpnOption("BASKETBALL", "篮球"), new CpnConfigurer.CpnOption("FOOTBALL", "足球"), new CpnConfigurer.CpnOption("BADMINTON", "羽毛球"), new CpnConfigurer.CpnOption("VOLLEYBALL", "排球"))                 )
              .defaultValue("[\"BASKETBALL\", \"FOOTBALL\"]")
              .validatorList(requiredValidatorList)
              .build();

      CpnConfigurer marriageCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.CHECKBOX)
              .name("marriage")
              .label("婚否")
//                .options(Arrays.asList(new CpnConfigurer.CpnOption("true", "")) )
              .options(Arrays.asList(new CpnConfigurer.CpnOption("1", "")) )
              .defaultValue("true")
              .build();

      CpnConfigurer introduceCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.TEXTAREA)
              .name("introduce")
              .label("自我介绍")
              .placeholder("请输入自我介绍")
              .defaultValue("我叫Rick...")
              .validatorList(textValidatorList)
              .build();

      CpnConfigurer attachmentCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.FILE)
              .name("attachment")
              .label("附件")
              .validatorList(requiredValidatorList)
              .build();

      CpnConfigurer yearCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.TEXT)
              .name("school_year")
              .label("年份")
              .defaultValue("Rick.Xu")
              .validatorList(textValidatorList)
              .build();

      CpnConfigurer schoolNameCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.TEXT)
              .name("school_name")
              .label("学校名称")
              .defaultValue("Rick.Xu")
              .validatorList(textValidatorList)
              .build();

      CpnConfigurer schoolExperienceCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.TABLE)
              .name("school_experience")
              .label("学习经历")
              .additionalInfo(Params.builder()
                        .pv("columns", Arrays.asList(yearCpn, schoolNameCpn))
                      .build())
              .defaultValue("[[\"2021-2022\", \"四川大学\"], [\"2012-2015\", \"永安高中\"]]")
              .build();

      CpnConfigurer statusCpn = CpnConfigurer.builder()
              .cpnType(CpnTypeEnum.SELECT)
              .name("status")
              .label("用户状态")
              .options(Arrays.asList(new CpnConfigurer.CpnOption("LOCKED", "锁定"), new CpnConfigurer.CpnOption("NORMAL", "正常")))
              .defaultValue("LOCKED")
              .build();

      List<CpnConfigurer> cpnConfigurerList = Lists.newArrayList(nameCpn, genderCpn, ageCpn, birthdayCpn,
              mobileCpn, emailCpn, nativePlaceCpn, hobbyCpn, marriageCpn, introduceCpn, attachmentCpn, schoolExperienceCpn, statusCpn);
      return cpnConfigurerList;
  }

访问表单

新增 POST /{form_id}
修改 POST /{form_id}/{instance_id}
查找 GET /{form_id}/{instance_id}

http://xhope.top/wp-content/uploads/2023/03/from.png

创建报表

@Test
public void testAddReport() {
    reportService.saveOrUpdate(Report.builder()
            .id(668074769748373504L)
            .name("人员信息表")
            .querySql("SELECT t_user.name              AS \"name\",\n" +
                    "#        t_user.gender            AS \"gender\",\n" +
                    "       sd.label AS \"gender\",\n" +
                    "       t_user.age               AS \"age\",\n" +
                    "       t_user.birthday          AS \"birthday\",\n" +
                    "       t_user.mobile            AS \"mobile\",\n" +
                    "       t_user.email             AS \"email\",\n" +
                    "       t_user.native_place      AS \"nativePlace\",\n" +
                    "       t_user.hobby             AS \"hobbyList\",\n" +
                    "       IF(t_user.marriage, '是', '否')         AS \"marriage\",\n" +
                    "       t_user.introduce         AS \"introduce\",\n" +
                    "       t_user.attachment        AS \"attachmentList\",\n" +
                    "       t_user.school_experience AS \"schoolExperienceList\",\n" +
                    "       t_user.status            AS \"status\",\n" +
                    "       t_user.created_by        AS \"createdBy\",\n" +
                    "       t_user.created_at        AS \"createdAt\",\n" +
                    "       t_user.updated_by        AS \"updatedBy\",\n" +
                    "       t_user.updated_at        AS \"updatedAt\",\n" +
                    "       t_user.is_deleted        AS \"deleted\",\n" +
                    "       t_user.id                AS \"id\"\n" +
                    "FROM t_user left JOIN sys_dict sd ON t_user.gender = sd.name" +
                    " WHERE is_deleted = 0 AND t_user.name like :name AND t_user.gender = :gender AND t_user.mobile = :mobile " +
                    "AND t_user.native_place IN (:nativePlace) AND t_user.status = :status AND t_user.hobby like :hobbyList" +
                            " AND t_user.birthday >= :birthday0 AND t_user.birthday <= :birthday1 AND marriage = :marriage")
            .reportColumnList(Arrays.asList(
                    new ReportColumn("name", "姓名", true),
                    new ReportColumn("gender", "性别"),
                    new ReportColumn("age", "年龄"),
                    new ReportColumn("birthday", "出生日期",false, null, Arrays.asList("localDateConverter")),
                    new ReportColumn("mobile", "手机号码"),
                    new ReportColumn("email", "邮箱"),
                    new ReportColumn("nativePlace", "籍贯"),
                    new ReportColumn("hobbyList", "兴趣爱好", false, "hobbyList", Arrays.asList("arrayDictConverter")),
                    new ReportColumn("marriage", "婚否"),
                    new ReportColumn("introduce", "自我介绍"),
//                        new ReportColumn("attachmentList", "附件"),
//                        new ReportColumn("schoolExperienceList", "学习经历"),
                    new ReportColumn("status", "状态", false, "status", Arrays.asList("dictConverter"))
            ))
            .queryFieldList(Arrays.asList(
                    new QueryField("name", "姓名"),
                    new QueryField("mobile", "手机号码"),
                    // sys_dic#gender
                    new QueryField("gender", "性别", QueryField.Type.SELECT, "gender"),
                    new QueryField("birthday", "出生日期", QueryField.Type.DATE_RANGE),
                    // 配置文件
                    new QueryField("nativePlace", "籍贯", QueryField.Type.MULTIPLE_SELECT, "nativePlace"),
                    // 接口实现
                    new QueryField("hobbyList", "兴趣爱好", QueryField.Type.SELECT, "hobbyList"),
                    // 配置文件
                    new QueryField("status", "状态", QueryField.Type.SELECT, "status"),
                    new QueryField("marriage", "婚否", QueryField.Type.SELECT, "bol")
            ))
            .pageable(true)
//                .summary(true)
            .summaryColumnNames("age, mobile")
            .sidx("name")
            .sord(SordEnum.ASC)
            .build());
}

http://xhope.top/wp-content/uploads/2023/03/list.png
参数

SELECT t_user.name                   AS "name",
       sd.label                      AS "gender",
       t_user.age                    AS "age",
       t_user.birthday               AS "birthday",
       t_user.mobile                 AS "mobile",
       t_user.email                  AS "email",
       t_user.native_place           AS "nativePlace",
       t_user.hobby                  AS "hobbyList",
       IF(t_user.marriage, '是', '否') AS "marriage",
       t_user.introduce              AS "introduce",
       t_user.attachment             AS "attachmentList",
       t_user.school_experience      AS "schoolExperienceList",
       t_user.status                 AS "status",
       t_user.created_by             AS "createdBy",
       t_user.created_at             AS "createdAt",
       t_user.updated_by             AS "updatedBy",
       t_user.updated_at             AS "updatedAt",
       t_user.is_deleted             AS "deleted",
       t_user.id                     AS "id"
FROM t_user
         LEFT JOIN sys_dict sd ON t_user.gender = sd.name
WHERE is_deleted = 0
  AND UPPER(t_user.name) LIKE CONCAT('%', UPPER(:name), '%') ESCAPE
      '\\' AND t_user.gender = :gender AND t_user.mobile = :mobile AND t_user.native_place IN (:nativePlace0) AND
      t_user.status = :status AND t_user.birthday >= :birthday0 AND t_user.birthday <= :birthday1 AND
      marriage = :marriage

[{gender=2, marriage=1, name=凡夫俗子, mobile=17798765543, nativePlace0=上海, birthday1=1994-01-17, birthday0=1984-01-17, status=NORMAL}]

sharp-database中的BaseCodeDAOImpl通过对实体字段code的支持

@BaseCodeDAOImpl 作用是对实体字段 code 的支持。一张表可能会有一个外部可见的唯一字段,可以做主键使用。比如序列号、注册的手机号都是属于业务主键。

示例如下:

实体 SerialNumber.java 继承 BaseCodeEntity ,表生成字段 code

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "mm_serial_number", comment = "序列号")
public class SerialNumber extends BaseCodeEntity {

    private SerialNumber.SerialNumberEnum status;

    @AllArgsConstructor
    @Getter
    public enum SerialNumberEnum {
        CREATED("创建"),
        AVAILABLE("在库"),
        SOLD("离库");

        @JsonValue
        public String getCode() {
            return this.name();
        }

        private final String label;

        public static SerialNumberEnum valueOfCode(String code) {
            return valueOf(code);
        }
    }

}

SerialNumberDAO.java 继承 BaseCodeDAOImpl

@Repository
public class SerialNumberDAO extends BaseCodeDAOImpl<SerialNumber, Long> {
}

测试

    @Autowired
    private SerialNumberDAO serialNumberDAO;

    @Autowired
    private BaseCodeEntityIdFillService idFillService;

    @Test
    public void testSave() {
        assertThrows(BizException.class, ()-> {
            serialNumberDAO.insert(SerialNumber.builder()
                    .code("11")
                    .status(SerialNumber.SerialNumberEnum.AVAILABLE)
                    .build());
        });
    }

    @Test
    public void testSaveOrUpdate() {
        // 可以根据code insertOrUpdate,也可以通过id insertOrUpdate
        serialNumberDAO.insertOrUpdate(SerialNumber.builder()
                .code("11")
                .status(SerialNumber.SerialNumberEnum.SOLD)
                .build());
    }

    @Test
    public void testFindByCode() {
        SerialNumber serialNumber = serialNumberDAO.selectByCode("11").get();
        assertThat(serialNumber.getStatus()).isEqualTo(SerialNumber.SerialNumberEnum.SOLD);
    }

    @Test
    public void testFillId1() {
        SerialNumber serialNumber = SerialNumber.builder().code("11").build();
        idFillService.fill(Arrays.asList(serialNumber));
        assertThat(serialNumber.getId()).isEqualTo(664984730659258368L);
    }

    @Test
    public void testFillId2() {
        SerialNumber serialNumber = SerialNumber.builder().code("11").build();
        idFillService.fill(serialNumber);
        assertThat(serialNumber.getId()).isEqualTo(664984730659258368L);
    }

    @Test
    public void testFillId3() {
        List<SerialNumber> list = idFillService.fill(SerialNumber.class, Arrays.asList("11"));
        assertThat(list.get(0).getId()).isEqualTo(664984730659258368L);
    }

sharp-database中的BaseDAOImpl通过注解@Embedded实现实体的”组件”

@Embedded 作用是将一个POJO作为实体的组成部分,但还是生成一张表。与 @ManyToOne OneToMany 不同的是,它们会生成新的表。

示例如下:

Company.java

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "t_company", comment = "公司")
public class Company extends SimpleEntity {

    private String name;

    private String address;

    private String phone;

    @Embedded
    private ContactPerson contactPerson;

}

ContactPerson.java

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ContactPerson {

    private String firstName;

    @Column(updatable = false)
    private String lastName;

    @Column("contact_phone")
    private String phone;

    @OneToMany(subTable = "t_company_vendor", joinValue = "company_id", cascadeInsertOrUpdate = true, reversePropertyName = "companyId")
    private List<Vendor> vendorList;
}

ContactPerson 只是 Company 的组成部分,本身不会生成新的表。
下面的示例 @Embedded,从数据库层面的设计,下面等价。

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "t_company", comment = "公司")
public class Company extends SimpleEntity {

    private String name;

    private String address;

    private String phone;

    private String firstName;

    @Column(updatable = false)
    private String lastName;

    @Column("contact_phone")
    private String phone;

    @OneToMany(subTable = "t_company_vendor", joinValue = "company_id", cascadeInsertOrUpdate = true, reversePropertyName = "companyId")
    private List<Vendor> vendorList;

}

sharp-database中的BaseDAOImpl使用注解@Select实现多表级联

@Select 作用是属性实体对象的级联查询。不用 @Select 也可以通过 OneToMany (cascadeSaveOrUpdate = false)来实现级联查询,但是外键只能参考id,不能是实体的其他属性。

Select.java 注解声明

public @interface Select {

    String table();

    String joinValue() default "";

    String referencePropertyName() default "";

    boolean oneToOne() default false;
}

下面的示例表示根据实体 Batch 的属性 materialId 去关联表mm_classification的外键字段material_id 做级联查询。

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "mm_batch", comment = "物料批次主数据")
public class Batch extends BaseEntity {

    private Long materialId;

    @Select(table = "mm_classification", joinValue = "material_id", referencePropertyName = "materialId")
    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private List<Classification> classificationList;
}

下面的示例 @Select 等价 OneToMany

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "mm_batch", comment = "物料批次主数据")
public class Batch extends BaseEntity {

    private Long materialId;

    @Select(table = "mm_classification", joinValue = "batch_id", referencePropertyName = "id")
    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private List<Classification> classificationList;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Table(value = "mm_batch", comment = "物料批次主数据")
public class Batch extends BaseEntity {

    private Long materialId;

    @OneToMany(subTable = "mm_classification", joinValue = "batch_id")
    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private List<Classification> classificationList;
}

sharp-mail中邮件发送

添加依赖

<dependency>
    <groupId>com.rick.mail</groupId>
    <artifactId>sharp-mail</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

邮件服务器配置

  • 阿里邮箱
spring:
  mail:  # SMTP 发邮件
    host: smtp.qiye.aliyun.com
    port: 25
    username: XXX
    password: XXX
    default-encoding: UTF-8
    imap: # IMAP 收邮件
      host: imap.qiye.aliyun.com
      port: 143
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
  • 163邮箱
spring:
  mail: # SMTP 发邮件
    host: smtp.163.com
    port: 25
    username: jkxyx205@163.com
    password: XXX # 将邮箱开启smtp服务后获取的授权码作为密码;https://help.mail.163.com/faqDetail.do?code=d7a5dc8471cd0c0e8b4b8f4f8e49998b374173cfe9171305fa1ce630d7f67ac21b8ba4d48ed49ebc
    default-encoding: UTF-8
    imap: # IMAP 收邮件
      host: imap.163.com
      port: 143
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
  • Gmail邮箱
spring:
  mail: # SMTP 发邮件
    host: smtp.gmail.com
    port: 587
    username: XX
    password: xxx
    default-encoding: UTF-8
    imap: # IMAP 收邮件
      host: imap.gmail.com
      port: 993
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
  • qq邮箱
spring:
  mail: # SMTP 发邮件
    host: smtp.qq.com
    port: 465
    username: 1050205@qq.com
    password: XXX
    default-encoding: UTF-8
    imap: # IMAP 收邮件
      host: imap.qq.com
      port: 993
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true

测试

邮件操作接口

MailHandler.java

public interface MailHandler {
    /**
     * 发送邮件
     * @param email
     * @return
     */
    MimeMessage send(Email email);

    /**
     * 发送邮件并保存到已发送
     * @param email
     * @return
     */
    MimeMessage sendAndSaveToOutbox(Email email);

    MimeMessage send(MimeMessage message);

    MimeMessage sendAndSaveToOutbox(MimeMessage message);

    /**
     * 根据Message ID 获取邮件
     * @param folderName
     * @param messageId
     * @return
     * @throws MessagingException
     */
    Optional<MimeMessage> searchByMessageId(String folderName, String messageId) throws MessagingException;

    Optional<MimeMessage> searchByMessageId(Folder folder, String messageId) throws MessagingException;

    /**
     * 邮箱文件夹列表
     * @return
     * @throws MessagingException
     */
    Folder[] listFolders() throws MessagingException;

    /**
     * 未读邮件列表
     * @param consumer
     * @throws MessagingException
     */
    void listUnreadMessage(Consumer<MimeMessage> consumer) throws MessagingException;

    /**
     * 文件夹所有邮件
     * @param folderName
     * @param consumer true 表示终止遍历
     * @throws MessagingException
     */
    void listMessages(String folderName, Function<MimeMessage, Boolean> consumer) throws MessagingException;

    void listMessages(Folder folder, Function<MimeMessage, Boolean> consumer) throws MessagingException;

}

测试

@SpringBootTest
public class AliyunMailTest {

    @Autowired
    private MailHandler mailHandler;

    @Test
    public void testUnReadMessages() throws MessagingException {
        mailHandler.listUnreadMessage(message -> {
            try {
                System.out.println(message.getSubject());
                System.out.println("---------");
                System.out.println(MailUtils.contentText(message.getContent()));
                System.out.println("----contentText end-----");
                System.out.println(MailUtils.hasAttachment(message));
                System.out.println("---------");
                System.out.println(MailUtils.downloadAttachments(message, "/Users/rick/Documents/mail"));
                System.out.println("---------");
//                System.out.println(IOUtils.toString(message.getRawInputStream(), "UTF-8"));
            } catch (MessagingException | IOException e) {
                e.printStackTrace();
            }
        });
    }

    @Test
    public void testListMessages() throws MessagingException {
        mailHandler.listMessages("INBOX", message -> {
            try {
                System.out.println(message.getSubject());
                System.out.println("---------");
                System.out.println(MailUtils.contentText(message.getContent()));
                System.out.println("---------");
                System.out.println(MailUtils.hasAttachment(message));
                System.out.println("---------");
                System.out.println(MailUtils.downloadAttachments(message, "/Users/rick/Documents/mail"));
                System.out.println("---------");
            } catch (MessagingException | IOException e) {
                e.printStackTrace();
            }
            return true;
        });
    }

    @Test
    public void testSearchById() throws MessagingException, IOException {
        Optional<MimeMessage> optional = mailHandler.searchByMessageId("INBOX", "<475871799.1.1668661823168@[192.168.2.7]>");
        System.out.println(optional.isPresent() == false);

        Optional<MimeMessage> optional2= mailHandler.searchByMessageId("已发送", "<1733354369.1.1668662103782@[192.168.2.7]>");
        System.out.println(optional2.get().getSubject());
    }

    @Test
    public void testSenderSimple0() throws IOException, MessagingException {
        MimeMessage message = mailHandler.send(Email.builder()
                .subject("no attachment testSenderSimple0")
                .from("xu.xu@yodean.com", "Rick.Xu")
                .to("jkxyx205@163.com", "JIM")
                .to("1050216579@qq.com")
                .plainText("Here is an example to send a simple e-mail from your machine. It is assumed that your localhost is connected to the Internet and capable enough to send an e-mail.\n" +
                        "\n")
                .build());
        System.out.println(message.getMessageID());
    }

    @Test
    public void testSenderSimple() throws IOException, MessagingException {
        mailHandler.send(Email.builder()
                .subject("no attachment")
                .from("xu.xu@yodean.com", "Rick.Xu")
                .to("jkxyx205@163.com", "JIM")
                .to("1050216579@qq.com")
//                .plainText("Here is an example to send a simple e-mail from your machine. It is assumed that your localhost is connected to the Internet and capable enough to send an e-mail.\n" +
//                        "\n")
                .htmlText("hello <span style='color: red'>world</span>用户登录Webmail端企业邮箱,通过“设置>反垃圾/黑白名单”进入反垃圾/黑白名单页面。如下图所示,用户可自助设置垃圾邮件的判定规则、垃圾邮件的处理规则等。")
                .build());
    }

    @Test
    public void testSender() throws IOException, MessagingException {
        mailHandler.send(Email.builder()
                .subject("has attachment")
                .from("xu.xu@yodean.com", "Rick.Xu")
                .to("jkxyx205@163.com", "JIM")
                .to("1050216579@qq.com")
//                .plainText("Here is an example to send a simple e-mail from your machine. It is assumed that your localhost is connected to the Internet and capable enough to send an e-mail.\n" +
//                        "\n")
                .htmlText("hello <img src=\"cid:learn.png\" /><span style='color: red'>world</span>用户登录Webmail端企业邮箱,通过“设置>反垃圾/黑白名单”进入反垃圾/黑白名单页面。如下图所示,用户可自助设置垃圾邮件的判定规则、垃圾邮件的处理规则等。")
                .embeddedImage("learn.png", Files.readAllBytes(Paths.get("/Users/rick/Desktop/learn.png")), "image/png")
                .attachment("pom_extend.txt", Files.readAllBytes(Paths.get("/Users/rick/Desktop/pom_extend.txt")), "text/plain")
                .attachment("部分合作单位.html", Files.readAllBytes(Paths.get("/Users/rick/Desktop/部分合作单位.html")), "text/html")
                .build());
    }

    @Test
    public void testSendAndSaveToOutbox() throws IOException, MessagingException {
        MimeMessage message = mailHandler.sendAndSaveToOutbox(Email.builder()
                .subject("Java - Sending Email-testSendAndSaveToOutBox")
                .from("xu.xu@yodean.com", "Rick.Xu")
                .to("jkxyx205@163.com", "JIM")
                .to("1050216579@qq.com")
                .cc("154894898@qq.com", "JK")
//                .plainText("Here is an example to send a simple e-mail from your machine. It is assumed that your localhost is connected to the Internet and capable enough to send an e-mail.\n" +
//                        "\n")
                .htmlText("hello <img src=\"cid:learn.png\" /><span style='color: red'>world</span>用户登录Webmail端企业邮箱,通过“设置>反垃圾/黑白名单”进入反垃圾/黑白名单页面。如下图所示,用户可自助设置垃圾邮件的判定规则、垃圾邮件的处理规则等。")
                .embeddedImage("learn.png", Files.readAllBytes(Paths.get("/Users/rick/Desktop/learn.png")), "image/png")
                .attachment("pom_extend.txt", Files.readAllBytes(Paths.get("/Users/rick/Desktop/pom_extend.txt")), "text/plain")
                .attachment("部分合作单位.html", Files.readAllBytes(Paths.get("/Users/rick/Desktop/部分合作单位.html")), "text/html")
                .build());
        System.out.println(message.getMessageID());
    }

    @Test
    public void listFolder() throws MessagingException {
        Folder[] folders = mailHandler.listFolders();
        for (Folder folder : folders) {
            System.out.println(folder.getFullName());
        }

        folders[2].open(Folder.READ_ONLY);
        System.out.println(folders[2].getMessage(1).getSubject());
        folders[2].close();
    }

}