本项目使用两个数据库:
根据FlinkDataSync项目的配置,以下表位于TugboatCommon库:
业务表位于LiandaTugboatMIS库:
只有tugboatcommon和liandatugboatmis这两个库同时存在的表才需要读写分离:
@Service
@DataSource(value = RoutingDataSourceConfig.DataSourceType.COMMON)
public class UserService {
// 使用TugboatCommon库
}
@Service
@DataSource(value = RoutingDataSourceConfig.DataSourceType.MIS)
public class BusinessService {
// 使用LiandaTugboatMIS库
}
以下字段为公共字段,所有表如果存在这些字段,都需要在新增和修改操作中处理:
@Transactional
public UserDTO createUser(UserCreateDTO userCreateDTO) {
User user = new User();
user.setId(UUID.randomUUID().toString());
user.setLoginId(userCreateDTO.getLoginId());
user.setName(userCreateDTO.getName());
// 设置公共字段
String currentUserId = getCurrentUserId();
user.setRecordStatus(1); // 新增默认RecordStatus为1(启用)
user.setCreateUserID(currentUserId); // 创建人为当前登录人
user.setCreateTime(new Date()); // 创建时间为当前时间
user.setModifyUserID(currentUserId); // 修改人为当前登录人
user.setModifyTime(new Date()); // 修改时间为当前时间
user = userRepository.save(user);
return UserDTO.fromEntity(user);
}
/**
* 获取当前登录用户ID
*/
private String getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails.getUsername();
}
return "admin"; // 默认返回admin
}
@Transactional
public UserDTO updateUser(String userId, UserUpdateDTO userUpdateDTO) {
User user = userRepository.findById(userId).orElseThrow(() ->
new RuntimeException("用户不存在"));
user.setLoginId(userUpdateDTO.getLoginId());
user.setName(userUpdateDTO.getName());
// 更新公共字段
String currentUserId = getCurrentUserId();
user.setModifyUserID(currentUserId); // 修改人为当前登录人
user.setModifyTime(new Date()); // 修改时间为当前时间
user = userRepository.save(user);
return UserDTO.fromEntity(user);
}
const addUserForm = reactive({
loginId: '',
name: '',
recordStatus: 1 // 默认设置为1(启用)
})
@Entity
@Table(name = "Sys_User")
public class User {
@Id
@Column(name = "UserID")
private String id;
@Column(name = "LoginID")
private String loginId;
@Column(name = "Name")
private String name;
@Column(name = "Password")
private String password;
@Column(name = "RecordStatus")
private Integer recordStatus;
@Column(name = "CreateUserID")
private String createUserID;
@Column(name = "CreateTime")
private Date createTime;
@Column(name = "ModifyUserID")
private String modifyUserID;
@Column(name = "ModifyTime")
private Date modifyTime;
}
获取Sys_DictionaryItem数据时,必须同时传递DictionaryCode和Value值:
// 错误:只传递Value值
SysDictionaryItem item = sysDictionaryItemRepository.findByValue(1);
// 正确:同时传递DictionaryCode和Value
SysDictionaryItem item = sysDictionaryItemRepository.findByDictionaryCodeAndValue("RecordStatus", 1);
@Repository
public interface SysDictionaryItemRepository extends JpaRepository<SysDictionaryItem, String> {
@Query(value = "SELECT * FROM Sys_DictionaryItem WHERE DictionaryCode = :dictionaryCode AND Value = :value", nativeQuery = true)
SysDictionaryItem findByDictionaryCodeAndValue(@Param("dictionaryCode") String dictionaryCode, @Param("value") Integer value);
@Query(value = "SELECT * FROM Sys_DictionaryItem WHERE DictionaryCode = :dictionaryCode ORDER BY OrderNo", nativeQuery = true)
List<SysDictionaryItem> findByDictionaryCode(@Param("dictionaryCode") String dictionaryCode);
}
@Transactional
public UserDTO createUser(UserCreateDTO userCreateDTO) {
// 重复校验:检查LoginID是否重复
if (userRepository.findByLoginID(userCreateDTO.getLoginId()) != null) {
throw new RuntimeException("账号已存在,请使用其他账号");
}
// 重复校验:检查Name是否重复
if (userRepository.findByName(userCreateDTO.getName()) != null) {
throw new RuntimeException("用户名已存在,请使用其他用户名");
}
// 创建用户...
}
@Transactional
public UserDTO updateUser(String userId, UserUpdateDTO userUpdateDTO) {
User user = userRepository.findById(userId).orElseThrow(() ->
new RuntimeException("用户不存在"));
// 重复校验:检查LoginID是否与其他用户重复
User existingUser = userRepository.findByLoginID(userUpdateDTO.getLoginId());
if (existingUser != null && !existingUser.getId().equals(userId)) {
throw new RuntimeException("账号已存在,请使用其他账号");
}
// 重复校验:检查Name是否与其他用户重复
existingUser = userRepository.findByName(userUpdateDTO.getName());
if (existingUser != null && !existingUser.getId().equals(userId)) {
throw new RuntimeException("用户名已存在,请使用其他用户名");
}
// 更新用户...
}
使用MD5加密密码:
private String md5(String password) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(password.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
throw new RuntimeException("密码加密失败", e);
}
}
新增用户时,默认密码为"TIMS123456"的MD5值:
user.setPassword(md5("TIMS123456"));
前端必须显示删除确认对话框:
const deleteUser = async (id) => {
try {
await ElMessageBox.confirm(
'确定要删除该用户吗?删除后用户及其角色关联将被永久删除,不可恢复。',
'删除确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await request.delete(`/user/delete/${id}`)
ElMessage.success('删除成功')
getUsers()
} catch (error) {
if (error !== 'cancel') {
console.error('删除用户失败:', error)
ElMessage.error('删除用户失败')
}
}
}
删除主表数据时,必须先删除关联表数据:
@Transactional
public void deleteUsers(List<String> userIds) {
for (String userId : userIds) {
// 先删除关联表数据
sysUserRoleRepository.deleteByUserId(userId);
// 再删除主表数据
userRepository.deleteById(userId);
}
}
对于需要动态拼接SQL的查询,使用JdbcTemplate:
@Autowired
@Qualifier("writeJdbcTemplate")
private JdbcTemplate writeJdbcTemplate;
public List<User> queryUsers(UserQuery query) {
StringBuilder sql = new StringBuilder("SELECT * FROM Sys_User WHERE 1=1");
List<Object> params = new ArrayList<>();
if (query.getLoginId() != null && !query.getLoginId().isEmpty()) {
sql.append(" AND LoginID LIKE ?");
params.add("%" + query.getLoginId() + "%");
}
if (query.getName() != null && !query.getName().isEmpty()) {
sql.append(" AND Name LIKE ?");
params.add("%" + query.getName() + "%");
}
return writeJdbcTemplate.query(sql.toString(), params.toArray(), new UserRowMapper());
}
// 错误:使用+号拼接字符串
@Query(value = "SELECT * FROM Sys_User WHERE LoginID LIKE '%' + :loginID + '%'", nativeQuery = true)
// 正确:使用CONCAT函数
@Query(value = "SELECT * FROM Sys_User WHERE LoginID LIKE CONCAT('%', :loginID, '%')", nativeQuery = true)
User findByLoginIDLike(@Param("loginID") String loginID);
使用Pageable处理排序,不要在SQL中写死ORDER BY:
public Page<User> queryUsers(UserQuery query, Pageable pageable) {
Sort sort = pageable.getSort();
if (query.getSortBy() != null && query.getSortOrder() != null) {
sort = Sort.by(
"asc".equalsIgnoreCase(query.getSortOrder()) ? Sort.Direction.ASC : Sort.Direction.DESC,
query.getSortBy()
);
}
return userRepository.findAll(pageable.withSort(sort));
}
使用RuntimeException抛出业务异常:
if (userRepository.findByLoginID(userCreateDTO.getLoginId()) != null) {
throw new RuntimeException("账号已存在,请使用其他账号");
}
try {
await request.post('/user/create', data)
ElMessage.success('操作成功')
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败,请稍后重试')
}
所有涉及多个数据库操作的方法都必须添加@Transactional注解:
@Transactional
public UserDTO createUser(UserCreateDTO userCreateDTO) {
// 创建用户
User user = userRepository.save(user);
// 创建用户角色关联
SysUserRole userRole = new SysUserRole();
userRole.setUserId(user.getId());
userRole.setRoleId(userCreateDTO.getRoleId());
sysUserRoleRepository.save(userRole);
return UserDTO.fromEntity(user);
}
查询方法可以使用@Transactional(readOnly = true):
@Transactional(readOnly = true)
public Page<User> queryUsers(UserQuery query, Pageable pageable) {
return userRepository.findAll(pageable);
}
public Page<User> queryUsers(UserQuery query, Pageable pageable) {
return userRepository.findAll(pageable);
}
const getUsers = async () => {
const response = await request.get('/user/list', {
params: {
page: currentPage.value,
pageSize: pageSize.value,
sortBy: sortMap.value.sortBy,
sortOrder: sortMap.value.sortOrder
}
})
userList.value = response.data.content
total.value = response.data.totalElements
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
public UserDTO createUser(UserCreateDTO userCreateDTO) {
logger.info("开始创建用户,账号:{}", userCreateDTO.getLoginId());
try {
// 创建用户逻辑
logger.info("用户创建成功,ID:{}", user.getId());
return UserDTO.fromEntity(user);
} catch (Exception e) {
logger.error("用户创建失败", e);
throw new RuntimeException("用户创建失败", e);
}
}
}
前端必须正确处理401(未授权)和403(禁止访问)错误,自动清除token并重定向到登录页面:
request.interceptors.response.use(
response => {
return response
},
error => {
if (error.response) {
switch (error.response.status) {
case 401:
console.error('未授权,请重新登录')
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
window.location.href = '/login'
break
case 403:
console.error('没有权限访问,请重新登录')
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
window.location.href = '/login'
break
case 404:
console.error('请求的资源不存在')
break
case 500:
console.error('服务器错误')
break
default:
console.error('请求失败:', error.response.status)
}
} else if (error.request) {
console.error('网络错误,请检查网络连接')
} else {
console.error('请求配置错误:', error.message)
}
return Promise.reject(error)
}
)
每次请求都必须在请求头中添加Bearer token:
request.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
return config
},
error => {
console.error('请求错误:', error)
return Promise.reject(error)
}
)
在调度列表查询和导出时,客户名称的显示规则如下:
public String queryCustomerName(String agencyId, String shipownerId) {
StringBuilder sql = new StringBuilder("SELECT ");
List<String> names = new ArrayList<>();
boolean hasAgency = agencyId != null && !agencyId.isEmpty();
boolean hasShipowner = shipownerId != null && !shipownerId.isEmpty();
if (hasAgency) {
sql.append("(SELECT ccb.BusinessCode FROM Bus_CustomerCompanyBusiness ccb WHERE ccb.CustomerCompanyBusinessID = ?) AS AgencyName");
names.add("AgencyName");
}
if (hasShipowner) {
if (hasAgency) {
sql.append(", ");
}
sql.append("(SELECT ccb.BusinessCode FROM Bus_CustomerCompanyBusiness ccb WHERE ccb.CustomerCompanyBusinessID = ?) AS ShipownerName");
names.add("ShipownerName");
}
if (names.isEmpty()) {
return "";
}
try {
List<Object> params = new ArrayList<>();
if (hasAgency) {
params.add(agencyId);
}
if (hasShipowner) {
params.add(shipownerId);
}
return jdbcTemplate.queryForObject(sql.toString(), (rs, rowNum) -> {
StringBuilder result = new StringBuilder();
for (String name : names) {
String value = rs.getString(name);
if (value != null && !value.isEmpty()) {
if (result.length() > 0) {
result.append(" ");
}
result.append(value);
}
}
return result.toString();
}, params.toArray());
} catch (Exception e) {
return "";
}
}
引航计划导入后复制到调度表时,PortID的填充逻辑如下:
第一步:通过Disp_BerthageDictionary的Keyword完全匹配泊位名称
第二步:如果第一步无法匹配,用Disp_PortDictionary的PortKeyword模糊匹配泊位名称
第三步:如果第二步没有找到,检查是否为"数字+#号"格式
第四步:如果还没找到,说明是锚地
第五步:用找到的港口名称、泊位名称查找对应的记录
@DataSource(value = RoutingDataSourceConfig.DataSourceType.COMMON)
@Transactional
public BerthageInfo findOrCreateBerthageInfo(String berthName) {
String portId = null;
String berthageId = null;
String portName = null;
String actualBerthName = null;
// 第一步:通过Disp_BerthageDictionary的Keyword完全匹配
// ...实现第一步逻辑...
// 第二步:用Disp_PortDictionary的PortKeyword模糊匹配
// ...实现第二步逻辑...
// 第三步:检查是否为"数字+#号"格式
if (portId == null) {
Pattern pattern = Pattern.compile("^(.+?)\\s*(\\d+#)$");
Matcher matcher = pattern.matcher(berthName);
if (matcher.matches()) {
portName = matcher.group(1).trim();
actualBerthName = matcher.group(2);
// 查找对应的Port和Berthage记录
List<DispPort> ports = dispPortRepository.findByName(portName);
if (!ports.isEmpty()) {
DispPort port = ports.get(0);
if (port.getPortType() != null && port.getPortType() == 1) {
// 确保是码头
portId = port.getPortId();
List<DispBerthage> berthages = dispBerthageRepository.findByNameAndPortId(actualBerthName, portId);
if (!berthages.isEmpty()) {
berthageId = berthages.get(0).getBerthageId();
}
}
}
}
}
// 第四步:锚地处理
// ...实现第四步逻辑...
// 第五步:新增记录
// ...实现第五步逻辑...
return new BerthageInfo(berthageId, portId);
}
@DataSource注解在同一个Service类的内部方法调用时不会生效,因为Spring AOP只对代理对象的方法调用有效。当在同一个Service类内部调用方法时,不会经过代理,因此@DataSource注解不会生效。
创建专门的CommonDataService来处理跨切面的数据操作,确保@DataSource注解生效:
@Service
public class CommonDataService {
/**
* 查找或创建泊位信息
* @param berthName 泊位名称
* @return 泊位信息
*/
@DataSource(value = RoutingDataSourceConfig.DataSourceType.COMMON)
@Transactional
public BerthageInfo findOrCreateBerthageInfo(String berthName) {
// 实现逻辑...
}
/**
* 查找或创建港口信息
* @param portName 港口名称
* @param portType 港口类型
* @return 港口信息
*/
@DataSource(value = RoutingDataSourceConfig.DataSourceType.COMMON)
@Transactional
public PortInfo findOrCreatePortInfo(String portName, Integer portType) {
// 实现逻辑...
}
}
@Service
public class PilotPlanService {
@Autowired
private CommonDataService commonDataService;
private void syncToDispatcherTable(DispPilotPlan pilotPlan, PilotPlanImportDTO importDTO) {
// 调用CommonDataService的方法,@DataSource注解会生效
CommonDataService.BerthageInfo fromBerthageInfo = commonDataService.findOrCreateBerthageInfo(importDTO.getFromBerthage());
CommonDataService.BerthageInfo toBerthageInfo = commonDataService.findOrCreateBerthageInfo(importDTO.getToBerthage());
// 使用返回的BerthageID和PortID...
}
}
引航计划导入后复制到调度表时,必须复制以下字段:
private void syncToDispatcherTable(DispPilotPlan pilotPlan, PilotPlanImportDTO importDTO,
CommonDataService.BerthageInfo fromBerthageInfo,
CommonDataService.BerthageInfo toBerthageInfo) {
DispDispatcher dispatcher = dispDispatcherRepository.findByPilotPlanId(pilotPlan.getPilotPlanId());
Date currentTime = new Date();
String currentUserId = getCurrentUserId();
if (dispatcher == null) {
dispatcher = new DispDispatcher();
dispatcher.setDispatcherId(UUID.randomUUID().toString().replace("-", ""));
dispatcher.setPilotPlanId(pilotPlan.getPilotPlanId());
dispatcher.setRecordStatus(1);
dispatcher.setCreateTime(currentTime);
dispatcher.setCreateUserId(currentUserId);
}
// 复制字段
dispatcher.setWorkTime(pilotPlan.getPlanTime());
dispatcher.setPortID(toBerthageInfo.getPortId());
dispatcher.setFromBerthageId(pilotPlan.getFromBerthageId());
dispatcher.setToBerthageId(pilotPlan.getToBerthageId());
dispatcher.setShipId(pilotPlan.getShipId());
dispatcher.setDeep(pilotPlan.getDeep());
dispatcher.setAgencyId(pilotPlan.getAgencyId());
dispatcher.setPilotTypeId(pilotPlan.getPilotType());
dispatcher.setPilotId(pilotPlan.getMainPilotId()); // 复制主引
dispatcher.setRemark(pilotPlan.getRemark()); // 复制备注
// 计算StartTime和EndTime
if (pilotPlan.getPilotType() != null) {
// 根据PilotType计算开始和结束时间
// ...实现计算逻辑...
}
dispatcher.setModifyTime(currentTime);
dispatcher.setModifyUserId(currentUserId);
dispDispatcherRepository.save(dispatcher);
}