跳转至

05-业务服务层 service

📄 创建: ZCode AI 2026-08-14 · 修改: JimWb 2026-08-14

https://doc.xpku.com/raw/tech/backend/bams-sdk/05-service.md

bams-service(70 个类)是 SDK 的业务核心。提供 ServiceExtImpl 业务 Service 基类、序列号生成、动态数据源、Session 管理、通用业务 Service。


一、ServiceExtImpl —— 业务 Service 基类(核心)

cn.cnbm.bams.service.ext.IServiceExt<T> / cn.cnbm.bams.service.ext.impl.ServiceExtImpl<MAPPER, ENTITY>

所有业务 Service 接口继承 IServiceExt,实现类继承 ServiceExtImpl。它在 MyBatis-Plus ServiceImpl 基础上增强了大量方法。

// 接口
public interface DemoService extends IServiceExt<DemoEntity> { }

// 实现
@Service
public class DemoServiceImpl extends ServiceExtImpl<DemoMapper, DemoEntity>
        implements DemoService { }

IServiceExt 扩展方法清单

方法签名 功能
<P,R> P pageResultMaps(P page, Wrapper params) 分页查 Map 结果(自动格式化时间)
Page<T> listPage(BasicParamWrapper) 按 BasicParamWrapper 分页查实体
List<T> tree(BasicParamWrapper) 查询并构建树形结构(支持 maxDeepLevel 过滤)
T saveOrUpdateExt(T entity) 增强保存/更新(带校验/序列号/重复检查/事务)
T saveOrUpdateExt(T entity, Collection<String> allowNullFields) 同上 + 指定字段强制写 null
Dict saveOrUpdateMap(Dict entity) Map 形式保存/更新
List<Dict> batchSaveOrUpdateMap(List<Dict>) 批量 Map 保存
Dict saveByMap(Dict) Map 新增(自动填 corp_code/created_time)
Dict updateByMap(Dict) Map 更新
Object saveOrUpdateBatchExt(List<T>) 批量增强保存
T getOne(BasicParamWrapper, Boolean first) 按参数查单条(first=true 取第一条)
Page<Dict> listMap(BasicParamWrapper) 分页查 Map 结果
T processImportRowData(Map headMap, Map entity) Excel 导入:按表头映射到实体
List<BdCorpCacheDTO> getCorpAndParentList(String corpCode) 取当前公司及父公司列表
BdCorp getCurrentCorp() 取当前登录公司
List<Dict> dictMap(BasicParamWrapper) 按指定列查数据(支持 distinct)
void deleteByParams(BasicParamWrapper) 按参数删除
List<BdDictionary> getBdDictionary(String dtype) 取字典数据

saveOrUpdateExt 详解(核心方法)

增强版保存,相比 MyBatis-Plus 原生 saveOrUpdate 多了以下流程:

saveOrUpdateExt(entity)
  └─ before(entity)              // 前置钩子
       ├─ 填 corpCode            // 当前公司编码
       ├─ 填 innerCode           // 内部编码
       ├─ 序列号注解处理          // @SequenceGenerator 生成单据号
       ├─ @CheckRepeat 重复校验   // 编码唯一性检查
       └─ JSR303 分组校验         // Insert/Update 组校验
  └─ 判断 id 有无
       ├─ 无 id → save(insert)
       └─ 有 id → updateById
  └─ 失败时 catch → rollBackSequenceFieldValue()  // 回滚序列号

指定字段强制写 null

MyBatis-Plus 默认不写 null 字段。用第二个参数指定要强制写 null 的字段:

entity.setRemark(null);  // 想把 remark 清空
service.saveOrUpdateExt(entity, Arrays.asList("remark"));  // 强制写 null

before() 钩子(protected,子类可覆写)

@Override
protected void before(DemoEntity entity) {
    super.before(entity);  // 先执行默认逻辑
    // 添加自定义前置逻辑
    if (entity.getCode() == null) {
        entity.setCode(generateCode());
    }
}

ServiceExtImpl 内部能力

能力 说明
getCurrentTableColumns() @PostConstruct,启动时加载表字段元数据(兼容 MySQL/DM)
getQueryWrapper(BasicParamWrapper) 自动构建 QueryWrapper,支持 @Formula
dictMap / listMap / pageResultMaps Map 结果查询,自动格式化时间
buildAllowNullUpdateWrapper 用 UpdateWrapper.set() 强制白名单字段写 null
getById 重写 拼 SELECT SQL 支持 @Formula 计算字段

二、序列号生成机制(重点)

SDK 提供完整的业务单据号自动生成机制,集成在 saveOrUpdateExtbefore() 钩子中。

@SequenceGenerator 注解

cn.cnbm.bams.common.annotations.sequence.SequenceGenerator(标记在实体字段上)

属性 默认值 说明
sequenceCode "" 序列号编码(对应 pub_sequence 表)
alwaysUpdate false true=无论有无值都重新生成
datePattern "" 日期格式串(如 yyyyMMdd)
strategy DefaultSequenceStrategy 生成策略类全限定名
prefixAviatorScript "" 前缀表达式
suffixAviatorScript "" 后缀表达式
tableName "" 业务表名(走表查最大值策略时用)
tableField "" 业务表编码字段名
engine Engines.AVIATOR 表达式引擎:AVIATOR 或 SPEL

使用示例

@Data
@TableName("bd_unit")
public class BdUnit extends BaseEntity {
    @SequenceGenerator(
        sequenceCode = "UNIT_CODE",      // 对应 pub_sequence 表的 sequenceCode
        datePattern = "yyyyMMdd",         // 含日期
        prefixAviatorScript = "'U'",      // 前缀 U(Aviator 表达式)
        engine = SequenceGenerator.Engines.SPEL  // 或 AVIATOR
    )
    private String unitCode;  // 保存时自动生成
}

生成流程

SequenceAnnotationHandler.findAndSetSequenceFieldValue(entity)
  ├─ 扫描实体的 @SequenceGenerator 字段
  ├─ 字段已有值且 alwaysUpdate=false → 跳过
  ├─ 无 tableName → 走 DefaultSequenceStrategy(pub_sequence 表)
  │     └─ sequenceService.next(code, datePattern)
  │         └─ pub_sequence 表自增 currNo + step,支持按 resetType 重置
  ├─ 有 tableName+tableField → 走 BatchSequenceStrategy(表查最大值)
  │     └─ select field from table where field like 'prefix%' order by desc limit 1
  └─ 拼接:prefix + 中间号 + suffix

生成的号格式

PubSequence.assembleCode() 拼装:

[前缀][分隔符][日期][分隔符][后缀][零填充流水号]

示例:prefix=BM, separator=-, date=20260814, currNo=5, noLength=4 → BM-20260814-0005

pub_sequence 表

字段 说明
sequenceCode 序列号编码
resetType 重置类型:NEVER / DAY / MONTH / YEAR
sequenceNoLength 流水号位数
sequencePrefix / sequenceSuffix 前缀 / 后缀
sequenceCurrNo 当前值
sequenceStep 步长
sequenceSeparator 分隔符
@Version 乐观锁

策略类

说明
Strategy 策略接口:generate(annotation, prefix, num) + rollbackNo(annotation)
SequenceStrategyFactory 双重检查锁单例,getStrategy(className) 先从 Spring 容器取 Bean
DefaultSequenceStrategy @Component,走 pub_sequence 表
BatchSequenceStrategy @Component,从业务表查当前最大编码

失败回滚

保存失败时 ServiceExtImpl 的 catch 块调用 rollBackSequenceFieldValue()strategy.rollbackNo → currNo - 1。


三、动态多数据源机制

⚠️ 半成品状态:框架已就绪,但缺少 AOP 切面和 Bean 注册。详见 12-FAQ与注意事项

架构(4 个核心类)

DynamicDataSourceContext (ThreadLocal)  ←──业务代码设置数据源 key
        ↓ determineCurrentLookupKey()
DynamicDataSource (继承 AbstractRoutingDataSource)
        ↓ targetDataSources
DataSourceManager (管理器)
        ↓ addDataSource(SysTenantDb)
DataSourceInitService (@TenantDataSource("sys"), 启动时加载)

各类职责

说明
DynamicDataSourceContext ThreadLocal 容器:setDataSourceKey(key) / getDataSourceKey() / clear()
DynamicDataSource 继承 Spring AbstractRoutingDataSource,重写 determineCurrentLookupKey() 返回 ThreadLocal 值
DataSourceManager @Service,ConcurrentHashMap 维护数据源池。key 格式 corpCode|dsKey。用 HikariCP 构建数据源
DataSourceInitService @Service @TenantDataSource("sys"),启动时查 sys_tenant_db(enableState=2)加载所有租户数据源

当前可用能力

  • ✅ 启动时从 sys_tenant_db 加载多数据源到路由表
  • ✅ 运行时新增数据源 addNewDataSource(config)
  • 缺少 AOP 切面@TenantDataSource 注解无法自动切换数据源(需手动调 DynamicDataSourceContext.setDataSourceKey()
  • DynamicDataSource Bean 未注册:无 @Configuration 配置类

四、Session 机制

cn.cnbm.bams.service.session.SessionService / SessionServiceImpl

基于 Redis 的自管理 Session(非 HttpSession)。

机制

说明
Session 标识 sessionid 放在 HTTP Header(非 Cookie),前端每次请求携带
存储 Redis,key = login:session: + sessionId,Hash 结构
过期 1 天(86400 秒),受 ${energy.login-cache:true} 开关控制

Session 内容

字段 说明
sessionId 会话 ID
userId 用户 ID
corpCode 公司编码
token Token
dataScope 数据权限范围
sysCode 系统编码
accountId 账户 ID
isSuperAdmin 是否超管
userInfo 用户 JSON

方法

方法 功能
createSession(param, request, response) 生成 UUID sessionId,写 Redis,设过期
lookupSession(sessionId) / lookupSession(request) 从 Header 读 sessionid,取全部字段
destroySession(request) 删 Redis key(登出)
isSessionValid(request/sessionId) 判断会话是否有效
lookupUserInfo(request) 从 session 取 userInfo 转 SysUser

登录流程(SysUserService.login)

1. 按 loginName 查 sys_user
2. 校验状态(非停用)
3. 查 bd_corp 公司信息
4. 密码校验:前端 RSA 公钥加密 → 后端 RSAUtils 私钥解密(兼容明文)
5. 查角色列表,拼 roleName/roleIdList
6. 算 dataScope:超管=DATA_ALL,否则取角色 dataScope 最小值(范围最大)
7. sessionService.createSession() 创建会话

五、CommonServiceImpl —— 通用查询服务

cn.cnbm.bams.service.common.CommonServiceImpl

方法签名 功能
getCurrentCorp() 从 session 取 corpCode,先查 Redis 再查库
getCorpAndParentList(corpCode) 取公司及父公司列表(Redis 缓存)
queryCommonData(sql, entityClass) 带数据权限执行 SQL(JSqlParser 自动注入 corp_code IN 条件)
queryEntityData(sql, entityClass) 直接执行 SQL(不加权限)
hasColumn(tableName, columnName) 判断表是否有某列(兼容 MySQL/DM)
getAllTables() 列出当前库所有表
getDictionary(dtype) 查 bd_dictionary 字典

queryCommonData 数据权限

// SQL 会被自动改写,加上 corp_code IN (本公司+父公司链) 条件
List<MyEntity> list = commonService.queryCommonData(
    "SELECT * FROM my_table WHERE status = 1", MyEntity.class);
// 实际执行:SELECT * FROM my_table WHERE status = 1
//         AND my_table.corp_code IN ('C001', 'C000')  -- 自动注入

六、其他服务

服务 功能
序列号 IPubSequenceService next(code, datePattern) / nextBatch(...) / rollbackNum(code)
文件上传 PubFileUploadService 文件上传记录管理
缓存 CacheService 基于 Ehcache 的缓存(getCache(key, clazz, supplier) 带回源)
事件 EventPublishService sendRefreshRedisEvent(keys) 发布刷新缓存事件
校验 ValidationUtils / ValidateObjectWrapper JSR303 分组校验工具

七、业务 Service 接口清单

bd 域 —— 基础数据

接口 实体 扩展方法
BdCorpService BdCorp getCorpAllParent 公司树
BdDeptService BdDept
BdPersonService BdPerson createAccountByPerson(personId) 根据人员生成账号
BdPostService BdPost
BdUnitService BdUnit
BdDictionaryService BdDictionary
BdDictionaryHeadService BdDictionaryHead

sys 域 —— 系统

接口 实体 扩展方法
SysUserService SysUser login / logout / updatePassword / getPermissionResourceListByUserId
SysRoleService SysRole bindResource / roleResourceTree
SysResourceService SysResource exist 存在性校验
SysAppService SysApp getAppListByRoleId/User
SysAppRoleService SysAppRole assignRolesToApp / assignAppsToRole 双向分配
ApexSysRoleResourceService SysRoleResource assign(全量覆盖) / unassign
SysUserRoleService SysUserRole batchBindUsers / getPersonsByRoleId
SysRoleButtonService SysRoleButton getButtonsByRoleId / assign / unassign
SysBusinessStatusService SysBusinessStatus getByBusinessCode(appCode, corpCode, code)
ISysConfigService / ISysLogService / ISysOperLogService 标准 CRUD
ISysButtonService / SysMenuButtonService 标准 CRUD

pub 域 —— 公共

接口 实体 扩展方法
PubFileUploadService PubFileUpload getPubFileUploadBy 按条件查
IPubSequenceService PubSequence next / nextBatch / rollbackNum

八、BaseServiceImpl(统计分析基类)

cn.cnbm.bams.service.base.BaseServiceImpl

注意:这个类不继承 IService,与 ServiceExtImpl 是两套独立体系。它面向统计分析/能源计算场景。

提供时间维度计算、同比环比、指标公式计算等能力:

方法 功能
getGroupBy(dimension) 时间维度→GROUP BY 列名
calculateYoyAndRrResult(current, previous) 同比环比 (本期-上期)/上期*100
calculatePercent(a, b) 占比
calculateQuotaResult(...) Aviator 表达式计算指标值
getDateRangeList(...) 日期范围列表
getTaosDatetime(...) TDengine 日期格式化

相关文档