diff --git a/ruoyi-admin/src/main/java/com/ruoyi/erp/push/controller/ErpPushExceptionController.java b/ruoyi-admin/src/main/java/com/ruoyi/erp/push/controller/ErpPushExceptionController.java new file mode 100644 index 0000000..46e9372 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/erp/push/controller/ErpPushExceptionController.java @@ -0,0 +1,46 @@ +package com.ruoyi.erp.push.controller; + +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.erp.push.domain.ErpPushException; +import com.ruoyi.erp.push.domain.ErpPushRetryRequest; +import com.ruoyi.erp.push.service.IErpPushExceptionService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/erp/push-exception") +public class ErpPushExceptionController extends BaseController { + private final IErpPushExceptionService service; + + public ErpPushExceptionController(IErpPushExceptionService service) { + this.service = service; + } + + @PreAuthorize("@ss.hasPermi('erp:pushException:list')") + @GetMapping("/list") + public TableDataInfo list(ErpPushException query) { + startPage(); + List list = service.selectFailureList(query); + return getDataTable(list); + } + + @PreAuthorize("@ss.hasPermi('erp:pushException:retry')") + @Log(title = "ERP推送异常重推", businessType = BusinessType.UPDATE) + @PostMapping("/retry") + public AjaxResult retry(@RequestBody ErpPushRetryRequest request) { + if (service.retry(request)) { + return AjaxResult.success("已加入重试队列"); + } + return AjaxResult.error("单据状态已变化,可能已被处理,请刷新列表"); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/erp/push/domain/ErpPushException.java b/ruoyi-admin/src/main/java/com/ruoyi/erp/push/domain/ErpPushException.java new file mode 100644 index 0000000..89523ee --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/erp/push/domain/ErpPushException.java @@ -0,0 +1,63 @@ +package com.ruoyi.erp.push.domain; + +import com.ruoyi.common.core.domain.BaseEntity; + +public class ErpPushException extends BaseEntity { + private static final long serialVersionUID = 1L; + + private String documentType; + private String recordId; + private String documentNo; + private String businessReference; + private String failureTime; + private String errorMessage; + + public String getDocumentType() { + return documentType; + } + + public void setDocumentType(String documentType) { + this.documentType = documentType; + } + + public String getRecordId() { + return recordId; + } + + public void setRecordId(String recordId) { + this.recordId = recordId; + } + + public String getDocumentNo() { + return documentNo; + } + + public void setDocumentNo(String documentNo) { + this.documentNo = documentNo; + } + + public String getBusinessReference() { + return businessReference; + } + + public void setBusinessReference(String businessReference) { + this.businessReference = businessReference; + } + + public String getFailureTime() { + return failureTime; + } + + public void setFailureTime(String failureTime) { + this.failureTime = failureTime; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/erp/push/domain/ErpPushRetryRequest.java b/ruoyi-admin/src/main/java/com/ruoyi/erp/push/domain/ErpPushRetryRequest.java new file mode 100644 index 0000000..678481b --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/erp/push/domain/ErpPushRetryRequest.java @@ -0,0 +1,22 @@ +package com.ruoyi.erp.push.domain; + +public class ErpPushRetryRequest { + private String documentType; + private String recordId; + + public String getDocumentType() { + return documentType; + } + + public void setDocumentType(String documentType) { + this.documentType = documentType; + } + + public String getRecordId() { + return recordId; + } + + public void setRecordId(String recordId) { + this.recordId = recordId; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/erp/push/mapper/ErpPushExceptionMapper.java b/ruoyi-admin/src/main/java/com/ruoyi/erp/push/mapper/ErpPushExceptionMapper.java new file mode 100644 index 0000000..caf801d --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/erp/push/mapper/ErpPushExceptionMapper.java @@ -0,0 +1,20 @@ +package com.ruoyi.erp.push.mapper; + +import com.ruoyi.erp.push.domain.ErpPushException; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface ErpPushExceptionMapper { + List selectFailureList(ErpPushException query); + + int requeueInboundHeader(@Param("recordId") String recordId); + + int requeueInboundDetails(@Param("recordId") String recordId); + + int requeueOutboundHeader(@Param("recordId") String recordId); + + int requeueOutboundDetails(@Param("recordId") String recordId); + + int requeueMove(@Param("recordId") String recordId); +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/erp/push/service/IErpPushExceptionService.java b/ruoyi-admin/src/main/java/com/ruoyi/erp/push/service/IErpPushExceptionService.java new file mode 100644 index 0000000..20404f5 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/erp/push/service/IErpPushExceptionService.java @@ -0,0 +1,12 @@ +package com.ruoyi.erp.push.service; + +import com.ruoyi.erp.push.domain.ErpPushException; +import com.ruoyi.erp.push.domain.ErpPushRetryRequest; + +import java.util.List; + +public interface IErpPushExceptionService { + List selectFailureList(ErpPushException query); + + boolean retry(ErpPushRetryRequest request); +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/erp/push/service/impl/ErpPushExceptionServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/erp/push/service/impl/ErpPushExceptionServiceImpl.java new file mode 100644 index 0000000..6acd657 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/erp/push/service/impl/ErpPushExceptionServiceImpl.java @@ -0,0 +1,69 @@ +package com.ruoyi.erp.push.service.impl; + +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.erp.push.domain.ErpPushException; +import com.ruoyi.erp.push.domain.ErpPushRetryRequest; +import com.ruoyi.erp.push.mapper.ErpPushExceptionMapper; +import com.ruoyi.erp.push.service.IErpPushExceptionService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Locale; + +@Service +public class ErpPushExceptionServiceImpl implements IErpPushExceptionService { + private final ErpPushExceptionMapper mapper; + + public ErpPushExceptionServiceImpl(ErpPushExceptionMapper mapper) { + this.mapper = mapper; + } + + @Override + public List selectFailureList(ErpPushException query) { + return mapper.selectFailureList(query); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean retry(ErpPushRetryRequest request) { + if (request == null || StringUtils.isEmpty(request.getDocumentType()) + || StringUtils.isEmpty(request.getRecordId())) { + throw new ServiceException("单据类型和记录编号不能为空"); + } + + String type = request.getDocumentType().trim().toUpperCase(Locale.ROOT); + String recordId = request.getRecordId().trim(); + switch (type) { + case "INBOUND": + return retryInbound(recordId); + case "OUTBOUND": + return retryOutbound(recordId); + case "MOVE": + return mapper.requeueMove(recordId) == 1; + default: + throw new ServiceException("不支持的单据类型: " + request.getDocumentType()); + } + } + + private boolean retryInbound(String recordId) { + if (mapper.requeueInboundHeader(recordId) != 1) { + return false; + } + if (mapper.requeueInboundDetails(recordId) < 1) { + throw new ServiceException("入库单缺少可重推的失败明细,已取消本次操作"); + } + return true; + } + + private boolean retryOutbound(String recordId) { + if (mapper.requeueOutboundHeader(recordId) != 1) { + return false; + } + if (mapper.requeueOutboundDetails(recordId) < 1) { + throw new ServiceException("出库单缺少可重推的失败明细,已取消本次操作"); + } + return true; + } +} diff --git a/ruoyi-admin/src/main/resources/mapper/erp/ErpPushExceptionMapper.xml b/ruoyi-admin/src/main/resources/mapper/erp/ErpPushExceptionMapper.xml new file mode 100644 index 0000000..3337edf --- /dev/null +++ b/ruoyi-admin/src/main/resources/mapper/erp/ErpPushExceptionMapper.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + SELECT 'INBOUND' AS document_type, + CONVERT(nvarchar(100), stockin_guid) AS record_id, + CONVERT(nvarchar(100), stockin_id) AS document_no, + CONVERT(nvarchar(300), stockin_id) AS business_reference, + CONVERT(nvarchar(30), send_time) AS failure_time, + CONVERT(nvarchar(max), msg) AS error_message + FROM in_stockin_back + WHERE send_status = '2' AND stock_source = '1' + AND TRY_CONVERT(datetime2, send_time, 120) >= DATEADD(month, -1, SYSDATETIME()) + UNION ALL + SELECT 'OUTBOUND', + CONVERT(nvarchar(100), stockout_guid), + CONVERT(nvarchar(100), stockout_id), + CONVERT(nvarchar(300), stockout_id), + CONVERT(nvarchar(30), send_time), + CONVERT(nvarchar(max), msg) + FROM out_stockout_back + WHERE send_status = '2' AND stockout_source = '1' + AND TRY_CONVERT(datetime2, send_time, 120) >= DATEADD(month, -1, SYSDATETIME()) + UNION ALL + SELECT 'MOVE', + CONVERT(nvarchar(100), id), + CONVERT(nvarchar(100), id), + CONCAT(ISNULL(tray_code, ''), ' ', ISNULL(old_space, ''), ' -> ', ISNULL(new_space, '')), + CONVERT(nvarchar(30), send_time), + CONVERT(nvarchar(max), msg) + FROM move_msg + WHERE status = '2' + AND TRY_CONVERT(datetime2, send_time, 120) >= DATEADD(month, -1, SYSDATETIME()) + + + + + + UPDATE in_stockin_back + SET send_status = '0', send_time = NULL, msg = N'已手动加入重试队列' + WHERE stockin_guid = #{recordId} AND send_status = '2' AND stock_source = '1' + AND TRY_CONVERT(datetime2, send_time, 120) >= DATEADD(month, -1, SYSDATETIME()) + + + + UPDATE in_stockin_detail_back + SET send_status = '0', send_time = NULL, msg = N'已手动加入重试队列' + WHERE stockin_guid = #{recordId} AND send_status = '2' + AND TRY_CONVERT(datetime2, send_time, 120) >= DATEADD(month, -1, SYSDATETIME()) + + + + UPDATE out_stockout_back + SET send_status = '0', send_time = NULL, msg = N'已手动加入重试队列' + WHERE stockout_guid = #{recordId} AND send_status = '2' AND stockout_source = '1' + AND TRY_CONVERT(datetime2, send_time, 120) >= DATEADD(month, -1, SYSDATETIME()) + + + + UPDATE out_stockout_detail_back + SET send_status = '0', send_time = NULL, msg = N'已手动加入重试队列' + WHERE stockout_guid = #{recordId} AND send_status = '2' + AND TRY_CONVERT(datetime2, send_time, 120) >= DATEADD(month, -1, SYSDATETIME()) + + + + UPDATE move_msg + SET status = '0', send_time = NULL, msg = N'已手动加入重试队列' + WHERE id = #{recordId} AND status = '2' + AND TRY_CONVERT(datetime2, send_time, 120) >= DATEADD(month, -1, SYSDATETIME()) + + diff --git a/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowV3OperationsPackageTest.java b/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowV3OperationsPackageTest.java index f349222..f647de1 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowV3OperationsPackageTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/algorithm/shadow/ShadowV3OperationsPackageTest.java @@ -292,6 +292,10 @@ class ShadowV3OperationsPackageTest { CmdResult result = runProcess(new ProcessBuilder( "git", "clone", "--quiet", root.toString(), destination.toString()), 60); assertThat(result.exitCode()).as(result.output()).isZero(); + CmdResult branch = runProcess(new ProcessBuilder( + "git", "-C", destination.toString(), "branch", "-m", + "test/ft546wms-shadow-v3-20260903"), 15); + assertThat(branch.exitCode()).as(branch.output()).isZero(); } private String gitHead(Path repository) throws Exception { diff --git a/ruoyi-admin/src/test/java/com/ruoyi/erp/push/mapper/ErpPushExceptionMapperSqlTest.java b/ruoyi-admin/src/test/java/com/ruoyi/erp/push/mapper/ErpPushExceptionMapperSqlTest.java new file mode 100644 index 0000000..2c4491c --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/erp/push/mapper/ErpPushExceptionMapperSqlTest.java @@ -0,0 +1,81 @@ +package com.ruoyi.erp.push.mapper; + +import com.ruoyi.erp.push.domain.ErpPushException; +import org.apache.ibatis.builder.xml.XMLMapperBuilder; +import org.apache.ibatis.session.Configuration; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; + +class ErpPushExceptionMapperSqlTest { + private static final String RESOURCE = "mapper/erp/ErpPushExceptionMapper.xml"; + private static final String NAMESPACE = + "com.ruoyi.erp.push.mapper.ErpPushExceptionMapper."; + private static final Pattern FROM_TABLE = Pattern.compile("\\bfrom\\s+([a-z0-9_]+)"); + private static final String RECENT_MONTH_PREDICATE = + "try_convert(datetime2, send_time, 120) >= dateadd(month, -1, sysdatetime())"; + + @Test + void failureListReadsOnlyRecentCurrentPushDocumentTables() throws Exception { + String sql = boundSql("selectFailureList"); + + assertThat(fromTables(sql)).containsExactly( + "in_stockin_back", + "out_stockout_back", + "move_msg"); + assertThat(occurrences(sql, RECENT_MONTH_PREDICATE)).isEqualTo(3); + assertThat(sql).doesNotContain("request_log", "history", "_finish"); + } + + @Test + void retryUpdatesOnlyRequeueCurrentFailedDocumentsFromTheRecentMonth() throws Exception { + assertThat(boundSql("requeueInboundHeader")) + .contains("update in_stockin_back", "set send_status = '0'", "send_status = '2'", + "stock_source = '1'", RECENT_MONTH_PREDICATE); + assertThat(boundSql("requeueInboundDetails")) + .contains("update in_stockin_detail_back", "set send_status = '0'", "send_status = '2'", + RECENT_MONTH_PREDICATE); + assertThat(boundSql("requeueOutboundHeader")) + .contains("update out_stockout_back", "set send_status = '0'", "send_status = '2'", + "stockout_source = '1'", RECENT_MONTH_PREDICATE); + assertThat(boundSql("requeueOutboundDetails")) + .contains("update out_stockout_detail_back", "set send_status = '0'", "send_status = '2'", + RECENT_MONTH_PREDICATE); + assertThat(boundSql("requeueMove")) + .contains("update move_msg", "set status = '0'", "status = '2'", RECENT_MONTH_PREDICATE); + } + + private String boundSql(String statementId) throws Exception { + Configuration configuration = new Configuration(); + try (InputStream stream = Objects.requireNonNull( + getClass().getClassLoader().getResourceAsStream(RESOURCE), RESOURCE)) { + new XMLMapperBuilder(stream, configuration, RESOURCE, configuration.getSqlFragments()).parse(); + } + return configuration.getMappedStatement(NAMESPACE + statementId) + .getBoundSql(new ErpPushException()) + .getSql() + .replaceAll("\\s+", " ") + .toLowerCase(Locale.ROOT); + } + + private List fromTables(String sql) { + List tables = new ArrayList<>(); + Matcher matcher = FROM_TABLE.matcher(sql); + while (matcher.find()) { + tables.add(matcher.group(1)); + } + return tables; + } + + private int occurrences(String value, String token) { + return (value.length() - value.replace(token, "").length()) / token.length(); + } +} diff --git a/ruoyi-admin/src/test/java/com/ruoyi/erp/push/service/ErpPushExceptionServiceTest.java b/ruoyi-admin/src/test/java/com/ruoyi/erp/push/service/ErpPushExceptionServiceTest.java new file mode 100644 index 0000000..50ba406 --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/erp/push/service/ErpPushExceptionServiceTest.java @@ -0,0 +1,100 @@ +package com.ruoyi.erp.push.service; + +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.erp.push.domain.ErpPushRetryRequest; +import com.ruoyi.erp.push.mapper.ErpPushExceptionMapper; +import com.ruoyi.erp.push.service.impl.ErpPushExceptionServiceImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ErpPushExceptionServiceTest { + @Mock + private ErpPushExceptionMapper mapper; + + private ErpPushExceptionServiceImpl service; + + @BeforeEach + void setUp() { + service = new ErpPushExceptionServiceImpl(mapper); + } + + @Test + void requeuesInboundHeaderAndDetailsTogether() { + when(mapper.requeueInboundHeader("IN-GUID")).thenReturn(1); + when(mapper.requeueInboundDetails("IN-GUID")).thenReturn(2); + + assertTrue(service.retry(request("INBOUND", "IN-GUID"))); + + verify(mapper).requeueInboundHeader("IN-GUID"); + verify(mapper).requeueInboundDetails("IN-GUID"); + } + + @Test + void rejectsInboundRetryWhenNoFailedDetailsExist() { + when(mapper.requeueInboundHeader("IN-GUID")).thenReturn(1); + when(mapper.requeueInboundDetails("IN-GUID")).thenReturn(0); + + assertThrows(ServiceException.class, + () -> service.retry(request("INBOUND", "IN-GUID"))); + } + + @Test + void staleFailureIsNotRequeuedTwice() { + when(mapper.requeueOutboundHeader("OUT-GUID")).thenReturn(0); + + assertFalse(service.retry(request("OUTBOUND", "OUT-GUID"))); + + verify(mapper, never()).requeueOutboundDetails("OUT-GUID"); + } + + @Test + void requeuesOutboundHeaderAndDetailsTogether() { + when(mapper.requeueOutboundHeader("OUT-GUID")).thenReturn(1); + when(mapper.requeueOutboundDetails("OUT-GUID")).thenReturn(3); + + assertTrue(service.retry(request("OUTBOUND", "OUT-GUID"))); + + verify(mapper).requeueOutboundDetails("OUT-GUID"); + } + + @Test + void requeuesMoveWithoutTouchingDocumentDetails() { + when(mapper.requeueMove("MOVE-ID")).thenReturn(1); + + assertTrue(service.retry(request("MOVE", "MOVE-ID"))); + + verify(mapper).requeueMove("MOVE-ID"); + } + + @Test + void rejectsUnknownDocumentType() { + assertThrows(ServiceException.class, + () -> service.retry(request("OTHER", "ID"))); + } + + @Test + void rejectsBlankDocumentIdentityBeforeCallingMapper() { + assertThrows(ServiceException.class, + () -> service.retry(request("INBOUND", " "))); + + verify(mapper, never()).requeueInboundHeader(org.mockito.ArgumentMatchers.anyString()); + } + + private ErpPushRetryRequest request(String documentType, String recordId) { + ErpPushRetryRequest request = new ErpPushRetryRequest(); + request.setDocumentType(documentType); + request.setRecordId(recordId); + return request; + } +} diff --git a/ruoyi-ui/src/api/erp/pushException.js b/ruoyi-ui/src/api/erp/pushException.js new file mode 100644 index 0000000..f778d0b --- /dev/null +++ b/ruoyi-ui/src/api/erp/pushException.js @@ -0,0 +1,17 @@ +import request from '@/utils/request' + +export function listPushExceptions(query) { + return request({ + url: '/erp/push-exception/list', + method: 'get', + params: query + }) +} + +export function retryPushException(data) { + return request({ + url: '/erp/push-exception/retry', + method: 'post', + data + }) +} diff --git a/ruoyi-ui/src/views/erp/pushException/index.vue b/ruoyi-ui/src/views/erp/pushException/index.vue new file mode 100644 index 0000000..6fd949e --- /dev/null +++ b/ruoyi-ui/src/views/erp/pushException/index.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/sql/upgrade-erp-push-exception-menu.sql b/sql/upgrade-erp-push-exception-menu.sql new file mode 100644 index 0000000..0506ef2 --- /dev/null +++ b/sql/upgrade-erp-push-exception-menu.sql @@ -0,0 +1,86 @@ +SET NOCOUNT ON; +SET XACT_ABORT ON; + +BEGIN TRANSACTION; + +DECLARE @menu_id bigint; +DECLARE @retry_id bigint; +DECLARE @source_menu_id bigint; +DECLARE @lock_result int; + +EXEC @lock_result = sys.sp_getapplock + @Resource = N'FT546WMS:upgrade-erp-push-exception-menu', + @LockMode = N'Exclusive', + @LockOwner = N'Transaction', + @LockTimeout = 10000; + +IF @lock_result < 0 +BEGIN + ROLLBACK TRANSACTION; + THROW 51000, N'无法获取 ERP 推送异常菜单升级锁', 1; +END; + +SELECT TOP (1) @menu_id = menu_id +FROM dbo.sys_menu +WHERE parent_id = 2 AND path = N'pushException' +ORDER BY menu_id; + +IF @menu_id IS NULL +BEGIN + SELECT @menu_id = ISNULL(MAX(menu_id), 0) + 1 FROM dbo.sys_menu WITH (UPDLOCK, HOLDLOCK); + + INSERT INTO dbo.sys_menu + (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, + is_frame, is_cache, menu_type, visible, status, perms, icon, + create_by, create_time, update_by, update_time, remark) + VALUES + (@menu_id, N'ERP推送异常', 2, 2, N'pushException', N'erp/pushException/index', NULL, N'', + N'1', N'0', N'C', N'0', N'0', N'erp:pushException:list', N'warning', + N'admin', SYSDATETIME(), N'', NULL, N'ERP回传失败列表'); +END; + +SELECT TOP (1) @retry_id = menu_id +FROM dbo.sys_menu +WHERE parent_id = @menu_id AND perms = N'erp:pushException:retry' +ORDER BY menu_id; + +IF @retry_id IS NULL +BEGIN + SELECT @retry_id = ISNULL(MAX(menu_id), 0) + 1 FROM dbo.sys_menu WITH (UPDLOCK, HOLDLOCK); + + INSERT INTO dbo.sys_menu + (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, + is_frame, is_cache, menu_type, visible, status, perms, icon, + create_by, create_time, update_by, update_time, remark) + VALUES + (@retry_id, N'ERP异常重推', @menu_id, 1, N'#', N'', NULL, N'', + N'1', N'0', N'F', N'0', N'0', N'erp:pushException:retry', N'#', + N'admin', SYSDATETIME(), N'', NULL, N'失败单据重新加入ERP回传队列'); +END; + +SELECT TOP (1) @source_menu_id = menu_id +FROM dbo.sys_menu +WHERE perms = N'requestInterface:log:list' +ORDER BY menu_id; + +INSERT INTO dbo.sys_role_menu (role_id, menu_id) +SELECT existing.role_id, @menu_id +FROM dbo.sys_role_menu existing +WHERE existing.menu_id = @source_menu_id + AND NOT EXISTS ( + SELECT 1 FROM dbo.sys_role_menu target + WHERE target.role_id = existing.role_id AND target.menu_id = @menu_id + ); + +INSERT INTO dbo.sys_role_menu (role_id, menu_id) +SELECT existing.role_id, @retry_id +FROM dbo.sys_role_menu existing +WHERE existing.menu_id = @source_menu_id + AND NOT EXISTS ( + SELECT 1 FROM dbo.sys_role_menu target + WHERE target.role_id = existing.role_id AND target.menu_id = @retry_id + ); + +COMMIT TRANSACTION; + +SELECT N'ERP_PUSH_EXCEPTION_MENU_OK' AS result, @menu_id AS menu_id, @retry_id AS retry_menu_id;