$ErrorActionPreference = 'Stop' $root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path $prepared = Join-Path $PSScriptRoot 'prepared' $docDir = Join-Path $root 'doc' $desktop = [Environment]::GetFolderPath('Desktop') $fileName = '大连元利流体技术有限公司MES系统_系统管理问题清单_2026-07-23' $docxPath = Join-Path $docDir ($fileName + '.docx') $pdfPath = Join-Path $docDir ($fileName + '.pdf') $desktopDocx = Join-Path $desktop ($fileName + '.docx') $desktopPdf = Join-Path $desktop ($fileName + '.pdf') New-Item -ItemType Directory -Force -Path $docDir | Out-Null $word = $null $doc = $null $buildError = $null try { $word = New-Object -ComObject Word.Application $word.Visible = $false $word.DisplayAlerts = 0 $doc = $word.Documents.Add() $selection = $word.Selection $doc.PageSetup.PageWidth = $word.CentimetersToPoints(21) $doc.PageSetup.PageHeight = $word.CentimetersToPoints(29.7) $doc.PageSetup.TopMargin = $word.CentimetersToPoints(2.2) $doc.PageSetup.BottomMargin = $word.CentimetersToPoints(2.0) $doc.PageSetup.LeftMargin = $word.CentimetersToPoints(2.1) $doc.PageSetup.RightMargin = $word.CentimetersToPoints(2.1) $normal = $doc.Styles.Item(-1) $normal.Font.NameFarEast = '宋体' $normal.Font.Name = 'Arial' $normal.Font.Size = 10.5 $normal.ParagraphFormat.SpaceAfter = 6 function Move-End { $end = $doc.Content.End - 1 $selection.SetRange($end, $end) } function Add-Text { param( [string]$Text, [double]$Size = 10.5, [bool]$Bold = $false, [int]$Align = 0, [int]$Color = 0, [double]$Before = 0, [double]$After = 6, [double]$FirstIndentCm = 0 ) $selection.Style = $normal $selection.Font.NameFarEast = '宋体' $selection.Font.Name = 'Arial' $selection.Font.Size = $Size $selection.Font.Bold = [int]$Bold $selection.Font.Color = $Color $selection.ParagraphFormat.Alignment = $Align $selection.ParagraphFormat.SpaceBefore = $Before $selection.ParagraphFormat.SpaceAfter = $After $selection.ParagraphFormat.FirstLineIndent = $word.CentimetersToPoints($FirstIndentCm) $selection.TypeText($Text) $selection.TypeParagraph() } function Add-Heading { param([string]$Text, [int]$Level) $styleId = switch ($Level) { 1 { -2 } 2 { -3 } default { -4 } } $selection.Style = $doc.Styles.Item($styleId) $selection.Font.NameFarEast = '微软雅黑' $selection.Font.Name = 'Arial' $selection.Font.Color = 0x8F4A31 $selection.Font.Bold = 1 $selection.ParagraphFormat.KeepWithNext = -1 $selection.TypeText($Text) $selection.TypeParagraph() $selection.Style = $normal } function Add-StepList { param([string[]]$Items) for ($i = 0; $i -lt $Items.Count; $i++) { Add-Text (('{0}. {1}' -f ($i + 1), $Items[$i])) 10.5 $false 0 0 0 4 0 } } function Add-ItemList { param([string[]]$Items) for ($i = 0; $i -lt $Items.Count; $i++) { Add-Text ('(' + ($i + 1) + ')' + $Items[$i]) 10.5 $false 0 0 0 4 0 } } function Add-Table { param([object[]]$Rows, [double[]]$WidthsCm = @()) $table = $doc.Tables.Add($selection.Range, $Rows.Count, $Rows[0].Count) $table.Borders.Enable = 1 $table.Range.Font.NameFarEast = '宋体' $table.Range.Font.Name = 'Arial' $table.Range.Font.Size = 8.8 $table.Range.ParagraphFormat.SpaceAfter = 0 for ($r = 0; $r -lt $Rows.Count; $r++) { for ($c = 0; $c -lt $Rows[$r].Count; $c++) { $table.Cell($r + 1, $c + 1).Range.Text = [string]$Rows[$r][$c] $table.Cell($r + 1, $c + 1).VerticalAlignment = 1 if ($WidthsCm.Count -gt $c) { $table.Cell($r + 1, $c + 1).Width = $word.CentimetersToPoints($WidthsCm[$c]) } } } $table.Rows.Item(1).Range.Bold = 1 $table.Rows.Item(1).Shading.BackgroundPatternColor = 0xE6D8D0 $table.Rows.Item(1).Range.ParagraphFormat.Alignment = 1 $table.AllowAutoFit = -1 Move-End $selection.TypeParagraph() } function Add-Note { param([string]$Title, [string]$Text, [string]$Kind = 'info') $table = $doc.Tables.Add($selection.Range, 1, 1) $cell = $table.Cell(1, 1) $table.Borders.Enable = 1 $cell.Range.Text = $Title + ':' + $Text $cell.Range.Font.NameFarEast = '微软雅黑' $cell.Range.Font.Size = 9.5 $cell.Range.ParagraphFormat.SpaceAfter = 0 if ($Kind -eq 'danger') { $cell.Shading.BackgroundPatternColor = 0xD9E2F3 } elseif ($Kind -eq 'warn') { $cell.Shading.BackgroundPatternColor = 0xD9F2FF } else { $cell.Shading.BackgroundPatternColor = 0xF4F1EE } Move-End $selection.TypeParagraph() } function Add-Figure { param([string]$Path, [string]$Caption, [double]$MaxWidthCm = 16.4) $selection.ParagraphFormat.Alignment = 1 $shape = $selection.InlineShapes.AddPicture($Path, $false, $true, $selection.Range) $shape.LockAspectRatio = -1 $maxWidth = $word.CentimetersToPoints($MaxWidthCm) if ($shape.Width -gt $maxWidth) { $shape.Width = $maxWidth } $selection.SetRange($shape.Range.End, $shape.Range.End) $selection.TypeParagraph() Add-Text $Caption 9 $false 1 0x777777 0 8 0 } function Add-Issue { param( [string]$Id, [string]$Title, [string]$Priority, [string]$Status, [string]$Description, [string]$Evidence, [string]$Impact, [string]$Recommendation, [string]$Verification ) Add-Heading ($Id + ' ' + $Title) 2 Add-Table @( @('优先级', '确认状态', '涉及模块'), @($Priority, $Status, $Id.Split('-')[0]) ) @(4, 5, 8) Add-Text ('问题描述:' + $Description) 10.5 $false 0 0 0 5 0 Add-Text ('证据:' + $Evidence) 10 $false 0 0x555555 0 5 0 Add-Text ('影响:' + $Impact) 10.5 $false 0 0 0 5 0 Add-Text ('建议:' + $Recommendation) 10.5 $false 0 0 0 5 0 Add-Text ('验证建议:' + $Verification) 10 $false 0 0x555555 0 9 0 } function Add-PageBreak { $selection.InsertBreak(7) } Add-Text '' 12 $false 1 0 0 18 0 Add-Text '大连元利流体技术有限公司' 22 $true 1 0x8F4A31 0 12 0 Add-Text 'MES 系统管理问题清单' 30 $true 1 0x8F4A31 0 16 0 Add-Text '操作手册编制发现项(独立文档)' 19 $true 1 0x4D4D4D 0 28 0 Add-Text '版本:V1.0' 12 $false 1 0 0 8 0 Add-Text '编制日期:2026年7月23日' 12 $false 1 0 0 8 0 Add-Text '问题来源:前端代码、正式菜单、数据库对象定义及只读页面检查' 11 $false 1 0 0 24 0 Add-Note '安全声明' '本清单通过只读查询、源代码检查和页面查看形成。没有在正式环境执行新增、修改、删除、密码重置、权限分配或其他写操作。涉及写操作的问题未在正式环境提交复现。' 'warn' Add-PageBreak Add-Text '目 录' 20 $true 1 0x8F4A31 0 12 0 $toc = $doc.TablesOfContents.Add($selection.Range, $true, 1, 3, $true, '', $true, $true, '', $true, $true, $true) Move-End Add-PageBreak Add-Heading '1 文档说明' 1 Add-Text '本文件只记录系统管理篇编制过程中发现的问题和风险,不与用户操作手册正文混排。问题按业务影响和修复紧迫程度分级,最终是否立项、修复方案和上线时间应由项目负责人、业务负责人及开发人员共同确认。' 10.5 $false 0 0 0 6 0.74 Add-Table @( @('级别', '定义', '处理建议'), @('P1 高', '可能造成账号安全、跨系统数据不一致、主数据不可恢复或权限风险', '优先评审,修复前设置管理措施'), @('P2 中', '影响常用查询、维护准确性、审计完整性或容易导致误操作', '纳入近期版本,补充测试用例'), @('P3 低', '文档、可维护性或未启用代码问题,当前业务影响较小', '随版本整理或技术债处理') ) @(2.5, 9, 5.5) Add-Note '确认状态说明' '“已确认”表示代码、页面或数据库定义可直接证明;“待测试环境复现”表示风险已从实现中识别,但为避免正式环境产生数据,尚未提交写操作验证。' 'info' Add-Heading '2 问题汇总' 1 Add-Table @( @('编号', '优先级', '问题摘要', '确认状态'), @('SEC-001', 'P1', '登录和人员接口存在明文密码暴露风险,初始密码固定', '已确认'), @('MENU-001', 'P1', '菜单管理前端发送原始SQL文本,接口授权边界风险', '已确认'), @('WS-001', 'P1', '工位MES与SAP B1写入非原子,失败无补偿', '待测试环境复现'), @('WS-002', 'P1', '工位删除无占用校验,且未同步删除B1资源', '已确认'), @('WS-003', 'P2', '工位名称查询框不生效', '已确认'), @('WS-004', 'P2', '工位名称/工位号校验不完整', '已确认'), @('PER-001', 'P2', '人员“永久删除”提示与实际处理语义不一致', '已确认'), @('LOG-001', 'P2', '报工记录精确匹配、最多100条且无分页', '已确认'), @('MENU-002', 'P2', '菜单结构维护和按钮级权限界面未开放', '已确认'), @('DOC-001', 'P3', '参考手册与当前正式菜单范围不一致', '已确认'), @('WS-005', 'P3', '工位分页回调引用不存在的方法', '已确认') ) @(2.4, 2.2, 9, 3.4) Add-PageBreak Add-Heading '3 P1 高优先级问题' 1 Add-Issue 'SEC-001' '密码处理与默认密码存在安全风险' 'P1 高' '已确认' ` '人员查询响应包含密码字段;前端虽然显示为星号,仍把返回值保存到页面对象。登录页“记住账号”会把用户输入的密码写入浏览器Cookie并保留7天。新增和重置密码统一使用固定初始密码123456。' ` 'src/views/SystemMaintenance/PersonnelManagement/index.vue:279、383、460;src/views/login/index.vue:206、243-245;数据库重置过程直接写入固定密码。' ` '拥有浏览器本机访问权限、调试权限或接口访问权限的人员可能取得密码;统一初始密码会扩大弱口令和批量撞库风险。' ` '后端仅保存加盐哈希;人员查询接口不得返回密码;移除密码Cookie,记住登录应使用短期安全令牌;新账号生成随机一次性密码并强制首次登录修改;重置后立即失效其他会话。' ` '在隔离测试环境完成登录、记住账号、创建账号、重置密码和首次改密的安全测试,并检查浏览器Cookie、网络响应和数据库字段。' Add-Figure (Join-Path $prepared 'personnel-list.png') '图3-1 人员页面虽然遮蔽密码,但接口对象仍接收密码字段' Add-Issue 'MENU-001' '菜单管理由浏览器发送原始SQL文本' 'P1 高' '已确认' ` '菜单管理的角色菜单查询、一级菜单初始化和二级菜单初始化由前端把SELECT语句放在请求name字段中发送给通用接口,其中角色编号直接插入SQL字符串。' ` 'src/api/BasicData/MenuManagement.js:20-59,getModule、initFisrtLevel、initSecondLevel均使用type=3和原始SQL文本。' ` '即使正常页面只允许选择数字角色,攻击者仍可绕过页面直接构造请求。若后端缺少严格白名单和参数化限制,可能形成越权查询或SQL注入入口。' ` '改为固定后端接口或存储过程,角色编号使用强类型参数;服务器按当前用户权限校验目标角色;关闭通用任意SQL执行能力;增加安全审计和异常请求告警。' ` '由安全人员在测试环境检查通用接口是否允许修改SQL、跨角色查询和非SELECT语句,禁止在正式环境进行攻击性验证。' Add-Issue 'WS-001' '工位MES与SAP B1写入缺少一致性保障' 'P1 高' '待测试环境复现' ` '新增和编辑工位时先等待MES接口成功,然后调用postB1或patchB1,但B1调用没有await、失败处理、重试或回滚。页面在MES成功后即可提示成功并关闭弹窗。' ` 'src/views/SystemMaintenance/WorkstationManagement/index.vue:193-205、228-249。' ` 'B1超时、证书、登录态或业务校验失败时,MES已经成功而B1可能失败,形成同一工位号在两个系统中的名称或存在状态不一致。用户重复提交还可能产生重复资源。' ` '将跨系统写入放到后端编排;记录同步状态和错误详情;B1成功后再向页面返回整体成功;失败时执行补偿或进入可重试队列;按工位号实现幂等。' ` '测试环境模拟B1成功、超时、业务拒绝和重复编码四种情况,核对MES、B1和页面提示是否一致。' Add-Issue 'WS-002' '工位删除无占用校验且未同步B1' 'P1 高' '已确认' ` '工位删除存储过程中的菜单/人员占用校验代码已注释,当前直接删除MES_资源主数据;前端删除成功后没有调用SAP B1资源删除或停用接口。' ` 'src/views/SystemMaintenance/WorkstationManagement/index.vue:257-280;数据库过程“系统管理_工位管理_工位信息删除”直接DELETE,依赖检查为注释。' ` '可能删除仍被排产、在制任务、人员绑定、设备或工时使用的工位;同时B1保留资源,造成主数据不一致和历史联查异常。' ` '恢复并扩展占用校验;有历史记录时优先停用而不是物理删除;同步B1停用状态;增加删除审批、影响预览和可恢复机制。' ` '只在测试环境建立完整的排产、人员绑定和工时引用场景,验证阻止删除、停用和跨系统同步。' Add-PageBreak Add-Heading '4 P2 中优先级问题' 1 Add-Issue 'WS-003' '工位名称查询框不生效' 'P2 中' '已确认' ` '页面绑定了工位名称输入框,但searchData构造的参数数组为空,没有把RoleFunctioning传入查询接口。输入任意名称点击查询仍刷新全部工位。' ` 'src/views/SystemMaintenance/WorkstationManagement/index.vue:4-10、122、163-169;只读页面检查结果与代码一致。' ` '工位数量增多后无法快速定位,用户可能误以为没有匹配结果或误操作同名/相近工位。' ` '前端把工位名称作为查询参数传入参数化存储过程;后端按业务要求采用包含匹配;增加空条件、中文关键字、无结果和特殊字符测试。' ` '测试环境准备多个相近名称工位,验证空条件、部分名称和完整名称查询。' Add-Figure (Join-Path $prepared 'workstation-list.png') '图4-1 工位页面显示查询框,但当前请求未使用输入值' 15 Add-Issue 'WS-004' '工位校验规则不完整' 'P2 中' '已确认' ` '新增只显式检查同名工位,没有在存储过程中预检查工位号重复。编辑的同名检查统计全部记录,包含当前工位本身,因此不修改名称直接保存时也会被判断为同名。' ` '数据库过程“系统管理_工位管理_工位信息增加”和“系统管理_工位管理_工位信息修改”;编辑过程的同名条件未排除当前工位号。' ` '重复工位号可能依赖数据库异常兜底并产生不友好提示;无变化保存可能失败,增加用户困惑和重复操作。' ` '新增时同时校验工位号和名称唯一;编辑时使用“Name=@名称 AND VisCode<>@当前工位号”;后端返回明确错误码和字段级提示。' ` '测试空值、重复名称、重复编号、原名称不变、名称仅大小写/空格变化等边界。' Add-Issue 'PER-001' '人员删除提示与实际处理语义不一致' 'P2 中' '已确认' ` '页面确认文字为“当前人员将永久删除”,成功提示为“人员离职成功”;数据库过程没有物理删除人员主记录,而是写Quit=0并删除人员角色关系。列表通过角色关系联查,因此人员从列表消失。' ` 'src/views/SystemMaintenance/PersonnelManagement/index.vue:345-366;数据库过程“人员管理_人员信息修改_删除”。' ` '管理员难以判断人员是离职、停用还是永久删除;人员主记录仍在但无角色关系,后续重新启用和历史查询缺少明确流程。' ` '统一业务语义为“停用/离职”;明确Quit字段状态定义;保留角色历史或增加有效期;页面展示在职/离职状态和恢复入口;禁止使用“永久删除”误导。' ` '测试环境验证离职后登录、列表、历史工时、报工记录和重新启用行为。' Add-Issue 'LOG-001' '报工操作记录查询能力不足' 'P2 中' '已确认' ` '订单编号和操作人采用精确相等;查询最多返回按记录编号倒序的前100条;页面分页控件整体注释,日期和操作类别等已存在的数据字段没有查询入口。' ` 'src/views/SystemMaintenance/OperationLog/index.vue:63-75、87-93、108-125;数据库过程“系统管理_报工操作记录_查询”使用TOP 100和等值条件。' ` '较早或同一订单超过100条的记录无法完整查看;姓名/订单输入不完整时无结果;审计人员无法按时间范围和操作类别定位。' ` '增加服务端分页、总数、开始/结束日期和操作类别;订单支持精确优先并可选包含匹配;提供导出并限制最大范围;建立合适索引。' ` '使用脱敏测试数据验证跨页、超过100条、时间边界、空条件和导出结果完整性。' Add-Figure (Join-Path $prepared 'operation-log.png') '图4-2 报工操作记录当前仅提供订单编号和操作人条件' Add-Issue 'MENU-002' '菜单结构维护和按钮级权限界面未开放' 'P2 中' '已确认' ` '页面存在“新增一级模块”弹窗和相关方法,但模板没有可见入口;查询、新增、编辑、删除按钮级权限复选框被注释。当前实际功能只是把既有一级、二级菜单分配给角色。' ` 'src/views/SystemMaintenance/MenuManagement/index.vue:82-89、152-170、367-393。' ` '旧手册描述新增、编辑、删除菜单和权限分配,容易让管理员寻找不存在的功能;页面级授权过粗,不能限制同页写操作。' ` '明确产品边界:如不开放结构维护,删除不可达代码并修订文档;如需按钮权限,完成前后端授权校验,不能只依赖前端隐藏按钮。' ` '测试不同角色的查询、新增、编辑、删除接口权限,确认直接请求也会被服务器拒绝。' Add-Figure (Join-Path $prepared 'menu-permission.png') '图4-3 当前菜单页实际为角色菜单树分配' Add-PageBreak Add-Heading '5 P3 低优先级问题' 1 Add-Issue 'DOC-001' '参考手册与当前正式菜单不一致' 'P3 低' '已确认' ` '参考手册系统管理包含“节假日设定”,但正式数据库菜单没有该页面;正式菜单新增“报工操作记录”,参考手册没有对应章节。项目中还存在OrgManagement页面,但未配置为正式菜单。' ` '参考文档系统管理章节;正式表“登录基础数据_二级菜单”;src/views/SystemMaintenance目录。' ` '继续沿用旧手册会遗漏正式功能并培训不存在的入口,造成操作人员和实施人员理解不一致。' ` '以正式菜单版本为基线建立文档版本号和发布日期;每次菜单上线同步更新手册;未上线页面标注“开发中/停用”而不是混入正式手册。' ` '发布前由业务、实施和开发三方按角色逐项核对左侧菜单与手册目录。' Add-Issue 'WS-005' '工位分页回调引用不存在的方法' 'P3 低' '已确认' ` '工位页面当前没有显示分页控件,但handleSizeChange和handleCurrentChange均调用this.searchTable(),组件实际查询方法名为searchData()。' ` 'src/views/SystemMaintenance/WorkstationManagement/index.vue:163-175、288-297。' ` '当前未触发;如果后续恢复分页控件,切换页码将产生运行时错误,查询无法执行。' ` '把回调统一改为searchData,补充组件测试;删除未使用的分页状态或完整实现服务端分页。' ` '在测试环境启用分页后验证切换每页条数、页码和末页行为。' Add-Heading '6 建议处理顺序' 1 Add-Table @( @('阶段', '建议事项', '目标'), @('立即管理措施', '禁用共享账号;不勾选记住密码;限制系统管理权限;禁止直接删除工位;工位变更人工核对MES/B1', '在代码修复前降低安全和数据风险'), @('第一批修复', 'SEC-001、MENU-001、WS-001、WS-002', '处理账号安全、接口授权和跨系统一致性'), @('第二批修复', 'WS-003、WS-004、PER-001、LOG-001、MENU-002', '改善维护准确性、审计完整性和权限粒度'), @('文档与技术债', 'DOC-001、WS-005', '统一正式菜单与文档,清理潜在故障代码') ) @(3, 10, 4) Add-Heading '7 验收要求' 1 Add-ItemList @( '所有写操作问题必须在测试环境复现和回归,不得以正式业务数据作为测试数据。', '涉及账号安全的修改需完成接口响应、Cookie、日志、数据库存储和会话失效检查。', '涉及MES与SAP B1的修改需覆盖单边成功、单边失败、超时、重复请求和补偿重试。', '删除/停用类功能需验证下游占用、历史查询、恢复和审计记录。', '菜单权限必须同时验证页面可见性和后端接口授权,不能只检查按钮是否隐藏。', '修复上线后同步更新操作手册、问题状态、版本号和发布日期。' ) Add-Note '结论' '本清单共记录11项:P1高优先级4项、P2中优先级5项、P3低优先级2项。问题清单与操作手册分开交付,便于技术整改和业务培训分别管理。' 'danger' foreach ($section in $doc.Sections) { $header = $section.Headers.Item(1).Range $header.Text = '大连元利流体技术有限公司 MES系统 | 系统管理问题清单' $header.Font.NameFarEast = '微软雅黑' $header.Font.Size = 8.5 $header.Font.Color = 0x777777 $header.ParagraphFormat.Alignment = 2 $footer = $section.Footers.Item(1).Range $footer.Text = '内部整改使用 ' $footer.Font.NameFarEast = '宋体' $footer.Font.Size = 8.5 $footer.Font.Color = 0x777777 $footer.ParagraphFormat.Alignment = 1 $footer.Collapse(0) $footer.Fields.Add($footer, 33) | Out-Null } foreach ($item in $doc.TablesOfContents) { $item.Update() } $doc.Fields.Update() | Out-Null $doc.Repaginate() if (Test-Path $docxPath) { Remove-Item -LiteralPath $docxPath -Force } if (Test-Path $pdfPath) { Remove-Item -LiteralPath $pdfPath -Force } $doc.SaveAs($docxPath, 12) $doc.ExportAsFixedFormat($pdfPath, 17) $pageCount = $doc.ComputeStatistics(2) $tableCount = $doc.Tables.Count $imageCount = $doc.InlineShapes.Count $doc.Close(0) $doc = $null Copy-Item -LiteralPath $docxPath -Destination $desktopDocx -Force Copy-Item -LiteralPath $pdfPath -Destination $desktopPdf -Force Write-Output ('DOCX=' + $docxPath) Write-Output ('PDF=' + $pdfPath) Write-Output ('PAGES=' + $pageCount) Write-Output ('TABLES=' + $tableCount) Write-Output ('IMAGES=' + $imageCount) } catch { $buildError = $_ Write-Output ('BUILD_ERROR=' + $_.Exception.Message) Write-Output ('BUILD_ERROR_LINE=' + $_.InvocationInfo.ScriptLineNumber) } finally { if ($doc -ne $null) { try { $doc.Close(0) } catch {} } if ($word -ne $null) { try { $word.Quit() } catch {} } [GC]::Collect() [GC]::WaitForPendingFinalizers() } if ($buildError -ne $null) { throw $buildError }