浙大预约报销:日常报销流程与函数契约¶
协议实现放在 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. 业务函数¶
一次日常报销对调用方只暴露一个写函数。只读查询分开,方便先选项目和档期。
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 无业务参数。成功条件是 resflag=ok 且 msg=密码为强密码。
openYb 拉脚本清单、角色、菜单、默认窗口。菜单要带角色拉第二次才有内容;这次角色是 6 / 学生。落地窗口固定为 WF_YB_NEW:116:报销单管理。getDefaultWinno 在部分会话返回 500,客户端不依赖它。
脚本清单 Customize/CustomJsList.json 给出需要加密图片的接口前缀:cjMem_、cjCustom_、CJBatDtl_、registered_、mo_、imgSign、customv1_、invoiceValidate_。版本号 85。
5.2 单据列表¶
无业务参数。数据来自窗口 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 这次是空列,先不命名。金额保持门户原文字符串。
菜单「历史报销单」是窗口 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 要求项目已经选定。
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 核对¶
拉 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 预约物流¶
窗口 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 里的学工号、手机号、项目负责人只作为字段位置的例子,不写入日志与测试夹具。