05-业务服务层 service¶
📄 创建: ZCode AI 2026-08-14 · 修改: JimWb 2026-08-14
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 提供完整的业务单据号自动生成机制,集成在 saveOrUpdateExt 的 before() 钩子中。
@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 日期格式化 |
相关文档¶
- 04-实体与数据访问 — BaseEntity / BaseMapperExt
- 07-数据权限 — 数据权限 SQL 改写
- 09-通用功能 — EnergyBaseController 调用 Service
- 12-FAQ与注意事项 — 动态数据源半成品状态