[add] app注册、登录、实名认证、人员展示、通知

This commit is contained in:
lcj
2025-07-24 17:36:30 +08:00
parent e6a98ad0bd
commit 3d2f4b05ff
55 changed files with 1998 additions and 213 deletions

View File

@ -80,7 +80,7 @@ public class AuthController {
* @param body 登录信息 * @param body 登录信息
* @return 结果 * @return 结果
*/ */
@ApiEncrypt // @ApiEncrypt
@PostMapping("/login") @PostMapping("/login")
public R<LoginVo> login(@RequestBody String body) { public R<LoginVo> login(@RequestBody String body) {
LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class); LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
@ -183,7 +183,7 @@ public class AuthController {
/** /**
* 用户注册 * 用户注册
*/ */
@ApiEncrypt // @ApiEncrypt
@PostMapping("/register") @PostMapping("/register")
public R<Void> register(@Validated @RequestBody RegisterBody user) { public R<Void> register(@Validated @RequestBody RegisterBody user) {
if (!configService.selectRegisterEnabled(user.getTenantId())) { if (!configService.selectRegisterEnabled(user.getTenantId())) {

View File

@ -1,6 +1,7 @@
package org.dromara.web.service; package org.dromara.web.service;
import cn.dev33.satoken.secure.BCrypt; import cn.dev33.satoken.secure.BCrypt;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.Constants; import org.dromara.common.core.constant.Constants;
@ -43,6 +44,7 @@ public class SysRegisterService {
public void register(RegisterBody registerBody) { public void register(RegisterBody registerBody) {
String tenantId = registerBody.getTenantId(); String tenantId = registerBody.getTenantId();
String username = registerBody.getUsername(); String username = registerBody.getUsername();
String phonenumber = registerBody.getPhonenumber();
String password = registerBody.getPassword(); String password = registerBody.getPassword();
// 校验用户类型是否存在 // 校验用户类型是否存在
String userType = UserType.getUserType(registerBody.getUserType()).getUserType(); String userType = UserType.getUserType(registerBody.getUserType()).getUserType();
@ -52,6 +54,15 @@ public class SysRegisterService {
if (!isValid) { if (!isValid) {
throw new UserException("注册失败密码需满足818位包含大小写字母、数字、特殊字符中的至少三种组合"); throw new UserException("注册失败密码需满足818位包含大小写字母、数字、特殊字符中的至少三种组合");
} }
String middleFour = phonenumber.substring(3, 7);
if (StringUtils.isBlank(username)) {
do {
// 生成 6 位随机字母,并转为小写
String randomSix = RandomUtil.randomString(6).toLowerCase();
username = randomSix + middleFour;
// 如果已有同名记录,循环继续
} while (existsUsername(username));
}
boolean captchaEnabled = captchaProperties.getEnable(); boolean captchaEnabled = captchaProperties.getEnable();
// 验证码开关 // 验证码开关
if (captchaEnabled) { if (captchaEnabled) {
@ -62,6 +73,7 @@ public class SysRegisterService {
sysUser.setNickName(username); sysUser.setNickName(username);
sysUser.setPassword(BCrypt.hashpw(password)); sysUser.setPassword(BCrypt.hashpw(password));
sysUser.setUserType(userType); sysUser.setUserType(userType);
sysUser.setPhonenumber(phonenumber);
boolean exist = TenantHelper.dynamic(tenantId, () -> { boolean exist = TenantHelper.dynamic(tenantId, () -> {
return userMapper.exists(new LambdaQueryWrapper<SysUser>() return userMapper.exists(new LambdaQueryWrapper<SysUser>()
@ -77,6 +89,13 @@ public class SysRegisterService {
recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.register.success")); recordLogininfor(tenantId, username, Constants.REGISTER, MessageUtils.message("user.register.success"));
} }
private boolean existsUsername(String username) {
LambdaQueryWrapper<SysUser> qw = new LambdaQueryWrapper<>();
qw.eq(SysUser::getUserName, username);
// selectCount 性能足够,若并发极高可改为 exists 查询
return userMapper.selectCount(qw) > 0;
}
/** /**
* 校验验证码 * 校验验证码
* *

View File

@ -54,17 +54,31 @@ public class PasswordAuthStrategy implements IAuthStrategy {
String tenantId = loginBody.getTenantId(); String tenantId = loginBody.getTenantId();
String username = loginBody.getUsername(); String username = loginBody.getUsername();
String password = loginBody.getPassword(); String password = loginBody.getPassword();
String phonenumber = loginBody.getPhonenumber();
String code = loginBody.getCode(); String code = loginBody.getCode();
String uuid = loginBody.getUuid(); String uuid = loginBody.getUuid();
// 2. 如果没传用户名,用手机号去查,并写到新的局部变量里
String finalUsername;
if (StringUtils.isAllBlank(username, phonenumber)) {
throw new UserException("用户名或手机号不能为空");
}
if (StringUtils.isBlank(username)) {
SysUserVo sysUserVo = userMapper.selectVoOne(
new LambdaQueryWrapper<SysUser>()
.eq(SysUser::getPhonenumber, phonenumber)
);
finalUsername = sysUserVo.getUserName();
} else {
finalUsername = username;
}
boolean captchaEnabled = captchaProperties.getEnable(); boolean captchaEnabled = captchaProperties.getEnable();
// 验证码开关 // 验证码开关
if (captchaEnabled) { if (captchaEnabled) {
validateCaptcha(tenantId, username, code, uuid); validateCaptcha(tenantId, finalUsername, code, uuid);
} }
LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> { LoginUser loginUser = TenantHelper.dynamic(tenantId, () -> {
SysUserVo user = loadUserByUsername(username); SysUserVo user = loadUserByUsername(finalUsername);
loginService.checkLogin(LoginType.PASSWORD, tenantId, username, () -> !BCrypt.checkpw(password, user.getPassword())); loginService.checkLogin(LoginType.PASSWORD, tenantId, finalUsername, () -> !BCrypt.checkpw(password, user.getPassword()));
// 此处可根据登录用户的数据不同 自行创建 loginUser // 此处可根据登录用户的数据不同 自行创建 loginUser
return loginService.buildLoginUser(user); return loginService.buildLoginUser(user);
}); });

View File

@ -8,7 +8,7 @@ ruoyi:
copyrightYear: 2024 copyrightYear: 2024
captcha: captcha:
enable: true enable: false
# 页面 <参数设置> 可开启关闭 验证码校验 # 页面 <参数设置> 可开启关闭 验证码校验
# 验证码类型 math 数组计算 char 字符验证 # 验证码类型 math 数组计算 char 字符验证
type: MATH type: MATH
@ -193,7 +193,6 @@ api-decrypt:
- /v3/api-docs/** # 放行OpenAPI文档 - /v3/api-docs/** # 放行OpenAPI文档
- /actuator/** # 放行监控接口 - /actuator/** # 放行监控接口
- /other/ys7Device/webhook # 放行萤石云设备回调接口 - /other/ys7Device/webhook # 放行萤石云设备回调接口
- /auth/register # 放行注册接口
springdoc: springdoc:
api-docs: api-docs:

View File

@ -17,7 +17,6 @@ public class PasswordLoginBody extends LoginBody {
/** /**
* 用户名 * 用户名
*/ */
@NotBlank(message = "{user.username.not.blank}")
@Length(min = 2, max = 20, message = "{user.username.length.valid}") @Length(min = 2, max = 20, message = "{user.username.length.valid}")
private String username; private String username;
@ -28,4 +27,9 @@ public class PasswordLoginBody extends LoginBody {
@Length(min = 5, max = 20, message = "{user.password.length.valid}") @Length(min = 5, max = 20, message = "{user.password.length.valid}")
private String password; private String password;
/**
* 手机号
*/
private String phonenumber;
} }

View File

@ -17,7 +17,7 @@ public class RegisterBody extends LoginBody {
/** /**
* 用户名 * 用户名
*/ */
@NotBlank(message = "{user.username.not.blank}") // @NotBlank(message = "{user.username.not.blank}")
@Length(min = 2, max = 20, message = "{user.username.length.valid}") @Length(min = 2, max = 20, message = "{user.username.length.valid}")
private String username; private String username;
@ -28,6 +28,12 @@ public class RegisterBody extends LoginBody {
@Length(min = 5, max = 20, message = "{user.password.length.valid}") @Length(min = 5, max = 20, message = "{user.password.length.valid}")
private String password; private String password;
/**
* 手机号码
*/
@NotBlank(message = "{user.phonenumber.not.blank}")
private String phonenumber;
private String userType; private String userType;
} }

View File

@ -77,7 +77,7 @@ public class CryptoFilter implements Filter {
// new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN)); // new ServiceException("没有访问权限,请联系管理员授权", HttpStatus.FORBIDDEN));
// return; // return;
// } // }
throw new ServiceException("无权访问接口", HttpStatus.BAD_METHOD); // throw new ServiceException("无权访问接口", HttpStatus.BAD_METHOD);
} }
} }

View File

@ -8,6 +8,8 @@ import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity; import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial; import java.io.Serial;
import java.util.ArrayList;
import java.util.List;
/** /**
* app菜单 * app菜单
@ -85,6 +87,18 @@ public class AppMenu extends BaseEntity {
*/ */
private String remark; private String remark;
/**
* 父菜单名称
*/
@TableField(exist = false)
private String parentName;
/**
* 子菜单
*/
@TableField(exist = false)
private List<AppMenu> children = new ArrayList<>();
@Serial @Serial
@TableField(exist = false) @TableField(exist = false)
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -3,6 +3,8 @@ package org.dromara.app.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.dromara.app.system.domain.AppMenu; import org.dromara.app.system.domain.AppMenu;
import java.util.List;
/** /**
* @author lilemy * @author lilemy
* @description 针对表【app_menu(app菜单)】的数据库操作Mapper * @description 针对表【app_menu(app菜单)】的数据库操作Mapper
@ -10,6 +12,14 @@ import org.dromara.app.system.domain.AppMenu;
*/ */
public interface AppMenuMapper extends BaseMapper<AppMenu> { public interface AppMenuMapper extends BaseMapper<AppMenu> {
/**
* 根据用户ID查询菜单
*
* @param userId 用户ID
* @return 菜单列表
*/
List<AppMenu> selectMenuTreeByUserId(Long userId);
} }

View File

@ -1,7 +1,6 @@
package org.dromara.app.system.service; package org.dromara.app.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import org.dromara.app.system.domain.AppMenu; import org.dromara.app.system.domain.AppMenu;
import org.dromara.app.system.domain.dto.menu.AppMenuCreateReq; import org.dromara.app.system.domain.dto.menu.AppMenuCreateReq;
@ -9,6 +8,8 @@ import org.dromara.app.system.domain.dto.menu.AppMenuQueryReq;
import org.dromara.app.system.domain.dto.menu.AppMenuUpdateReq; import org.dromara.app.system.domain.dto.menu.AppMenuUpdateReq;
import org.dromara.app.system.domain.vo.menu.AppMenuVo; import org.dromara.app.system.domain.vo.menu.AppMenuVo;
import java.util.List;
/** /**
* @author lilemy * @author lilemy
* @description 针对表【app_menu(app菜单)】的数据库操作Service * @description 针对表【app_menu(app菜单)】的数据库操作Service
@ -16,6 +17,22 @@ import org.dromara.app.system.domain.vo.menu.AppMenuVo;
*/ */
public interface AppMenuService extends IService<AppMenu> { public interface AppMenuService extends IService<AppMenu> {
/**
* 根据id查询app菜单
*
* @param id app菜单id
* @return app菜单视图
*/
AppMenuVo queryById(Long id);
/**
* 根据用户ID查询菜单树信息
*
* @param userId 用户ID
* @return 菜单列表
*/
List<AppMenu> selectMenuTreeByUserId(Long userId);
/** /**
* 新增app菜单 * 新增app菜单
* *
@ -32,6 +49,14 @@ public interface AppMenuService extends IService<AppMenu> {
*/ */
Boolean updateByReq(AppMenuUpdateReq req); Boolean updateByReq(AppMenuUpdateReq req);
/**
* 校验app菜单名称唯一
*
* @param menu app菜单
* @return 是否唯一
*/
Boolean checkMenuNameUnique(AppMenu menu);
/** /**
* 获取app菜单视图 * 获取app菜单视图
* *
@ -49,11 +74,11 @@ public interface AppMenuService extends IService<AppMenu> {
LambdaQueryWrapper<AppMenu> buildQueryWrapper(AppMenuQueryReq req); LambdaQueryWrapper<AppMenu> buildQueryWrapper(AppMenuQueryReq req);
/** /**
* 获取app菜单分页对象视图 * 获取app菜单列表对象视图
* *
* @param menuPage app菜单分页对象 * @param menuList app菜单列表对象
* @return app菜单分页对象视图 * @return app菜单列表对象视图
*/ */
Page<AppMenuVo> getVoPage(Page<AppMenu> menuPage); List<AppMenuVo> getVoList(List<AppMenu> menuList);
} }

View File

@ -1,7 +1,11 @@
package org.dromara.app.system.service; package org.dromara.app.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import org.dromara.app.system.domain.AppRole; import org.dromara.app.system.domain.AppRole;
import org.dromara.app.system.domain.dto.role.AppRoleQueryReq;
import org.dromara.app.system.domain.vo.role.AppRoleVo;
/** /**
* @author lilemy * @author lilemy
@ -10,4 +14,28 @@ import org.dromara.app.system.domain.AppRole;
*/ */
public interface AppRoleService extends IService<AppRole> { public interface AppRoleService extends IService<AppRole> {
/**
* 获取app角色视图
*
* @param menu app角色
* @return app角色视图
*/
AppRoleVo getVo(AppRole menu);
/**
* 构建查询条件封装
*
* @param req 查询条件
* @return 查询条件封装
*/
LambdaQueryWrapper<AppRole> buildQueryWrapper(AppRoleQueryReq req);
/**
* 获取app角色视图列表
*
* @param rolePage app角色列表
* @return app角色视图列表
*/
Page<AppRoleVo> getVoList(Page<AppRole> rolePage);
} }

View File

@ -1,8 +1,7 @@
package org.dromara.app.system.service.impl; package org.dromara.app.system.service.impl;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.dromara.app.system.domain.AppMenu; import org.dromara.app.system.domain.AppMenu;
import org.dromara.app.system.domain.dto.menu.AppMenuCreateReq; import org.dromara.app.system.domain.dto.menu.AppMenuCreateReq;
@ -11,10 +10,16 @@ import org.dromara.app.system.domain.dto.menu.AppMenuUpdateReq;
import org.dromara.app.system.domain.vo.menu.AppMenuVo; import org.dromara.app.system.domain.vo.menu.AppMenuVo;
import org.dromara.app.system.mapper.AppMenuMapper; import org.dromara.app.system.mapper.AppMenuMapper;
import org.dromara.app.system.service.AppMenuService; import org.dromara.app.system.service.AppMenuService;
import org.dromara.common.core.constant.HttpStatus;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -26,6 +31,43 @@ import java.util.List;
public class AppMenuServiceImpl extends ServiceImpl<AppMenuMapper, AppMenu> public class AppMenuServiceImpl extends ServiceImpl<AppMenuMapper, AppMenu>
implements AppMenuService { implements AppMenuService {
/**
* 根据id查询app菜单
*
* @param id app菜单id
* @return app菜单视图
*/
@Override
public AppMenuVo queryById(Long id) {
AppMenu menu = this.getById(id);
if (menu == null) {
throw new ServiceException("app菜单不存在", HttpStatus.NOT_FOUND);
}
return this.getVo(menu);
}
/**
* 根据用户ID查询菜单树信息
*
* @param userId 用户ID
* @return 菜单列表
*/
@Override
public List<AppMenu> selectMenuTreeByUserId(Long userId) {
List<AppMenu> menus;
if (LoginHelper.isSuperAdmin(userId)) {
menus = this.lambdaQuery()
.in(AppMenu::getMenuType, SystemConstants.TYPE_DIR, SystemConstants.TYPE_MENU)
.eq(AppMenu::getStatus, SystemConstants.NORMAL)
.orderByAsc(AppMenu::getParentId)
.orderByAsc(AppMenu::getOrderNum)
.list();
} else {
menus = baseMapper.selectMenuTreeByUserId(userId);
}
return getChildPerms(menus, 0);
}
/** /**
* 新增app菜单 * 新增app菜单
* *
@ -48,6 +90,23 @@ public class AppMenuServiceImpl extends ServiceImpl<AppMenuMapper, AppMenu>
return null; return null;
} }
/**
* 校验app菜单名称唯一
*
* @param menu app菜单
* @return 是否唯一
*/
@Override
public Boolean checkMenuNameUnique(AppMenu menu) {
boolean exists = this.exists(
new LambdaQueryWrapper<AppMenu>()
.eq(AppMenu::getMenuName, menu.getMenuName())
.eq(AppMenu::getParentId, menu.getParentId())
.ne(ObjectUtil.isNotNull(menu.getId()), AppMenu::getId, menu.getId())
);
return !exists;
}
/** /**
* 获取app菜单视图 * 获取app菜单视图
* *
@ -88,29 +147,53 @@ public class AppMenuServiceImpl extends ServiceImpl<AppMenuMapper, AppMenu>
} }
/** /**
* 获取app菜单分页对象视图 * 获取app菜单列表对象视图
* *
* @param menuPage app菜单分页对象 * @param menuList app菜单列表对象
* @return app菜单分页对象视图 * @return app菜单列表对象视图
*/ */
@Override @Override
public Page<AppMenuVo> getVoPage(Page<AppMenu> menuPage) { public List<AppMenuVo> getVoList(List<AppMenu> menuList) {
List<AppMenu> menuList = menuPage.getRecords(); return menuList.stream().map(entity -> {
Page<AppMenuVo> menuVoPage = new Page<>(
menuPage.getCurrent(),
menuPage.getSize(),
menuPage.getTotal());
if (CollUtil.isEmpty(menuList)) {
return menuVoPage;
}
List<AppMenuVo> menuVoList = menuList.stream().map(entity -> {
AppMenuVo menuVo = new AppMenuVo(); AppMenuVo menuVo = new AppMenuVo();
BeanUtils.copyProperties(entity, menuVo); BeanUtils.copyProperties(entity, menuVo);
return menuVo; return menuVo;
}).toList(); }).toList();
menuVoPage.setRecords(menuVoList);
return menuVoPage;
} }
/* 根据父节点的ID获取所有子节点
*
* @param list 分类表
* @param parentId 传入的父节点ID
* @return String
*/
private List<AppMenu> getChildPerms(List<AppMenu> list, int parentId) {
List<AppMenu> returnList = new ArrayList<>();
for (AppMenu t : list) {
// 一、根据传入的某个父节点ID,遍历该父节点的所有子节点
if (t.getParentId() == parentId) {
recursionFn(list, t);
returnList.add(t);
}
}
return returnList;
}
/**
* 递归列表
*/
private void recursionFn(List<AppMenu> list, AppMenu t) {
// 得到子节点列表
List<AppMenu> childList = StreamUtils.filter(list, n -> n.getParentId().equals(t.getId()));
t.setChildren(childList);
for (AppMenu tChild : childList) {
// 判断是否有子节点
if (list.stream().anyMatch(n -> n.getParentId().equals(tChild.getId()))) {
recursionFn(list, tChild);
}
}
}
} }

View File

@ -1,7 +1,11 @@
package org.dromara.app.system.service.impl; package org.dromara.app.system.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.dromara.app.system.domain.AppRole; import org.dromara.app.system.domain.AppRole;
import org.dromara.app.system.domain.dto.role.AppRoleQueryReq;
import org.dromara.app.system.domain.vo.role.AppRoleVo;
import org.dromara.app.system.mapper.AppRoleMapper; import org.dromara.app.system.mapper.AppRoleMapper;
import org.dromara.app.system.service.AppRoleService; import org.dromara.app.system.service.AppRoleService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -15,6 +19,38 @@ import org.springframework.stereotype.Service;
public class AppRoleServiceImpl extends ServiceImpl<AppRoleMapper, AppRole> public class AppRoleServiceImpl extends ServiceImpl<AppRoleMapper, AppRole>
implements AppRoleService { implements AppRoleService {
/**
* 获取app角色视图
*
* @param menu app角色
* @return app角色视图
*/
@Override
public AppRoleVo getVo(AppRole menu) {
return null;
}
/**
* 构建查询条件封装
*
* @param req 查询条件
* @return 查询条件封装
*/
@Override
public LambdaQueryWrapper<AppRole> buildQueryWrapper(AppRoleQueryReq req) {
return null;
}
/**
* 获取app角色视图列表
*
* @param rolePage app角色列表
* @return app角色视图列表
*/
@Override
public Page<AppRoleVo> getVoList(Page<AppRole> rolePage) {
return null;
}
} }

View File

@ -31,4 +31,26 @@
icon,create_dept,remark,create_by,update_by, icon,create_dept,remark,create_by,update_by,
create_time,update_time create_time,update_time
</sql> </sql>
<select id="selectMenuTreeByUserId" parameterType="Long" resultType="org.dromara.app.system.domain.AppMenu">
select distinct m.id,
m.parent_id,
m.menu_name,
m.path,
m.component,
m.query_param,
m.visible,
m.status,
m.perms,
m.menu_type,
m.icon,
m.order_num,
m.create_time
from app_menu m
left join app_role_menu rm on m.id = rm.menu_id and m.status = '0'
left join app_role r on rm.role_id = r.id and r.status = '0'
where m.menu_type in ('M', 'C')
and r.id in (select role_id from sys_user_role where user_id = #{userId})
order by m.parent_id, m.order_num
</select>
</mapper> </mapper>

View File

@ -1,12 +1,13 @@
package org.dromara.common.utils.baiduUtil; package org.dromara.common.utils.baiduUtil;
import cn.hutool.json.JSONUtil;
import com.google.gson.Gson; import com.google.gson.Gson;
import org.dromara.common.utils.baiduUtil.entity.AccessTokenResponse; import org.dromara.common.utils.baiduUtil.entity.AccessTokenResponse;
import org.glassfish.jaxb.runtime.v2.runtime.reflect.opt.Const;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
@ -20,7 +21,7 @@ import static org.dromara.common.constant.businessConstant.REDIS_BAIDU_KEY;
* @Author 铁憨憨 * @Author 铁憨憨
* @Date 2025/7/18 9:46 * @Date 2025/7/18 9:46
* @Version 1.0 * @Version 1.0
* * <p>
* 获取百度AccessToken * 获取百度AccessToken
*/ */
@Service @Service
@ -88,7 +89,7 @@ public class BaiDuCommon {
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) { if (response.statusCode() == 200) {
AccessTokenResponse tokenResponse = gson.fromJson(response.body(), AccessTokenResponse.class); AccessTokenResponse tokenResponse = JSONUtil.toBean(response.body(), AccessTokenResponse.class);
return tokenResponse.getAccessToken(); return tokenResponse.getAccessToken();
} else { } else {
throw new IOException("获取AccessToken失败状态码: " + response.statusCode()); throw new IOException("获取AccessToken失败状态码: " + response.statusCode());

View File

@ -1,6 +1,8 @@
package org.dromara.common.utils.baiduUtil; package org.dromara.common.utils.baiduUtil;
import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.utils.baiduUtil.entity.ocr.IDCardInfo; import org.dromara.common.utils.baiduUtil.entity.ocr.IDCardInfo;
import org.dromara.common.utils.baiduUtil.entity.ocr.OcrReq; import org.dromara.common.utils.baiduUtil.entity.ocr.OcrReq;
import org.dromara.common.utils.baiduUtil.entity.ocr.Result; import org.dromara.common.utils.baiduUtil.entity.ocr.Result;
@ -9,20 +11,18 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/** /**
* @Author 铁憨憨 * @Author 铁憨憨
* @Date 2025/7/18 10:45 * @Date 2025/7/18 10:45
* @Version 1.0 * @Version 1.0
* * <p>
* 处理百度OCR相关逻辑身份证识别、银行卡识别 * 处理百度OCR相关逻辑身份证识别、银行卡识别
*/ */
@Slf4j
@Service @Service
public class BaiDuOCR { public class BaiDuOCR {
@Autowired @Autowired
@ -33,17 +33,16 @@ public class BaiDuOCR {
private ObjectMapper objectMapper; //ObjectMapper 是 Java 处理 JSON 的核心工具,项目中使用它进行 JSON 与 Java 对象的相互转换。 private ObjectMapper objectMapper; //ObjectMapper 是 Java 处理 JSON 的核心工具,项目中使用它进行 JSON 与 Java 对象的相互转换。
String baseUrlTemplate = "https://aip.baidubce.com/rest/2.0/ocr/v1/bankcard?access_token=%s"; String baseUrlTemplate = "https://aip.baidubce.com/rest/2.0/ocr/v1/bankcard";
String BASE_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard?access_token=%s"; String BASE_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/idcard";
/** /**
* @param vr 请求参数
* @return 识别结果Result
* @description 银行卡OCR识别 * @description 银行卡OCR识别
* @author 铁憨憨 * @author 铁憨憨
* @date 2025/7/18 11:50 * @date 2025/7/18 11:50
* @param vr 请求参数
* @return 识别结果Result
**/ **/
public Result bankCardOCRRecognition(OcrReq vr) { public Result bankCardOCRRecognition(OcrReq vr) {
// 先从缓存里面捞取token若为空直接抛出异常 // 先从缓存里面捞取token若为空直接抛出异常
@ -53,62 +52,41 @@ public class BaiDuOCR {
} }
try { try {
// 构建请求URL包含token参数 String param;
String requestUrl = String.format(baseUrlTemplate, URLEncoder.encode(atStr, StandardCharsets.UTF_8)); if (vr.getImage() != null) {
String imgParam = URLEncoder.encode(vr.getImage(), StandardCharsets.UTF_8);
// 准备请求体将请求参数转为JSON param = "image=" + imgParam;
String requestBody = objectMapper.writeValueAsString(vr); } else if (vr.getUrl() != null) {
if (requestBody == null) { param = "url=" + vr.getUrl();
throw new RuntimeException("请求参数序列化失败"); } else {
throw new RuntimeException("请传入图片或图片URL");
} }
// 构建HTTP请求 // 构建HTTP请求
HttpRequest request = HttpRequest.newBuilder() String resultStr = HttpUtil.post(baseUrlTemplate, atStr, param);
.uri(URI.create(requestUrl))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
// 发送请求并获取响应 log.info("百度OCR识别结果{}", resultStr);
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); Result result = JSON.parseObject(resultStr, Result.class);
// 处理响应状态码非200直接抛出异常
if (response.statusCode() != 200) {
throw new RuntimeException("接口请求失败,状态码:" + response.statusCode() + ",响应信息:" + response.body());
}
// 解析响应结果(若解析失败抛出异常)
Result result = objectMapper.readValue(response.body(), Result.class);
if (result == null) { if (result == null) {
throw new RuntimeException("响应结果解析失败"); throw new RuntimeException("未识别到银行卡信息");
} }
// // 若接口返回错误码如百度API的error_code抛出异常
// if (result.getErrorCode() != null && result.getErrorCode() != 0) {
// throw new RuntimeException("接口返回错误:" + result.getErrorMessage() + "(错误码:" + result.getErrorCode() + "");
// }
return result; return result;
} catch (IOException e) { } catch (IOException e) {
// IO异常如序列化失败、网络IO错误 // IO异常如序列化失败、网络IO错误
throw new RuntimeException("IO处理异常" + e.getMessage(), e); throw new RuntimeException("IO处理异常" + e.getMessage(), e);
} catch (InterruptedException e) { } catch (Exception e) {
// 线程中断异常 throw new RuntimeException(e);
Thread.currentThread().interrupt(); // 恢复中断状态
throw new RuntimeException("请求被中断:" + e.getMessage(), e);
} }
} }
/** /**
* @param request 请求参数
* @return 识别结果WordsResult
* @description 身份证OCR识别 * @description 身份证OCR识别
* @author 铁憨憨 * @author 铁憨憨
* @date 2025/7/18 16:53 * @date 2025/7/18 16:53
* @param request 请求参数
* @return 识别结果WordsResult
**/ **/
public WordsResult idCardOCR(OcrReq request) { public WordsResult idCardOCR(OcrReq request) {
// 获取AccessToken为空直接抛异常 // 获取AccessToken为空直接抛异常
@ -117,41 +95,28 @@ public class BaiDuOCR {
throw new RuntimeException("获取访问令牌失败token为空"); throw new RuntimeException("获取访问令牌失败token为空");
} }
// 构建请求URL
String requestUrl = String.format(BASE_URL, accessToken);
try { try {
// 准备请求体(序列化失败抛异常) // 准备请求体(序列化失败抛异常)
String requestBody = objectMapper.writeValueAsString(request); String requestBody = objectMapper.writeValueAsString(request);
if (requestBody == null) { if (requestBody == null) {
throw new RuntimeException("请求参数序列化失败"); throw new RuntimeException("请求参数序列化失败");
} }
String param = "id_card_side=" + request.getIdCardSide();
// 构建HTTP请求 if (request.getImage() != null) {
HttpRequest httpRequest = HttpRequest.newBuilder() String imgParam = URLEncoder.encode(request.getImage(), StandardCharsets.UTF_8);
.uri(URI.create(requestUrl)) param = param + "&image=" + imgParam;
.header("Content-Type", "application/x-www-form-urlencoded") } else if (request.getUrl() != null) {
.POST(HttpRequest.BodyPublishers.ofString(requestBody)) param = param + "&url=" + request.getUrl();
.build(); } else {
throw new RuntimeException("请传入图片或图片URL");
// 发送请求并获取响应
HttpResponse<String> response = httpClient.send(
httpRequest,
HttpResponse.BodyHandlers.ofString()
);
// 处理响应状态码非200直接抛异常
if (response.statusCode() != 200) {
throw new RuntimeException("接口请求失败,状态码:" + response.statusCode() + ",响应信息:" + response.body());
} }
// 构建HTTP请求
// 处理响应内容(去除空格并解析) String result = HttpUtil.post(BASE_URL, accessToken, param);
String responseBody = response.body().replaceAll("\\s+", ""); // 处理响应内容
IDCardInfo idCardInfo = objectMapper.readValue(responseBody, IDCardInfo.class); IDCardInfo idCardInfo = JSON.parseObject(result, IDCardInfo.class);
if (idCardInfo == null) { if (idCardInfo == null) {
throw new RuntimeException("响应结果解析失败"); throw new RuntimeException("响应结果解析失败");
} }
// 检查身份证状态状态异常由validateImageStatus抛异常 // 检查身份证状态状态异常由validateImageStatus抛异常
validateImageStatus(idCardInfo.getImageStatus()); validateImageStatus(idCardInfo.getImageStatus());
@ -166,20 +131,18 @@ public class BaiDuOCR {
} catch (IOException e) { } catch (IOException e) {
// IO异常序列化、网络IO等 // IO异常序列化、网络IO等
throw new RuntimeException("IO处理异常" + e.getMessage(), e); throw new RuntimeException("IO处理异常" + e.getMessage(), e);
} catch (InterruptedException e) { } catch (Exception e) {
// 线程中断异常 throw new RuntimeException(e);
Thread.currentThread().interrupt(); // 恢复中断状态
throw new RuntimeException("请求被中断:" + e.getMessage(), e);
} }
} }
/** /**
* @param imageStatus 图片状态
* @throws RuntimeException 当状态不正常时抛出运行时异常
* @description 验证身份证图片状态 * @description 验证身份证图片状态
* @author 铁憨憨 * @author 铁憨憨
* @date 2025/7/18 17:08 * @date 2025/7/18 17:08
* @param imageStatus 图片状态
* @throws RuntimeException 当状态不正常时抛出运行时异常
**/ **/
private void validateImageStatus(String imageStatus) { private void validateImageStatus(String imageStatus) {
if (imageStatus == null || "normal".equals(imageStatus)) { if (imageStatus == null || "normal".equals(imageStatus)) {

View File

@ -0,0 +1,77 @@
package org.dromara.common.utils.baiduUtil;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
/**
* http 工具类
*/
public class HttpUtil {
public static String post(String requestUrl, String accessToken, String params)
throws Exception {
String contentType = "application/x-www-form-urlencoded";
return HttpUtil.post(requestUrl, accessToken, contentType, params);
}
public static String post(String requestUrl, String accessToken, String contentType, String params)
throws Exception {
String encoding = "UTF-8";
if (requestUrl.contains("nlp")) {
encoding = "GBK";
}
return HttpUtil.post(requestUrl, accessToken, contentType, params, encoding);
}
public static String post(String requestUrl, String accessToken, String contentType, String params, String encoding)
throws Exception {
String url = requestUrl + "?access_token=" + accessToken;
return HttpUtil.postGeneralUrl(url, contentType, params, encoding);
}
public static String postGeneralUrl(String generalUrl, String contentType, String params, String encoding)
throws Exception {
URL url = new URL(generalUrl);
// 打开和URL之间的连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
// 设置通用的请求属性
connection.setRequestProperty("Content-Type", contentType);
connection.setRequestProperty("Connection", "Keep-Alive");
connection.setUseCaches(false);
connection.setDoOutput(true);
connection.setDoInput(true);
// 得到请求的输出流对象
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
out.write(params.getBytes(encoding));
out.flush();
out.close();
// 建立实际的连接
connection.connect();
// 获取所有响应头字段
Map<String, List<String>> headers = connection.getHeaderFields();
// 遍历所有的响应头字段
for (String key : headers.keySet()) {
System.err.println(key + "--->" + headers.get(key));
}
// 定义 BufferedReader输入流来读取URL的响应
BufferedReader in = null;
in = new BufferedReader(
new InputStreamReader(connection.getInputStream(), encoding));
String result = "";
String getLine;
while ((getLine = in.readLine()) != null) {
result += getLine;
}
in.close();
System.err.println("result:" + result);
return result;
}
}

View File

@ -1,10 +1,11 @@
package org.dromara.common.utils.baiduUtil.entity; package org.dromara.common.utils.baiduUtil.entity;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.io.Serial;
import java.io.Serializable;
/** /**
* @Author 铁憨憨 * @Author 铁憨憨
* @Date 2025/7/18 9:56 * @Date 2025/7/18 9:56
@ -12,7 +13,10 @@ import lombok.experimental.Accessors;
*/ */
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
public class AccessTokenResponse { public class AccessTokenResponse implements Serializable {
@Serial
private static final long serialVersionUID = 5429166046159819095L;
private String accessToken; private String accessToken;
private int expiresIn; private int expiresIn;
private String scope; private String scope;

View File

@ -1,13 +1,14 @@
package org.dromara.common.utils.baiduUtil.entity.ocr; package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson2.annotation.JSONField; import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data; import lombok.Data;
import org.dromara.system.domain.vo.SysOssVo;
/** /**
* @Author 铁憨憨 * @Author 铁憨憨
* @Date 2025/7/18 10:58 * @Date 2025/7/18 10:58
* @Version 1.0 * @Version 1.0
* * <p>
* 银行卡具体信息 * 银行卡具体信息
*/ */
@Data @Data
@ -36,4 +37,9 @@ public class BankData {
@JSONField(name = "holder_name", label = "持卡人姓名,不能识别时为空") @JSONField(name = "holder_name", label = "持卡人姓名,不能识别时为空")
private String holderName; private String holderName;
/**
* 银行卡图片信息
*/
private SysOssVo image;
} }

View File

@ -1,5 +1,6 @@
package org.dromara.common.utils.baiduUtil.entity.ocr; package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data; import lombok.Data;
/** /**
@ -11,6 +12,8 @@ import lombok.Data;
*/ */
@Data @Data
public class Field { public class Field {
@JSONField(name = "location")
private Location location; // 位置信息 private Location location; // 位置信息
@JSONField(name = "words")
private String words; // 识别文字 private String words; // 识别文字
} }

View File

@ -1,6 +1,6 @@
package org.dromara.common.utils.baiduUtil.entity.ocr; package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson2.annotation.JSONField; import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data; import lombok.Data;
/** /**

View File

@ -1,18 +1,26 @@
package org.dromara.common.utils.baiduUtil.entity.ocr; package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data; import lombok.Data;
/** /**
* @Author 铁憨憨 * @Author 铁憨憨
* @Date 2025/7/18 10:56 * @Date 2025/7/18 10:56
* @Version 1.0 * @Version 1.0
* * <p>
* 位置信息(坐标) * 位置信息(坐标)
*/ */
@Data @Data
public class Location { public class Location {
private int top; // 顶部坐标 @JSONField(name = "top")
private int left; // 左侧坐标 private int top;
private int width; // 宽度
private int height; // 高度 @JSONField(name = "left")
private int left;
@JSONField(name = "width")
private int width;
@JSONField(name = "height")
private int height;
} }

View File

@ -1,24 +1,30 @@
package org.dromara.common.utils.baiduUtil.entity.ocr; package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson2.annotation.JSONField; import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data; import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/** /**
* @Author 铁憨憨 * @Author 铁憨憨
* @Date 2025/7/18 10:47 * @Date 2025/7/18 10:47
* @Version 1.0 * @Version 1.0
* * <p>
* OCR请求参数身份证/银行卡通用) * OCR请求参数身份证/银行卡通用)
*/ */
@Data @Data
public class OcrReq { public class OcrReq implements Serializable {
@Serial
private static final long serialVersionUID = -8670697823104813789L;
private String image; // 图像base64与url二选一 private String image; // 图像base64与url二选一
private String url; // 图像URL与image二选一 private String url; // 图像URL与image二选一
@JSONField(name = "id_card_side") @JsonProperty("id_card_side")
private String idCardSide; // 身份证正反面front/back银行卡无需 private String idCardSide; // 身份证正反面front/back银行卡无需
@JSONField(name = "detect_photo") @JsonProperty("detect_photo")
private boolean detectPhoto;// 是是否开启银行卡质量类型(清晰模糊、边框/四角不完整检测功能默认不开启false。 private boolean detectPhoto;// 是是否开启银行卡质量类型(清晰模糊、边框/四角不完整检测功能默认不开启false。
} }

View File

@ -1,6 +1,6 @@
package org.dromara.common.utils.baiduUtil.entity.ocr; package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson2.annotation.JSONField; import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data; import lombok.Data;
/** /**

View File

@ -1,33 +1,38 @@
package org.dromara.common.utils.baiduUtil.entity.ocr; package org.dromara.common.utils.baiduUtil.entity.ocr;
import com.alibaba.fastjson2.annotation.JSONField; import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data; import lombok.Data;
import org.dromara.system.domain.vo.SysOssVo;
/** /**
* @Author 铁憨憨 * @Author 铁憨憨
* @Date 2025/7/18 10:50 * @Date 2025/7/18 10:50
* @Version 1.0 * @Version 1.0
* * <p>
* 身份证识别字段集合 * 身份证识别字段集合
*/ */
@Data @Data
public class WordsResult { public class WordsResult {
@JSONField(name = "姓名") @JSONField(name = "姓名")
private Field name; // 姓名 private Field name;
@JSONField(name = "民族") @JSONField(name = "民族")
private Field nation; // 民族 private Field nation;
@JSONField(name = "住址") @JSONField(name = "住址")
private Field address; // 住址 private Field address;
@JSONField(name = "公民身份号码") @JSONField(name = "公民身份号码")
private Field citizenIdentification; // 身份证号 private Field citizenIdentification;
@JSONField(name = "出生") @JSONField(name = "出生")
private Field birth; // 出生日期 private Field birth;
@JSONField(name = "性别") @JSONField(name = "性别")
private Field gender; // 性别 private Field gender;
@JSONField(name = "失效日期")
private Field expirationDate; // 失效日期 /**
@JSONField(name = "签发机关") * 身份证图片信息
private Field issuingAuthority; // 签发机关 */
@JSONField(name = "签发日期") private SysOssVo image;
private Field issueDate; // 签发日期
} }

View File

@ -1,40 +0,0 @@
package org.dromara.contractor.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.web.core.BaseController;
import org.dromara.contractor.domain.dto.constructionuser.AppRealNameAuthenticationReq;
import org.dromara.contractor.domain.dto.constructionuser.SubConstructionUserUpdateClockReq;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* @Author 铁憨憨
* @Date 2025/7/23 16:48
* @Version 1.0
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/contractor/app")
public class AppController extends BaseController {
private final ISubConstructionUserService constructionUserService;
@Log(title = "施工人员-实名认证", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/realNameAuthentication")
public R<Void> realNameAuthentication(@RequestBody AppRealNameAuthenticationReq req) {
// return toAjax(constructionUserService.updateClock(req));
return toAjax(constructionUserService.realNameAuthentication(req));
}
}

View File

@ -0,0 +1,77 @@
package org.dromara.contractor.controller.app;
import jakarta.annotation.Resource;
import org.dromara.common.core.domain.R;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.utils.baiduUtil.entity.ocr.BankData;
import org.dromara.common.utils.baiduUtil.entity.ocr.WordsResult;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.domain.dto.constructionuser.SubConstructionUserAuthenticationReq;
import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserVo;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 施工人员app
*
* @author lilemy
* @date 2025-07-23 18:44
*/
@Validated
@RestController
@RequestMapping("/app/contractor/constructionUser")
public class SubConstructionUserAppController {
@Resource
private ISubConstructionUserService constructionUserService;
/**
* 获取当前登录用户实名信息
*/
@GetMapping("/loginUser")
public R<SubConstructionUserVo> getLoginUserInfo() {
SubConstructionUser constructionUser = constructionUserService.getBySysUserId(LoginHelper.getUserId());
return R.ok(constructionUserService.getVo(constructionUser));
}
/**
* 根据身份证图片获取身份证信息
*/
@Log(title = "施工人员", businessType = BusinessType.OTHER)
@PostMapping("/idCard")
public R<WordsResult> getIdCardMessage(@RequestParam("file") MultipartFile file, String idCardSide) {
return R.ok(constructionUserService.getIdCardMessageByPic(file, idCardSide));
}
/**
* 根据银行卡图片获取银行卡信息
*/
@Log(title = "施工人员", businessType = BusinessType.OTHER)
@PostMapping("/bankCard")
public R<BankData> getBankCardMessage(@RequestParam("file") MultipartFile file) {
return R.ok(constructionUserService.getBankCardMessageByPic(file));
}
/**
* 施工人员实名认证
*/
@Log(title = "施工人员", businessType = BusinessType.INSERT)
@PostMapping("/authentication")
public R<Long> insertByAuthentication(@RequestParam("file") MultipartFile file, @Validated SubConstructionUserAuthenticationReq req) {
return R.ok(constructionUserService.insertByAuthentication(file, req));
}
/**
* 施工人员人脸比对
*/
@Log(title = "施工人员", businessType = BusinessType.OTHER)
@PostMapping("/face/comparison")
public R<Boolean> faceComparison(@RequestParam("file") MultipartFile file) {
return R.ok(constructionUserService.faceComparison(file));
}
}

View File

@ -34,6 +34,11 @@ public class SubConstructionUser extends BaseEntity {
*/ */
private String facePic; private String facePic;
/**
* 系统用户id
*/
private Long userId;
/** /**
* 人员姓名 * 人员姓名
*/ */

View File

@ -0,0 +1,150 @@
package org.dromara.contractor.domain.dto.constructionuser;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.dromara.common.core.constant.RegexConstants;
import java.io.Serial;
import java.io.Serializable;
/**
* @author lilemy
* @date 2025-07-24 10:48
*/
@Data
public class SubConstructionUserAuthenticationReq implements Serializable {
@Serial
private static final long serialVersionUID = 1001024234468070802L;
/**
* 人员姓名
*/
@NotBlank(message = "人员姓名不能为空")
private String userName;
/**
* 项目id
*/
@NotNull(message = "项目id不能为空")
private Long projectId;
/**
* 分包公司id
*/
@NotNull(message = "分包公司id不能为空")
private Long contractorId;
/**
* 联系电话
*/
@NotBlank(message = "联系电话不能为空")
@Pattern(regexp = RegexConstants.MOBILE, message = "手机号格式不正确")
private String phone;
/**
* 0:保密 1:男 2女
*/
@NotBlank(message = "性别不能为空")
private String sex;
/**
* 民族
*/
@NotBlank(message = "民族不能为空")
private String nation;
/**
* 身份证正面图片
*/
@NotBlank(message = "身份证正面图片不能为空")
private String sfzFrontPic;
/**
* 身份证反面图片
*/
@NotBlank(message = "身份证反面图片不能为空")
private String sfzBackPic;
/**
* 身份证号码
*/
@NotBlank(message = "身份证号码不能为空")
private String sfzNumber;
/**
* 身份证有效开始期
*/
@NotBlank(message = "身份证有效开始期不能为空")
private String sfzStart;
/**
* 身份证有效结束期
*/
@NotBlank(message = "身份证有效结束期不能为空")
private String sfzEnd;
/**
* 身份证地址
*/
@NotBlank(message = "身份证地址不能为空")
private String sfzSite;
/**
* 身份证出生日期
*/
@NotBlank(message = "身份证出生日期不能为空")
private String sfzBirth;
/**
* 籍贯
*/
@NotBlank(message = "籍贯不能为空")
private String nativePlace;
/**
* 银行卡图片
*/
@NotBlank(message = "银行卡图片不能为空")
private String yhkPic;
/**
* 银行卡号
*/
@NotBlank(message = "银行卡号不能为空")
private String yhkNumber;
/**
* 开户行
*/
private String yhkOpeningBank;
/**
* 持卡人
*/
private String yhkCardholder;
/**
* 工种
*/
@NotBlank(message = "工种不能为空")
private String typeOfWork;
/**
* 工资计量单位
*/
private String wageMeasureUnit;
/**
* 特种工作证图片
*/
private String specialWorkPic;
/**
* 备注
*/
private String remark;
}

View File

@ -0,0 +1,24 @@
package org.dromara.contractor.domain.enums;
import lombok.Getter;
/**
* @author lilemy
* @date 2025-07-24 15:52
*/
@Getter
public enum SubConstructionUserRoleEnum {
NORMAL("普通用户", "1"),
ADMIN("管理员", "2");
private final String text;
private final String value;
SubConstructionUserRoleEnum(String text, String value) {
this.text = text;
this.value = value;
}
}

View File

@ -5,14 +5,16 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.utils.baiduUtil.entity.ocr.BankData;
import org.dromara.common.utils.baiduUtil.entity.ocr.WordsResult;
import org.dromara.contractor.domain.SubConstructionUser; import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.domain.dto.constructionuser.*; import org.dromara.contractor.domain.dto.constructionuser.*;
import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserVo;
import org.dromara.contractor.domain.exportvo.BusConstructionUserExportVo; import org.dromara.contractor.domain.exportvo.BusConstructionUserExportVo;
import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserAttendanceMonthVo; import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserAttendanceMonthVo;
import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserAttendanceTotalVo; import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserAttendanceTotalVo;
import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserGisVo; import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserGisVo;
import org.springframework.web.bind.annotation.RequestBody; import org.dromara.contractor.domain.vo.constructionuser.SubConstructionUserVo;
import org.springframework.web.multipart.MultipartFile;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -174,9 +176,46 @@ public interface ISubConstructionUserService extends IService<SubConstructionUse
PageQuery pageQuery); PageQuery pageQuery);
/** /**
* @description app 实名认证 * 获取施工人员身份证信息
* @author 铁憨憨 *
* @date 2025/7/23 16:59 * @param file 图片文件
**/ * @param idCardSide 身份证正反面front/back
Boolean realNameAuthentication(@RequestBody AppRealNameAuthenticationReq req); * @return 身份证信息
*/
WordsResult getIdCardMessageByPic(MultipartFile file, String idCardSide);
/**
* 获取施工人员银行卡信息
*
* @param file 图片文件
* @return 银行卡信息
*/
BankData getBankCardMessageByPic(MultipartFile file);
/**
* 实名认证
*
* @param file 人脸图片
* @param req 身份信息认证对象
* @return 是否认证成功
*/
Long insertByAuthentication(MultipartFile file, SubConstructionUserAuthenticationReq req);
/**
* 人脸识别
*
* @param file 图片文件
* @return 是否匹配成功
*/
Boolean faceComparison(MultipartFile file);
/**
* 根据系统用户id查询施工人员
*
* @param sysUserId 系统用户id
* @return 施工人员
*/
SubConstructionUser getBySysUserId(Long sysUserId);
} }

View File

@ -1,6 +1,7 @@
package org.dromara.contractor.service.impl; package org.dromara.contractor.service.impl;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.DesensitizedUtil; import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.IdcardUtil; import cn.hutool.core.util.IdcardUtil;
import cn.hutool.core.util.PhoneUtil; import cn.hutool.core.util.PhoneUtil;
@ -18,8 +19,17 @@ import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.oss.core.OssClient;
import org.dromara.common.oss.exception.OssException;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.utils.IdCardEncryptorUtil; import org.dromara.common.utils.IdCardEncryptorUtil;
import org.dromara.common.utils.baiduUtil.BaiDuFace;
import org.dromara.common.utils.baiduUtil.BaiDuOCR;
import org.dromara.common.utils.baiduUtil.entity.face.HumanFaceReq;
import org.dromara.common.utils.baiduUtil.entity.ocr.BankData;
import org.dromara.common.utils.baiduUtil.entity.ocr.OcrReq;
import org.dromara.common.utils.baiduUtil.entity.ocr.WordsResult;
import org.dromara.contractor.constant.SubConstructionUserConstant; import org.dromara.contractor.constant.SubConstructionUserConstant;
import org.dromara.contractor.domain.SubConstructionUser; import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.domain.SubConstructionUserFile; import org.dromara.contractor.domain.SubConstructionUserFile;
@ -48,7 +58,12 @@ import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.YearMonth; import java.time.YearMonth;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -99,6 +114,12 @@ public class SubConstructionUserServiceImpl extends ServiceImpl<SubConstructionU
@Resource @Resource
private IdCardEncryptorUtil idCardEncryptorUtil; private IdCardEncryptorUtil idCardEncryptorUtil;
@Resource
private BaiDuOCR baiDuOCR;
@Resource
private BaiDuFace baiDuFace;
/** /**
* 查询施工人员 * 查询施工人员
* *
@ -957,13 +978,192 @@ public class SubConstructionUserServiceImpl extends ServiceImpl<SubConstructionU
} }
/** /**
* @description app 实名认证 * 获取施工人员身份证信息
* @author 铁憨憨 *
* @date 2025/7/23 17:00 * @param file 图片文件
**/ * @param idCardSide 身份证正反面front/back
* @return 身份证信息
*/
@Override @Override
public Boolean realNameAuthentication(AppRealNameAuthenticationReq req) { public WordsResult getIdCardMessageByPic(MultipartFile file, String idCardSide) {
return null; if (file == null) {
throw new ServiceException("请上传图片", HttpStatus.BAD_REQUEST);
}
if (StringUtils.isBlank(idCardSide)) {
throw new ServiceException("请选择身份证正反面", HttpStatus.BAD_REQUEST);
}
if (!"front".equals(idCardSide) && !"back".equals(idCardSide)) {
throw new ServiceException("请选择正确的身份证正反面", HttpStatus.BAD_REQUEST);
}
String base64;
try {
// 获取文件字节数组
byte[] bytes = file.getBytes();
// Base64 编码
base64 = Base64.getEncoder().encodeToString(bytes);
} catch (IOException e) {
throw new ServiceException("图片转换失败,请重新上传");
}
OcrReq request = new OcrReq();
request.setImage(base64);
request.setIdCardSide(idCardSide);
WordsResult wordsResult = baiDuOCR.idCardOCR(request);
if (wordsResult == null) {
throw new ServiceException("识别失败,请重新上传");
}
// 获取数据成功,保存图片信息
SysOssVo upload = ossService.upload(file);
wordsResult.setImage(upload);
return wordsResult;
}
/**
* 获取施工人员银行卡信息
*
* @param file 图片文件
* @return 银行卡信息
*/
@Override
public BankData getBankCardMessageByPic(MultipartFile file) {
if (file == null) {
throw new ServiceException("请上传图片", HttpStatus.BAD_REQUEST);
}
String base64;
try {
// 获取文件字节数组
byte[] bytes = file.getBytes();
// Base64 编码
base64 = Base64.getEncoder().encodeToString(bytes);
} catch (IOException e) {
throw new ServiceException("图片转换失败,请重新上传");
}
OcrReq request = new OcrReq();
request.setImage(base64);
request.setDetectPhoto(false);
BankData res = baiDuOCR.bankCardOCRRecognition(request).getRes();
if (res == null) {
throw new ServiceException("识别失败,请重新上传");
}
// 获取数据成功,保存图片信息
SysOssVo upload = ossService.upload(file);
res.setImage(upload);
return res;
}
/**
* 实名认证
*
* @param file 人脸图片
* @param req 身份信息认证对象
* @return 是否认证成功
*/
@Override
public Long insertByAuthentication(MultipartFile file, SubConstructionUserAuthenticationReq req) {
// 先进行人脸识别
if (file == null) {
throw new ServiceException("请上传图片", HttpStatus.BAD_REQUEST);
}
String base64;
try {
// 获取文件字节数组
byte[] bytes = file.getBytes();
// Base64 编码
base64 = URLEncoder.encode(Base64.getEncoder().encodeToString(bytes), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new ServiceException("图片转换失败,请重新上传");
}
HumanFaceReq request = new HumanFaceReq();
request.setImage(base64);
baiDuFace.humanFace(request);
// 人脸识别成功,保存人脸数据
SubConstructionUser user = new SubConstructionUser();
BeanUtils.copyProperties(req, user);
SysOssVo upload = ossService.upload(file);
user.setFacePic(upload.getOssId().toString());
// 关联系统用户
Long userId = LoginHelper.getUserId();
// 判断当前系统用户是否已关联
Long count = this.lambdaQuery()
.eq(SubConstructionUser::getUserId, userId)
.count();
if (count > 0) {
throw new ServiceException("当前系统用户已关联施工人员信息");
}
user.setUserId(userId);
// 保存施工人员信息
boolean save = this.save(user);
if (!save) {
throw new ServiceException("施工人员信息保存失败");
}
return user.getId();
}
/**
* 人脸识别
*
* @param file 图片文件
* @return 是否匹配成功
*/
@Override
public Boolean faceComparison(MultipartFile file) {
if (file == null) {
throw new ServiceException("请上传图片", HttpStatus.BAD_REQUEST);
}
String reqBase64;
try {
// 获取文件字节数组
byte[] bytes = file.getBytes();
// Base64 编码
reqBase64 = URLEncoder.encode(Base64.getEncoder().encodeToString(bytes), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new ServiceException("图片转换失败,请重新上传");
}
HumanFaceReq request = new HumanFaceReq();
request.setImage(reqBase64);
Long userId = LoginHelper.getUserId();
SubConstructionUser constructionUser = this.getById(userId);
if (constructionUser == null || StringUtils.isBlank(constructionUser.getFacePic())) {
throw new ServiceException("未进行实名认证");
}
String facePic = constructionUser.getFacePic();
SysOssVo sysOssVo = ossService.getById(Long.parseLong(facePic));
if (sysOssVo == null) {
throw new ServiceException("未进行实名认证");
}
// 获取文件输入流
OssClient storage = OssFactory.instance(sysOssVo.getService());
String path = sysOssVo.getUrl();
String faceBase64;
try (InputStream is = storage.getObjectContent(path)) {
byte[] bytes = IoUtil.readBytes(is);
// Base64 编码
faceBase64 = URLEncoder.encode(Base64.getEncoder().encodeToString(bytes), StandardCharsets.UTF_8);
} catch (IOException e) {
// 针对单个文件处理异常,可以选择记录日志或终止处理
throw new OssException("处理文件[" + path + "]失败,错误信息: " + e.getMessage());
}
HumanFaceReq faceReq = new HumanFaceReq();
faceReq.setImage(faceBase64);
List<HumanFaceReq> list = List.of(request, faceReq);
double comparison = baiDuFace.comparison(list);
return comparison > 80;
}
/**
* 根据系统用户id查询施工人员
*
* @param sysUserId 系统用户id
* @return 施工人员
*/
@Override
public SubConstructionUser getBySysUserId(Long sysUserId) {
SubConstructionUser constructionUser = this.lambdaQuery()
.eq(SubConstructionUser::getUserId, sysUserId)
.one();
if (constructionUser == null) {
throw new ServiceException("实名认证信息不存在", HttpStatus.NOT_FOUND);
}
return constructionUser;
} }
} }

View File

@ -0,0 +1,61 @@
package org.dromara.project.controller.app;
import jakarta.annotation.Resource;
import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.dromara.project.domain.dto.attendance.BusAttendanceByDayReq;
import org.dromara.project.domain.dto.attendance.BusAttendanceByMonthReq;
import org.dromara.project.domain.dto.attendance.BusAttendanceMonthByUserIdReq;
import org.dromara.project.domain.vo.attendance.BusAttendanceMonthByUserIdVo;
import org.dromara.project.domain.vo.attendance.BusAttendanceVo;
import org.dromara.project.service.IBusAttendanceService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 考勤app
*
* @author lilemy
* @date 2025-07-24 11:53
*/
@Validated
@RestController
@RequestMapping("/app/project/attendance")
public class BusAttendanceAppController {
@Resource
private IBusAttendanceService attendanceService;
@Resource
private ISubConstructionUserService constructionUserService;
/**
* 获取当前登录用户的考勤列表
*
* @param req 查询参数
* @return 考勤列表
*/
@GetMapping("/list/loginUser")
public R<List<BusAttendanceVo>> listLoginUser(BusAttendanceByDayReq req) {
return R.ok(attendanceService.getDayByUserId(LoginHelper.getUserId(), req.getClockDate()));
}
/**
* 获取当前登录用户月份考勤列表
*/
@GetMapping("/list/month/loginUser")
public R<List<BusAttendanceMonthByUserIdVo>> listAttendanceMonthListByUserId(BusAttendanceByMonthReq req) {
Long userId = LoginHelper.getUserId();
SubConstructionUser constructionUser = constructionUserService.getBySysUserId(userId);
BusAttendanceMonthByUserIdReq dto = new BusAttendanceMonthByUserIdReq();
dto.setUserId(constructionUser.getId());
dto.setClockMonth(req.getClockMonth());
return R.ok(attendanceService.listAttendanceMonthListByUserId(dto));
}
}

View File

@ -0,0 +1,42 @@
package org.dromara.project.controller.app;
import jakarta.annotation.Resource;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.dromara.project.domain.dto.leave.BusLeaveQueryReq;
import org.dromara.project.domain.vo.leave.BusLeaveVo;
import org.dromara.project.service.IBusLeaveService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lilemy
* @date 2025-07-24 14:51
*/
@Validated
@RestController
@RequestMapping("/app/project/leave")
public class BusLeaveAppController {
@Resource
private IBusLeaveService leaveService;
@Resource
private ISubConstructionUserService constructionUserService;
/**
* 查询当前登录用户请假申请列表
*/
@GetMapping("/list/loginUser")
public TableDataInfo<BusLeaveVo> listByLoginUser(BusLeaveQueryReq req, PageQuery pageQuery) {
SubConstructionUser constructionUser = constructionUserService.getBySysUserId(LoginHelper.getUserId());
req.setUserId(constructionUser.getId());
return leaveService.queryPageList(req, pageQuery);
}
}

View File

@ -0,0 +1,33 @@
package org.dromara.project.controller.app;
import jakarta.annotation.Resource;
import org.dromara.common.core.domain.R;
import org.dromara.project.domain.vo.project.BusProjectContractorListVo;
import org.dromara.project.service.IBusProjectService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author lilemy
* @date 2025-07-24 15:14
*/
@Validated
@RestController
@RequestMapping("/app/project/project")
public class BusProjectAppController {
@Resource
private IBusProjectService projectService;
/**
* 查询项目以及项目下的分包公司列表
*/
@GetMapping("/list/project/contractorList")
public R<List<BusProjectContractorListVo>> listProjectContractorList() {
return R.ok(projectService.queryProjectContractorList());
}
}

View File

@ -0,0 +1,41 @@
package org.dromara.project.controller.app;
import jakarta.annotation.Resource;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.dromara.project.domain.dto.reissuecard.BusReissueCardQueryReq;
import org.dromara.project.domain.vo.reissuecard.BusReissueCardVo;
import org.dromara.project.service.IBusReissueCardService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lilemy
* @date 2025-07-24 14:51
*/
@Validated
@RestController
@RequestMapping("/app/project/reissueCard")
public class BusReissueCardAppController {
@Resource
private IBusReissueCardService reissueCardService;
@Resource
private ISubConstructionUserService constructionUserService;
/**
* 查询当前登录用户补卡申请列表
*/
@GetMapping("/list/loginUser")
public TableDataInfo<BusReissueCardVo> listByLoginUser(BusReissueCardQueryReq req, PageQuery pageQuery) {
SubConstructionUser constructionUser = constructionUserService.getBySysUserId(LoginHelper.getUserId());
req.setUserId(constructionUser.getId());
return reissueCardService.queryPageList(req, pageQuery);
}
}

View File

@ -60,7 +60,7 @@ public class BusAttendance extends BaseEntity {
private Date clockDate; private Date clockDate;
/** /**
* 打卡状态(1正常,2迟到,3早退,4缺勤,5补卡) * 打卡状态
*/ */
private String clockStatus; private String clockStatus;

View File

@ -124,6 +124,11 @@ public class BusLeave extends BaseEntity {
*/ */
private Long teamId; private Long teamId;
/**
* 分包公司id
*/
private Long contractorId;
/** /**
* 备注 * 备注
*/ */

View File

@ -0,0 +1,25 @@
package org.dromara.project.domain.dto.attendance;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* @author lilemy
* @date 2025-07-24 12:28
*/
@Data
public class BusAttendanceByDayReq implements Serializable {
@Serial
private static final long serialVersionUID = 1800816047235448533L;
/**
* 打卡日期
*/
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date clockDate;
}

View File

@ -0,0 +1,22 @@
package org.dromara.project.domain.dto.attendance;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author lilemy
* @date 2025-07-24 13:51
*/
@Data
public class BusAttendanceByMonthReq implements Serializable {
@Serial
private static final long serialVersionUID = 4257724767091558549L;
/**
* 打卡月份
*/
private String clockMonth;
}

View File

@ -0,0 +1,41 @@
package org.dromara.project.domain.dto.leave;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author lilemy
* @date 2025-07-24 15:28
*/
@Data
public class BusLeaveGangerReviewReq implements Serializable {
@Serial
private static final long serialVersionUID = 6403690469809708710L;
/**
* 主键id
*/
@NotNull(message = "主键不能为空")
private Long id;
/**
* 管理员意见1未读 2同意 3拒绝
*/
@NotNull(message = "管理员意见不能为空")
private String gangerOpinion;
/**
* 管理员说明
*/
private String gangerExplain;
/**
* 备注
*/
private String remark;
}

View File

@ -89,6 +89,11 @@ public class BusLeaveVo implements Serializable {
*/ */
private Long projectId; private Long projectId;
/**
* 分包公司id
*/
private Long contractorId;
/** /**
* 班组长id * 班组长id
*/ */

View File

@ -13,6 +13,7 @@ import org.dromara.project.domain.vo.attendance.BusAttendanceClockDateForTwoWeek
import org.dromara.project.domain.vo.attendance.BusAttendanceMonthByUserIdVo; import org.dromara.project.domain.vo.attendance.BusAttendanceMonthByUserIdVo;
import org.dromara.project.domain.vo.attendance.BusAttendanceVo; import org.dromara.project.domain.vo.attendance.BusAttendanceVo;
import java.util.Date;
import java.util.List; import java.util.List;
/** /**
@ -96,4 +97,13 @@ public interface IBusAttendanceService extends IService<BusAttendance> {
*/ */
Page<BusAttendanceVo> getVoPage(Page<BusAttendance> attendancePage); Page<BusAttendanceVo> getVoPage(Page<BusAttendance> attendancePage);
/**
* 根据系统用户id和日期查询考勤
*
* @param userId 用户id
* @param clockDate 日期
* @return 考勤
*/
List<BusAttendanceVo> getDayByUserId(Long userId, Date clockDate);
} }

View File

@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.project.domain.BusLeave; import org.dromara.project.domain.BusLeave;
import org.dromara.project.domain.dto.leave.BusLeaveGangerReviewReq;
import org.dromara.project.domain.dto.leave.BusLeaveManagerReviewReq; import org.dromara.project.domain.dto.leave.BusLeaveManagerReviewReq;
import org.dromara.project.domain.dto.leave.BusLeaveQueryReq; import org.dromara.project.domain.dto.leave.BusLeaveQueryReq;
import org.dromara.project.domain.vo.leave.BusLeaveVo; import org.dromara.project.domain.vo.leave.BusLeaveVo;
@ -46,6 +47,14 @@ public interface IBusLeaveService extends IService<BusLeave> {
*/ */
List<BusLeaveVo> queryList(BusLeaveQueryReq req); List<BusLeaveVo> queryList(BusLeaveQueryReq req);
/**
* 班长审核施工人员请假申请
*
* @param req 班长审核施工人员请假申请
* @return 是否审核成功
*/
Boolean gangerReview(BusLeaveGangerReviewReq req);
/** /**
* 管理员审核施工人员请假申请 * 管理员审核施工人员请假申请
* *

View File

@ -19,18 +19,21 @@ import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.service.ISubConstructionUserService; import org.dromara.contractor.service.ISubConstructionUserService;
import org.dromara.project.domain.BusAttendance; import org.dromara.project.domain.BusAttendance;
import org.dromara.project.domain.BusProjectTeamMember; import org.dromara.project.domain.BusProjectTeamMember;
import org.dromara.project.domain.enums.BusAttendanceClockStatusEnum;
import org.dromara.project.domain.enums.BusAttendanceCommuterEnum;
import org.dromara.project.domain.enums.BusAttendanceStatusEnum;
import org.dromara.project.domain.dto.attendance.BusAttendanceMonthByUserIdReq; import org.dromara.project.domain.dto.attendance.BusAttendanceMonthByUserIdReq;
import org.dromara.project.domain.dto.attendance.BusAttendanceQueryReq; import org.dromara.project.domain.dto.attendance.BusAttendanceQueryReq;
import org.dromara.project.domain.dto.attendance.BusAttendanceQueryTwoWeekReq; import org.dromara.project.domain.dto.attendance.BusAttendanceQueryTwoWeekReq;
import org.dromara.project.domain.enums.BusAttendanceClockStatusEnum;
import org.dromara.project.domain.enums.BusAttendanceCommuterEnum;
import org.dromara.project.domain.enums.BusAttendanceStatusEnum;
import org.dromara.project.domain.vo.attendance.BusAttendanceClockDateForTwoWeekVo; import org.dromara.project.domain.vo.attendance.BusAttendanceClockDateForTwoWeekVo;
import org.dromara.project.domain.vo.attendance.BusAttendanceListByDay; import org.dromara.project.domain.vo.attendance.BusAttendanceListByDay;
import org.dromara.project.domain.vo.attendance.BusAttendanceMonthByUserIdVo; import org.dromara.project.domain.vo.attendance.BusAttendanceMonthByUserIdVo;
import org.dromara.project.domain.vo.attendance.BusAttendanceVo; import org.dromara.project.domain.vo.attendance.BusAttendanceVo;
import org.dromara.project.mapper.BusAttendanceMapper; import org.dromara.project.mapper.BusAttendanceMapper;
import org.dromara.project.service.*; import org.dromara.project.service.IBusAttendanceService;
import org.dromara.project.service.IBusConstructionBlacklistService;
import org.dromara.project.service.IBusProjectService;
import org.dromara.project.service.IBusProjectTeamMemberService;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -404,4 +407,24 @@ public class BusAttendanceServiceImpl extends ServiceImpl<BusAttendanceMapper, B
return attendanceVoPage; return attendanceVoPage;
} }
/**
* 根据系统用户id和日期查询考勤
*
* @param userId 系统用户id
* @param clockDate 日期
* @return 考勤
*/
@Override
public List<BusAttendanceVo> getDayByUserId(Long userId, Date clockDate) {
SubConstructionUser constructionUser = constructionUserService.getBySysUserId(userId);
// 当日期未指定时,默认为今天
if (clockDate == null) {
clockDate = DateUtils.parseDateTime(FormatsType.YYYY_MM_DD, DateUtils.getDate());
}
LambdaQueryWrapper<BusAttendance> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(BusAttendance::getUserId, constructionUser.getId())
.eq(BusAttendance::getClockDate, clockDate);
return this.list(queryWrapper).stream().map(this::getVo).toList();
}
} }

View File

@ -12,19 +12,24 @@ import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.StringUtils; import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.contractor.domain.SubConstructionUser;
import org.dromara.contractor.domain.enums.SubConstructionUserRoleEnum;
import org.dromara.contractor.service.ISubConstructionUserService;
import org.dromara.contractor.service.ISubContractorService;
import org.dromara.project.domain.BusAttendance; import org.dromara.project.domain.BusAttendance;
import org.dromara.project.domain.BusLeave; import org.dromara.project.domain.BusLeave;
import org.dromara.project.domain.BusProjectTeam; import org.dromara.project.domain.BusProjectTeam;
import org.dromara.project.domain.enums.BusAttendanceClockStatusEnum; import org.dromara.project.domain.BusProjectTeamMember;
import org.dromara.project.domain.enums.BusAttendanceCommuterEnum; import org.dromara.project.domain.dto.leave.BusLeaveGangerReviewReq;
import org.dromara.project.domain.enums.BusOpinionStatusEnum;
import org.dromara.project.domain.enums.BusReviewStatusEnum;
import org.dromara.project.domain.dto.leave.BusLeaveManagerReviewReq; import org.dromara.project.domain.dto.leave.BusLeaveManagerReviewReq;
import org.dromara.project.domain.dto.leave.BusLeaveQueryReq; import org.dromara.project.domain.dto.leave.BusLeaveQueryReq;
import org.dromara.project.domain.enums.*;
import org.dromara.project.domain.vo.leave.BusLeaveVo; import org.dromara.project.domain.vo.leave.BusLeaveVo;
import org.dromara.project.mapper.BusLeaveMapper; import org.dromara.project.mapper.BusLeaveMapper;
import org.dromara.project.service.IBusAttendanceService; import org.dromara.project.service.IBusAttendanceService;
import org.dromara.project.service.IBusLeaveService; import org.dromara.project.service.IBusLeaveService;
import org.dromara.project.service.IBusProjectTeamMemberService;
import org.dromara.project.service.IBusProjectTeamService; import org.dromara.project.service.IBusProjectTeamService;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@ -48,6 +53,15 @@ public class BusLeaveServiceImpl extends ServiceImpl<BusLeaveMapper, BusLeave>
@Resource @Resource
private IBusProjectTeamService projectTeamService; private IBusProjectTeamService projectTeamService;
@Resource
private IBusProjectTeamMemberService projectTeamMemberService;
@Resource
private ISubConstructionUserService constructionUserService;
@Resource
private ISubContractorService contractorService;
@Lazy @Lazy
@Resource @Resource
private IBusAttendanceService attendanceService; private IBusAttendanceService attendanceService;
@ -88,6 +102,63 @@ public class BusLeaveServiceImpl extends ServiceImpl<BusLeaveMapper, BusLeave>
return baseMapper.selectVoList(lqw); return baseMapper.selectVoList(lqw);
} }
/**
* 班长审核施工人员请假申请
*
* @param req 班长审核施工人员请假申请
* @return 是否审核成功
*/
@Override
public Boolean gangerReview(BusLeaveGangerReviewReq req) {
Long id = req.getId();
String gangerOpinion = req.getGangerOpinion();
// 判断该请假记录是否存在
BusLeave oldLeave = this.getById(id);
if (oldLeave == null) {
throw new ServiceException("施工人员请假申请不存在", HttpStatus.NOT_FOUND);
}
// 如果审核状态相同,则返回
if (Objects.equals(gangerOpinion, oldLeave.getGangerOpinion())) {
throw new ServiceException("请勿重复操作", HttpStatus.BAD_REQUEST);
}
// 如果已经审核过,则返回
if (BusOpinionStatusEnum.PASS.getValue().equals(oldLeave.getManagerOpinion())) {
throw new ServiceException("该请假已审核通过,请勿重复操作", HttpStatus.BAD_REQUEST);
}
// 获取当前用户实名信息
SubConstructionUser constructionUser = constructionUserService.getBySysUserId(LoginHelper.getUserId());
// 判断当前用户是否有权审核
Long teamId = oldLeave.getTeamId();
Long count = projectTeamMemberService.lambdaQuery()
.eq(BusProjectTeamMember::getProjectId, oldLeave.getProjectId())
.eq(BusProjectTeamMember::getMemberId, constructionUser.getId())
.eq(BusProjectTeamMember::getTeamId, teamId)
.eq(BusProjectTeamMember::getPostId, BusProjectTeamMemberPostEnum.FOREMAN.getValue())
.count();
if (count <= 0) {
throw new ServiceException("您无权审核该请假申请", HttpStatus.FORBIDDEN);
}
BusLeave newLeave = new BusLeave();
newLeave.setId(id);
newLeave.setGangerOpinion(gangerOpinion);
newLeave.setGangerExplain(req.getGangerExplain());
newLeave.setGangerTime(new Date());
newLeave.setRemark(req.getRemark());
boolean result = this.updateById(newLeave);
if (!result) {
throw new ServiceException("更新班长审核操作失败", HttpStatus.ERROR);
}
// 向分包管理员发送通知
List<SubConstructionUser> constructionAdminUsers = constructionUserService.lambdaQuery()
.eq(SubConstructionUser::getContractorId, oldLeave.getContractorId())
.eq(SubConstructionUser::getUserRole, SubConstructionUserRoleEnum.ADMIN.getValue())
.list();
if (CollUtil.isNotEmpty(constructionAdminUsers)){
}
return true;
}
/** /**
* 管理员审核施工人员请假申请 * 管理员审核施工人员请假申请
* *
@ -114,9 +185,20 @@ public class BusLeaveServiceImpl extends ServiceImpl<BusLeaveMapper, BusLeave>
throw new ServiceException("请等待班组长审核通过后再进行操作", HttpStatus.BAD_REQUEST); throw new ServiceException("请等待班组长审核通过后再进行操作", HttpStatus.BAD_REQUEST);
} }
// todo 判断当前用户是否为项目管理员 // todo 判断当前用户是否为项目管理员
// 判断是否为分包公司管理员
Long contractorId = oldLeave.getContractorId();
SubConstructionUser constructionUser = constructionUserService.lambdaQuery()
.eq(SubConstructionUser::getUserId, LoginHelper.getUserId())
.eq(SubConstructionUser::getContractorId, contractorId)
.eq(SubConstructionUser::getUserRole, SubConstructionUserRoleEnum.ADMIN.getValue())
.one();
if (constructionUser == null) {
throw new ServiceException("您无权审核该请假申请", HttpStatus.FORBIDDEN);
}
// 填充默认值,更新数据 // 填充默认值,更新数据
BusLeave leave = new BusLeave(); BusLeave leave = new BusLeave();
leave.setId(id); leave.setId(id);
leave.setManagerId(constructionUser.getId());
leave.setManagerOpinion(managerOpinion); leave.setManagerOpinion(managerOpinion);
leave.setManagerExplain(req.getManagerExplain()); leave.setManagerExplain(req.getManagerExplain());
leave.setManagerTime(new Date()); leave.setManagerTime(new Date());

View File

@ -0,0 +1,90 @@
package org.dromara.system.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 系统通知
*
* @TableName sys_notifications
*/
@Data
@TableName(value = "sys_notifications")
public class SysNotifications implements Serializable {
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* 接收通知的用户ID
*/
private Long recipientId;
/**
* 发送通知的用户ID系统通知则为null
*/
private Long senderId;
/**
* 通知类型
*/
private String type;
/**
* 通知标题
*/
private String title;
/**
* 通知的主要内容
*/
private String content;
/**
* 通知状态0未读 1已读
*/
private String status;
/**
* 用户读取通知的时间
*/
private Date readAt;
/**
* 点击通知后跳转的目标URL
*/
private String actionUrl;
/**
* 通知附件
*/
private String file;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
@Serial
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,59 @@
package org.dromara.system.domain.dto.notifications;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* @author lilemy
* @date 2025-07-24 16:35
*/
@Data
public class SysNotificationsCreateBatchReq implements Serializable {
@Serial
private static final long serialVersionUID = 7786269708778844999L;
/**
* 接收通知的用户ID
*/
private List<Long> recipientIds;
/**
* 发送通知的用户ID系统通知则为null
*/
private Long senderId;
/**
* 通知类型
*/
private String type;
/**
* 通知标题
*/
private String title;
/**
* 通知的主要内容
*/
private String content;
/**
* 点击通知后跳转的目标URL
*/
private String actionUrl;
/**
* 通知附件
*/
private String file;
/**
* 备注
*/
private String remark;
}

View File

@ -0,0 +1,48 @@
package org.dromara.system.domain.dto.notifications;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* @author lilemy
* @date 2025-07-24 16:37
*/
@Data
public class SysNotificationsQueryReq implements Serializable {
@Serial
private static final long serialVersionUID = 2227178507874081416L;
/**
* 接收通知的用户ID
*/
private Long recipientId;
/**
* 发送通知的用户ID系统通知则为null
*/
private Long senderId;
/**
* 通知类型
*/
private String type;
/**
* 通知标题
*/
private String title;
/**
* 通知的主要内容
*/
private String content;
/**
* 通知状态0未读 1已读
*/
private String status;
}

View File

@ -0,0 +1,80 @@
package org.dromara.system.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* @author lilemy
* @date 2025-07-24 16:31
*/
@Data
public class SysNotificationsVo implements Serializable {
@Serial
private static final long serialVersionUID = -2755920219118735240L;
/**
* 主键ID
*/
private Long id;
/**
* 接收通知的用户ID
*/
private Long recipientId;
/**
* 发送通知的用户ID系统通知则为null
*/
private Long senderId;
/**
* 通知类型
*/
private String type;
/**
* 通知标题
*/
private String title;
/**
* 通知的主要内容
*/
private String content;
/**
* 通知状态0未读 1已读
*/
private String status;
/**
* 用户读取通知的时间
*/
private Date readAt;
/**
* 点击通知后跳转的目标URL
*/
private String actionUrl;
/**
* 通知附件
*/
private String file;
/**
* 附件列表
*/
private List<SysOssVo> fileVo;
/**
* 备注
*/
private String remark;
}

View File

@ -0,0 +1,17 @@
package org.dromara.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.dromara.system.domain.SysNotifications;
/**
* @author lilemy
* @description 针对表【sys_notifications(系统通知)】的数据库操作Mapper
* @createDate 2025-07-24 16:19:58
*/
public interface SysNotificationsMapper extends BaseMapper<SysNotifications> {
}

View File

@ -0,0 +1,68 @@
package org.dromara.system.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import org.dromara.system.domain.SysNotifications;
import org.dromara.system.domain.dto.notifications.SysNotificationsCreateBatchReq;
import org.dromara.system.domain.dto.notifications.SysNotificationsQueryReq;
import org.dromara.system.domain.vo.SysNotificationsVo;
import java.util.Collection;
/**
* @author lilemy
* @description 针对表【sys_notifications(系统通知)】的数据库操作Service
* @createDate 2025-07-24 16:19:58
*/
public interface SysNotificationsService extends IService<SysNotifications> {
/**
* 查询系统通知
*
* @param id 系统通知主键
* @return 系统通知
*/
SysNotificationsVo queryById(Long id);
/**
* 批量发送通知
*
* @param req 通知
* @return 是否成功
*/
Boolean sendBatch(SysNotificationsCreateBatchReq req);
/**
* 删除通知
*
* @param ids 通知ID串
* @return 结果
*/
Boolean deleteByIds(Collection<Long> ids);
/**
* 获取系统通知
*
* @param notifications 系统通知
* @return 系统通知
*/
SysNotificationsVo getVo(SysNotifications notifications);
/**
* 构建查询条件
*
* @param req 查询参数
* @return 查询条件
*/
LambdaQueryWrapper<SysNotifications> buildQueryWrapper(SysNotificationsQueryReq req);
/**
* 获取系统通知分页
*
* @param notificationsPage 系统通知分页
* @return 系统通知分页
*/
Page<SysNotificationsVo> getVoPage(Page<SysNotifications> notificationsPage);
}

View File

@ -0,0 +1,186 @@
package org.dromara.system.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import org.dromara.common.core.constant.HttpStatus;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.ObjectUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.sse.dto.SseMessageDto;
import org.dromara.common.sse.utils.SseMessageUtils;
import org.dromara.system.domain.SysNotifications;
import org.dromara.system.domain.dto.notifications.SysNotificationsCreateBatchReq;
import org.dromara.system.domain.dto.notifications.SysNotificationsQueryReq;
import org.dromara.system.domain.vo.SysNotificationsVo;
import org.dromara.system.mapper.SysNotificationsMapper;
import org.dromara.system.service.ISysOssService;
import org.dromara.system.service.SysNotificationsService;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @author lilemy
* @description 针对表【sys_notifications(系统通知)】的数据库操作Service实现
* @createDate 2025-07-24 16:19:58
*/
@Service
public class SysNotificationsServiceImpl extends ServiceImpl<SysNotificationsMapper, SysNotifications>
implements SysNotificationsService {
@Resource
private ISysOssService ossService;
@Resource
private ScheduledExecutorService scheduledExecutorService;
/**
* 查询系统通知
*
* @param id 系统通知主键
* @return 系统通知
*/
@Override
public SysNotificationsVo queryById(Long id) {
SysNotifications notifications = this.getById(id);
if (notifications == null) {
throw new ServiceException("当前通知不存在", HttpStatus.NOT_FOUND);
}
return this.getVo(notifications);
}
/**
* 批量发送通知
*
* @param req 通知
* @return 是否成功
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean sendBatch(SysNotificationsCreateBatchReq req) {
List<Long> recipientIds = req.getRecipientIds();
if (CollUtil.isEmpty(recipientIds)) {
throw new ServiceException("请选择接收人", HttpStatus.BAD_REQUEST);
}
List<SysNotifications> sysNotificationsList = recipientIds.stream().map(recipientId -> {
SysNotifications notifications = new SysNotifications();
notifications.setRecipientId(recipientId);
notifications.setSenderId(req.getSenderId());
notifications.setType(req.getType());
notifications.setTitle(req.getTitle());
notifications.setContent(req.getContent());
notifications.setActionUrl(req.getActionUrl());
notifications.setFile(req.getFile());
notifications.setRemark(req.getRemark());
return notifications;
}).toList();
boolean result = this.saveBatch(sysNotificationsList);
if (!result) {
throw new ServiceException("通知发送失败", HttpStatus.ERROR);
}
// 通过 sse 发送通知
scheduledExecutorService.schedule(() -> {
SseMessageDto dto = new SseMessageDto();
dto.setMessage(StringUtils.isNotBlank(req.getTitle()) ? req.getTitle() : req.getContent().substring(0, 50));
dto.setUserIds(recipientIds);
SseMessageUtils.publishMessage(dto);
}, 2, TimeUnit.SECONDS);
return true;
}
/**
* 删除通知
*
* @param ids 通知ID串
* @return 结果
*/
@Override
public Boolean deleteByIds(Collection<Long> ids) {
return null;
}
/**
* 获取系统通知
*
* @param notifications 系统通知
* @return 系统通知
*/
@Override
public SysNotificationsVo getVo(SysNotifications notifications) {
SysNotificationsVo sysNotificationsVo = new SysNotificationsVo();
if (notifications == null) {
return sysNotificationsVo;
}
BeanUtils.copyProperties(notifications, sysNotificationsVo);
// 关联文件信息
Set<Long> ossIds = Arrays.stream(notifications.getFile().split(",")).map(Long::parseLong).collect(Collectors.toSet());
if (CollUtil.isNotEmpty(ossIds)) {
sysNotificationsVo.setFileVo(ossService.listByIds(ossIds));
}
return sysNotificationsVo;
}
/**
* 构建查询条件
*
* @param req 查询参数
* @return 查询条件
*/
@Override
public LambdaQueryWrapper<SysNotifications> buildQueryWrapper(SysNotificationsQueryReq req) {
LambdaQueryWrapper<SysNotifications> lqw = new LambdaQueryWrapper<>();
if (req == null) {
return lqw;
}
Long recipientId = req.getRecipientId();
Long senderId = req.getSenderId();
String type = req.getType();
String title = req.getTitle();
String content = req.getContent();
String status = req.getStatus();
lqw.like(StringUtils.isNotBlank(title), SysNotifications::getTitle, title);
lqw.like(StringUtils.isNotBlank(content), SysNotifications::getContent, content);
lqw.eq(StringUtils.isNotBlank(type), SysNotifications::getType, type);
lqw.eq(StringUtils.isNotBlank(status), SysNotifications::getStatus, status);
lqw.eq(ObjectUtils.isNotEmpty(recipientId), SysNotifications::getRecipientId, recipientId);
lqw.eq(ObjectUtils.isNotEmpty(senderId), SysNotifications::getSenderId, senderId);
return lqw;
}
/**
* 获取系统通知分页
*
* @param notificationsPage 系统通知分页
* @return 系统通知分页
*/
@Override
public Page<SysNotificationsVo> getVoPage(Page<SysNotifications> notificationsPage) {
List<SysNotifications> notificationsList = notificationsPage.getRecords();
Page<SysNotificationsVo> notificationsVoPage = new Page<>(
notificationsPage.getCurrent(),
notificationsPage.getSize(),
notificationsPage.getTotal()
);
if (CollUtil.isEmpty(notificationsList)) {
return notificationsVoPage;
}
List<SysNotificationsVo> notificationsVoList = notificationsList.stream().map(this::getVo).toList();
notificationsVoPage.setRecords(notificationsVoList);
return notificationsVoPage;
}
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.system.mapper.SysNotificationsMapper">
<resultMap id="BaseResultMap" type="org.dromara.system.domain.SysNotifications">
<id property="id" column="id"/>
<result property="recipientId" column="recipient_id"/>
<result property="senderId" column="sender_id"/>
<result property="type" column="type"/>
<result property="title" column="title"/>
<result property="content" column="content"/>
<result property="status" column="status"/>
<result property="readAt" column="read_at"/>
<result property="actionUrl" column="action_url"/>
<result property="file" column="file"/>
<result property="remark" column="remark"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="Base_Column_List">
id,recipient_id,sender_id,type,title,content,
status,read_at,action_url,file,remark,
create_time,update_time
</sql>
</mapper>

View File

@ -1590,3 +1590,25 @@ CREATE TABLE `hse_violation_record`
index `idx_project_id` (`project_id` asc) using btree comment '项目id' index `idx_project_id` (`project_id` asc) using btree comment '项目id'
) comment '违规记录' collate = utf8mb4_unicode_ci; ) comment '违规记录' collate = utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `sys_notifications`;
CREATE TABLE `sys_notifications`
(
`id` bigint not null auto_increment comment '主键ID',
`recipient_id` bigint not null comment '接收通知的用户ID',
`sender_id` bigint default 0 not null comment '发送通知的用户ID系统通知则为null',
`type` char(2) not null comment '通知类型',
`title` varchar(255) default '' not null comment '通知标题',
`content` text not null comment '通知的主要内容',
`status` char(1) default '0' not null comment '通知状态0未读 1已读',
`read_at` datetime null comment '用户读取通知的时间',
`action_url` varchar(1024) default '' not null comment '点击通知后跳转的目标URL',
`file` varchar(1024) null comment '通知附件',
`remark` varchar(255) null comment '备注',
`create_time` datetime default CURRENT_TIMESTAMP null comment '创建时间',
`update_time` datetime default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间',
primary key (`id`) using btree,
INDEX `idx_recipient_id` (`recipient_id`),
INDEX `idx_sender_id` (`sender_id`),
index `idx_type` (`type`)
) comment '系统通知' collate = utf8mb4_unicode_ci;