Bläddra i källkod

更新最佳实践文档,添加最近解决的问题

## 新增章节

### 14. 前端认证错误处理规范
- 401和403错误处理逻辑:自动清除token并重定向到登录页面
- 请求拦截器配置:在请求头中添加Bearer token

### 15. 客户名称显示规范
- 客户名称显示逻辑:代理公司名+空格+船公司名
- 后端实现示例:使用JdbcTemplate动态拼接SQL查询

### 16. 引航计划PortID填充规范
- 五步法PortID填充逻辑:从字典匹配到锚地处理
- 实现示例:完整的findOrCreateBerthageInfo方法实现

### 17. CommonDataService使用规范
- 为什么需要CommonDataService:解决@DataSource注解在内部方法调用不生效的问题
- 解决方案:创建专门的CommonDataService处理跨切面数据操作

### 18. 引航计划复制到调度表规范
- 字段复制规则:包括主引和备注的复制
- 实现示例:完整的syncToDispatcherTable方法实现

## 文档价值
这些新增章节记录了最近解决的重要问题,为后续开发提供了参考和规范。
heyiwen 1 vecka sedan
förälder
incheckning
ade707b06a
1 ändrade filer med 315 tillägg och 0 borttagningar
  1. 315 0
      .trae/rules/backend-dev-best-practices.md

+ 315 - 0
.trae/rules/backend-dev-best-practices.md

@@ -479,3 +479,318 @@ public class UserService {
 8. **记录日志**:重要操作必须记录日志
 9. **处理异常**:使用RuntimeException抛出业务异常,前端统一处理错误提示
 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);
+}
+```