|
@@ -479,3 +479,318 @@ public class UserService {
|
|
|
8. **记录日志**:重要操作必须记录日志
|
|
8. **记录日志**:重要操作必须记录日志
|
|
|
9. **处理异常**:使用RuntimeException抛出业务异常,前端统一处理错误提示
|
|
9. **处理异常**:使用RuntimeException抛出业务异常,前端统一处理错误提示
|
|
|
10. **使用MD5加密密码**:密码必须使用MD5加密存储
|
|
10. **使用MD5加密密码**:密码必须使用MD5加密存储
|
|
|
|
|
+
|
|
|
|
|
+## 14. 前端认证错误处理规范
|
|
|
|
|
+
|
|
|
|
|
+### 14.1 401和403错误处理
|
|
|
|
|
+前端必须正确处理401(未授权)和403(禁止访问)错误,自动清除token并重定向到登录页面:
|
|
|
|
|
+```javascript
|
|
|
|
|
+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)
|
|
|
|
|
+ }
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 14.2 请求拦截器
|
|
|
|
|
+每次请求都必须在请求头中添加Bearer token:
|
|
|
|
|
+```javascript
|
|
|
|
|
+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)
|
|
|
|
|
+ }
|
|
|
|
|
+)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 15. 客户名称显示规范
|
|
|
|
|
+
|
|
|
|
|
+### 15.1 客户名称显示逻辑
|
|
|
|
|
+在调度列表查询和导出时,客户名称的显示规则如下:
|
|
|
|
|
+- 如果同时存在代理公司和船公司,则显示为:代理公司名 + 空格 + 船公司名
|
|
|
|
|
+- 如果只有代理公司,则只显示代理公司名
|
|
|
|
|
+- 如果只有船公司,则只显示船公司名
|
|
|
|
|
+- 如果两者都不存在,则显示为空
|
|
|
|
|
+
|
|
|
|
|
+### 15.2 后端实现示例
|
|
|
|
|
+```java
|
|
|
|
|
+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 "";
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 16. 引航计划PortID填充规范
|
|
|
|
|
+
|
|
|
|
|
+### 16.1 PortID填充逻辑
|
|
|
|
|
+引航计划导入后复制到调度表时,PortID的填充逻辑如下:
|
|
|
|
|
+
|
|
|
|
|
+**第一步**:通过Disp_BerthageDictionary的Keyword完全匹配泊位名称
|
|
|
|
|
+- 如果匹配成功,读取BerthageID
|
|
|
|
|
+- 通过Disp_Berthage表查找对应的记录,获取PortID
|
|
|
|
|
+- 检查Disp_Port表中该PortID的PortType,如果是1(码头)且还未匹配到PortID,则使用该PortID
|
|
|
|
|
+
|
|
|
|
|
+**第二步**:如果第一步无法匹配,用Disp_PortDictionary的PortKeyword模糊匹配泊位名称
|
|
|
|
|
+- 如果匹配成功,港口名称为Disp_PortDictionary.Name
|
|
|
|
|
+- 泊位名称为除港口名称外的剩余部分
|
|
|
|
|
+
|
|
|
|
|
+**第三步**:如果第二步没有找到,检查是否为"数字+#号"格式
|
|
|
|
|
+- 如果是,数字+#号的部分是泊位名称,其余部分是港口名称
|
|
|
|
|
+- 匹配成功的港口都是码头(PortType=1)
|
|
|
|
|
+
|
|
|
|
|
+**第四步**:如果还没找到,说明是锚地
|
|
|
|
|
+- 港口名称=泊位名称=导入的泊位名称
|
|
|
|
|
+- PortType值为2
|
|
|
|
|
+
|
|
|
|
|
+**第五步**:用找到的港口名称、泊位名称查找对应的记录
|
|
|
|
|
+- 如果数据库中没有记录,则新增记录(Disp_Port、Disp_Berthage、Disp_BerthageSetting)
|
|
|
|
|
+- Disp_BerthageSetting的IsInner字段默认为1
|
|
|
|
|
+
|
|
|
|
|
+### 16.2 实现示例
|
|
|
|
|
+```java
|
|
|
|
|
+@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);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 17. CommonDataService使用规范
|
|
|
|
|
+
|
|
|
|
|
+### 17.1 为什么需要CommonDataService
|
|
|
|
|
+@DataSource注解在同一个Service类的内部方法调用时不会生效,因为Spring AOP只对代理对象的方法调用有效。当在同一个Service类内部调用方法时,不会经过代理,因此@DataSource注解不会生效。
|
|
|
|
|
+
|
|
|
|
|
+### 17.2 解决方案
|
|
|
|
|
+创建专门的CommonDataService来处理跨切面的数据操作,确保@DataSource注解生效:
|
|
|
|
|
+```java
|
|
|
|
|
+@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) {
|
|
|
|
|
+ // 实现逻辑...
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 17.3 调用示例
|
|
|
|
|
+```java
|
|
|
|
|
+@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...
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 18. 引航计划复制到调度表规范
|
|
|
|
|
+
|
|
|
|
|
+### 18.1 字段复制规则
|
|
|
|
|
+引航计划导入后复制到调度表时,必须复制以下字段:
|
|
|
|
|
+- **WorkTime**:从PlanTime复制
|
|
|
|
|
+- **PortID**:根据FromBerthageID和ToBerthageID计算得出
|
|
|
|
|
+- **FromBerthageID**:直接复制
|
|
|
|
|
+- **ToBerthageID**:直接复制
|
|
|
|
|
+- **ShipID**:直接复制
|
|
|
|
|
+- **Deep**:直接复制
|
|
|
|
|
+- **AgencyID**:直接复制
|
|
|
|
|
+- **PilotTypeID**:从PilotType复制
|
|
|
|
|
+- **PilotID**:从MainPilotID复制(主引)
|
|
|
|
|
+- **Remark**:直接复制(备注)
|
|
|
|
|
+- **StartTime**:根据PlanTime和PilotType计算得出
|
|
|
|
|
+- **EndTime**:根据PlanTime和PilotType计算得出
|
|
|
|
|
+
|
|
|
|
|
+### 18.2 实现示例
|
|
|
|
|
+```java
|
|
|
|
|
+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);
|
|
|
|
|
+}
|
|
|
|
|
+```
|