Skip to content

服务端模块开发流程

遵循传统三层结构

实体定义 -> Repository定义 -> Service定义 -> Controller定义

实体定义

kernel 模块 cn.gson.financial.kernel.model.entity 包下创建业务实体类

实体类定义遵循jpa标准,字段定义和配置好后,启动项目可自动建表

⚠️注意事项:

  1. 账套关联数据,实体类需要继承 AbsEntity 类,AbsEntity定义了联合主键(accountSetsId,id)
  2. 非账套关联数据不需要继承

已凭证字模块为例:

java
@Getter
@Setter
@Entity
@Table(uniqueConstraints = {
        @UniqueConstraint(name = "uc_voucherword_accountsetsid", columnNames = {"accountSetsId", "word"})
})
@DynamicUpdate
@DynamicInsert
@Comment("凭证字")
public class VoucherWord extends AbsEntity {
    @Comment("凭证字")
    @Column(nullable = false, length = 32)
    private String word;

    @Comment("打印标题")
    @Column(nullable = false, length = 32)
    private String printTitle;

    @Comment("是否默认")
    @ColumnDefault("b'1'")
    @Column(nullable = false)
    private Boolean isDefault;
}
@Getter
@Setter
@Entity
@Table(uniqueConstraints = {
        @UniqueConstraint(name = "uc_voucherword_accountsetsid", columnNames = {"accountSetsId", "word"})
})
@DynamicUpdate
@DynamicInsert
@Comment("凭证字")
public class VoucherWord extends AbsEntity {
    @Comment("凭证字")
    @Column(nullable = false, length = 32)
    private String word;

    @Comment("打印标题")
    @Column(nullable = false, length = 32)
    private String printTitle;

    @Comment("是否默认")
    @ColumnDefault("b'1'")
    @Column(nullable = false)
    private Boolean isDefault;
}

@Comment@ColumnDefault 是v4版本才支持,v3版本不支持这两个注解,v3可以通过 @Column 注解的 columnDefinition 属性实现类似功能

Repository定义

单表数据的 save 操作,需要通过JPA的Repository进行

kernel 模块 cn.gson.financial.kernel.model.repo 包下创建Repository

java
public interface VoucherWordRepository extends JpaRepository<VoucherWord, PrimaryKey> {
}
public interface VoucherWordRepository extends JpaRepository<VoucherWord, PrimaryKey> {
}

Service定义

kernel 模块 cn.gson.financial.kernel.service 包下创建Repository

service需要继承AbsService

java
public abstract class AbsService {

    @Resource
    protected SqlToyLazyDao lazyDao;

    @Resource
    protected JPAQueryFactory jqf;

    @Resource
    @Lazy
    protected BlazeJPAQueryFactory bqf;

    /**
     * 雪花ID生成器
     */
    @Resource
    @Lazy
    protected SnowflakeId snowflakeId;
}
public abstract class AbsService {

    @Resource
    protected SqlToyLazyDao lazyDao;

    @Resource
    protected JPAQueryFactory jqf;

    @Resource
    @Lazy
    protected BlazeJPAQueryFactory bqf;

    /**
     * 雪花ID生成器
     */
    @Resource
    @Lazy
    protected SnowflakeId snowflakeId;
}

AbsService提供了三个对象:

  1. lazyDao:sqlToy调用
  2. jqf:queryDsl的update和delete操作对象
  3. bqf:queryDsl的select操作对象
  4. snowflakeId:雪花ID生成器

lazyDao、jqf、bqf三个对象的使用可参考以下文档

目前系统数据操作层面,大多数情况下querydsl能满足业务数据的查询操作,只有涉及到特别复杂的查询,以及需要传递大量参数的情况下,会需要用到sqltoy对象进行原生sql查询。例如报表模块的查询基本都用到了sqltoylazyDao进行操作

⚠️如果业务模块是账套关联数据,则服务需要实现 AccountSetsProcessor, BackupProcessor两个接口。如果未实现接口,则创建,删除和备份还原账套时会丢失该业务产生的数据。

AccountSetsProcessor 提供了账套初始化和重置时对相关业务数据的切入。

java
public interface AccountSetsProcessor {

    /**
     * 账套数据初始化
     *
     * @param accountSets
     * @param accountSetsTemplate
     */
    default void init(AccountSets accountSets, AccountSetsTemplate accountSetsTemplate) {

    }

    /**
     * 账套数据清理
     *
     * @param accountSetsId
     */
    void clear(Long accountSetsId);
}
public interface AccountSetsProcessor {

    /**
     * 账套数据初始化
     *
     * @param accountSets
     * @param accountSetsTemplate
     */
    default void init(AccountSets accountSets, AccountSetsTemplate accountSetsTemplate) {

    }

    /**
     * 账套数据清理
     *
     * @param accountSetsId
     */
    void clear(Long accountSetsId);
}

BackupProcessor 提供了账套备份和还原时对相关业务数据的切入。

java
public interface BackupProcessor {

    /**
     * 备份
     *
     * @param accountSetsId
     * @return
     */
    String backup(Long accountSetsId);

    /**
     * 备份类型
     *
     * @return
     */
    Class<?> backupType();

    /**
     * 恢复备份
     *
     * @param accountSetsId
     * @return
     */
    void recover(Long accountSetsId, String json);

}
public interface BackupProcessor {

    /**
     * 备份
     *
     * @param accountSetsId
     * @return
     */
    String backup(Long accountSetsId);

    /**
     * 备份类型
     *
     * @return
     */
    Class<?> backupType();

    /**
     * 恢复备份
     *
     * @param accountSetsId
     * @return
     */
    void recover(Long accountSetsId, String json);

}

已凭证字模块的服务类为例:

java
@Service
@RequiredArgsConstructor
public class VoucherWordService extends AbsService implements AccountSetsProcessor, BackupProcessor {

    private final VoucherWordRepository voucherWordRepository;

    private final QVoucherWord qVoucherWord = QVoucherWord.voucherWord;


    @Transactional
    @CacheRemove(value = "voucherWord", key = "#entity.accountSetsId")
    public VoucherWord save(VoucherWord entity) {
        if (entity.getPrimaryKey() == null) {
            //判断是否重复
            if (voucherWordRepository.count(qVoucherWord.word.eq(entity.getWord().trim())
                    .and(qVoucherWord.accountSetsId.eq(entity.getAccountSetsId()))) > 0) {
                throw new ServiceException("亲,保存失败啦!凭证字【%s】已经存在!", entity.getWord());
            }
        } else {
            //判断是否重复
            if (voucherWordRepository.count(qVoucherWord.word.eq(entity.getWord().trim())
                    .and(qVoucherWord.accountSetsId.eq(entity.getAccountSetsId()))
                    .and(qVoucherWord.id.ne(entity.getId()))) > 0) {
                throw new ServiceException("亲,保存失败啦!凭证字【%s】已经存在!", entity.getWord());
            }
        }
        return voucherWordRepository.save(entity);
    }


    @Transactional
    @CacheRemove(value = "voucherWord", key = "#accountSetsId")
    public void remove(Long voucherWordId, Long accountSetsId) {
        VoucherWord word = jqf.selectFrom(qVoucherWord).where(qVoucherWord.accountSetsId.eq(accountSetsId).and(qVoucherWord.id.eq(voucherWordId))).fetchFirst();
        if (word.getIsDefault()) {
            throw new ServiceException("默认凭证字不能被删除!");
        }
        voucherWordRepository.delete(word);
    }

    /**
     * 如果新增为默认,则把其他的都设置为非默认
     *
     * @param entity
     */
    @Transactional
    @CacheRemove(value = "voucherWord", key = "#entity.accountSetsId")
    public void updateDefault(VoucherWord entity) {
        if (entity.getIsDefault()) {
            jqf.update(qVoucherWord)
                    .set(qVoucherWord.isDefault, false)
                    .where(qVoucherWord.accountSetsId.eq(entity.getAccountSetsId()))
                    .execute();
            jqf.update(qVoucherWord)
                    .set(qVoucherWord.isDefault, true)
                    .where(qVoucherWord.id.eq(entity.getId())
                            .and(qVoucherWord.accountSetsId.eq(entity.getAccountSetsId())))
                    .execute();
        }
    }

    @Cacheable(value = "voucherWord", key = "#accountSetsId")
    public List<VoucherWord> list(Long accountSetsId) {
        return bqf.selectFrom(qVoucherWord)
                .where(qVoucherWord.accountSetsId.eq(accountSetsId))
                .orderBy(qVoucherWord.isDefault.desc())
                .fetch();
    }

    @Transactional
    @Override
    @CacheRemove(value = "voucherWord", key = "#accountSets.id")
    public void init(AccountSets accountSets, AccountSetsTemplate accountSetsTemplate) {
        //默认凭证字
        List<VoucherWord> defaultVw = bqf.selectFrom(qVoucherWord)
                .where(qVoucherWord.accountSetsId.eq(accountSetsTemplate.getId()))
                .fetch();
        voucherWordRepository.saveAll(defaultVw.stream().map(vw -> {
            VoucherWord bean = BeanUtil.toBean(vw, VoucherWord.class);
            bean.setAccountSetsId(accountSets.getId());
            return bean;
        }).collect(Collectors.toList()));
    }

    @Transactional
    @Override
    @CacheRemove(value = "voucherWord", key = "#accountSetsId")
    public void clear(Long accountSetsId) {
        jqf.delete(QVoucherWord.voucherWord)
                .where(QVoucherWord.voucherWord.accountSetsId.eq(accountSetsId))
                .execute();
    }

    @Override
    public String backup(Long accountSetsId) {
        return JSON.toJSONString(bqf.selectFrom(qVoucherWord)
                .where(qVoucherWord.accountSetsId.eq(accountSetsId)).fetch());
    }

    @Override
    public Class<?> backupType() {
        return VoucherWord.class;
    }

    @Override
    @CacheRemove(value = "voucherWord", key = "#accountSetsId")
    public void recover(Long accountSetsId, String json) {
        List<VoucherWord> entityList = JSON.parseArray(json, VoucherWord.class);
        for (VoucherWord entity : entityList) {
            entity.setAccountSetsId(accountSetsId);
            voucherWordRepository.save(entity);
        }
    }
}
@Service
@RequiredArgsConstructor
public class VoucherWordService extends AbsService implements AccountSetsProcessor, BackupProcessor {

    private final VoucherWordRepository voucherWordRepository;

    private final QVoucherWord qVoucherWord = QVoucherWord.voucherWord;


    @Transactional
    @CacheRemove(value = "voucherWord", key = "#entity.accountSetsId")
    public VoucherWord save(VoucherWord entity) {
        if (entity.getPrimaryKey() == null) {
            //判断是否重复
            if (voucherWordRepository.count(qVoucherWord.word.eq(entity.getWord().trim())
                    .and(qVoucherWord.accountSetsId.eq(entity.getAccountSetsId()))) > 0) {
                throw new ServiceException("亲,保存失败啦!凭证字【%s】已经存在!", entity.getWord());
            }
        } else {
            //判断是否重复
            if (voucherWordRepository.count(qVoucherWord.word.eq(entity.getWord().trim())
                    .and(qVoucherWord.accountSetsId.eq(entity.getAccountSetsId()))
                    .and(qVoucherWord.id.ne(entity.getId()))) > 0) {
                throw new ServiceException("亲,保存失败啦!凭证字【%s】已经存在!", entity.getWord());
            }
        }
        return voucherWordRepository.save(entity);
    }


    @Transactional
    @CacheRemove(value = "voucherWord", key = "#accountSetsId")
    public void remove(Long voucherWordId, Long accountSetsId) {
        VoucherWord word = jqf.selectFrom(qVoucherWord).where(qVoucherWord.accountSetsId.eq(accountSetsId).and(qVoucherWord.id.eq(voucherWordId))).fetchFirst();
        if (word.getIsDefault()) {
            throw new ServiceException("默认凭证字不能被删除!");
        }
        voucherWordRepository.delete(word);
    }

    /**
     * 如果新增为默认,则把其他的都设置为非默认
     *
     * @param entity
     */
    @Transactional
    @CacheRemove(value = "voucherWord", key = "#entity.accountSetsId")
    public void updateDefault(VoucherWord entity) {
        if (entity.getIsDefault()) {
            jqf.update(qVoucherWord)
                    .set(qVoucherWord.isDefault, false)
                    .where(qVoucherWord.accountSetsId.eq(entity.getAccountSetsId()))
                    .execute();
            jqf.update(qVoucherWord)
                    .set(qVoucherWord.isDefault, true)
                    .where(qVoucherWord.id.eq(entity.getId())
                            .and(qVoucherWord.accountSetsId.eq(entity.getAccountSetsId())))
                    .execute();
        }
    }

    @Cacheable(value = "voucherWord", key = "#accountSetsId")
    public List<VoucherWord> list(Long accountSetsId) {
        return bqf.selectFrom(qVoucherWord)
                .where(qVoucherWord.accountSetsId.eq(accountSetsId))
                .orderBy(qVoucherWord.isDefault.desc())
                .fetch();
    }

    @Transactional
    @Override
    @CacheRemove(value = "voucherWord", key = "#accountSets.id")
    public void init(AccountSets accountSets, AccountSetsTemplate accountSetsTemplate) {
        //默认凭证字
        List<VoucherWord> defaultVw = bqf.selectFrom(qVoucherWord)
                .where(qVoucherWord.accountSetsId.eq(accountSetsTemplate.getId()))
                .fetch();
        voucherWordRepository.saveAll(defaultVw.stream().map(vw -> {
            VoucherWord bean = BeanUtil.toBean(vw, VoucherWord.class);
            bean.setAccountSetsId(accountSets.getId());
            return bean;
        }).collect(Collectors.toList()));
    }

    @Transactional
    @Override
    @CacheRemove(value = "voucherWord", key = "#accountSetsId")
    public void clear(Long accountSetsId) {
        jqf.delete(QVoucherWord.voucherWord)
                .where(QVoucherWord.voucherWord.accountSetsId.eq(accountSetsId))
                .execute();
    }

    @Override
    public String backup(Long accountSetsId) {
        return JSON.toJSONString(bqf.selectFrom(qVoucherWord)
                .where(qVoucherWord.accountSetsId.eq(accountSetsId)).fetch());
    }

    @Override
    public Class<?> backupType() {
        return VoucherWord.class;
    }

    @Override
    @CacheRemove(value = "voucherWord", key = "#accountSetsId")
    public void recover(Long accountSetsId, String json) {
        List<VoucherWord> entityList = JSON.parseArray(json, VoucherWord.class);
        for (VoucherWord entity : entityList) {
            entity.setAccountSetsId(accountSetsId);
            voucherWordRepository.save(entity);
        }
    }
}

⚠️ Service类中涉及到的Q类型类,例如QVoucherWord,是queryDsl框架根据系统中定义的实体类自动生成,不需要手动编写。

如果提示找不到此类,可重新build对应模块。

在模块build/generated/sources/annotationProcessor/java可看到动态生成的此类。

Controller定义

Controller定义在对应服务的server模块中

bs-server 模块 cn.gson.financial.controller 包下创建Controller

java
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/voucher-word")
public class VoucherWordController {

    private final VoucherWordService voucherWordService;

    @GetMapping
    public JsonResult list(@SessionAccountSetId Long accountSetsId) {
        return JsonResult.successful(voucherWordService.list(accountSetsId));
    }

    @PostMapping
    @SaCheckPermission("setting-vouchergroup-canedit")
    public JsonResult create(@RequestBody VoucherWord voucherWord, @SessionAccountSetId Long accountSetsId) {
        voucherWord.setAccountSetsId(accountSetsId);
        voucherWordService.save(voucherWord);
        return JsonResult.successful();
    }

    @PutMapping
    @SaCheckPermission("setting-vouchergroup-canedit")
    public JsonResult update(@RequestBody VoucherWord voucherWord, @SessionAccountSetId Long accountSetsId) {
        voucherWord.setAccountSetsId(accountSetsId);
        voucherWordService.save(voucherWord);
        return JsonResult.successful();
    }

    @DeleteMapping("{voucherWordId:\\d+}")
    @SaCheckPermission("setting-vouchergroup-candelete")
    public JsonResult delete(@PathVariable Long voucherWordId, @SessionAccountSetId Long accountSetsId) {
        voucherWordService.remove(voucherWordId, accountSetsId);
        return JsonResult.successful();
    }
}
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/voucher-word")
public class VoucherWordController {

    private final VoucherWordService voucherWordService;

    @GetMapping
    public JsonResult list(@SessionAccountSetId Long accountSetsId) {
        return JsonResult.successful(voucherWordService.list(accountSetsId));
    }

    @PostMapping
    @SaCheckPermission("setting-vouchergroup-canedit")
    public JsonResult create(@RequestBody VoucherWord voucherWord, @SessionAccountSetId Long accountSetsId) {
        voucherWord.setAccountSetsId(accountSetsId);
        voucherWordService.save(voucherWord);
        return JsonResult.successful();
    }

    @PutMapping
    @SaCheckPermission("setting-vouchergroup-canedit")
    public JsonResult update(@RequestBody VoucherWord voucherWord, @SessionAccountSetId Long accountSetsId) {
        voucherWord.setAccountSetsId(accountSetsId);
        voucherWordService.save(voucherWord);
        return JsonResult.successful();
    }

    @DeleteMapping("{voucherWordId:\\d+}")
    @SaCheckPermission("setting-vouchergroup-candelete")
    public JsonResult delete(@PathVariable Long voucherWordId, @SessionAccountSetId Long accountSetsId) {
        voucherWordService.remove(voucherWordId, accountSetsId);
        return JsonResult.successful();
    }
}

权限检查通过sa-token的 @SaCheckPermission 注解控制,权限key可再管理后台权限管理进行定义和查看。

Controller中可以通过

  • @SessionAccountSetId:注解自动获取到当前登录用户的当前账套ID,
  • @SessionAccountSet:获取当前账套对象,
  • @SessionUser:获取当前登录用户对象。

具体实现 SessionUserHandlerMethodArgumentResolver.java

java
@Component
public class SessionUserHandlerMethodArgumentResolver implements SessionHandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return ((parameter.getParameterType().isAssignableFrom(UserVo.class)) && parameter.hasParameterAnnotation(SessionUser.class))
                || ((parameter.getParameterType().isAssignableFrom(Long.class)) && parameter.hasParameterAnnotation(SessionAccountSetId.class))
                || ((parameter.getParameterType().isAssignableFrom(AccountSets.class)) && parameter.hasParameterAnnotation(SessionAccountSet.class));
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        UserVo user = StpUtil.getSession().getModel(S_USER_KEY, UserVo.class);

        if (user != null) {
            if (parameter.hasParameterAnnotation(SessionUser.class)) {
                return user;
            } else if (parameter.hasParameterAnnotation(SessionAccountSetId.class)) {
                return user.getAccountSets() != null ? user.getAccountSets().getId() : null;
            } else if (parameter.hasParameterAnnotation(SessionAccountSet.class)) {
                return user.getAccountSets();
            }
        }
        return null;
    }
}
@Component
public class SessionUserHandlerMethodArgumentResolver implements SessionHandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return ((parameter.getParameterType().isAssignableFrom(UserVo.class)) && parameter.hasParameterAnnotation(SessionUser.class))
                || ((parameter.getParameterType().isAssignableFrom(Long.class)) && parameter.hasParameterAnnotation(SessionAccountSetId.class))
                || ((parameter.getParameterType().isAssignableFrom(AccountSets.class)) && parameter.hasParameterAnnotation(SessionAccountSet.class));
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        UserVo user = StpUtil.getSession().getModel(S_USER_KEY, UserVo.class);

        if (user != null) {
            if (parameter.hasParameterAnnotation(SessionUser.class)) {
                return user;
            } else if (parameter.hasParameterAnnotation(SessionAccountSetId.class)) {
                return user.getAccountSets() != null ? user.getAccountSets().getId() : null;
            } else if (parameter.hasParameterAnnotation(SessionAccountSet.class)) {
                return user.getAccountSets();
            }
        }
        return null;
    }
}