feat: add ERP agent pet bridge and startup guide

This commit is contained in:
郎速科技
2026-08-14 14:28:28 +08:00
parent a803070819
commit 4c08f4c948
252 changed files with 134752 additions and 72 deletions
@@ -0,0 +1,58 @@
# lserp_AI 只读复核档案
`lserp-ai.readonly-map.json` 是基于客户提供的 `lserp_AI` SQL Server 数据库系统目录和低代码配置生成的 1.2 版人工复核材料;它本身不是业务适配器配置。启用采购或请假后,`business-adapters.json` 1.1 必须通过 `customerProfilePath` 引用它,并由每个工作流的签名清单绑定其原始字节 SHA-256;AgentBridge 会在启动、计划和确认执行前自动加载并用固定只读系统目录查询重新比较。目录内两个 `*.candidate.json` 仍只是 `lserp-cli adapters validate-fields` 的候选输入,不是 `LSERP_BUSINESS_ADAPTER_CONFIG` 可加载的运行时结构。画像的 `criticalCatalogContract` 明确列出采购来源/目标表、请假与审批表、低代码配置表以及三条旧保存过程必须存在的列和参数,避免表总数未变时漏掉字段改名或过程签名漂移。
随包 `../business-adapters.example.json` 已把本画像当前复核出的 `acc_1007` 采购字段、`hr_4011` 请假字段和采购含税口径 `lineAmountMode=2` 投影成严格 1.1 配置候选,但两个工作流固定保持 `enabled=false`。它用于让配置人员从真实低代码映射继续评审,不是可直接上线的默认值:采购币种控件、币种换算、行级范围和写包装尚未验收,请假流程配置与兼容写包装也未验收;不得仅把 `enabled` 改为 `true`。最终配置必须从当前已登录 ERP 重新运行字段门禁、在线画像复核和签名验收后另存到包外 ACL 受控目录。
档案中的 `purchaseTargetSelection` 固化了三个采购相关模块的用途与选择结论,`purchaseActivationBlockers``leaveActivationBlockers` 则是机器可读的工作流门禁清单。当前选择 `acc_1007` 只表示它是唯一结构匹配的草稿写入候选,不表示允许激活;在线元数据复核、客户配置和全部验收证据完成前,`activationAllowed` 必须保持 `false`。采购 5 项和请假 4 项的阻断码是代码锁定的精确集合:不能删除、替换或追加。`open` 项必须保持 `resolution: null`;只有真实完成对应证据并经客户复核后才能改为 `resolved`,并填写结构严格的 `resolution={evidenceArtifact,evidenceSha256,approvedBy,approvedAtUtc}``purchase_currency_field_not_configured` 必须绑定最终 `field_mapping`;其余采购阻断项和全部请假阻断项必须绑定本工作流的 `write_integration`。其 SHA-256 必须与本次 RSA 签名验收清单中的精确原始制品哈希一致,不是自由填写的文本证明;画像中采购选定模块或请假模块也必须与清单 `moduleCode` 逐字一致。只要当前工作流任一阻断项仍为 `open`,签发脚本和 ERP 启动/计划/执行门禁都会拒绝;采购全部关闭后还必须把选择状态同步改为 `selected_and_activation_approved``activationAllowed=true`。请假可独立批准,不要求采购同时完成,反之亦然。
实施人员不应直接编辑这些字段。最终字段映射、只读契约证据和 Windows 写集成报告齐备后,使用已登录客户 ERP 的内置管理员运行 `lserp-cli adapters prepare-profile-activation <purchase|leave>`;命令会绑定当前账套、子系统、模块、源码提交、商用包和运行配置,重新执行只读关键目录复核,并只用新建文件语义输出未签名候选。原画像不会覆盖,数据库不会修改,写命令也不会注册。候选仍须交给 `New-WorkflowAcceptanceEvidence.ps1` 在线复核和 RSA 签名。
实库核验还发现:服务器引擎虽为 SQL Server 2022,但 `lserp_AI` 数据库兼容级别是 `100`SQL Server 2008 语义),因此库内不能使用 `OPENJSON``ISJSON``TRY_CONVERT``THROW`。AgentBridge 现在会在调用前读取 `sys.databases.compatibility_level`:低于 `130` 时,由受信任 ERP 进程用严格 Newtonsoft JSON 先验解析;普通读取和请假写入只传固定白名单标量,采购明细则由 `XmlWriter` 编码成固定结构、限量且再次校验的 XML 行集,不接受模型原始 XML。采购和请假均已有固定兼容写分派,但缺少严格运行配置、数据库 V2 就绪证据或 TrustedPeople 签名验收时不会注册命令;采购草案内部两道审核开关仍固定为 `0`,当前仍无法写入。兼容级别无法确认时同样失败关闭。
它明确记录了三类结果:
- `PUR_5001` 是采购订单主从单据,可作为来源单候选。
- `acc_1007` 是唯一满足现有采购适配器“bill + 主从字段”门禁的采购发票候选;但币种字段没有在 `p_systembillInfo/p_systembillDetail` 暴露,且来源订单字段存在历史命名歧义,因此保持不可启用。
- `acc_1002` 是“采购发票登记”基础档案菜单,适合导航和只读诊断,不能绕过采购发票主从写入契约。
- `hr_4011` 的请假字段已经能与现有 leave 适配器语义对齐。流转类别控件保存的是 `p_systemdlltabflowtype.id`;其旧天数/岗位联动仍指向已经不存在的 3195-3200,而当前流程步骤使用 3629-3634。Agent 当前返回有效候选并在不唯一时追问,不能沿用这段失效配置自动选路。
- `lserp-ai.workflow-read.compat100.draft.sql` 是能被兼容级别 100 解析的只读过程审查草案。它以 `SET NOEXEC ON` 开头、内部审核开关固定为 `0`,所以不能部署也不能被调用;正式脚本必须由客户 DBA 另行生成。
- `lserp-ai.workflow-write.leave.compat100.draft.sql` 是请假创建/提交的强类型事务写审查草案,同样受 `NOEXEC` 和固定关闭审核开关保护。它派生员工姓名、部门、岗位和天数,调用原 `p_BaseSave70/p_baseApply`,并要求幂等、审计、事务证据和 Outbox 同事务完成;未签署前不会注册成可用写命令。
实库过程签名确认 `p_BaseSave70` 的保存确认参数当前默认值为 `0`;草案仍显式传入固定 `@comfirmFlag = 0`,从而把“只创建草稿、不隐式确认”的语义绑定在客户包装合同中,不依赖未来可能漂移的过程默认值。
- `lserp-ai.workflow-write.purchase.compat100.draft.sql` 是采购发票创建的固定标量加 XML 行集审查草案。它同时固定关闭 DBA 审核和采购行级范围审核,复核目标币种字段、签字币种换算、采购订单快照和可用量后,才会调用 WinForm 同一条 `P_BillSavePr70` 保存链生成未提交草稿;ERP 网关已有精确分派,但只要配置、V2 就绪证据、Windows 验收或 TrustedPeople 签名任一缺失,命令就不会注册。
安全边界:本档案只含对象名、模块编号、字段名和配置摘要;生成时没有读取业务单据行,没有执行存储过程,没有执行 INSERT/UPDATE/DELETE/DDL,也没有写回数据库。正式启用仍必须在客户 Windows ERP 进程中运行:
Agent 的内置管理员身份已收紧为 `user_id=1` 且员工名称精确为 `管理员`,不再继承旧客户端仅按显示名放行的行为。只读过程、请假写包装和采购写包装采用同样的成对判断;名称为“管理员”但 ID 不是 1 的账号仍必须通过对应菜单权限和业务范围门禁。若客户需要委派其他配置管理员,必须设计独立签字授权,不得复制或放宽该名称判断。
```text
lserp-cli adapters inspect purchase acc_1007
lserp-cli adapters inspect leave hr_4011
lserp-cli adapters validate-fields purchase --input acc-1007.purchase.fields.candidate.json
lserp-cli adapters validate-fields leave --input hr-4011.leave.fields.candidate.json
lserp-cli adapters revalidate-profile --input lserp-ai.readonly-map.json
lserp-cli adapters prepare-profile-activation purchase --input lserp-ai.readonly-map.json --field-mapping acc-1007.purchase.fields.final.json --read-evidence purchase.read-evidence.json --write-evidence purchase.write-evidence.json --runtime-sha256 <SHA256> --source-commit <COMMIT> --package-sha256 <SHA256> --output lserp-ai.purchase.approved-candidate.json
```
最后一条命令只能由当前已登录 ERP 的内置管理员执行。它只运行代码内固定的一批 `DB_NAME/SERVERPROPERTY/sys.*` 系统目录结果集,比较数据库身份、兼容级别、用户对象计数、Agent 对象状态,以及档案声明的关键对象、列和过程参数;目录成员最多读取 100000 条,超限、缺少第二结果集、重复或格式异常都会失败关闭。它不会读取业务行、调用存储过程或执行写入。输出只包含档案 SHA-256、关键目录契约是否匹配、稳定漂移码、总阻断项数量,以及采购/请假各自的 `approved/openBlockerCount/openBlockerCodes`,不回显画像证据正文、当前数据库名、对象名、字段名、参数名或计数。命令级 `activationAllowed``registrationReady` 始终固定为 `false`;即使某个工作流 `approved=true`,该结果也不能单独启用写命令。签发脚本只接受本次工作流 `approved=true/openBlockerCount=0`,并且只允许 Agent 支撑对象部署引起的非关键计数/状态变化;运行门禁锁定批准状态、数据库身份、兼容级别和关键目录契约。只有画像哈希进入 RSA 签名工作流清单、与最终运行配置及 V2 就绪行共同通过,并在每次运行时复核仍匹配时,才满足完整门禁中的画像一项。
请假候选应在当前 ERP 管理员会话中通过字段门禁;仍需继续完成只读过程与写链路验收。请假写草案会把 ERP `user_id` 与只读上下文返回的当前员工再次绑定,只接受 `p_SubsysPurviewTab.hrPurview` 中菜单 `16629` 的完整编辑标记;`16629|` 只读标记必须拒绝。创建与提交分别以账套、子系统、用户、命令和幂等键获取事务级 `sp_getapplock`,防止多个 ERP/桌宠进程同时插入同一幂等请求。客户库当前仍没有这些 Agent 对象,草案也保持 `SET NOEXEC ON` 和审核开关为 `0`,因此上述加固不会造成任何实际写入。
采购候选刻意把物理列 `acc_mphhscm_currency` 写入 `currencyCode`:该列存在于主表,但没有暴露在 `acc_1007` 的低代码主表控件配置中,所以当前 `validate-fields purchase` 应以 `mapped_field_not_exposed``mapped_field_not_found` 失败关闭。字段检查会区分主表控件 `visible=1`(显示)与单据明细 `isVisible=1`(隐藏)这两套相反的旧框架语义,并把宽度为 0 或受字段权限隐藏的列排除在候选和注册范围外。只有客户配置人员正确暴露币种字段,并由财务/DBA 签字确认币种换算后,才可生成新的人工复核包。在此之前,不要把 `acc_1007` 写入 `LSERP_BUSINESS_ADAPTER_CONFIG` 的启用配置;不要把基础档案菜单 `acc_1002` 当成采购发票主从写入目标。
虽然 `acc_1007` 当前“来源单”配置只有物料类别和自身单据管理,没有采购订单来源,但两个现存数据库对象给出了可交叉验证的业务证据:`Proc_GetScmMainBillInfo``Scm_ScminvoiceMoneyView` 都用 `acc_lphhscm_ScmPoid = scm_lpo_id` 跟踪采购订单明细;`acc_lphhscm_ScmPrid` 则标注为“申请id”。因此兼容草案中的 `purchase.open_sources` 已改为固定、限量的只读候选查询,返回采购系统单据号、人工单号、明细 ID、单位、原币含税单价、税率、汇率和扣除现有有效发票占用后的剩余数量。它不仅复核采购订单菜单权限,还要求每一条返回行的组织、部门和采购员精确命中当前账套、子系统、ERP 用户的一条有效签字范围;范围表缺失、空表、过期、哈希无效或元组不匹配都会失败关闭,内置管理员也不绕过。这样不可写的采购订单不会先泄露给 AstrBot/MiniMax。草案继续受 `SET NOEXEC ON``@customer_dba_reviewed = 0` 双重锁定,不能因此启用采购写入。
采购订单币种来自 `P_CurrencyType`,而目标发票主表的物理币种列沿用 `P_BaseMixInfoTab(Tag='L000101')`。只读证据显示的 `1→22、2→23、3→24` 只是候选换算,不能硬编码为生产规则。兼容只读草案的 `resolve_currency` 现在只返回已存在有效签字映射的来源币种 ID;它可以精确接受来源字典的 ID/代码/名称,也可以接受发票 OCR 常见的目标字典 ID/编号/名称(例如“人民币元”),但后者仍必须先通过同一条已审批映射反查,映射缺失或失效时固定返回 `purchase_currency_crosswalk_not_approved`。这样后续 `open_sources` 和写包装始终接收采购订单使用的来源币种 ID,不会把目标字典 ID 误传给来源单查询。`acc_1007` 明细公式将 `amount * price` 作为含税金额,所以该客户的正式匹配配置必须使用 `lineAmountMode = 2`;单位或汇率缺失、同一发票命中多个汇率时均应失败关闭。
采购订单配置还明确暴露了组织、部门和采购员字段。基础 Agent 架构脚本现会创建默认空的 `p_agent_purchase_row_scope`,正式写包装要求来源单的组织、部门和采购员与当前账套、子系统、ERP 用户组成一个有效期内、带签字证据哈希的精确元组。该表不支持 NULL 或通配符,管理员也不绕过;客户未审批并填充范围时,写过程固定返回 `purchase_row_scope_denied`。这只建立了可执行的失败关闭机制,并不替客户决定谁能看哪些采购单,所以 `purchase_row_scope_not_approved` 仍保持打开。
ERP 网关只从固定客户包装过程异常中提取代码仓库白名单内的稳定业务码,并使用本地固定中文说明返回桌宠;SQL Server 原始异常、对象名、行号、连接信息和业务值不会向 AstrBot/MiniMax 透传。未知错误统一返回 `workflow_database_error`。因此币种字段缺失、换算未审批、精确采购范围未授权和请假权限/冲突可以被桌宠准确说明,同时不扩大数据库信息泄露面。
WinForm 保存链已经从源码和实库过程定义交叉验证:`GetDetailRecord` 先把明细写入 `ACC_billscmInvoicelistPIDHxtab_temp``BillSave` 再调用 `P_BillSavePr70`,由 `P_BillSavePr70_3` 生成正式单号、迁移明细并执行 ERP 保存副作用。因此采购兼容草案不会直接写最终主从表。目标临时明细的数量、单价、税率和汇率只有两位小数,候选合同会提前拒绝更高精度,避免旧保存链静默舍入。当前 `acc_1007` 保存事件不会自动提交;98/99 事件属于后续流转,桌宠创建动作也只允许生成草稿。
## 过程层结论
数据库目录中可以看到旧的通用过程(包括请假界面实际使用的 `p_BaseSave70``p_baseApply`),但没有 AgentBridge 固定调用的就绪、只读和强类型写包装过程。这些旧过程自身的参数契约不足以证明当前桌宠请求的账套/用户/模块权限、输入指纹、幂等占用、业务审计和事务证据,因此只能由固定客户包装过程调用,不能直接暴露为自然语言工具。
客户实施时应在 DBA 评审下,用固定参数化过程包裹现有 ERP 保存入口;先完成只读契约探针,再完成 SQL 事务、持久化幂等、权限复核和 Windows 集成验收。未完成前,AgentBridge 必须保持只读/导航/诊断能力,写命令不注册。
@@ -0,0 +1,16 @@
{
"moduleCode": "acc_1007",
"supplierCode": "acc_mphhscm_Providerid",
"invoiceNumber": "acc_mphhscm_invoice",
"invoiceDate": "acc_mphhscm_invoiceDate",
"currencyCode": "acc_mphhscm_currency",
"materialCode": "acc_lphhscm_productid",
"unit": "acc_lphhscm_Productunitname",
"quantity": "acc_lphhscm_amount",
"unitPrice": "acc_lphhscm_price",
"taxRate": "acc_lphhscm_taxRate",
"exchangeRate": "acc_lphhscm_exchangeRate",
"lineAmount": "acc_lphhscm_summoney",
"sourceOrderId": "acc_lphhscm_sourcebillid",
"sourceLineId": "acc_lphhscm_ScmPoid"
}
@@ -0,0 +1,10 @@
{
"moduleCode": "hr_4011",
"employeeId": "hr_ela_empid",
"leaveTypeCode": "hr_ela_type",
"flowTypeCode": "hr_ela_billtype",
"startLocal": "hr_ela_starttime",
"endLocal": "hr_ela_finishtime",
"requestedHours": "hr_ela_totals",
"reason": "hr_ela_Leavebak"
}
@@ -0,0 +1,460 @@
{
"schemaVersion": "1.2",
"profileType": "readonly_low_code_metadata_review",
"database": {
"name": "lserp_AI",
"sqlServerMajorVersion": 16,
"compatibilityLevel": 100,
"compatibilityContract": "fixed_scalar_and_schema_validated_xml_rowsets_in_trusted_erp_process",
"userTableCount": 832,
"userViewCount": 109,
"userProcedureCount": 319,
"userTriggerCount": 31,
"agentWorkflowObjectsPresent": false,
"criticalCatalogContract": {
"contractVersion": "1.0",
"requirements": [
{
"schemaName": "dbo",
"objectName": "p_systembilltype",
"objectKind": "table",
"requiredColumns": ["typeCode", "masterTable", "detailTable", "MasterSql", "DetailSql", "formKey"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "P_systemdlltab",
"objectKind": "table",
"requiredColumns": ["typeCode", "SQLDT1", "SQL", "formKey"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "p_systembillInfo",
"objectKind": "table",
"requiredColumns": ["typeCode", "fieldName", "defaultValue"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "p_systembillDetail",
"objectKind": "table",
"requiredColumns": ["typeCode", "fieldName", "isVisible"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "scm_BillPoMainTab",
"objectKind": "table",
"requiredColumns": ["scm_mpo_Billdocument_Id", "scm_mpo_handcraftId", "scm_mpo_Providerid", "scm_mpo_Currency", "scm_mpo_ExchangeRate", "scm_mpo_stepover", "scm_mpo_cancelFlag", "scm_mpo_wasteoper", "scm_mpo_ban", "scm_mpo_Groupid", "scm_mpo_Departmentid", "scm_mpo_PurchaseUserId"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "scm_BillPolistTab",
"objectKind": "table",
"requiredColumns": ["scm_lpo_Billdocument_id", "scm_lpo_id", "scm_lpo_Productid", "scm_lpo_ProductUnitName", "scm_lpo_Amount", "scm_lpo_Fprice", "scm_lpo_taxRate", "scm_lpo_ExchangeRate"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "ACC_billscmInvoicemainPIDHxtab",
"objectKind": "table",
"requiredColumns": ["acc_mphhscm_billdocument_id", "acc_mphhscm_groupid", "acc_mphhscm_Departmentid", "acc_mphhscm_Providerid", "acc_mphhscm_billtype", "acc_mphhscm_InvoiceType", "acc_mphhscm_SettlementType", "acc_mphhscm_invoice", "acc_mphhscm_summoney", "acc_mphhscm_nosummoney", "acc_mphhscm_taxsummoney", "acc_mphhscm_invoiceDate", "acc_mphhscm_operatorid", "acc_mphhscm_operatedate", "acc_mphhscm_currency", "acc_mphhscm_sign", "acc_mphhscm_Affirmer", "acc_mphhscm_cancelFlag", "acc_mphhscm_wasteoper", "acc_mphhscm_delid"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "ACC_billscmInvoicelistPIDHxtab",
"objectKind": "table",
"requiredColumns": ["acc_lphhscm_billdocument_id", "acc_lphhscm_productid", "acc_lphhscm_Productunitname", "acc_lphhscm_amount", "acc_lphhscm_price", "acc_lphhscm_summoney", "acc_lphhscm_taxsummoney", "acc_lphhscm_taxRate", "acc_lphhscm_exchangeRate", "acc_lphhscm_ScmPrid", "acc_lphhscm_ScmPoid", "acc_lphhscm_ScmJoinid", "acc_lphhscm_ScmDzid", "acc_lphhscm_sourcebillid", "acc_lphhscm_sourcename"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "ACC_billscmInvoicelistPIDHxtab_temp",
"objectKind": "table",
"requiredColumns": ["acc_lphhscm_productid", "acc_lphhscm_Productunitname", "acc_lphhscm_amount", "acc_lphhscm_price", "acc_lphhscm_summoney", "acc_lphhscm_taxsummoney", "acc_lphhscm_taxRate", "acc_lphhscm_exchangeRate", "acc_lphhscm_ScmPrid", "acc_lphhscm_ScmPoid", "acc_lphhscm_ScmJoinid", "acc_lphhscm_ScmDzid", "acc_lphhscm_sourcebillid", "acc_lphhscm_sourcename", "sysstr"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "HR_EmpLeaveAloneTab",
"objectKind": "table",
"requiredColumns": ["hr_ela_id", "hr_ela_no", "hr_ela_empid", "hr_ela_employeename", "hr_ela_depid", "hr_ela_postid", "hr_ela_type", "hr_ela_billtype", "hr_ela_starttime", "hr_ela_finishtime", "hr_ela_totals", "hr_ela_totals1", "hr_ela_Leavebak", "hr_ela_operatorid", "hr_ela_operatorname", "hr_ela_operatedate", "hr_ela_cancelFlag"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "p_employeetab",
"objectKind": "table",
"requiredColumns": ["employeeid", "EmployeeName", "p_emp_status", "Departmentid", "p_emp_postid"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "p_SubsysPurviewTab",
"objectKind": "table",
"requiredColumns": ["employeeid", "scmPurview", "accPurview", "hrPurview"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "p_systemdlltabflowtype",
"objectKind": "table",
"requiredColumns": ["id", "typeCode", "billTypeName", "billflowban"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "p_systemdlltabflowtypestep",
"objectKind": "table",
"requiredColumns": ["typeCode", "billType", "stepClosed", "autoStep", "comfirmOper"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "p_systemNotification",
"objectKind": "table",
"requiredColumns": ["stepCode"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "p_baseflowOper",
"objectKind": "table",
"requiredColumns": ["modid", "keyvalue", "billtype"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "HR_ScheduleTab",
"objectKind": "table",
"requiredColumns": ["HR_Schedule_billdocument_d", "HR_Schedule_employeeid", "HR_Schedule_Date"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "P_SystemCheckParamTab",
"objectKind": "table",
"requiredColumns": ["Billdocument_Id", "shichang"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "P_CurrencyType",
"objectKind": "table",
"requiredColumns": ["id"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "P_BaseMixInfoTab",
"objectKind": "table",
"requiredColumns": ["ID", "Tag", "Ban", "Delid"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "p_systembillflowtype",
"objectKind": "table",
"requiredColumns": ["id", "typeCode", "billflowban", "operUser"],
"requiredParameters": []
},
{
"schemaName": "dbo",
"objectName": "P_BillSavePr70",
"objectKind": "procedure",
"requiredColumns": [],
"requiredParameters": ["@Sql", "@billdocument_id", "@tmpstr", "@Operatorid", "@Fbilltagid", "@operateway", "@msg", "@auditFlag", "@comfirmFlag"]
},
{
"schemaName": "dbo",
"objectName": "p_BaseSave70",
"objectKind": "procedure",
"requiredColumns": [],
"requiredParameters": ["@baseSql", "@saveType", "@modid", "@tablename", "@keyfield", "@keyvalue", "@Operatorid", "@operatorName", "@msg", "@comfirmFlag"]
},
{
"schemaName": "dbo",
"objectName": "p_baseApply",
"objectKind": "procedure",
"requiredColumns": [],
"requiredParameters": ["@typeCode", "@billDocumentId", "@Operatorid", "@operatorName", "@comfirmType", "@msg"]
}
]
}
},
"safety": {
"source": "system_catalog_and_low_code_configuration_only",
"businessRowsRead": false,
"storedProceduresExecuted": false,
"writesPerformed": false,
"runtimeEnabled": false,
"requiresCustomerReview": true
},
"purchaseTargetSelection": {
"selectedModuleCode": "acc_1007",
"selectedRole": "purchase_invoice_draft_write_candidate",
"selectionState": "selected_but_activation_blocked",
"activationAllowed": false,
"onlineRevalidationRequiredBeforeActivation": true,
"candidatesEvaluated": [
{
"moduleCode": "PUR_5001",
"moduleKind": "bill",
"decision": "source_only",
"reasonCode": "purchase_order_source_only"
},
{
"moduleCode": "acc_1002",
"moduleKind": "base",
"decision": "rejected",
"reasonCode": "base_module_without_purchase_invoice_detail_contract"
},
{
"moduleCode": "acc_1007",
"moduleKind": "bill",
"decision": "selected_but_activation_blocked",
"reasonCodes": [
"purchase_currency_field_not_configured",
"purchase_currency_crosswalk_not_approved",
"purchase_row_scope_not_approved",
"purchase_compat100_write_contract_not_approved",
"purchase_windows_integration_not_verified"
]
}
]
},
"purchaseActivationBlockers": [
{
"code": "purchase_currency_field_not_configured",
"status": "open",
"resolution": null,
"evidence": "acc_mphhscm_currency 是物理列,但 acc_1007 的 p_systembillInfo 没有对应主表控件。"
},
{
"code": "purchase_currency_crosswalk_not_approved",
"status": "open",
"resolution": null,
"evidence": "采购订单与目标发票使用不同币种字典,候选 1→22、2→23、3→24 尚未由财务/DBA 签字。"
},
{
"code": "purchase_row_scope_not_approved",
"status": "open",
"resolution": null,
"evidence": "已识别采购订单组织、部门和采购员字段,并补充默认空表、无通配符的精确签字范围契约;客户尚未批准或填充任何账套/子系统/ERP 用户范围。"
},
{
"code": "purchase_compat100_write_contract_not_approved",
"status": "open",
"resolution": null,
"evidence": "已形成固定标量加受信任 XmlWriter 行集的 NOEXEC 审查草案并在客户库只编译通过;ERP 网关已有受商用证据门禁保护的精确分派,但 SQL 两道审核开关、运行配置和数据库 V2 就绪证据仍关闭。"
},
{
"code": "purchase_windows_integration_not_verified",
"status": "open",
"resolution": null,
"evidence": "尚未在客户 Windows ERP 进程内完成保存链、回滚、幂等、审计和原生确认验收。"
}
],
"leaveActivationBlockers": [
{
"code": "leave_flow_type_rules_stale",
"status": "open",
"resolution": null,
"evidence": "请假天数联动仍返回已不存在的旧流转配置行 3195-3200;当前有效行是 3629-3634,修复前只能要求用户显式选择候选。"
},
{
"code": "leave_agent_schema_not_deployed",
"status": "open",
"resolution": null,
"evidence": "客户库尚无只读/写兼容过程、V2 就绪证据表及 Agent 幂等审计对象;当前只存在不落库的审查草案。"
},
{
"code": "leave_compat100_write_contract_not_approved",
"status": "open",
"resolution": null,
"evidence": "固定强类型草案已补 ERP 用户作用域、hr_4011 编辑权限复核和按幂等键的数据库应用锁,但 DBA 审核开关仍固定关闭。"
},
{
"code": "leave_windows_integration_not_verified",
"status": "open",
"resolution": null,
"evidence": "尚未在客户 Windows ERP 进程内完成草稿创建、独立二次提交、回滚、重复请求、只读权限拒绝及审计验收。"
}
],
"menus": [
{
"menuId": 187,
"caption": "采购订单下达",
"moduleCode": "PUR_5001",
"dllFileName": "Lskj.PubBill.dll",
"subSystemId": 5,
"enabled": true
},
{
"menuId": 2413,
"caption": "采购发票登记",
"moduleCode": "acc_1002",
"dllFileName": "Lskj.PubModule.dll",
"subSystemId": 10,
"enabled": true
},
{
"menuId": 18848,
"caption": "采购开票核销",
"moduleCode": "acc_1007",
"dllFileName": "Lskj.PubBill.dll",
"subSystemId": 5,
"enabled": true
},
{
"menuId": 16629,
"caption": "请假申请管理",
"moduleCode": "hr_4011",
"dllFileName": "Lskj.PubModuleDetail.dll",
"subSystemId": 7,
"enabled": true
}
],
"modules": {
"purchaseOrderSource": {
"moduleCode": "PUR_5001",
"kind": "bill",
"masterTable": "scm_BillPoMainTab",
"detailTable": "scm_BillPolistTab",
"formKey": "89010284-C0C0-4A64-996D-7F0FDB9C83B0",
"sourceFields": {
"supplierCode": "scm_mpo_Providerid",
"currencyCode": "scm_mpo_Currency",
"materialCode": "scm_lpo_Productid",
"unit": "scm_lpo_ProductUnitName",
"quantity": "scm_lpo_Amount",
"unitPrice": "scm_lpo_Fprice",
"taxRate": "scm_lpo_taxRate",
"exchangeRate": "scm_lpo_ExchangeRate",
"lineAmount": "scm_lpo_FSummoney",
"sourceOrderId": "scm_lpo_Billdocument_id",
"sourceOrderNumber": "scm_mpo_handcraftId",
"sourceLineId": "scm_lpo_id"
}
},
"purchaseInvoiceWriteCandidate": {
"moduleCode": "acc_1007",
"kind": "bill",
"caption": "采购发票核销",
"masterTable": "ACC_billscmInvoicemainPIDHxtab",
"detailTable": "ACC_billscmInvoicelistPIDHxtab",
"formKey": "E7CF6797-E68F-4922-925D-B2BD6165B249",
"requiredMatchOptions": {
"lineAmountMode": 2,
"unitPriceBasis": "purchase_order_original_currency_tax_inclusive",
"mixedExchangeRatesAllowed": false
},
"compatibilityWriteCandidate": {
"procedure": "p_lserp_agent_workflow_write_purchase_compat100",
"status": "inert_review_draft",
"inputContract": "fixed_scalars_and_schema_validated_xml_rowsets",
"legacySaveChain": "P_BillSavePr70_to_P_BillSavePr70_3",
"creates": "unsubmitted_draft_only",
"rowScopeContract": "exact_signed_account_subsystem_erp_user_group_department_purchase_user_tuple",
"gatewayRoute": "implemented_fail_closed_by_commercial_readiness_and_signed_acceptance",
"runtimeRegistered": false
},
"fieldMap": {
"supplierCode": "acc_mphhscm_Providerid",
"invoiceNumber": "acc_mphhscm_invoice",
"invoiceDate": "acc_mphhscm_invoiceDate",
"currencyCode": "acc_mphhscm_currency",
"materialCode": "acc_lphhscm_productid",
"unit": "acc_lphhscm_Productunitname",
"quantity": "acc_lphhscm_amount",
"unitPrice": "acc_lphhscm_price",
"taxRate": "acc_lphhscm_taxRate",
"exchangeRate": "acc_lphhscm_exchangeRate",
"lineAmount": "acc_lphhscm_summoney",
"sourceOrderId": "acc_lphhscm_sourcebillid",
"sourceLineId": "acc_lphhscm_ScmPoid"
},
"reviewFindings": [
"主表物理列 acc_mphhscm_currency 存在,但 p_systembillInfo 未为 acc_1007 暴露该主表控件;字段门禁必须失败关闭。",
"采购订单 scm_mpo_Currency 使用 P_CurrencyType.id(当前只读证据为 1/2/3),目标发票币种使用 P_BaseMixInfoTab Tag=L000101(当前只读证据为 22/23/24);1→22、2→23、3→24 只能作为待签字候选映射。",
"resolve_currency 只通过有效签字映射返回采购订单使用的来源币种 ID;来源字典 ID/代码/名称与发票目标字典 ID/编号/名称均为精确匹配,未审批映射固定失败关闭。",
"Proc_GetScmMainBillInfo 与 Scm_ScminvoiceMoneyView 均证明 acc_lphhscm_ScmPoid 对应 scm_lpo_idacc_lphhscm_ScmPrid 标注为申请id,不得当成采购订单明细。",
"acc_1007 的 p_systembillsource 未配置采购订单,但现有流程过程和视图已给出来源行语义;open_sources 只读草案可供 DBA 审核,全部激活门禁完成前采购写入仍不得注册。",
"采购订单低代码配置明确暴露组织 scm_mpo_Groupid、部门 scm_mpo_Departmentid 和采购员 scm_mpo_PurchaseUserId;兼容草案要求三者与当前账套、子系统、ERP 用户组成一个有效期内且带签字哈希的精确范围元组,不支持 NULL 或通配符,空表默认拒绝。",
"acc_1007 明细公式使用 amount*price 作为含税金额,正式匹配必须配置 lineAmountMode=2TaxInclusive)。",
"WinForm 的 GetAddRecord/GetDetailRecord/BillSave 会先把明细写入 ACC_billscmInvoicelistPIDHxtab_temp,再由 P_BillSavePr70 路由到 P_BillSavePr70_3;正式包装不得绕开这条保存链直接插入最终主从表。",
"P_BillSavePr70_3 会生成正式单号、迁移临时明细、写 ERP 日志并调用 eventType=1 业务处理;acc_1007 的当前保存事件没有额外配置逻辑,eventType=98/99 属于后续流转,所以 Agent 创建只能停在未提交草稿。",
"临时明细表的数量、单价、税率和汇率均为 decimal(18,2),兼容写候选因此拒绝超过两位小数的值,避免保存链静默舍入后破坏来源快照。",
"acc_1007 当前唯一可用流转类别是 p_systembillflowtype.id=4869,且只向其 operUser 中的操作员开放;包装过程必须在保存前重新校验。"
]
},
"purchaseInvoiceRegistrationMenu": {
"moduleCode": "acc_1002",
"kind": "base",
"table": "ACC_billscminvoicemaintab",
"formKey": "BAE677C9-18DE-4257-8E83-C4D88B33371D",
"use": "navigation_or_read_only_diagnosis_only",
"reason": "该菜单是基础档案型发票登记,当前没有采购明细字段,不能替代 acc_1007 的主从单据写入契约。"
},
"leave": {
"moduleCode": "hr_4011",
"kind": "base",
"table": "HR_EmpLeaveAloneTab",
"formKey": "65C24153-A824-40E8-B8D9-CD817E448DEF",
"compatibilityWriteCandidate": {
"procedure": "p_lserp_agent_workflow_write_leave_compat100",
"status": "inert_review_draft",
"inputContract": "fixed_typed_scalars",
"legacyCreateChain": "p_BaseSave70",
"legacySubmitChain": "p_baseApply",
"menuPermission": "edit_token_16629_only",
"approvalSchemaColumnsPresent": true,
"gatewayRoute": "implemented_fail_closed_by_commercial_readiness_and_signed_acceptance",
"runtimeRegistered": false
},
"fieldMap": {
"employeeId": "hr_ela_empid",
"leaveTypeCode": "hr_ela_type",
"flowTypeCode": "hr_ela_billtype",
"startLocal": "hr_ela_starttime",
"endLocal": "hr_ela_finishtime",
"requestedHours": "hr_ela_totals",
"reason": "hr_ela_Leavebak"
},
"derivedWriteFields": {
"employeeName": "hr_ela_employeename",
"departmentId": "hr_ela_depid",
"postId": "hr_ela_postid",
"calculatedDays": "hr_ela_totals1",
"operatorId": "hr_ela_operatorid",
"operatorName": "hr_ela_operatorname",
"operateDate": "hr_ela_operatedate"
},
"flowTypeLookup": {
"candidateCode": "p_systemdlltabflowtype.id",
"candidateName": "p_systemdlltabflowtype.billTypeName",
"stepJoin": "p_systemdlltabflowtypestep.billType = CONVERT(varchar(10), p_systemdlltabflowtype.id)"
},
"reviewFindings": [
"流转类别控件以配置行 id 为保存值;p_systemdlltabflowtype.billType 存在重复值,不能作为 Agent 候选编码。",
"请假天数联动配置仍返回已不存在的旧流转 id 3195-3200,而当前有效配置 id 为 3629-3634;正式修复前必须显式选择流转类别,禁止自动推断。",
"审批步骤会引用 hr_ela_depid;Agent 写过程必须从请假人员主数据重新派生并保存部门、岗位和姓名快照。",
"p_BaseSave70_3 的成功路径会产生多个旧调试结果集;ERP 网关必须忽略这些输出,并且只接受列集合完全匹配固定 BusinessWriteResult 的唯一结果集。",
"p_SubsysPurviewTab.hrPurview 是 hr_4011 的权限来源;16629| 仅为只读,兼容写草案只接受完整编辑标记 16629,并在确认后、保存前再次复核。",
"p_systemdlltabflowtypestep.autoStep/comfirmOper 与 p_systemNotification.stepCode 当前物理列均存在,提交草案仍会在每次调用前复核,禁止触发 p_baseApply 的旧自迁移分支。"
],
"workflowDependencies": [
"p_employeetab",
"P_EmployeePostTab",
"P_DepartmentTab",
"p_systemdlltabflow",
"p_systemdlltabflowtype",
"p_systemdlltabflowtypestep",
"p_baseflowOper"
]
}
}
}
@@ -0,0 +1,765 @@
/*
Customer-specific review draft for hr_4011 leave writes in the lserp_AI
compatibility-level-100 database.
THIS FILE IS INERT:
1. SET NOEXEC ON prevents CREATE PROCEDURE from being applied.
2. @customer_dba_reviewed is deliberately fixed to 0.
3. The script must never be edited into a production deployment artifact. A customer
DBA must create and sign a separate script after reviewing every dependency below.
Reviewed design assumptions that still require customer DBA acceptance:
- p_lserp_agent_workflow_read_compat100 is the approved fixed-parameter read contract;
- hr_4011 is a newVer=1 base module backed by HR_EmpLeaveAloneTab;
- p_BaseSave70 is the exact ERP create path and generates hr_ela_no through the
configured document-number rules;
- p_baseApply is the exact ERP submit path;
- p_SubsysPurviewTab.hrPurview is the customer menu permission source, menu
16629 is hr_4011, and only the edit token (without the read-only "|" suffix)
authorizes a write;
- p_agent_command_idempotency, p_agent_business_audit and
p_agent_integration_outbox were created from 001_agent_business_idempotency.sql;
- the legacy procedures may emit diagnostic result sets; the trusted ERP gateway
discards those and accepts exactly one result set whose ten columns exactly match
the fixed BusinessWriteResult contract;
- the fixed INSERT column list below matches the approved UI save payload, including
nullable department/post and secondary-hours fields that are intentionally omitted.
No table name, column name, SQL fragment or procedure name is accepted from the Agent.
The only dynamic SQL string is an internally constructed, fixed-template @base_sql
required by the legacy p_BaseSave70 contract. Every text value is length checked and
escaped inside this wrapper.
*/
SET NOEXEC ON;
GO
CREATE PROCEDURE dbo.p_lserp_agent_workflow_write_leave_compat100
@action VARCHAR(64),
@module_code NVARCHAR(64),
@account_book NVARCHAR(64),
@subsystem_id NVARCHAR(32),
@user_id NVARCHAR(64),
@correlation_id VARCHAR(128),
@idempotency_key VARCHAR(128),
@input_fingerprint CHAR(64),
@employee_id NVARCHAR(64) = NULL,
@leave_type_code NVARCHAR(64) = NULL,
@flow_type_code NVARCHAR(64) = NULL,
@start_local DATETIME = NULL,
@end_local DATETIME = NULL,
@requested_hours DECIMAL(18, 6) = NULL,
@reason NVARCHAR(500) = NULL,
@submit_after_save_intent BIT = NULL,
@record_id NVARCHAR(128) = NULL
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @customer_dba_reviewed BIT;
SET @customer_dba_reviewed = 0;
IF @customer_dba_reviewed <> 1
BEGIN
RAISERROR(N'customer_dba_review_required', 16, 1);
RETURN;
END;
IF DB_NAME() <> N'lserp_AI'
OR @module_code <> N'hr_4011'
OR @subsystem_id <> N'7'
OR @action NOT IN ('create_draft', 'submit')
OR NULLIF(LTRIM(RTRIM(@account_book)), N'') IS NULL
OR NULLIF(LTRIM(RTRIM(@user_id)), N'') IS NULL
OR @@TRANCOUNT < 1 OR XACT_STATE() <> 1
BEGIN
RAISERROR(N'leave_write_scope_invalid', 16, 1);
RETURN;
END;
IF @correlation_id IS NULL
OR LEN(@correlation_id) < 8 OR LEN(@correlation_id) > 128
OR @correlation_id LIKE '%[^A-Za-z0-9_.:-]%'
OR @idempotency_key IS NULL
OR LEN(@idempotency_key) < 8 OR LEN(@idempotency_key) > 128
OR @idempotency_key LIKE '%[^A-Za-z0-9_.:-]%'
OR @input_fingerprint IS NULL OR LEN(@input_fingerprint) <> 64
OR @input_fingerprint LIKE '%[^A-Fa-f0-9]%'
BEGIN
RAISERROR(N'leave_write_evidence_invalid', 16, 1);
RETURN;
END;
DECLARE @context TABLE
(
current_employee_id NVARCHAR(64) NOT NULL,
can_apply_for_others BIT NOT NULL,
now_local DATETIME NOT NULL
);
INSERT INTO @context
(current_employee_id, can_apply_for_others, now_local)
EXEC dbo.p_lserp_agent_workflow_read_compat100
@workflow = 'leave',
@action = 'context',
@module_code = @module_code,
@account_book = @account_book,
@subsystem_id = @subsystem_id,
@user_id = @user_id;
IF (SELECT COUNT_BIG(*) FROM @context) <> 1
BEGIN
RAISERROR(N'leave_context_contract_invalid', 16, 1);
RETURN;
END;
DECLARE @current_employee_text NVARCHAR(64);
DECLARE @can_apply_for_others BIT;
DECLARE @now_local DATETIME;
SELECT
@current_employee_text = current_employee_id,
@can_apply_for_others = can_apply_for_others,
@now_local = now_local
FROM @context;
IF LEN(@current_employee_text) > 10
OR @current_employee_text LIKE N'%[^0-9]%'
BEGIN
RAISERROR(N'leave_context_contract_invalid', 16, 1);
RETURN;
END;
DECLARE @current_employee_number DECIMAL(10, 0);
DECLARE @current_employee_id INT;
SET @current_employee_number = CONVERT(DECIMAL(10, 0), @current_employee_text);
IF @current_employee_number < 1 OR @current_employee_number > 2147483647
BEGIN
RAISERROR(N'leave_context_contract_invalid', 16, 1);
RETURN;
END;
SET @current_employee_id = CONVERT(INT, @current_employee_number);
IF @user_id IS NULL OR LEN(@user_id) > 10
OR @user_id LIKE N'%[^0-9]%'
BEGIN
RAISERROR(N'leave_user_scope_invalid', 16, 1);
RETURN;
END;
DECLARE @user_number DECIMAL(10, 0);
SET @user_number = CONVERT(DECIMAL(10, 0), @user_id);
IF @user_number < 1 OR @user_number > 2147483647
OR CONVERT(INT, @user_number) <> @current_employee_id
BEGIN
RAISERROR(N'leave_user_scope_invalid', 16, 1);
RETURN;
END;
DECLARE @operator_name NVARCHAR(50);
SELECT @operator_name = CONVERT(NVARCHAR(50), EmployeeName)
FROM dbo.p_employeetab WITH (HOLDLOCK)
WHERE employeeid = @current_employee_id
AND ISNULL(p_emp_status, '') <> N'离职';
IF @operator_name IS NULL
BEGIN
RAISERROR(N'leave_context_contract_invalid', 16, 1);
RETURN;
END;
/*
Read-only menu tokens end in "|". A write must have the exact edit token
",16629,"; the trusted WinForm authorizer performs the same check before
confirmation, and this database check closes the time-of-check/use gap.
*/
DECLARE @leave_permission_text NVARCHAR(MAX);
SET @leave_permission_text = N',';
SELECT TOP (1)
@leave_permission_text = N','
+ CONVERT(NVARCHAR(MAX), ISNULL(hrPurview, '')) + N','
FROM dbo.p_SubsysPurviewTab WITH (UPDLOCK, HOLDLOCK)
WHERE employeeid = @current_employee_id;
IF (@current_employee_id <> 1 OR @operator_name <> N'管理员')
AND CHARINDEX(N',16629,', @leave_permission_text) = 0
BEGIN
RAISERROR(N'leave_write_permission_denied', 16, 1);
RETURN;
END;
DECLARE @command_name VARCHAR(128);
SET @command_name = CASE WHEN @action = 'create_draft'
THEN 'hr.leave.create' ELSE 'hr.leave.submit' END;
IF OBJECT_ID(N'dbo.p_agent_command_idempotency', N'U') IS NULL
OR OBJECT_ID(N'dbo.p_agent_business_audit', N'U') IS NULL
OR OBJECT_ID(N'dbo.p_agent_integration_outbox', N'U') IS NULL
BEGIN
RAISERROR(N'leave_agent_evidence_schema_missing', 16, 1);
RETURN;
END;
DECLARE @application_lock_result INT;
DECLARE @application_lock_resource NVARCHAR(255);
SET @application_lock_resource = N'lserp-agent:leave:'
+ LEFT(@account_book, 40) + N':' + LEFT(@subsystem_id, 20)
+ N':' + LEFT(@user_id, 10) + N':' + @command_name
+ N':' + CONVERT(NVARCHAR(128), @idempotency_key);
EXEC @application_lock_result = sys.sp_getapplock
@Resource = @application_lock_resource,
@LockMode = 'Exclusive',
@LockOwner = 'Transaction',
@LockTimeout = 10000;
IF @application_lock_result < 0
BEGIN
RAISERROR(N'leave_idempotency_lock_failed', 16, 1);
RETURN;
END;
DECLARE @idempotency_id BIGINT;
DECLARE @stored_fingerprint CHAR(64);
DECLARE @stored_status TINYINT;
DECLARE @stored_result_code VARCHAR(128);
DECLARE @stored_record_id NVARCHAR(128);
DECLARE @stored_transaction_id VARCHAR(128);
DECLARE @stored_audit_id VARCHAR(128);
SELECT
@idempotency_id = id,
@stored_fingerprint = input_fingerprint,
@stored_status = status,
@stored_result_code = result_code,
@stored_record_id = record_id,
@stored_transaction_id = transaction_evidence_id,
@stored_audit_id = business_audit_id
FROM dbo.p_agent_command_idempotency WITH (UPDLOCK, HOLDLOCK)
WHERE account_book = @account_book
AND subsystem_id = @subsystem_id
AND user_id = @user_id
AND command_name = @command_name
AND idempotency_key = @idempotency_key;
IF @idempotency_id IS NOT NULL
AND @stored_fingerprint <> @input_fingerprint
BEGIN
SELECT
CONVERT(BIT, 0) AS success,
CONVERT(VARCHAR(128), 'idempotency_key_conflict') AS code,
CONVERT(NVARCHAR(1000), N'同一幂等键已绑定不同的业务输入。') AS message,
CONVERT(NVARCHAR(128), NULL) AS record_id,
CONVERT(BIT, 0) AS needs_ui,
CONVERT(BIT, 0) AS idempotency_replayed,
@idempotency_key AS applied_idempotency_key,
@input_fingerprint AS applied_input_fingerprint,
CONVERT(VARCHAR(128), NULL) AS transaction_evidence_id,
CONVERT(VARCHAR(128), NULL) AS business_audit_id;
RETURN;
END;
IF @idempotency_id IS NOT NULL AND @stored_status = 1
BEGIN
SELECT
CONVERT(BIT, 1) AS success,
@stored_result_code AS code,
CONVERT(NVARCHAR(1000), N'已返回同一幂等请求的原事务结果。') AS message,
@stored_record_id AS record_id,
CONVERT(BIT, 0) AS needs_ui,
CONVERT(BIT, 1) AS idempotency_replayed,
@idempotency_key AS applied_idempotency_key,
@input_fingerprint AS applied_input_fingerprint,
@stored_transaction_id AS transaction_evidence_id,
@stored_audit_id AS business_audit_id;
RETURN;
END;
IF @idempotency_id IS NOT NULL
BEGIN
SELECT
CONVERT(BIT, 0) AS success,
CONVERT(VARCHAR(128), 'idempotency_request_not_replayable') AS code,
CONVERT(NVARCHAR(1000), N'同一幂等请求尚未形成可重放的成功结果。') AS message,
CONVERT(NVARCHAR(128), NULL) AS record_id,
CONVERT(BIT, 0) AS needs_ui,
CONVERT(BIT, 0) AS idempotency_replayed,
@idempotency_key AS applied_idempotency_key,
@input_fingerprint AS applied_input_fingerprint,
CONVERT(VARCHAR(128), NULL) AS transaction_evidence_id,
CONVERT(VARCHAR(128), NULL) AS business_audit_id;
RETURN;
END;
INSERT INTO dbo.p_agent_command_idempotency
(
account_book, subsystem_id, user_id, command_name,
idempotency_key, input_fingerprint, status
)
VALUES
(
@account_book, @subsystem_id, @user_id, @command_name,
@idempotency_key, @input_fingerprint, 0
);
SET @idempotency_id = CONVERT(BIGINT, SCOPE_IDENTITY());
DECLARE @effective_record_id NVARCHAR(128);
DECLARE @result_code VARCHAR(128);
DECLARE @result_message NVARCHAR(1000);
IF @action = 'create_draft'
BEGIN
SET @employee_id = NULLIF(LTRIM(RTRIM(@employee_id)), N'');
SET @leave_type_code = NULLIF(LTRIM(RTRIM(@leave_type_code)), N'');
SET @flow_type_code = NULLIF(LTRIM(RTRIM(@flow_type_code)), N'');
SET @reason = NULLIF(LTRIM(RTRIM(@reason)), N'');
IF @employee_id IS NULL OR LEN(@employee_id) > 10
OR @employee_id LIKE N'%[^0-9]%'
OR @leave_type_code IS NULL OR LEN(@leave_type_code) > 10
OR @leave_type_code LIKE N'%[^0-9]%'
OR @flow_type_code IS NULL OR LEN(@flow_type_code) > 10
OR @flow_type_code LIKE N'%[^0-9]%'
OR @start_local IS NULL OR @end_local IS NULL
OR @end_local <= @start_local
OR DATEDIFF(DAY, CONVERT(DATE, @start_local),
CONVERT(DATE, @end_local)) > 31
OR @requested_hours IS NULL
OR @requested_hours < 0 OR @requested_hours > 744
OR @reason IS NULL OR LEN(@reason) < 2 OR LEN(@reason) > 500
OR DATALENGTH(CONVERT(VARCHAR(8000), @reason)) > 500
OR CONVERT(NVARCHAR(500), CONVERT(VARCHAR(500), @reason)) <> @reason
BEGIN
RAISERROR(N'leave_create_input_invalid', 16, 1);
RETURN;
END;
DECLARE @control_code INT;
SET @control_code = 1;
WHILE @control_code <= 31
BEGIN
IF CHARINDEX(NCHAR(@control_code), @reason) > 0
BEGIN
RAISERROR(N'leave_create_input_invalid', 16, 1);
RETURN;
END;
SET @control_code = @control_code + 1;
END;
IF CHARINDEX(NCHAR(127), @reason) > 0
BEGIN
RAISERROR(N'leave_create_input_invalid', 16, 1);
RETURN;
END;
DECLARE @requested_employee_number DECIMAL(10, 0);
DECLARE @leave_type_number DECIMAL(10, 0);
DECLARE @flow_type_number DECIMAL(10, 0);
SET @requested_employee_number = CONVERT(DECIMAL(10, 0), @employee_id);
SET @leave_type_number = CONVERT(DECIMAL(10, 0), @leave_type_code);
SET @flow_type_number = CONVERT(DECIMAL(10, 0), @flow_type_code);
IF @requested_employee_number < 1
OR @requested_employee_number > 2147483647
OR @leave_type_number < 1 OR @leave_type_number > 2147483647
OR @flow_type_number < 1 OR @flow_type_number > 2147483647
OR (@requested_employee_number <> @current_employee_id
AND ISNULL(@can_apply_for_others, 0) <> 1)
BEGIN
RAISERROR(N'leave_apply_for_others_denied', 16, 1);
RETURN;
END;
IF @start_local < CONVERT(DATE, @now_local)
BEGIN
RAISERROR(N'leave_past_start_denied', 16, 1);
RETURN;
END;
DECLARE @type_state TABLE (enabled BIT NOT NULL);
INSERT INTO @type_state(enabled)
EXEC dbo.p_lserp_agent_workflow_read_compat100
@workflow = 'leave',
@action = 'type_enabled',
@module_code = @module_code,
@account_book = @account_book,
@subsystem_id = @subsystem_id,
@user_id = @user_id,
@leave_type_code = @leave_type_code;
IF (SELECT COUNT_BIG(*) FROM @type_state) <> 1
OR NOT EXISTS (SELECT 1 FROM @type_state WHERE enabled = 1)
BEGIN
RAISERROR(N'leave_type_not_enabled', 16, 1);
RETURN;
END;
DECLARE @flow_state TABLE (enabled BIT NOT NULL);
INSERT INTO @flow_state(enabled)
EXEC dbo.p_lserp_agent_workflow_read_compat100
@workflow = 'leave',
@action = 'flow_type_enabled',
@module_code = @module_code,
@account_book = @account_book,
@subsystem_id = @subsystem_id,
@user_id = @user_id,
@flow_type_code = @flow_type_code;
IF (SELECT COUNT_BIG(*) FROM @flow_state) <> 1
OR NOT EXISTS (SELECT 1 FROM @flow_state WHERE enabled = 1)
BEGIN
RAISERROR(N'leave_flow_type_not_enabled', 16, 1);
RETURN;
END;
DECLARE @hours_state TABLE (hours DECIMAL(18, 6) NOT NULL);
INSERT INTO @hours_state(hours)
EXEC dbo.p_lserp_agent_workflow_read_compat100
@workflow = 'leave',
@action = 'calculate_hours',
@module_code = @module_code,
@account_book = @account_book,
@subsystem_id = @subsystem_id,
@user_id = @user_id,
@employee_id = @employee_id,
@start_local = @start_local,
@end_local = @end_local;
IF (SELECT COUNT_BIG(*) FROM @hours_state) <> 1
BEGIN
RAISERROR(N'leave_hours_contract_invalid', 16, 1);
RETURN;
END;
DECLARE @calculated_hours DECIMAL(18, 6);
SELECT @calculated_hours = hours FROM @hours_state;
IF @calculated_hours <= 0
OR (@requested_hours > 0
AND ABS(@requested_hours - @calculated_hours) > 0.01)
BEGIN
RAISERROR(N'leave_hours_mismatch', 16, 1);
RETURN;
END;
DECLARE @conflict_state TABLE (has_conflict BIT NOT NULL);
INSERT INTO @conflict_state(has_conflict)
EXEC dbo.p_lserp_agent_workflow_read_compat100
@workflow = 'leave',
@action = 'has_conflict',
@module_code = @module_code,
@account_book = @account_book,
@subsystem_id = @subsystem_id,
@user_id = @user_id,
@employee_id = @employee_id,
@start_local = @start_local,
@end_local = @end_local;
IF (SELECT COUNT_BIG(*) FROM @conflict_state) <> 1
OR EXISTS (SELECT 1 FROM @conflict_state WHERE has_conflict = 1)
BEGIN
RAISERROR(N'leave_time_conflict', 16, 1);
RETURN;
END;
DECLARE @employee_name NVARCHAR(50);
DECLARE @employee_department_id INT;
DECLARE @employee_post_id INT;
SELECT
@employee_name = CONVERT(NVARCHAR(50), EmployeeName),
@employee_department_id = Departmentid,
@employee_post_id = p_emp_postid
FROM dbo.p_employeetab WITH (HOLDLOCK)
WHERE employeeid = CONVERT(INT, @requested_employee_number)
AND ISNULL(p_emp_status, '') <> N'离职';
IF @employee_name IS NULL
OR @employee_department_id IS NULL
OR @employee_post_id IS NULL
OR DATALENGTH(CONVERT(VARCHAR(8000), @employee_name)) > 50
OR DATALENGTH(CONVERT(VARCHAR(8000), @operator_name)) > 10
BEGIN
RAISERROR(N'leave_employee_not_available', 16, 1);
RETURN;
END;
DECLARE @standard_day_hours DECIMAL(18, 6);
DECLARE @calculated_days DECIMAL(18, 6);
SELECT TOP (1)
@standard_day_hours = CONVERT(DECIMAL(18, 6),
CASE WHEN ISNULL(check_param.shichang, 0) = 0
THEN 7.5 ELSE check_param.shichang END)
FROM dbo.HR_ScheduleTab AS schedule WITH (HOLDLOCK)
LEFT JOIN dbo.P_SystemCheckParamTab AS check_param WITH (HOLDLOCK)
ON schedule.HR_Schedule_billdocument_d = check_param.Billdocument_Id
WHERE schedule.HR_Schedule_employeeid =
CONVERT(INT, @requested_employee_number)
AND schedule.HR_Schedule_Date <= CONVERT(DATE, @start_local)
ORDER BY schedule.HR_Schedule_Date DESC;
IF @standard_day_hours IS NULL OR @standard_day_hours <= 0
BEGIN
RAISERROR(N'leave_day_hours_not_configured', 16, 1);
RETURN;
END;
SET @calculated_days = ROUND(
@calculated_hours / @standard_day_hours, 1);
IF @calculated_days <= 0 OR @calculated_days > 366
BEGIN
RAISERROR(N'leave_calculated_days_invalid', 16, 1);
RETURN;
END;
DECLARE @number_lock_result INT;
EXEC @number_lock_result = sys.sp_getapplock
@Resource = N'lserp-agent:hr_4011:document-number',
@LockMode = 'Exclusive',
@LockOwner = 'Transaction',
@LockTimeout = 10000;
IF @number_lock_result < 0
BEGIN
RAISERROR(N'leave_document_number_lock_failed', 16, 1);
RETURN;
END;
/*
Hold the identity tail until the outer transaction completes. The legacy
save procedure can overwrite @msg after assigning the document number, so
the created row is identified by the protected identity interval instead of
trusting that output text.
*/
DECLARE @identity_before INT;
SELECT @identity_before = ISNULL(MAX(hr_ela_id), 0)
FROM dbo.HR_EmpLeaveAloneTab WITH (UPDLOCK, HOLDLOCK);
DECLARE @base_sql NVARCHAR(MAX);
SET @base_sql =
N'INSERT INTO dbo.HR_EmpLeaveAloneTab '
+ N'(hr_ela_empid,hr_ela_employeename,hr_ela_depid,hr_ela_postid,'
+ N'hr_ela_type,hr_ela_billtype,'
+ N'hr_ela_Leavebak,hr_ela_starttime,hr_ela_finishtime,'
+ N'hr_ela_totals,hr_ela_totals1,hr_ela_operatorid,hr_ela_operatorname,'
+ N'hr_ela_operatedate) VALUES ('
+ CONVERT(NVARCHAR(20), CONVERT(INT, @requested_employee_number))
+ N',N''' + REPLACE(@employee_name, N'''', N'''''') + N''','
+ N'N''' + CONVERT(NVARCHAR(20), @employee_department_id) + N''','
+ N'N''' + CONVERT(NVARCHAR(20), @employee_post_id) + N''','
+ CONVERT(NVARCHAR(20), CONVERT(INT, @leave_type_number))
+ N',' + CONVERT(NVARCHAR(20), CONVERT(INT, @flow_type_number)) + N','
+ N'N''' + REPLACE(@reason, N'''', N'''''') + N''','''
+ CONVERT(NVARCHAR(23), @start_local, 121) + N''','''
+ CONVERT(NVARCHAR(23), @end_local, 121) + N''','
+ CONVERT(NVARCHAR(50), @calculated_hours) + N','
+ CONVERT(NVARCHAR(50), @calculated_days) + N','
+ CONVERT(NVARCHAR(20), @current_employee_id) + N',N'''
+ REPLACE(@operator_name, N'''', N'''''') + N''','''
+ CONVERT(NVARCHAR(23), @now_local, 121) + N''')';
DECLARE @legacy_message NVARCHAR(4000);
DECLARE @legacy_return INT;
SET @legacy_message = N'';
EXEC @legacy_return = dbo.p_BaseSave70
@baseSql = @base_sql,
@saveType = 1,
@modid = 'hr_4011',
@tablename = 'HR_EmpLeaveAloneTab',
@keyfield = 'hr_ela_id',
@keyvalue = '',
@Operatorid = @current_employee_id,
@operatorName = @operator_name,
@msg = @legacy_message OUTPUT,
@comfirmFlag = 0;
IF @legacy_return <> 1 OR @@TRANCOUNT < 1 OR XACT_STATE() <> 1
BEGIN
RAISERROR(N'leave_legacy_create_failed', 16, 1);
RETURN;
END;
DECLARE @created_record_count BIGINT;
DECLARE @created_identity INT;
SELECT
@created_record_count = COUNT_BIG(*),
@created_identity = MAX(hr_ela_id),
@effective_record_id = MAX(CONVERT(NVARCHAR(50), hr_ela_no))
FROM dbo.HR_EmpLeaveAloneTab WITH (UPDLOCK, HOLDLOCK)
WHERE hr_ela_id > @identity_before;
IF @created_record_count <> 1
OR @created_identity IS NULL
OR NULLIF(LTRIM(RTRIM(@effective_record_id)), N'') IS NULL
OR NOT EXISTS
(
SELECT 1
FROM dbo.HR_EmpLeaveAloneTab WITH (UPDLOCK, HOLDLOCK)
WHERE hr_ela_id = @created_identity
AND CONVERT(NVARCHAR(50), hr_ela_no) = @effective_record_id
AND hr_ela_empid = CONVERT(INT, @requested_employee_number)
AND hr_ela_employeename = CONVERT(VARCHAR(50), @employee_name)
AND hr_ela_depid = CONVERT(VARCHAR(20), @employee_department_id)
AND hr_ela_postid = CONVERT(VARCHAR(20), @employee_post_id)
AND hr_ela_type = CONVERT(INT, @leave_type_number)
AND hr_ela_billtype = CONVERT(INT, @flow_type_number)
AND hr_ela_starttime = @start_local
AND hr_ela_finishtime = @end_local
AND ABS(ISNULL(hr_ela_totals, 0) - @calculated_hours) <= 0.01
AND ABS(ISNULL(hr_ela_totals1, 0) - @calculated_days) <= 0.01
AND hr_ela_Leavebak = CONVERT(VARCHAR(500), @reason)
AND hr_ela_operatorid = @current_employee_id
AND ISNULL(hr_ela_cancelFlag, 0) = 0
)
BEGIN
RAISERROR(N'leave_legacy_create_evidence_invalid', 16, 1);
RETURN;
END;
SET @result_code = 'leave_draft_created';
SET @result_message = N'请假申请草稿已通过 ERP 原保存链创建。';
/* submit_after_save_intent is intentionally never executed here. */
SET @submit_after_save_intent = ISNULL(@submit_after_save_intent, 0);
END
ELSE
BEGIN
SET @record_id = NULLIF(LTRIM(RTRIM(@record_id)), N'');
IF @record_id IS NULL OR LEN(@record_id) > 128
BEGIN
RAISERROR(N'leave_submit_record_invalid', 16, 1);
RETURN;
END;
IF DATALENGTH(CONVERT(VARCHAR(8000), @operator_name)) > 20
BEGIN
RAISERROR(N'leave_submit_operator_invalid', 16, 1);
RETURN;
END;
/* p_baseApply contains legacy self-migration branches; never let an Agent
request reach those branches. Schema repair remains a DBA operation. */
IF COL_LENGTH('dbo.p_systemdlltabflowtypestep', 'autoStep') IS NULL
OR COL_LENGTH('dbo.p_systemdlltabflowtypestep', 'comfirmOper') IS NULL
OR COL_LENGTH('dbo.p_systemNotification', 'stepCode') IS NULL
BEGIN
RAISERROR(N'leave_approval_schema_not_ready', 16, 1);
RETURN;
END;
DECLARE @submit_state TABLE
(
can_submit BIT NOT NULL,
reason NVARCHAR(1000) NULL
);
INSERT INTO @submit_state(can_submit, reason)
EXEC dbo.p_lserp_agent_workflow_read_compat100
@workflow = 'leave',
@action = 'can_submit',
@module_code = @module_code,
@account_book = @account_book,
@subsystem_id = @subsystem_id,
@user_id = @user_id,
@record_id = @record_id;
IF (SELECT COUNT_BIG(*) FROM @submit_state) <> 1
OR NOT EXISTS (SELECT 1 FROM @submit_state WHERE can_submit = 1)
BEGIN
RAISERROR(N'leave_submit_not_allowed', 16, 1);
RETURN;
END;
DECLARE @submit_employee_id INT;
DECLARE @submit_record_count BIGINT;
SELECT
@submit_record_count = COUNT_BIG(*),
@submit_employee_id = MAX(hr_ela_empid),
@effective_record_id = MAX(CONVERT(NVARCHAR(50), hr_ela_no))
FROM dbo.HR_EmpLeaveAloneTab WITH (UPDLOCK, HOLDLOCK)
WHERE CONVERT(NVARCHAR(32), hr_ela_id) = @record_id
OR CONVERT(NVARCHAR(50), hr_ela_no) = @record_id;
IF @submit_record_count <> 1
OR @submit_employee_id <> @current_employee_id
OR NULLIF(@effective_record_id, N'') IS NULL
BEGIN
RAISERROR(N'leave_submit_record_scope_invalid', 16, 1);
RETURN;
END;
DECLARE @apply_message NVARCHAR(4000);
DECLARE @apply_return INT;
SET @apply_message = N'';
EXEC @apply_return = dbo.p_baseApply
@typeCode = 'hr_4011',
@billDocumentId = @effective_record_id,
@Operatorid = @current_employee_id,
@operatorName = @operator_name,
@comfirmType = 1,
@msg = @apply_message OUTPUT;
IF @apply_return <> 1 OR @@TRANCOUNT < 1 OR XACT_STATE() <> 1
BEGIN
RAISERROR(N'leave_legacy_submit_failed', 16, 1);
RETURN;
END;
IF NOT EXISTS
(
SELECT 1
FROM dbo.HR_EmpLeaveAloneTab AS leave_record WITH (UPDLOCK, HOLDLOCK)
JOIN dbo.p_baseflowOper AS flow_record WITH (UPDLOCK, HOLDLOCK)
ON flow_record.modid = 'hr_4011'
AND flow_record.keyvalue = leave_record.hr_ela_no
AND flow_record.billtype = leave_record.hr_ela_billtype
WHERE leave_record.hr_ela_no = @effective_record_id
AND leave_record.hr_ela_empid = @current_employee_id
)
BEGIN
RAISERROR(N'leave_legacy_submit_evidence_invalid', 16, 1);
RETURN;
END;
SET @result_code = 'leave_submitted';
SET @result_message = N'请假申请已通过 ERP 原审批流提交。';
END;
DECLARE @guid_text VARCHAR(32);
DECLARE @transaction_evidence_id VARCHAR(128);
DECLARE @business_audit_id VARCHAR(128);
DECLARE @outbox_event_id VARCHAR(128);
SET @guid_text = LOWER(REPLACE(CONVERT(VARCHAR(36), NEWID()), '-', ''));
SET @transaction_evidence_id = 'tx-leave-' + @guid_text;
SET @business_audit_id = 'audit-leave-' + @guid_text;
SET @outbox_event_id = 'outbox-leave-' + @guid_text;
INSERT INTO dbo.p_agent_business_audit
(
business_audit_id, transaction_evidence_id, correlation_id,
idempotency_id, account_book, subsystem_id, user_id,
command_name, module_code, action_name, record_id,
input_fingerprint
)
VALUES
(
@business_audit_id, @transaction_evidence_id, @correlation_id,
@idempotency_id, @account_book, @subsystem_id, @user_id,
@command_name, @module_code, @action, @effective_record_id,
@input_fingerprint
);
INSERT INTO dbo.p_agent_integration_outbox
(
event_id, business_audit_id, correlation_id, account_book,
subsystem_id, module_code, action_name, record_id, status
)
VALUES
(
@outbox_event_id, @business_audit_id, @correlation_id, @account_book,
@subsystem_id, @module_code, @action, @effective_record_id, 0
);
UPDATE dbo.p_agent_command_idempotency
SET status = 1,
result_code = @result_code,
record_id = @effective_record_id,
transaction_evidence_id = @transaction_evidence_id,
business_audit_id = @business_audit_id,
completed_at_utc = SYSUTCDATETIME()
WHERE id = @idempotency_id AND status = 0;
IF @@ROWCOUNT <> 1
BEGIN
RAISERROR(N'leave_idempotency_completion_failed', 16, 1);
RETURN;
END;
SELECT
CONVERT(BIT, 1) AS success,
@result_code AS code,
@result_message AS message,
@effective_record_id AS record_id,
CONVERT(BIT, 0) AS needs_ui,
CONVERT(BIT, 0) AS idempotency_replayed,
@idempotency_key AS applied_idempotency_key,
@input_fingerprint AS applied_input_fingerprint,
@transaction_evidence_id AS transaction_evidence_id,
@business_audit_id AS business_audit_id;
END;
GO