跳转至

浙大预约报销:日常报销流程与函数契约

协议实现放在 packages/providers 的 ZjuFinanceClient(红线 7)。业务层只消费本文的函数,不拼门户表单、不直连 *.action。

依据是 2026-09-26 两份门户 HAR(WF_YB_NEW)。差旅、出国、论文版面费走别的业务大类,不在本文范围。金额填写、发票挂接、支付明细两次录制都没有成功提交,本文不给这三步发明参数。

1. 两个应用

应用 基址 职责
门户 https://cwcx.zju.edu.cn/WFManager 登录、会话探测
预约报销 https://cwcx.zju.edu.cn/WF_YB_NEW 建单、选项目、预约、打印

登录之后先对 WFManager 做空 POST loginAction_validateLogin.action。成功体是 {"resflag":"ok","msg":"密码为强密码"}。通过后才进入 WF_YB_NEW。真正的 loginAction_doLogin.action 不在这两份 HAR 里,沿用 03-architecture.md §6.1 的现场登录。

WF_YB_NEW 的业务参数名和值都走与登录相同的 swapString + base64 混淆,参数名每次不同。埋点 logRemark 和 redirectCustomPage 的 url 是明文。会话上下文由 Referer 的 context= 带上:业务 YBX、项目 WF_YBX、学工号、账套日期。客户端要保留完整 cookie jar;只记 JSESSIONID 不够跨进子系统(§6.1)。

页面是窗口号驱动的。一次按钮往返通常是:

commonUpdate_responseButtonEvent.action   按钮校验,pass / passlocal(...) / 明文错误
common_updateUserContext.action           把向导变量写入会话,成功体 ok
loadDefinition.action                     拉窗口定义
logRemark.action                          埋点,成功体 success
commonQuery_doQuery.action                jqGrid 拉列表

只读查询不必走保存按钮。

2. 日常报销是什么

日用品、3C、打车在门户里没有各自的窗口,都落在业务大类「日常报销业务」:

字段 值
bxOrd 1
科目集合 B06,B10,B11,B12,B60,B70,B76,B80,B90,B75,B91,B19
业务类型 普通业务
支付方式 14 混合支付;另一可选项是 10 多笔转卡

已观察到的列表形态:状态码 10 对应「已预约」,旁边还有一列「等待财务入账」。金额是元的小数字符串,不是分。支付数组和 dzfp 在没有收款明细时为空。具体预约号、申请号、项目号不写入文档。

3. 流程

validateLogin
   ▼
openYb                         默认落地 116 报销单管理
   ▼
listClaims                     可选,看历史单
   ▼
startApply                     116 → 4910 → 3970 → 234
   ▼
selectBxType(bxOrd=1)          234 先选大类,否则项目会被清空
   ▼
listProjects                   3510 / funcno 3486
   ▼
selectProject(uniPrjCode)
   ▼
issueSerial                    582 发号。单独调用只占号,不产生可打印单据
   ▼
saveAmountLine                 2360 写入调用方指定的一行报销项,空行会被拒绝
   ▼
acceptPledge                   9180,三项承诺都要勾选
   ▼
listCampuses
   ▼
listAppointmentSlots(campus)   2164 / funcno 2179
   ▼
bookWindow(campus, date, window)
   ▼
readPrint(serialNo)            880 / 1807,核对而不是提交

窗口职责:

窗口 名称 函数
116 当前报销申请 / 报销单管理 listClaims、startApply
4910 申请报销单壳,sqbxd.jsp 只调用 openSqbxd() startApply 内部
3970 打印壳,setGeneralBaseInfo_CLR.html 只调用 openClr() startApply 内部
234 填写报销基本信息。标题写明先选大类再选项目 selectBxType
3510 可选择项目,funcno 3486 listProjects、selectProject
3647 已选项目,只有项目号 selectProject 的旁路结果
582 设置通用基本信息。页面脚本 yb_init() 在这里发号 issueSerial
2360 填写报销金额。一行报销项由调用方指定 saveAmountLine
9180 报销承诺 acceptPledge
2163 选择校区 listCampuses
2164 选择预约时间,funcno 2179 listAppointmentSlots、bookWindow
880 / 1807 打印预览 readPrint
1921 预约物流情况(对话框),funcno 1937 readClaimLogistics

startApply 在离开 116 时写入 startYb=startYb、isModifyWrite=F,并清空 230 上的项目号。234 的 bindSQL 回填学工号、姓名、经办人、手机号、币种人民币、NEED_INV=N、SUBS_WLMSG=def。这些不作为函数参数。

选项目保存后调用 custom_findWhData 和 customv1_checkProjectIsFei。国创子项目返回 passlocal(is_kyxm:N),按非科研、授权项目继续。发号接口 customv1_getNewSerialNoAndclrresult 返回 newserialno、项目名、tcode、pcode、isSQ、kyflag。项目号必须是 3486 的星号形式;连字符项目号也能发号,但打印数据和列表都没有这张单。单独发号只占序列,不落单。

2360 下一步按钮 `$5806313e07799d9fd8b3095024aaac72 读取 0-2375Bxmultirows。空行返回「填报金额不能为空!@」。一行的字段顺序是金额、bu_code、b_code,计数 1。金额和报销项由调用方传入,不在协议里写死。通过条件是 passlocal(bmftype9161:23;jhmsg:;moreprojFlag:F)。

9180 必填三项是不重复报销、业务真实、材料不涉密,对应 bmf_check2、bmf_check3、bmf_check9,值都是 Y。缺任何一项时按钮不发请求。承诺通过后 custom_checkjhAfteryewu 的 result 为空,custom_isNeedPay 返回 isNeedPay=F。F 表示不需要收款明细,不是落单失败。

预约窗口有类型约束。省国库专窗对非省国库项目返回明文:「省国库专用预约窗口只给省国库项目预约开放,其他类型项目请预约其他的窗口!」中央国库预约专窗(紫金港西区)对这个授权、非科研项目可以通过。

4. 业务函数

一次日常报销对调用方只暴露一个写函数。只读查询分开,方便先选项目和档期。

createDailyClaim(session, input: DailyClaimInput): Promise<DailyClaimDraft>

session 是已经过 validateLogin 并 openYb 的 WF_YB_NEW 会话。

interface DailyClaimInput {
  /** 固定 "1"。先选大类,否则 234 会清空项目。 */
  bxOrd: '1';
  /** 项目代码,来自 listProjects。 */
  uniPrjCode: string;
  /** 来自 listCampuses 的名称,如 "紫金港西区"。 */
  campus: string;
  /** YYYY-MM-DD,来自 listAppointmentSlots。 */
  arrangeDate: string;
  /** 窗口全名,如 "中央国库预约专窗(紫金港西区)"。 */
  arrangeWindow: string;
  /** 默认 "人民币"。234 回填也是这个值。 */
  currency?: string;
  /**
   * 默认 "14" 混合支付,与 234 下拉的预选一致。
   * 传 "10" 时改为多笔转卡。
   */
  payType?: '14' | '10';
  /** 附件张数。234 上没有预填,调用方必须传入。 */
  addition: string;
  /** 2360 的一行。金额是元字符串,编码来自标准分类叶子。 */
  line: { amountYuan: string; bCode: string; buCode: string };
}

interface DailyClaimDraft {
  serialNo: string;
  reqNo: string;
  /** 已观察到的成功值是 "10",列表文案「已预约」。 */
  status: string;
  amount: string;
  arrangePlace: string;
  arrangeDate: string;
  arrangeTime: string;
}

人的字段不进 input。234 进入后已经回填申请人姓名、实际报销人、预约联系人手机、固定电话、币种人民币、支付方式「混合支付」、物流提醒「默认」。这些由会话读取,调用方不传姓名、电话或收款卡。

页面上仍要调用方决定的是:附件张数、项目号、是否认证发票、报销项与金额、校区和预约日期。9180 的应知应会勾选沿用已核对的三项,不作为参数。不选发票时 NEED_INV 保持回填的 N。没有收款明细时 isNeedPay=F,表示这一步可以跳过。非零金额打到哪张卡,还没有成功请求。

createDailyClaim 内部顺序就是第 3 节从 selectBxType 到 readPrint 的那一段。任一步不是 pass / ok,把门户明文原样抛出,不自动换窗口重试。

5. 协议函数

共同参数只有 session。窗口号留在实现里。

5.1 会话

validateLogin(managerSession): Promise<void>
openYb(managerSession): Promise<YbSession>

validateLogin 无业务参数。成功条件是 resflag=ok 且 msg=密码为强密码。

openYb 拉脚本清单、角色、菜单、默认窗口。菜单要带角色拉第二次才有内容;这次角色是 6 / 学生。落地窗口固定为 WF_YB_NEW:116:报销单管理。getDefaultWinno 在部分会话返回 500,客户端不依赖它。

interface YbSession {
  roleId: string;
  roleName: string;
  defaultWinno: string;
}

脚本清单 Customize/CustomJsList.json 给出需要加密图片的接口前缀:cjMem_、cjCustom_、CJBatDtl_、registered_、mo_、imgSign、customv1_、invoiceValidate_。版本号 85。

5.2 单据列表

listClaims(session): Promise<ClaimSummary[]>

无业务参数。数据来自窗口 116、funcno 110 的 commonQuery_doQuery,按 yta.req_no DESC,每页 100。这是「当前报销申请」,只含尚未归档的单。仪表盘财务门户页用这一份列表展示金额、预约状态、入账状态和申请/预约时间。

单元格顺序已由非空行对上:

下标 字段 例子
0 reqNo 申请号
1 bxName 日常报销业务
2 uniPrjCode 项目代码
3 applyDate YYYY-MM-DD
5 amount 元的小数字符串
6 arrangeStatus 已预约 / 临时挂起
7 arrangeDate YYYY-MM-DD
8 arrangeTime 08:30-16:30
9 arrangePlace 窗口全名
12 bType 普通业务
13 serialNo 预约号
15 filterBcode 科目集合
20 status 10
21 accountStatus 等待财务入账
23 bxOrd 1

下标 4、14、16–19、22、24–25 这次是空列,先不命名。金额保持门户原文字符串。

listHistoryClaims(session, range: { startDate: string; endDate: string }): Promise<HistoryClaim[]>

菜单「历史报销单」是窗口 181,主查询 funcno 175,标题「历史申请记录」。它不读窗口 116 的列表。查询函数是 ybnew_bx_tree.GetRows_his_query,参数来自窗口 4850 / funcno 4351 的筛选表单,空筛选返回 0 行。仪表盘默认查最近 3 个月到当天,项目号、预约号和其他筛选留空。查询前把这些字段写入会话上下文;只送增量会覆盖学工号,所以仍走整表合并。

列定义顺序:

下标 字段 含义
0 reqNo 预约号(列定义名如此)
1 uniPrjCode 项目号
2 applyDate 申请日期
3 operator 实际报销人
4 status 状态原文
5 remark 备注
6 accountDate 入账日期
7 voucherNo 财务凭证号
8 amount 元的小数字符串
9 filterBcode 科目集合
12 serialNo 预约号
13 bType 业务类型原文

下标 10、11、14–16 这次是空列。排序 yta.serial_no DESC,每页 100。金额保持门户原文字符串,API 边界再转成分。这份列表没有预约窗口和时段。

5.3 选大类与项目

listBxTypes(session): Promise<BxType[]>
listPayTypes(session, bxOrd: string): Promise<PayType[]>
readOperatorContact(session): Promise<OperatorContact>
startApply(session): Promise<void>
selectBxType(session, bxOrd: string): Promise<BxFormDefaults>
listProjects(session, bxOrd: string): Promise<Project[]>
inspectProject(session, uniPrjCode: string): Promise<ProjectInspection>
selectProject(session, uniPrjCode: string): Promise<ProjectPick>

listBxTypes、listPayTypes、readOperatorContact、inspectProject 只读,不写上下文。业务大类下拉的绑定 sqlid 是 a5c089f537ca76a8ba294a10120dead2。日常报销的支付方式下拉是 922b31db4aaf93383faa52b2200801fa;其它大类的支付方式 sqlid 还没录到,调用时原样拒绝。经办人手机和固定电话分别由按钮函数 `$8a41cc685b9b66f15917c2d3fbde981e、`$948fce4117f495559afcd77f7b2b8306 按学工号回填。

inspectProject 调 customv1_checkProjectIsFei,参数是账套年、项目号、学工号。返回 isSQ、clt_mode、hasvalidGrant。它不发号。

selectBxType 只收 bxOrd。返回回填后的姓名、手机号、币种和可选支付方式。日常报销的支付方式会收成 10:多笔转卡_2;14:混合支付。

业务大类下拉是明文:

bxOrd 名称
1 日常报销业务
30 科研预开票抵押业务
31 版面费
20 国内差旅费
3 暂借款业务
4 酬金申报
17 会议费
13 因公出国(境)
19 学生实习报销
15 境外专家来访

listProjects 必须在 selectBxType 之后。数据来自 funcno 3486,每页 200,按 up.uni_prj_code ASC。列定义:

字段 列名
uniPrjCode 项目代码
activityName 项目名称
grantId 授权号
balance 余额
upChargeName 上级项目负责人
finishDate 有效期
tCode 隐藏列
grantType 隐藏列

3486 能列出当前人在该大类下的项目,但余额列这次是 0,不是项目可用余额。项目余额、额度和已报销来自 custom_findprjbudgets:对每个星号项目号查询,取 BU_NAME=项目余额 的 BALANCE、TOTAL_PLAN_AMT、TOTAL_EXEC_AMT。预约号传空,不发号。连字符项目号不进这张额度表。

selectProject 保存后看 is_kyxm。ProjectPick 带上项目号、isKyxm,以及发号前能取到的 tCode。

5.4 发号与承诺

issueSerial(session): Promise<SerialTicket>
acceptPledge(session): Promise<void>

两者都没有业务参数。issueSerial 要求项目已经选定。

interface SerialTicket {
  serialNo: string;
  activityName: string;
  tCode: string;
  pcode: string;
  /** "授权项目" 或其他门户原文。 */
  grantLabel: string;
  /** "Y" / "N"。 */
  kyFlag: string;
}

acceptPledge 的通过条件见第 3 节。不满足就抛门户原文。

5.5 预约档期

listCampuses(session): Promise<Campus[]>
listAppointmentSlots(session, campus: string): Promise<AppointmentSlot[]>
bookWindow(session, campus: string, date: string, window: string): Promise<void>

校区来自 2163 的绑定数据,无业务参数:

名称 代码
紫金港东区 1
紫金港西区 2
玉泉 3
西溪 4
华家池 5
海宁 6
舟山 7
继教 8
基建 9

listAppointmentSlots 必须先把校区写入上下文。未选校区时 funcno 2179 返回 records:0;选定校区后返回档期行。日期不作为入参,网格自己从近到远给出。

一行 8 列,已用东区和西区的非空行对上:

下标 字段 含义
0 date YYYY-MM-DD
1 time 如 08:30-16:30
2 window 窗口全名
3 capacity 名额
4 remaining 剩余;满员时原文是「已满」
6 bizType 普通业务
7 campus 校区名

下标 5 这次是空列,不命名。满员档和省国库专窗由调用方过滤。bookWindow 提交失败时保留门户明文,不改投别的窗口。

已有预约的地点和日期在 listClaims 里,不是这个函数。

5.6 核对

readPrint(session, serialNo: string): Promise<ClaimPrint>

拉 custom_findMultiPrintData。参数是 mcount=1、学工号、mrows=预约号、账套年。用于确认 STATUS、金额、预约地点和时段。它不是提交动作。

issueSerial 调 customv1_getNewSerialNoAndclrresult。参数是账套年、星号项目号、isold=F、币种、isPrjattr、学工号,以及空的冲借字段。它只返回预约号。随后调用 custom_findprjbudgets,参数是账套年、项目号、预约号、学工号。单据头要等 2360 的报销行写入之后才出现在 readPrint。

listAppointmentSlots 的网格 SQL 是 bx_tree.GetRows_arrangetime(校区, 起始日, bxOrd, 预约号)。校区、业务大类和预约号要先写入服务端上下文,否则返回空。bookWindow 用按钮 `$00868ba050094b3a723d4f91dee49cb4 校验,再用 `$eb9e44c0bf43f73f9f7fe66874f57c6e 把日期、时段、窗口名写入该预约号。

createDailyClaim 的顺序是 inspectProject → issueSerial → saveAmountLine → acceptPledge → listAppointmentSlots → bookWindow → readPrint。发票和收款明细仍不填。报销行由调用方传入。承诺勾选沿用已核对的三项,不作为调用方参数。

5.7 预约物流

readClaimLogistics(session, source: ClaimRowRef): Promise<ClaimLogisticsEntry[]>

窗口 1921「预约物流情况」,funcno 1937,以对话框形式打开。按钮「查看预约物流」在「当前报销申请」(116,网格 func 110)和「历史报销单」(181,网格 func 175)两处都有。这是只读查询:不改单据、不发号、不订窗口。

链路(历史单实测):

选中列表行
   ▼
common_updateUserContext.action   把选中行写进服务端会话
   ▼
loadDefinition.action             winno=1921
   ▼
logRemark.action                  remark=1921_预约物流情况
   ▼
loadDefinition.action             对话框定义
   ▼
commonQuery_doQuery.action        ?funcno=1937&projid=WF_YB_NEW&needcount=true

116 的按钮定义(实测):btnguid=96495BEDDCD244F7A51476D643E728FE、check_func=#$b8cf057902e6e9e5eadb92a76ad45827:#110-MultiRows#;#110-MultiCount#、acts=1==1:1920。acts 的 1920 只是按钮分组,真正落地的窗口是 1921、查询 funcno 是 1937(由 logRemark 与请求参数实测)。

会话字段(历史单实测,源网格 func 175;当前单据按钮 check_func 推断同构,源前缀换成 110、0-CURRWIN=116、0-REFFUNC1937=110):

字段 值
0-CURRWIN 源窗口号,历史单 181
0-REFFUNC1937 源网格 funcno,历史单 175
WF_YB_NEW$175-YTA.SERIAL_NO 选中行预约号
WF_YB_NEW$175-YTA.REQ_NO 选中行申请号
WF_YB_NEW$175-YTA.… 选中行其余各列(日期/状态/金额/凭证号)
WF_YB_NEW$175-ONEROWDATA / SINGLECOLNAMES 选中行原文与列名
WF_YB_NEW$1937-CURRPAGE / -COND 0 / 空

网格 6 列,按 yfl.log_date ASC,每页 20:

下标 字段 含义
0 yfl.log_date 时间
1 yfs.status_name 状态
2 yfl.user_name 操作人
3 u.user_seat 窗口坐席
4 u.user_contact 联系方式
5 yfl.remark 备注

status_name 是流程推进文案,实测序列形如「投递成功 → 已分配, 等待制单 → 临时挂起 → 已制单, 等待复核 → 已复核, 等待支付 → 报销完成」;remark 带制单人、窗口、凭证号和给经办人的留言。金额不在本表。

6. 暂不封装

步骤 窗口 缺什么
多行金额、税金、发票明细 2360 已核对的提交只有一行。多行、税金和发票明细没有成功请求
挂发票 — 脚本清单有 processInvoiceDtl.js、invoiceValidate_,两次都没打到
填收款明细 — 没有收款明细时 isNeedPay=F,打印数据没有支付数组。非零金额打到哪张预存卡,还没有成功请求
修改已有单 116 按钮会写 isModifyWrite,没有一次成功修改的记录

2360 网格的 13 个大类、48 个叶子是发票标准分类。名称、b_code、bu_code 与 packages/core 的 CAMPUS_TAXONOMY 对齐。发票 category 仍写 一级/二级;预约金额行用同一叶子的 b_code 与 bu_code。打车在网格说明里对应市内交通费 B0402,3C 外设沿用办公用品费 B0101。除一行提交形态外,其它叶子还没有成功提交。

7. 错误

沿用 §6.1 的分类,并补门户业务拒绝:

情况 类型 处理
validateLogin 不是强密码、doLogin 密码错 / 弱密码 / okupd / 锁定 ZjuCredentialError 不重试
验证码认错 换码,上限 4 次 见 §6.1
会话失效、302 回登录页 ZjuSessionError 重新登录
按钮返回明文业务拒绝(省国库专窗、项目类型不符) 门户业务错误,原文上抛 不换窗口重试
非 JSON、维护页 ZjuUnexpectedResponseError 人工看
超时、网络 ZjuNetworkError 可重试

凭据只在内存中解密后上送,不进日志、不进异常栈(红线 4)。HAR 里的学工号、手机号、项目负责人只作为字段位置的例子,不写入日志与测试夹具。