首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >用 WorkBuddy 给一套老 ERP 做自动对账:抓订单、匹配日记账、回写收付款

用 WorkBuddy 给一套老 ERP 做自动对账:抓订单、匹配日记账、回写收付款

原创
作者头像
用户12721756
修改2026-09-03 08:54:45
修改2026-09-03 08:54:45
350
举报

公司账上每个周都有一堆订单要核。ERP 里存着订单和结算价,支付宝导出来的日记账里记着实际收付的流水,两边得靠金额一笔一笔对上,把订单号标到日记账上,再回 ERP 里做收付款。

这活不难,烦在来回切换并仔细核对,我就想是不是可以交给WB去做?我不懂啥很高深的AI方面的技术知识,就是单纯的想让它帮我解决问题,开始注册了wb一直没用,完全没方向,好久后才又开始登录研究,这时发现官方奖励的大把积分都已经过期了。然后自己开始摸索,使用的过程中遇到问题就问AI,开始一天我试着只让它标注订单编号掉日记账,它顺利做完了,而且非常满意,今天我就想尝试让它做erp系统的订单和明细对应并把订单收付款操作也做掉。然后就有了下面WB的这些操作。

这篇记一下过程,重点是踩到的几个坑——都不是什么高深技术,但每一个都让我卡了一阵。

文中的系统地址、接口路径、字段名、Cookie 名都做了脱敏,业务数据(订单号、金额)也换成了等同样式的示例值。匹配逻辑和代码结构是原样保留的。

环境长什么样

  • ERP:一套 ASP.NET 老系统,订单模块 + 财务下的「出纳」模块
  • 日记账:一个 xlsx,28 个 sheet,内嵌 13 张 PNG(后面这张图是重点)
  • 目标:把 ERP 订单号写成 付-10021 / 退-10036 这样的格式,填进日记账「支付宝流水」表的 G 列

坑一:浏览器打不开,但 curl 能通

第一个意外来得很快。ERP 用 curl 请求一切正常,换成浏览器打开就被拦。

具体机制我没深挖,处理办法是起一个本地反向代理,把请求原样转给源站,浏览器访问 127.0.0.1:8899

代码语言:python
复制
import http.server, urllib.request, urllib.error

UPSTREAM = "http://erp.example.com"
PORT = 8899

class Proxy(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.proxy("GET")

    def do_POST(self):
        self.proxy("POST")

    def proxy(self, method):
        url = UPSTREAM + self.path
        body = None
        if method == "POST":
            length = int(self.headers.get("Content-Length") or 0)
            body = self.rfile.read(length) if length else b""
        req = urllib.request.Request(url, data=body, method=method)
        for h in ("Cookie", "Content-Type", "Referer", "Accept", "User-Agent"):
            v = self.headers.get(h)
            if v:
                req.add_header(h, v)
        req.add_header("Referer", UPSTREAM + "/Main.aspx")
        try:
            resp = urllib.request.urlopen(req, timeout=30)
            data, code, headers = resp.read(), resp.getcode(), resp.headers
        except urllib.error.HTTPError as e:
            data, code, headers = e.read(), e.code, e.headers
        except Exception as e:
            self.send_response(502)
            self.send_header("Content-Type", "text/plain; charset=utf-8")
            self.end_headers()
            self.wfile.write(str(e).encode("utf-8"))
            return
        self.send_response(code)
        self.send_header("Content-Type", headers.get("Content-Type", "text/html; charset=utf-8"))
        for h in ("Set-Cookie", "Location"):
            vals = headers.get_all(h) if hasattr(headers, "get_all") else None
            if vals:
                for v in vals:
                    self.send_header(h, v)
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

if __name__ == "__main__":
    http.server.ThreadingHTTPServer(("127.0.0.1", PORT), Proxy).serve_forever()

Set-CookieLocation 必须原样透传,否则登录态建不起来。

登录页有验证码。这个我没绕,开一个可见窗口让人输完,再从 document.cookie 里把会话 Cookie 取出来。后面所有请求带上这个 Cookie 就行,不用再碰浏览器。

还有个小插曲:代理进程僵死的时候,taskkill //PID 在 Git Bash 下不生效,得用 PowerShell 的 Stop-Process -Id <PID> -Force

坑二:结算价藏在 input 的 value 里

订单列表页没有结算价,得逐单进 /order/edit.aspx?id=<订单号>

我一开始按常规思路去读表格单元格文本,结果全是空的。翻了 HTML 才发现,那个「旅客信息」表里的值都渲染在 <input value="..."> 上,单元格本身没文本。

更麻烦的是页面上还有另一个「结算信息」合计表,表头也带「结算价」三个字,很容易取错。区分方法是看表头里有没有同时出现「旅客姓名 / 结算价 / 扣率」,而且这些表头单元格带着前缀文本(实际拿到的是类似 旅客信息\n\n\t\t旅客姓名 这种),不能按精确相等去匹配,得把整行拼起来用 in 判断:

代码语言:python
复制
for row in ROW_RE.findall(tb):
    cells = [re.sub(r"\s+", "", TAG_RE.sub("", c)) for c in CELL_RE.findall(row)]
    joined = "".join(cells)
    if "旅客姓名" in joined and "结算价" in joined and "扣率" in joined:
        hdr_ok = True
        break

锁定表之后,每个旅客行的 input 索引是固定的:

索引

字段

索引

字段

0

旅客姓名

6

票号

2

销售价

7

结算价

4

燃油

8

结算税

同一订单里同名同价的旅客会重复出现,我用 (姓名, 结算价) 做过去重。

另外两个实测经验:源站会限流,批量抓取得加 0.35–0.4 秒延时,超时之后等一两分钟会自己恢复;火车票订单不用去火车票模块找,订单模块里就能搜到,供应商那一栏显示「线上订单」。

坑三:openpyxl 保存会吃掉 13 张图

这个是三个坑里最阴的。日记账是个 2MB 多的 xlsx,我按习惯用 openpyxl 打开、写 G 列、保存。G 列写对了,但 13 张内嵌 PNG 全没了。

原因是 openpyxl 不保留它不认识的那部分 XML。带图片的 xlsx,要么别用 openpyxl 写,要么上 Excel COM:

代码语言:python
复制
import win32com.client

excel = win32com.client.DispatchEx("Excel.Application")
excel.Visible = False
excel.DisplayAlerts = False
wb = excel.Workbooks.Open(PATH)
ws = wb.Worksheets("支付宝流水")
ws.Cells(row, 7).Value = "付-10021"
wb.Save()
wb.Close()
excel.Quit()

写完要验一下图片还在不在,别等到同事打开才发现:

代码语言:python
复制
import zipfile
z = zipfile.ZipFile(PATH)
len([x for x in z.namelist() if x.startswith("xl/media/")])   # 应该还是 13

COM 写入之前先备份。这个我做了两次备份,第二次是因为要做标红操作,又存了一份。

匹配:三条证据,按可靠性排序

把订单的结算价抓回来以后,剩下的工作是把它们和日记账里的流水行对上。我用了三条证据,可靠性从高到低:

1. 流水号直连。 日记账第 11 列是流水号。退款行的流水号如果和某笔付款行一致,那就直接锁定原订单,不用看金额。比如一笔 41.00 的退款,流水号 ...88421306 和付款行「付-10036」那笔 45.00 完全一样。

2. 结算价 + 结算税 = 付款金额。 主力规则。我先用三个订单验过这条等式才敢批量跑,实跑的例子像这样:

代码语言:txt
复制
订单结算价 1480.00 + 结算税 0.60 = 1480.60,日记账付款金额 1480.60 ✓
订单结算价  742.00 + 结算税 0.30 =  742.30,日记账付款金额  742.30 ✓

3. 多笔拆分。 N 笔退款加起来等于一笔付款,而且这个订单正好 N 个乘客。实际碰到的是 318.00 × 3 = 954.00,对应一个 3 人订单。

对不上的时候不硬凑。我的处理是:摘要为空 + 整数大额,按账期付款标 付-账期付款;下结论之前先在订单号区间里把结算价搜一遍确认真的没有。这次有 2 笔是推断出来的——一笔退款流水号为空,只能按同批退款加同乘客去猜;另一笔摘要为空的整额支出,我把订单号区间 161 个订单的结算价全搜了一遍也没对上,才按账期付款处理。这两笔我单独标了「推测,请核对」,后来和下面那 4 笔退款一起标了红。

顺带一提标注格式本身也有讲究,是照着历史写法来的:付款 付-<订单号>,退款 退-<订单号>,内部划转(账户间转账、提现这类)不标。

回写 ERP:收付款其实是纯 HTTP

标注完日记账,还得回 ERP 里做收付款。我原以为要点一堆右键菜单,翻了 /Finance/payment.js 才发现,「收款操作」「付款操作」最终都走 POST /Ajax/payment_submit.ashx。页面上的 confirm() 弹窗只是前端提示,后端并不校验,直接 POST 就落账了。

既然是纯 HTTP,就不用开浏览器,带 Cookie 走代理即可。

订单参数从 /Json_db/cashier_grid.aspx 拿,加 &rows=300 一次取全量。每行有 main_id(主单)、sub_id(明细)、customer_id(客户)、supplier_id(供应商)、payable(应付)、paid(已付)。

这里有个细节:每个订单在列表里有两行,「已出票」是正数,「退票」是负数。付款要用已出票那一行的 sub_id,用错了金额对不上。

付款(stype=1)的提交字段:

代码语言:python
复制
def post_pay(order, sid, supplier, amount, sdate):
    data = {
        "stype":  "1",
        "mid":    supplier,          # 付款填供应商 id
        "selnum": "1",
        "idlist": str(order),        # 主单 id
        "sidlist": str(sid),         # 已出票行的明细 id
        "kf_ssk": f"{amount:.2f}",   # 实付
        "zk":     "0",
        "ysk":    f"{amount:.2f}",   # 应付 - 已付 - 折扣
        "sfkcj":  "0.00",
        "yc":     "0",
        "pay":    "支付宝",
        "bank":   "支付宝",
        "sdate":  sdate,             # 2026/9/2
        "bz":     f"付-{order}",
    }
    body = urllib.parse.urlencode(data).encode("utf-8")
    req = urllib.request.Request(SUBMIT, data=body, headers={
        "Cookie": COOKIE,
        "User-Agent": UA,
        "Content-Type": "application/x-www-form-urlencoded",
        "Referer": PROXY + "/Finance/cashier.aspx",
    })
    with urllib.request.urlopen(req, timeout=30) as r:
        return r.read().decode("utf-8", "ignore").strip()

收款(stype=0)字段基本一样,区别是 mid 填客户的 customer_id,金额取应收那一侧。

权限这块,页面隐藏域 pay_flag / recv_flag 得是 1,为 0 的话前端直接 alert 没权限。支付方式这两个下拉框的值是从 /Finance/cashier.aspx#pay / #bank 里取的,「支付宝流水」这张表两个下拉都填「支付宝」。

每笔提交完我都会回查一次 cashier_grid.aspx,确认那一行的 paid(已付)变成金额了才算成功——接口返回 1 不代表一定对,回查才是真的。这次 9 笔付款全部回查通过。

最后一道坎:退票行是负数

这是整件事里我唯一主动停下来的地方。

4 笔退款,金额看着都能对上,但我没敢录。原因有两条:

  1. 其中两笔在日记账里的金额是交叉的——一笔标着 68.00,一笔标着 41.00,但按订单号反推方向对不上,搞不清哪个是退哪个;
  2. 退票行的应收、应付字段都是负数,而提交函数里有个 差额 = 实收 - 应收 的计算,在负数下会算错,还可能把差额自动挂到预存款上去。

方向搞反的代价是把账记错,这比慢一点严重得多。所以我把这 4 笔在日记账里标了红,留给同事手动录。标红用的还是 Excel COM,保住那 13 张图。

自动化做到这里刹车,是我觉得最该留下的一个判断:金额对得上不等于方向对

一点复盘

跑完这一轮,我心里对这类事情的分界线更清楚了。

重复抓取、机械比对、格式固定的写入,这些很适合交出去。规则明确,做错了回查一次就能发现。这次扫了 161 个订单的结算价、写了 16 笔标注、提交了 9 笔付款,都属于这一类。

需要业务判断的地方就不适合了。退款的方向、摘要为空的大额支出、证据不足的推断,这些让规则去猜,猜对了没人夸你,猜错了账就乱了。

还有一条经验是「先跑空再落库」。write_codes.py 我写成默认只读取预览,加 --apply 才真写。每一次真写之前,先把文件复制一份。这 16 笔我写了两次备份,第二次是因为标红又动了一遍文件。

最后是那 13 张 PNG。如果没验图片数量,openpyxl 那一下就把同事的附件全弄没了,而且是在我保存之后、她打开文件之前,谁也不知道。写别人的文件之前,先搞清楚那个文件里到底装了什么。


这套流程我没让它散在一堆临时脚本里,最后收成了一个可复用的 WorkBuddy Skill:ERP 登录、订单抓取、金额匹配、Excel 写入、收付款提交都在里面,踩过的坑也一并写进去了。下次再跑,说一句「把这周的订单对应到日记账」就行。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 环境长什么样
  • 坑一:浏览器打不开,但 curl 能通
  • 坑二:结算价藏在 input 的 value 里
  • 坑三:openpyxl 保存会吃掉 13 张图
  • 匹配:三条证据,按可靠性排序
  • 回写 ERP:收付款其实是纯 HTTP
  • 最后一道坎:退票行是负数
  • 一点复盘
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档