@@ -1,25 +1,54 @@
package tech.easyflow.system.service.impl ;
import cn.dev33.satoken.stp.StpUtil ;
import cn.hutool.crypto.digest.BCrypt ;
import cn.idev.excel.EasyExcel ;
import cn.idev.excel.FastExcel ;
import cn.idev.excel.context.AnalysisContext ;
import cn.idev.excel.metadata.data.ReadCellData ;
import cn.idev.excel.read.listener.ReadListener ;
import com.mybatisflex.core.query.QueryWrapper ;
import com.mybatisflex.spring.service.impl.ServiceImpl ;
import org.springframework.stereotype.Service ;
import org.springframework.transaction.annotation.Transactional ;
import org.springframework.transaction.PlatformTransactionManager ;
import org.springframework.transaction.support.TransactionTemplate ;
import org.springframework.web.multipart.MultipartFile ;
import tech.easyflow.common.cache.RedisLockExecutor ;
import tech.easyflow.common.constant.enums.EnumAccountType ;
import tech.easyflow.common.constant.enums.EnumDataStatus ;
import tech.easyflow.common.entity.LoginAccount ;
import tech.easyflow.common.util.StringUtil ;
import tech.easyflow.common.web.exceptions.BusinessException ;
import tech.easyflow.system.entity.SysAccount ;
import tech.easyflow.system.entity.SysAccountPosition ;
import tech.easyflow.system.entity.SysAccountRole ;
import tech.easyflow.system.entity.SysDept ;
import tech.easyflow.system.entity.SysPosition ;
import tech.easyflow.system.entity.SysRole ;
import tech.easyflow.system.entity.vo.SysAccountImportErrorRowVo ;
import tech.easyflow.system.entity.vo.SysAccountImportResultVo ;
import tech.easyflow.system.mapper.SysAccountMapper ;
import tech.easyflow.system.mapper.SysAccountPositionMapper ;
import tech.easyflow.system.mapper.SysAccountRoleMapper ;
import tech.easyflow.system.mapper.SysDeptMapper ;
import tech.easyflow.system.mapper.SysPositionMapper ;
import tech.easyflow.system.mapper.SysRoleMapper ;
import tech.easyflow.system.service.SysAccountService ;
import javax.annotation.Resource ;
import java.time.Duration ;
import java.io.InputStream ;
import java.io.OutputStream ;
import java.math.BigInteger ;
import java.time.Duration ;
import java.util.ArrayList ;
import java.util.Collections ;
import java.util.Date ;
import java.util.HashMap ;
import java.util.LinkedHashMap ;
import java.util.LinkedHashSet ;
import java.util.List ;
import java.util.Map ;
import java.util.Set ;
/**
@@ -34,6 +63,18 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
private static final String ACCOUNT_RELATION_LOCK_KEY_PREFIX = " easyflow:lock:sys:account:relation: " ;
private static final Duration LOCK_WAIT_TIMEOUT = Duration . ofSeconds ( 2 ) ;
private static final Duration LOCK_LEASE_TIMEOUT = Duration . ofSeconds ( 10 ) ;
private static final String DEFAULT_RESET_PASSWORD = " 123456 " ;
private static final long MAX_IMPORT_FILE_SIZE_BYTES = 10L * 1024 * 1024 ;
private static final int MAX_IMPORT_ROWS = 5000 ;
private static final String IMPORT_HEAD_DEPT_CODE = " 部门编码 " ;
private static final String IMPORT_HEAD_LOGIN_NAME = " 登录账号 " ;
private static final String IMPORT_HEAD_NICKNAME = " 昵称 " ;
private static final String IMPORT_HEAD_MOBILE = " 手机号 " ;
private static final String IMPORT_HEAD_EMAIL = " 邮箱 " ;
private static final String IMPORT_HEAD_STATUS = " 状态 " ;
private static final String IMPORT_HEAD_ROLE_KEYS = " 角色编码 " ;
private static final String IMPORT_HEAD_POSITION_CODES = " 岗位编码 " ;
private static final String IMPORT_HEAD_REMARK = " 备注 " ;
@Resource
private SysAccountRoleMapper sysAccountRoleMapper ;
@@ -42,7 +83,13 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
@Resource
private SysRoleMapper sysRoleMapper ;
@Resource
private SysPositionMapper sysPositionMapper ;
@Resource
private SysDeptMapper sysDeptMapper ;
@Resource
private RedisLockExecutor redisLockExecutor ;
@Resource
private PlatformTransactionManager transactionManager ;
@Override
@Transactional ( rollbackFor = Exception . class )
@@ -55,7 +102,6 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
LOCK_WAIT_TIMEOUT ,
LOCK_LEASE_TIMEOUT ,
( ) - > {
//sync roleIds
List < BigInteger > roleIds = entity . getRoleIds ( ) ;
if ( roleIds ! = null ) {
QueryWrapper delW = QueryWrapper . create ( ) ;
@@ -79,7 +125,6 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
}
}
//sync positionIds
List < BigInteger > positionIds = entity . getPositionIds ( ) ;
if ( positionIds ! = null ) {
QueryWrapper delW = QueryWrapper . create ( ) ;
@@ -112,4 +157,483 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
w . eq ( SysAccount : : getLoginName , userKey ) ;
return getOne ( w ) ;
}
@Override
public void resetPassword ( BigInteger accountId , BigInteger operatorId ) {
SysAccount record = getById ( accountId ) ;
if ( record = = null ) {
throw new BusinessException ( " 用户不存在 " ) ;
}
Integer accountType = record . getAccountType ( ) ;
if ( EnumAccountType . SUPER_ADMIN . getCode ( ) . equals ( accountType ) ) {
throw new BusinessException ( " 不能重置超级管理员密码 " ) ;
}
if ( EnumAccountType . TENANT_ADMIN . getCode ( ) . equals ( accountType ) ) {
throw new BusinessException ( " 不能重置租户管理员密码 " ) ;
}
SysAccount update = new SysAccount ( ) ;
update . setId ( accountId ) ;
update . setPassword ( BCrypt . hashpw ( DEFAULT_RESET_PASSWORD ) ) ;
update . setPasswordResetRequired ( true ) ;
update . setModified ( new Date ( ) ) ;
update . setModifiedBy ( operatorId ) ;
updateById ( update ) ;
StpUtil . kickout ( accountId ) ;
}
@Override
public SysAccountImportResultVo importAccounts ( MultipartFile file , LoginAccount loginAccount ) {
validateImportFile ( file ) ;
List < SysAccountImportRow > rows = parseImportRows ( file ) ;
SysAccountImportResultVo result = new SysAccountImportResultVo ( ) ;
result . setTotalCount ( rows . size ( ) ) ;
if ( rows . isEmpty ( ) ) {
return result ;
}
Map < String , SysDept > deptMap = buildDeptCodeMap ( ) ;
Map < String , SysRole > roleMap = buildRoleKeyMap ( ) ;
Map < String , SysPosition > positionMap = buildPositionCodeMap ( ) ;
for ( SysAccountImportRow row : rows ) {
try {
executeInRowTransaction ( ( ) - > importSingleRow ( row , loginAccount , deptMap , roleMap , positionMap ) ) ;
result . setSuccessCount ( result . getSuccessCount ( ) + 1 ) ;
} catch ( Exception e ) {
result . setErrorCount ( result . getErrorCount ( ) + 1 ) ;
appendImportError ( result , row , extractImportErrorMessage ( e ) ) ;
}
}
return result ;
}
@Override
public void writeImportTemplate ( OutputStream outputStream ) {
EasyExcel . write ( outputStream )
. head ( buildImportHeadList ( ) )
. sheet ( " 模板 " )
. doWrite ( new ArrayList < > ( ) ) ;
}
private void executeInRowTransaction ( Runnable action ) {
TransactionTemplate template = new TransactionTemplate ( transactionManager ) ;
template . executeWithoutResult ( status - > {
try {
action . run ( ) ;
} catch ( RuntimeException e ) {
status . setRollbackOnly ( ) ;
throw e ;
}
} ) ;
}
private void importSingleRow ( SysAccountImportRow row ,
LoginAccount loginAccount ,
Map < String , SysDept > deptMap ,
Map < String , SysRole > roleMap ,
Map < String , SysPosition > positionMap ) {
String deptCode = trimToNull ( row . getDeptCode ( ) ) ;
String loginName = trimToNull ( row . getLoginName ( ) ) ;
String nickname = trimToNull ( row . getNickname ( ) ) ;
if ( deptCode = = null ) {
throw new BusinessException ( " 部门编码不能为空 " ) ;
}
if ( loginName = = null ) {
throw new BusinessException ( " 登录账号不能为空 " ) ;
}
if ( nickname = = null ) {
throw new BusinessException ( " 昵称不能为空 " ) ;
}
SysDept dept = deptMap . get ( deptCode ) ;
if ( dept = = null ) {
throw new BusinessException ( " 部门编码不存在: " + deptCode ) ;
}
ensureLoginNameNotExists ( loginName ) ;
List < BigInteger > roleIds = resolveRoleIds ( row . getRoleKeys ( ) , roleMap ) ;
List < BigInteger > positionIds = resolvePositionIds ( row . getPositionCodes ( ) , positionMap ) ;
SysAccount entity = new SysAccount ( ) ;
entity . setDeptId ( dept . getId ( ) ) ;
entity . setTenantId ( loginAccount . getTenantId ( ) ) ;
entity . setLoginName ( loginName ) ;
entity . setPassword ( BCrypt . hashpw ( DEFAULT_RESET_PASSWORD ) ) ;
entity . setPasswordResetRequired ( true ) ;
entity . setAccountType ( EnumAccountType . NORMAL . getCode ( ) ) ;
entity . setNickname ( nickname ) ;
entity . setMobile ( trimToNull ( row . getMobile ( ) ) ) ;
entity . setEmail ( trimToNull ( row . getEmail ( ) ) ) ;
entity . setStatus ( parseStatus ( row . getStatus ( ) ) ) ;
entity . setRemark ( trimToNull ( row . getRemark ( ) ) ) ;
entity . setCreated ( new Date ( ) ) ;
entity . setCreatedBy ( loginAccount . getId ( ) ) ;
entity . setModified ( new Date ( ) ) ;
entity . setModifiedBy ( loginAccount . getId ( ) ) ;
entity . setRoleIds ( roleIds ) ;
entity . setPositionIds ( positionIds ) ;
save ( entity ) ;
syncRelations ( entity ) ;
}
private void ensureLoginNameNotExists ( String loginName ) {
QueryWrapper wrapper = QueryWrapper . create ( ) ;
wrapper . eq ( SysAccount : : getLoginName , loginName ) ;
if ( count ( wrapper ) > 0 ) {
throw new BusinessException ( " 登录账号已存在: " + loginName ) ;
}
}
private Integer parseStatus ( String rawStatus ) {
String status = trimToNull ( rawStatus ) ;
if ( status = = null ) {
return EnumDataStatus . AVAILABLE . getCode ( ) ;
}
if ( " 1 " . equals ( status ) | | " 已启用 " . equals ( status ) | | " 启用 " . equals ( status ) ) {
return EnumDataStatus . AVAILABLE . getCode ( ) ;
}
if ( " 0 " . equals ( status ) | | " 未启用 " . equals ( status ) | | " 停用 " . equals ( status ) | | " 禁用 " . equals ( status ) ) {
return EnumDataStatus . UNAVAILABLE . getCode ( ) ;
}
throw new BusinessException ( " 状态不合法,仅支持 1/0 或 已启用/未启用 " ) ;
}
private List < BigInteger > resolveRoleIds ( String roleKeysText , Map < String , SysRole > roleMap ) {
List < String > roleKeys = splitCodes ( roleKeysText ) ;
if ( roleKeys . isEmpty ( ) ) {
return Collections . emptyList ( ) ;
}
List < BigInteger > roleIds = new ArrayList < > ( roleKeys . size ( ) ) ;
for ( String roleKey : roleKeys ) {
SysRole role = roleMap . get ( roleKey ) ;
if ( role = = null ) {
throw new BusinessException ( " 角色编码不存在: " + roleKey ) ;
}
roleIds . add ( role . getId ( ) ) ;
}
return roleIds ;
}
private List < BigInteger > resolvePositionIds ( String positionCodesText , Map < String , SysPosition > positionMap ) {
List < String > positionCodes = splitCodes ( positionCodesText ) ;
if ( positionCodes . isEmpty ( ) ) {
return Collections . emptyList ( ) ;
}
List < BigInteger > positionIds = new ArrayList < > ( positionCodes . size ( ) ) ;
for ( String positionCode : positionCodes ) {
SysPosition position = positionMap . get ( positionCode ) ;
if ( position = = null ) {
throw new BusinessException ( " 岗位编码不存在: " + positionCode ) ;
}
positionIds . add ( position . getId ( ) ) ;
}
return positionIds ;
}
private List < String > splitCodes ( String rawCodes ) {
String codes = trimToNull ( rawCodes ) ;
if ( codes = = null ) {
return Collections . emptyList ( ) ;
}
String [ ] values = codes . split ( " [,, ] " ) ;
List < String > result = new ArrayList < > ( ) ;
Set < String > uniqueValues = new LinkedHashSet < > ( ) ;
for ( String value : values ) {
String trimmed = trimToNull ( value ) ;
if ( trimmed ! = null & & uniqueValues . add ( trimmed ) ) {
result . add ( trimmed ) ;
}
}
return result ;
}
private Map < String , SysDept > buildDeptCodeMap ( ) {
List < SysDept > deptList = sysDeptMapper . selectListByQuery ( QueryWrapper . create ( ) ) ;
Map < String , SysDept > deptMap = new HashMap < > ( ) ;
for ( SysDept dept : deptList ) {
String deptCode = trimToNull ( dept . getDeptCode ( ) ) ;
if ( deptCode ! = null ) {
deptMap . putIfAbsent ( deptCode , dept ) ;
}
}
return deptMap ;
}
private Map < String , SysRole > buildRoleKeyMap ( ) {
List < SysRole > roleList = sysRoleMapper . selectListByQuery ( QueryWrapper . create ( ) ) ;
Map < String , SysRole > roleMap = new HashMap < > ( ) ;
for ( SysRole role : roleList ) {
String roleKey = trimToNull ( role . getRoleKey ( ) ) ;
if ( roleKey ! = null ) {
roleMap . putIfAbsent ( roleKey , role ) ;
}
}
return roleMap ;
}
private Map < String , SysPosition > buildPositionCodeMap ( ) {
List < SysPosition > positionList = sysPositionMapper . selectListByQuery ( QueryWrapper . create ( ) ) ;
Map < String , SysPosition > positionMap = new HashMap < > ( ) ;
for ( SysPosition position : positionList ) {
String positionCode = trimToNull ( position . getPositionCode ( ) ) ;
if ( positionCode ! = null ) {
positionMap . putIfAbsent ( positionCode , position ) ;
}
}
return positionMap ;
}
private List < SysAccountImportRow > parseImportRows ( MultipartFile file ) {
try ( InputStream inputStream = file . getInputStream ( ) ) {
SysAccountExcelReadListener listener = new SysAccountExcelReadListener ( ) ;
FastExcel . read ( inputStream , listener )
. sheet ( )
. doRead ( ) ;
return listener . getRows ( ) ;
} catch ( BusinessException e ) {
throw e ;
} catch ( Exception e ) {
throw new BusinessException ( " 用户导入文件解析失败 " ) ;
}
}
private void validateImportFile ( MultipartFile file ) {
if ( file = = null | | file . isEmpty ( ) ) {
throw new BusinessException ( " 导入文件不能为空 " ) ;
}
if ( file . getSize ( ) > MAX_IMPORT_FILE_SIZE_BYTES ) {
throw new BusinessException ( " 导入文件大小不能超过10MB " ) ;
}
String fileName = file . getOriginalFilename ( ) ;
String lowerName = fileName = = null ? " " : fileName . toLowerCase ( ) ;
if ( ! ( lowerName . endsWith ( " .xlsx " ) | | lowerName . endsWith ( " .xls " ) ) ) {
throw new BusinessException ( " 仅支持 xlsx/xls 文件 " ) ;
}
}
private void appendImportError ( SysAccountImportResultVo result , SysAccountImportRow row , String reason ) {
SysAccountImportErrorRowVo errorRow = new SysAccountImportErrorRowVo ( ) ;
errorRow . setRowNumber ( row . getRowNumber ( ) ) ;
errorRow . setDeptCode ( row . getDeptCode ( ) ) ;
errorRow . setLoginName ( row . getLoginName ( ) ) ;
errorRow . setReason ( reason ) ;
result . getErrorRows ( ) . add ( errorRow ) ;
}
private String extractImportErrorMessage ( Exception e ) {
if ( e = = null ) {
return " 导入失败 " ;
}
if ( e . getCause ( ) ! = null & & StringUtil . hasText ( e . getCause ( ) . getMessage ( ) ) ) {
return e . getCause ( ) . getMessage ( ) ;
}
if ( StringUtil . hasText ( e . getMessage ( ) ) ) {
return e . getMessage ( ) ;
}
return " 导入失败 " ;
}
private List < List < String > > buildImportHeadList ( ) {
List < List < String > > headList = new ArrayList < > ( 9 ) ;
headList . add ( Collections . singletonList ( IMPORT_HEAD_DEPT_CODE ) ) ;
headList . add ( Collections . singletonList ( IMPORT_HEAD_LOGIN_NAME ) ) ;
headList . add ( Collections . singletonList ( IMPORT_HEAD_NICKNAME ) ) ;
headList . add ( Collections . singletonList ( IMPORT_HEAD_MOBILE ) ) ;
headList . add ( Collections . singletonList ( IMPORT_HEAD_EMAIL ) ) ;
headList . add ( Collections . singletonList ( IMPORT_HEAD_STATUS ) ) ;
headList . add ( Collections . singletonList ( IMPORT_HEAD_ROLE_KEYS ) ) ;
headList . add ( Collections . singletonList ( IMPORT_HEAD_POSITION_CODES ) ) ;
headList . add ( Collections . singletonList ( IMPORT_HEAD_REMARK ) ) ;
return headList ;
}
private String trimToNull ( String value ) {
if ( ! StringUtil . hasText ( value ) ) {
return null ;
}
return value . trim ( ) ;
}
private static class SysAccountImportRow {
private Integer rowNumber ;
private String deptCode ;
private String loginName ;
private String nickname ;
private String mobile ;
private String email ;
private String status ;
private String roleKeys ;
private String positionCodes ;
private String remark ;
public Integer getRowNumber ( ) {
return rowNumber ;
}
public void setRowNumber ( Integer rowNumber ) {
this . rowNumber = rowNumber ;
}
public String getDeptCode ( ) {
return deptCode ;
}
public void setDeptCode ( String deptCode ) {
this . deptCode = deptCode ;
}
public String getLoginName ( ) {
return loginName ;
}
public void setLoginName ( String loginName ) {
this . loginName = loginName ;
}
public String getNickname ( ) {
return nickname ;
}
public void setNickname ( String nickname ) {
this . nickname = nickname ;
}
public String getMobile ( ) {
return mobile ;
}
public void setMobile ( String mobile ) {
this . mobile = mobile ;
}
public String getEmail ( ) {
return email ;
}
public void setEmail ( String email ) {
this . email = email ;
}
public String getStatus ( ) {
return status ;
}
public void setStatus ( String status ) {
this . status = status ;
}
public String getRoleKeys ( ) {
return roleKeys ;
}
public void setRoleKeys ( String roleKeys ) {
this . roleKeys = roleKeys ;
}
public String getPositionCodes ( ) {
return positionCodes ;
}
public void setPositionCodes ( String positionCodes ) {
this . positionCodes = positionCodes ;
}
public String getRemark ( ) {
return remark ;
}
public void setRemark ( String remark ) {
this . remark = remark ;
}
}
private class SysAccountExcelReadListener implements ReadListener < LinkedHashMap < Integer , Object > > {
private final Map < String , Integer > headIndex = new HashMap < > ( ) ;
private final List < SysAccountImportRow > rows = new ArrayList < > ( ) ;
private int sheetRowNo ;
@Override
public void invoke ( LinkedHashMap < Integer , Object > data , AnalysisContext context ) {
sheetRowNo + + ;
String deptCode = getCellValue ( data , IMPORT_HEAD_DEPT_CODE ) ;
String loginName = getCellValue ( data , IMPORT_HEAD_LOGIN_NAME ) ;
String nickname = getCellValue ( data , IMPORT_HEAD_NICKNAME ) ;
String mobile = getCellValue ( data , IMPORT_HEAD_MOBILE ) ;
String email = getCellValue ( data , IMPORT_HEAD_EMAIL ) ;
String status = getCellValue ( data , IMPORT_HEAD_STATUS ) ;
String roleKeys = getCellValue ( data , IMPORT_HEAD_ROLE_KEYS ) ;
String positionCodes = getCellValue ( data , IMPORT_HEAD_POSITION_CODES ) ;
String remark = getCellValue ( data , IMPORT_HEAD_REMARK ) ;
if ( ! StringUtil . hasText ( deptCode )
& & ! StringUtil . hasText ( loginName )
& & ! StringUtil . hasText ( nickname )
& & ! StringUtil . hasText ( mobile )
& & ! StringUtil . hasText ( email )
& & ! StringUtil . hasText ( status )
& & ! StringUtil . hasText ( roleKeys )
& & ! StringUtil . hasText ( positionCodes )
& & ! StringUtil . hasText ( remark ) ) {
return ;
}
if ( rows . size ( ) > = MAX_IMPORT_ROWS ) {
throw new BusinessException ( " 单次最多导入5000个用户 " ) ;
}
SysAccountImportRow row = new SysAccountImportRow ( ) ;
row . setRowNumber ( sheetRowNo + 1 ) ;
row . setDeptCode ( deptCode ) ;
row . setLoginName ( loginName ) ;
row . setNickname ( nickname ) ;
row . setMobile ( mobile ) ;
row . setEmail ( email ) ;
row . setStatus ( status ) ;
row . setRoleKeys ( roleKeys ) ;
row . setPositionCodes ( positionCodes ) ;
row . setRemark ( remark ) ;
rows . add ( row ) ;
}
@Override
public void invokeHead ( Map < Integer , ReadCellData < ? > > headMap , AnalysisContext context ) {
for ( Map . Entry < Integer , ReadCellData < ? > > entry : headMap . entrySet ( ) ) {
String headValue = entry . getValue ( ) = = null ? null : entry . getValue ( ) . getStringValue ( ) ;
String header = trimToNull ( headValue ) ;
if ( header ! = null ) {
headIndex . put ( header , entry . getKey ( ) ) ;
}
}
List < String > requiredHeads = List . of (
IMPORT_HEAD_DEPT_CODE ,
IMPORT_HEAD_LOGIN_NAME ,
IMPORT_HEAD_NICKNAME ,
IMPORT_HEAD_MOBILE ,
IMPORT_HEAD_EMAIL ,
IMPORT_HEAD_STATUS ,
IMPORT_HEAD_ROLE_KEYS ,
IMPORT_HEAD_POSITION_CODES ,
IMPORT_HEAD_REMARK
) ;
for ( String requiredHead : requiredHeads ) {
if ( ! headIndex . containsKey ( requiredHead ) ) {
throw new BusinessException ( " 导入模板表头不正确,必须包含: " + String . join ( " 、 " , requiredHeads ) ) ;
}
}
}
@Override
public void doAfterAllAnalysed ( AnalysisContext context ) {
// no-op
}
public List < SysAccountImportRow > getRows ( ) {
return rows ;
}
private String getCellValue ( Map < Integer , Object > row , String headName ) {
Integer index = headIndex . get ( headName ) ;
if ( index = = null ) {
return null ;
}
Object value = row . get ( index ) ;
return value = = null ? null : String . valueOf ( value ) . trim ( ) ;
}
}
}