diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ExceptionDisposalUpdateRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ExceptionDisposalUpdateRequest.java new file mode 100644 index 00000000..b1f233f8 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/ExceptionDisposalUpdateRequest.java @@ -0,0 +1,63 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *
+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *
+ * 1. This software is for development use only under a valid license + * from BladeX. + *
+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *
+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *
+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *
+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 异常处置编辑请求 + *
+ * 只承载上报四件套。车牌、项目、承运商、配载单号等字段由上报时从运单回填, + * 不在此列,避免编辑后与运单脱钩。 + *
+ * 不含处置状态:状态只能经跟进与完成流转,编辑不得改变它。
+ *
+ * @author Chill
+ */
+@Data
+@Schema(description = "异常处置编辑请求")
+public class ExceptionDisposalUpdateRequest implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "异常处置ID")
+ private Long id;
+
+ @Schema(description = "异常类型")
+ private String exceptionType;
+
+ @Schema(description = "异常原因")
+ private String exceptionReason;
+
+ @Schema(description = "上报说明")
+ private String reportDescription;
+
+ @Schema(description = "现场照片,逗号分隔")
+ private String scenePhotos;
+
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ExceptionDisposalController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ExceptionDisposalController.java
index 2fc52aa9..855f37ec 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ExceptionDisposalController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ExceptionDisposalController.java
@@ -30,11 +30,14 @@ import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
+import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
+import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
import org.springblade.transport.pojo.entity.ExceptionDisposal;
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
import org.springblade.transport.service.IExceptionDisposalService;
+import org.springblade.transport.support.ExceptionDisposalPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -102,4 +105,13 @@ public class ExceptionDisposalController extends BladeController {
return R.success("批量完成成功");
}
+ @PostMapping("/update")
+ @PreAuth(ExceptionDisposalPermissions.EDIT)
+ @ApiOperationSupport(order = 7)
+ @Operation(summary = "编辑异常处置", description = "运管员订正上报内容;不改动处置状态")
+ public R update(@RequestBody ExceptionDisposalUpdateRequest request) {
+ exceptionDisposalService.update(request);
+ return R.success("编辑成功");
+ }
+
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IExceptionDisposalService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IExceptionDisposalService.java
index b04ee264..d23ca19c 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IExceptionDisposalService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IExceptionDisposalService.java
@@ -25,6 +25,7 @@ package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
+import org.springblade.transport.pojo.dto.ExceptionDisposalUpdateRequest;
import org.springblade.transport.pojo.entity.ExceptionDisposal;
import org.springblade.transport.pojo.vo.ExceptionDisposalVO;
@@ -44,6 +45,11 @@ public interface IExceptionDisposalService extends BaseService
+ * 结果长度受 latest_follow_content / follow_content 的 varchar(500) 限制,
+ * 而变更说明会内嵌修改前后的原文,长文本字段一次编辑就可能超限,故须截断。
+ */
+ private String buildChangeLog(ExceptionDisposal before, String exceptionType, String exceptionReason,
+ String reportDescription, String scenePhotos) {
+ List
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.support;
+
+/**
+ * 异常处置的权限表达式
+ *
+ * 用菜单码而不是角色名:本项目的权限走「角色-菜单」授权。
+ *
+ * 表达式由框架交给 SpEL 求值,root object 是 {@code AuthFun},
+ * 故函数名必须与它的公开方法一致({@code hasMenu})。
+ * 写成 {@code @PreAuth("exception_disposal_edit")} 这样的裸字符串会被当作
+ * 属性访问而非权限判断,请求时抛 500 而不是 403。
+ *
+ * @author Chill
+ */
+public final class ExceptionDisposalPermissions {
+
+ /**
+ * 编辑异常处置的上报内容
+ */
+ public static final String EDIT = "hasMenu('exception_disposal_edit')";
+
+ private ExceptionDisposalPermissions() {
+ }
+
+}
diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/ExceptionDisposalPermissionsTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/ExceptionDisposalPermissionsTest.java
new file mode 100644
index 00000000..f082dc22
--- /dev/null
+++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/ExceptionDisposalPermissionsTest.java
@@ -0,0 +1,73 @@
+package org.springblade.transport.support;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springblade.core.secure.auth.AuthFun;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * 异常处置权限表达式的可求值性测试。
+ *
+ * 表达式写在 {@code @PreAuth} 注解里,由框架在请求时交给 SpEL 求值,
+ * root object 是 {@code AuthFun}。写成裸字符串(如 {@code "exception_disposal_edit"})
+ * 会被当作属性访问而抛异常,表现为接口 500 而不是 403——用测试把它提前到构建期。
+ *
+ * @author Chill
+ */
+class ExceptionDisposalPermissionsTest {
+
+ private final SpelExpressionParser parser = new SpelExpressionParser();
+
+ @Test
+ @DisplayName("编辑权限:拥有编辑菜单即通过")
+ void editWithMenu() {
+ AuthFun authFun = mock(AuthFun.class);
+ when(authFun.hasMenu("exception_disposal_edit")).thenReturn(true);
+
+ assertTrue(evaluate(ExceptionDisposalPermissions.EDIT, authFun),
+ "授予编辑菜单的用户应能编辑");
+ }
+
+ @Test
+ @DisplayName("编辑权限:没有编辑菜单则拒绝")
+ void editWithoutMenu() {
+ assertFalse(evaluate(ExceptionDisposalPermissions.EDIT, mock(AuthFun.class)),
+ "未授予编辑菜单的用户不得编辑");
+ }
+
+ @Test
+ @DisplayName("编辑权限:只有查看/跟进菜单不能编辑")
+ void editRequiresDedicatedMenu() {
+ AuthFun authFun = mock(AuthFun.class);
+ when(authFun.hasMenu("exception_disposal_view")).thenReturn(true);
+ when(authFun.hasMenu("exception_disposal_follow")).thenReturn(true);
+ when(authFun.hasMenu("exception_disposal_complete")).thenReturn(true);
+
+ assertFalse(evaluate(ExceptionDisposalPermissions.EDIT, authFun),
+ "能跟进、能完成不等于能编辑");
+ }
+
+ @Test
+ @DisplayName("表达式必须引用真实存在的菜单码")
+ void expressionReferencesRealMenuCode() {
+ assertTrue(ExceptionDisposalPermissions.EDIT.contains("exception_disposal_edit"));
+ assertFalse(ExceptionDisposalPermissions.EDIT.contains("hasAnyRole"));
+ }
+
+ /**
+ * 用与框架相同的方式求值:AuthFun 作为 root object
+ */
+ private boolean evaluate(String expression, AuthFun authFun) {
+ Expression parsed = parser.parseExpression(expression);
+ StandardEvaluationContext context = new StandardEvaluationContext(authFun);
+ return Boolean.TRUE.equals(parsed.getValue(context, Boolean.class));
+ }
+
+}
diff --git a/doc/sql/transport/blade_exception_disposal_edit_menu_20261010.sql b/doc/sql/transport/blade_exception_disposal_edit_menu_20261010.sql
new file mode 100644
index 00000000..51ad9dfb
--- /dev/null
+++ b/doc/sql/transport/blade_exception_disposal_edit_menu_20261010.sql
@@ -0,0 +1,33 @@
+-- 异常处置新增编辑权限码
+-- 日期:2026-10-10
+
+INSERT INTO `blade_menu`
+ (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`)
+VALUES
+ (2090000000000002006,2090000000000002001,'exception_disposal_edit','编辑','exception_disposal_edit','','',5,2,0,1,NULL,'',0)
+ON DUPLICATE KEY UPDATE
+ `parent_id`=VALUES(`parent_id`),
+ `name`=VALUES(`name`),
+ `path`=VALUES(`path`),
+ `sort`=VALUES(`sort`),
+ `is_deleted`=0;
+
+-- 授权给超级管理员角色。
+-- 不能按「持有 exception_disposal 菜单的角色」批量授权:司机角色同样持有该菜单,
+-- 会被一并授予编辑权——而编辑是运管员才有的操作。
+-- 注意:运管角色当前并未持有 exception_disposal 菜单,故此处不涉及它;
+-- 若日后把异常处置开放给运管,需另行授予本编辑码。
+INSERT INTO `blade_role_menu` (`id`, `menu_id`, `role_id`)
+SELECT 2090000000000002006 + (r.`id` % 1000), 2090000000000002006, r.`id`
+FROM `blade_role` r
+WHERE r.`role_alias` = 'administrator'
+ AND NOT EXISTS (
+ SELECT 1 FROM `blade_role_menu` x
+ WHERE x.`menu_id` = 2090000000000002006 AND x.`role_id` = r.`id`
+ );
+
+-- 幂等收口:本编辑码只应挂在超级管理员上,其余角色一律收回。
+-- 上面的 SELECT 是按授权规则下发,这条是按同一规则回收,二者合起来可重复执行。
+DELETE FROM `blade_role_menu`
+WHERE `menu_id` = 2090000000000002006
+ AND `role_id` NOT IN (SELECT `id` FROM `blade_role` WHERE `role_alias` = 'administrator');