问题背景

外地车辆进入北京,需要办理《进京证》,不办理证件驶入后会被执法设备抓拍,一次罚 100 扣 1 分,目前唯一的线上办理通道是下载《北京交警》App,注册后添加车辆,就可以为自己的爱车随时随地办理进京证了。注意如果有违法记录,需要缴纳罚款后才可以办理,缴纳罚款的线上办理通道是《交管12123》。

最早的时候,进京证只限制进五环的外地车辆,一年可以办理 12 次,一次 7 天。后来扩大范围到了六环,刚开始监控设备没健全还能进一下,目前是不行了,基本一进六环就会被拍。那六环外是不是随便跑呢?也不是的,像一些远郊县城如昌平、延庆、顺义、密云、怀柔、平谷,虽然地处六环之外,但是县城城区也有一个环,凡进此环者,也要办理进京证,对我这种混迹在县城周边的外地车主就非常不友好了,周末去县城里的购物中心、电影院也得办理进京证。也许是倾听了民意,后来改革了,进京证变为六环内与六环外两种:六环内还是一年 12 次,每次 7 天;六环外是一次 7 天但不限次数。一个例外是通州,虽然在六环外,也得按六环内办理,毕竟是城市副中心嘛!

虽然不限次数了,但也不是随意办理的,主要有以下规则:

  • 有未缴纳罚款不能办理
  • 中午 12 点后不能申请当天进京证
  • 可以提前申请,最多提前 3 天
  • 一个帐户可以挂多辆车,但只能为其中一辆车办理
  • 已办理六环外进京证,可以续办六环内进京证,成功后六环外进京证失效;反之不行,也没必要,因为持有六环内进京证可以跑六环外
  • 如果申请的不是当天的进京证,可以取消,但一天内只有一次取消机会,用完就不能再取消了
  • 进京证在最后一天有效期时可以办理新的进京证,也称续期
  • 特殊时期不能办理,比如疫情管控期间 (已翻篇,只是举例)

线上办理速度还是比较快的,基本一分钟左右就能出结果。但对我这种健忘症患者,往往临出门才想起来要办进京证,一看时间,已经过中午 12 点了,就比较尴尬了。

这个时候就想,有一个定时办理进京证的工具就好了,例如每周一凌晨自动办理这一周的进京证。

解决方案

需求搞明白了,下面着手实现。

抓包

首先要搞明白服务端的接口,需要通过抓包对《北京交警》进行报文分析。

之前抓包一直是用 mac 上的 Charles,后来偶然看到有个 VNET App,不需要安装证书、不需要启动代理就能抓包,拿来试了下,果然好用:

注意登录过程不要开启抓包,否则《北京交警》的登录界面会调不出来。除此之外,其它请求基本上都能抓到,从中挑选了两个我们需要关注的接口。

获取当前进京证

这个 stateList 接口可以获取用户账户下所有车辆的进京证信息,有些字段对于下一步申请进京证是必需的,另外了解当前进京证状态也有利于决定是否申请新的进京证。

header

> POST /pro//applyRecordController/stateList HTTP/1.1
> Host:jjz.jtgl.beijing.gov.cn
> Accept: */*
> Accept-Language:zh-CN,zh;q=0.8
> User-Agent:okhttp-okgo/jeasonlzy
> source:8724a2428c3f47358741f978fd082810
> authorization:f36abdfa-8878-46bf-91d9-5666f808e9a4
> Content-Type:application/json;charset=utf-8
> Connection:Keep-Alive
> Accept-Encoding:gzip
> Content-Length:97

比较重要的是 source 和 authorization 字段,前者标识了设备,后者标识了用户 token,也是主要的抓取对象之一,会用于之后的请求中。

data

{
  "v": "3.4.1",
  "sfzmhm": "150121198603226428",
  "s-source": "bjjj-android",
  "timestamp": "1676016273000"
}

请求时发送的 json 数据,亲测没什么用,发个空也能得到响应,看起来身份信息通过 http 头的 authorization 字段获取的。

响应

查看代码

 

返回的 json 比较大,基本按 data->bzclxx[]->bzxx[] 的结构组织,其中 data 存储用户信息;bzclxx 是车辆数组,存储与车相关的信息;bzxx 是进京证数组,存储与证相关的信息。

下面对重点的字段个简单说明:

  • data.sfzmhm:账户所有者身份证号
  • bzclxx[].vId:车辆唯一标识,后面申请时会用到
  • bzclxx[].hpzl:未知,后面申请时会用到
  • bzclxx[].hphm:车辆号牌,后面申请时会用到
  • bzclxx[].bzxx[].blztmc:进京证状态,包括但不限于:
    • 审核通过(生效中)
    • 审核通过(待生效)
    • 审核通过(已失效)
    • 审核通过(已作废)
    • 审核中
    • 失败(审核不通过)
    • 取消办理中
    • 已取消
    • ......
  • bzclxx[].bzxx[].jsrxm:驾驶者姓名,可以与账户所有者不同
  • bzclxx[].bzxx[].jszh:驾驶者身份证,必需与驾驶者姓名一致 (即查有此人)
  • bzclxx[].bzxx[].jjzzlmc:进京证类型,主要有:
    • 进京证(六环内)
    • 进京证(六环外)
  • bzclxx[].bzxx[].yxqs:进京证首次生效日期
  • bzclxx[].bzxx[].yxqz:进京证最后生效日期
  • bzclxx[].bzxx[].sxsyts:进京证剩余有效期,单位天

其中后三个字段仅在某些状态下才会有,例如 yxqs 和  yxqz 在生效中、待生效、已取消状态有效,szsyts 在生效中、待生效状态有效,无效值就是 null。如果车辆压根没有进京证,整个 bzxx[] 就是 null。

理论上一个车辆最多只能有一个进京证存在,或者六环内,或者六环外,但在某些场景下,两个证可以短暂的同时存在,这是我一开始认为 bzxx 要以数组形式存在的原因。这种场景主要分为两种

  • 在有六环外进京证的前提下,办理成功六环内进京证,此时六环外进京证会自动作废,两者会有短暂的共存期,过后就只保留生效的六环内进京证了
  • 在有进京证的前提下,如果进京证是最后一天有效期,此时续办同样类型的进京证,新的证件将处在待生效状态,此时两者会有一天的共存期,一天后旧的证件失效,新的证件生效

下面看第一种场景下一个实际的例子:

查看代码

 

可以看到待生效的进京证实际上是放在了和 bzxx 同级的 ecbzxx 中,而不是放在 bzxx 数组中,可见之前的猜测是错误的,虽然 bzxx 和 ecbzxx 都被设计为 json 数组,实际上它们最多只有一个元素,如果没有对应的信息,保持 null。

说句题外话,政务的接口特别钟意用拼音缩写为字段起名,光看字面意思费半天劲儿也猜不出,必需得结合取值情况,取值是字符串的还好些,遇上那种整数枚举,根本就猜不透。不过换个角度看,起到了混淆加固的作用,哈哈。

申请新的进京证

下面进入正题:这个 insertApplyRecord 接口用来申请进京证。

header

从抓包数据看,http 头和 stateList 请求完全一样,参考上一节。

data

{
  "dabh": "null",
  "hphm": "津ADY1951",
  "hpzl": "52",
  "vId": "1479816562371952600",
  "jjdq": "海淀区",
  "jjlk": "00401",
  "jjlkmc": "京藏高速",
  "jjmd": "01",
  "jjmdmc": "自驾旅游",
  "jjrq": "2023-02-13",
  "jjzzl": "02",
  "jsrxm": "云海",
  "jszh": "150121198603226428",
  "sfzmhm": "150121198603226428",
  "xxdz": "百度大厦",
  "sqdzbdjd": 116.307393,
  "sqdzbdwd": 40.057771
}

这里的请求数据就不是可有可无的了,其中比较关键的是 hphm / hpzl / vId / jjrq / jsrxm / jszh / sfzmhm 等几个字段,大部分在前一节介绍过了,新出现的只有 jjrq 一个,表示办理进京证的起始时间。

响应

{
  "code": 200,
  "msg": "信息已提交,正在审核!",
  "data": [
    "温馨提示",
    "1、请务必在进京之前,查看进京通行证是否审核通过;",
    "2、若审核未通过,请按提示信息调整并重新申请;",
    "3、若审核通过,可在证件生效之前申请取消,每位注册用户每天仅有1次取消机会;",
    "4、在进京通行证未生效的情况下,外埠机动车禁止在限行区域内行驶;"
  ],
  "from": "v2"
}

成功时返回上面的结果,再次调用 stateList 接口,进京证状态将变为审核中。下面列几种常见的错误响应:

{
  "code": 500,
  "msg": "审核未通过,申请时间已超过中午12时,无法申请当日生效的进京证。",
  "data": "20015",
  "from": "v2"
}

时间不正确,过了中午 12 点申请当天进京证,或者 jjrq 使用了一个过去的时间。

{
  "code": 500,
  "msg": "每个用户同一时间只能为一辆机动车申请办理进京证。",
  "data": "20005",
  "from": "v2"
}

已办理了名下另一辆车的进京证时不能继续办理。

注意身份证不匹配、车辆进京证已被其它帐户办理等错误情况,并不会立刻返回,而是在一段时间后再次调用 stateList 接口才会返回。

"bzxx": [
  {
    "vId": "1479816562371952600",
    "applyId": "831666307626696704",
    "blzt": 4,
    "blztmc": "失败(审核不通过)",
    "sxrqmc": "02月13日",
    "yxqs": "2023-02-13",
    "yxqz": null,
    "sxsyts": null,
    "jjzzl": "02",
    "jjzzlmc": "进京证(六环外)",
    "jjzh": null,
    "sqsj": "2023-02-12 23:04:59",
    "jsrxm": "云海",
    "jszh": "150121198603226428",
    "sfzmhm": null,
    "shsbyy": "10014",
    "shsbyyms": "审核未通过,驾驶人信息不正确,请核实后再次提交或到进京证办证窗口办理。",
    "tphtml": null,
    "hphm": "津ADY1951",
    "hpzl": "52",
    "vid": 1479816562371952600
  }
]

像上面这种就是因为身份证与名称不匹配出错。

{
  "msg": "目前办理业务人数较多,请稍后再试。",
  "code": 500
}

如果返回上面的信息,说明是程序有问题了,一般是 header 或 data 没设置对。

模拟申请

报文摸清楚后就可以用 shell 脚本模拟了,下面是脚本代码:

查看代码

 

完整的代码放在了 github 上:

GitHub - goodpaperman/jinjing365: deal with enter permits for Beijing 365 days

代码仓库中除了脚本,还包含了配置文件 (config.ini) 与请求模板 (*.json)。

脚本不到 300 行,不太难读,这里就不逐行解说了,捡其中的几个关键点说明一下

jq

因为要解析 json,jq 是必不可少的,如果你的系统上缺少它,执行脚本会报一行错误:

please install jq before run this script, fatal error!

其它用到的命令如 curl、awk 也都做了检查,防止在一些特殊的场合下依赖缺失。

jq 在这里主要有两种用法,一种是解析响应内容;一种是生成请求内容。

解析

解析比较简单了,例如想取 data.sfzmhm 字段,直接用一行代码搞定:

local cardid=$(echo "${resp}" | jq -r '.data.sfzmhm') 

脚本中大量使用,其中 -r 选项可以除去字符串的双引号。

内置管道线

需要注意的是 jq 支持内置管道线,在某些场景中会很有用,例如:

local vsize=$(echo "${resp}" | jq -r '.data.bzclxx|length')
local psize=$(echo "${resp}" | jq -r ".data.bzclxx[${index}].bzxx|length")

分别获取车辆个数和某个车辆下的进京证个数,其中 length 是 jq 内置函数,可以放在内置管道线右侧使用。

注意第二个例子中,直接在 jq 语句中嵌入了 shell 变量,此时要使用双引号而不是单引号,否则 shell 变量无法展开。

如果需要获取数组中所有的值,光使用内置管道线就不够了:

local vehicles=$(echo "${resp}" | jq -r '.data.bzclxx[].hphm')
local find=0
local index=0
# echo "${#vehicles}"
for var in ${vehicles}
do
    echo "try ${var} "
    if [ "${var}" = "${vehicle}" ]; then 
        # match
        find=1
        break; 
    fi
    index=$((index+1))
done

上面的例子中获取所有车辆的车牌号到 shell 变量 vehicles,然后用 for..in 进行遍历,以寻找指定车牌的车辆索引。

内置变量

jq 的第二种用法是生成请求内容,这里主要使用了 jq 内置变量:

 local statereq=$(cat statereq.json | jq --arg sfzmhm "${userid}" --arg timestamp $(date "+%s000") -c '{ v, sfzmhm: $sfzmhm, "s-source", timestamp: $timestamp }')

通过 --arg 传递变量名和它的值,例如 --arg sfzmhm "${userid}" 为 jq 生成了一个名为 sfzmhm 的变量,它的值是 shell 变量 userid。

在后面的 jq 脚本中 (通过 -c 指定),就可以直接使用$sfzmhm 来引用这个变量啦,注意使用$前缀的才是 jq 变量,否则就是字面值,表示 json 的字段名。

结合 statereq.json 的内容,看看这句代码到底做了什么:

{
  "v": "3.4.1",
  "sfzmhm": "",
  "s-source": "bjjj-android",
  "timestamp": ""
}

将 json 模板读入,并对指定了值的字段 (sfzmhm/timestamp) 进行设置,指定了字段名没指定值的 (v/s-source) 延用模板中的值,没指定字段名的不会出现在最终结果。

这样替换的好处是全交给 jq 处理,避免手动构造的字符串不符合 json 语法。有几点需要注意:

  • jq 中的变量不能在 jq 外使用
  • jq 中的变量只能用 $xxx 形式引用,${xxx} 引用不了
  • jq 中的字段名如果包含特殊符号 (如 s-source),在使用时需要加双引号,否则 jq 会报错

内置变量 vs shell 变量

有的读者比较细心,可能会问了,“内置管道线”第二个例子中不是可以直接在 jq 中使用 shell 变量吗,那能否在构造请求时也直接使用 shell 变量?当然可以,就拿上一节的例子来说,用下面的 shell 脚本代替也是没问题的:

# local statereq=$(cat statereq.json | jq --arg sfzmhm "${userid}" --arg timestamp $(date "+%s000") -c '{ v, sfzmhm: $sfzmhm, "s-source", timestamp: $timestamp }')

local statereq="{\"v\":\"3.4.1\",\"sfzmhm\":\"${userid}\",\"s-source\":\"bjjj-android\",\"timestamp\":\"$(date +%s000)\"}"

得到的内容是一样的。

可以看到,因为要包含 shell 变量,整个 json 字符串需要被双引号包围,而其中大量的 json 字段名本身就有双引号,不得不使用反斜杠进行转义,

这样一来手工修改工作会特别多,可读性也比较差。可能这个例子还不怎么直观,换 issuereq 的构造过程对比一下:

# local issuereq=$(cat issuereq.json | jq --arg hphm "${vehicle}" --arg hpzl "${hpzl}" --arg vid "${vid}" --arg jjrq "${issuedate}" --arg jsrxm "${drivername}" --arg jszh "${driverid}" --arg sfzmhm "${userid}" --arg timestamp $(date "+%s000") --arg data "${statereq}" -c '{ dabh, hphm: $hphm, hpzl: $hpzl, vId: $vid, jjdq, jjlk, jjlkmc, jjmd, jjmdmc, jjrq: $jjrq, jjzzl, jsrxm: $jsrxm, jszh: $jszh, sfzmhm: $sfzmhm, xxdz, sqdzbdjd, sqdzbdwd}')

local issuereq="{\"dabh\":\"null\",\"hphm\":\"${vehicle}\",\"hpzl\":\"${hpzl}\",\"vId\":\"${vid}\",\"jjdq\":\"海淀区\",\"jjlk\":\"00401\",\"jjlkmc\":\"京藏高速\",\"jjmd\":\"01\",\"jjmdmc\":\"自驾旅游\",\"jjrq\":\"${issuedate}\",\"jjzzl\":\"02\",\"jsrxm\":\"${drivername}\",\"jszh\":\"${driverid}\",\"sfzmhm\":\"${userid}\",\"xxdz\":\"百度大厦\",\"sqdzbdjd\":116.307393,\"sqdzbdwd\":40.057771}"

这个引号看得眼睛都花了,如果一个不小心配错了,可够人找半天的。

其实使用 jq 内置变量的重要原因,可读性只是一方面,正确性是另一方面。

假设这样一个场景,在 issuereq 中新增一个 data 字段,内容也是 json (为了简化例子,直接使用 statereq 充任),那么两种方式还会一致吗?

local issuereq=$(cat issuereq.json | jq --arg hphm "${vehicle}" --arg hpzl "${hpzl}" --arg vid "${vid}" --arg jjrq "${issuedate}" --arg jsrxm "${drivername}" --arg jszh "${driverid}" --arg sfzmhm "${userid}" --arg timestamp $(date "+%s000") --arg data "${statereq}" -c '{ dabh, hphm: $hphm, hpzl: $hpzl, vId: $vid, jjdq, jjlk, jjlkmc, jjmd, jjmdmc, jjrq: $jjrq, jjzzl, jsrxm: $jsrxm, jszh: $jszh, sfzmhm: $sfzmhm, xxdz, sqdzbdjd, sqdzbdwd, data: $data }')
echo "issue req: ${issuereq}"

issuereq="{\"dabh\":\"null\",\"hphm\":\"${vehicle}\",\"hpzl\":\"${hpzl}\",\"vId\":\"${vid}\",\"jjdq\":\"海淀区\",\"jjlk\":\"00401\",\"jjlkmc\":\"京藏高速\",\"jjmd\":\"01\",\"jjmdmc\":\"自驾旅游\",\"jjrq\":\"${issuedate}\",\"jjzzl\":\"02\",\"jsrxm\":\"${drivername}\",\"jszh\":\"${driverid}\",\"sfzmhm\":\"${userid}\",\"xxdz\":\"百度大厦\",\"sqdzbdjd\":116.307393,\"sqdzbdwd\":40.057771,\"data\":\"${statereq}\"}"
echo "issue req: ${issuereq}"

运行上面的脚本片段会得到如下的内容:

issue req: {"dabh":"null","hphm":"津ADY1951","hpzl":"52","vId":"1480773467139342337","jjdq":"海淀区","jjlk":"00401","jjlkmc":"京藏高速","jjmd":"01","jjmdmc":"自驾旅游","jjrq":"2023-02-13","jjzzl":"02","jsrxm":"云海","jszh":"150121198603226428","sfzmhm":"150121198603226428","xxdz":"百度大厦","sqdzbdjd":116.307393,"sqdzbdwd":40.057771,"data":"{"v":"3.4.1","sfzmhm":"150121198603226428","s-source":"bjjj-android","timestamp":"1676214143000"}"}

issue req: {"dabh":"null","hphm":"津ADY1951","hpzl":"52","vId":"1480773467139342337","jjdq":"海淀区","jjlk":"00401","jjlkmc":"京藏高速","jjmd":"01","jjmdmc":"自驾旅游","jjrq":"2023-02-13","jjzzl":"02","jsrxm":"云海","jszh":"150121198603226428","sfzmhm":"150121198603226428","xxdz":"百度大厦","sqdzbdjd":116.307393,"sqdzbdwd":40.057771,"data":"{\"v\":\"3.4.1\",\"sfzmhm\":\"150121198603226428\",\"s-source\":\"bjjj-android\",\"timestamp\":\"1676214296000\"}"}

data 字段的对比非常明显,第一种使用 shell 变量,直接将双引号放入生成的 json 中了,导致引号匹配出错;后一种使用 jq 内置变量,会将 data 内部的双引号自动转义,从而符合 json 语法。

总结一下,使用 jq 变量和 json 模板构造请求将使生成的 json 字符串符合语法、脚本变得清晰、数据也便于维护,推荐指数五颗星。

date

脚本中大量的日期处理依赖 date 命令,其中比较有趣的一点是 mac date 和 unix date 的区别:

# mac date performs differs with other unix..
if [ ${IS_MAC} -eq 1 ]; then 
    issuedate=$(date "-v+${expire}d" '+%Y-%m-%d')
else 
    issuedate=$(date '+%Y-%m-%d' -d "+${expire} days")
fi

当计算从今天开始的 N 天后日期时,mac date 使用的参数和 unix date 不同,它的形式是-v+Nd,unix date 的形式是-d "+N days"。为此在脚本一开头增加了一个判断当前平台是否为 macOS 的函数:is_macos。

另外需要注意的一点是请求参数中的 timestamp 是精确到毫秒的,date "+%s"只能精确到秒,这里采取了一个讨巧的方法,直接加 3 个零完事:date "+%s000"

curl

curl 作为请求的主力没什么可说的,一直是那么的可靠。这里主要注意两个小点。

headers

请求头是存放在 shell 数组中拼成一整个字符串的:

local stateheader #=() adb shell not support =() initialize an array..
stateheader[0]="Accept-Language:${lang}"
stateheader[1]="User-Agent:${agent}"
stateheader[2]="source:${source}"
stateheader[3]="authorization:${auth}"
stateheader[4]="Content-Type:${content}"
stateheader[5]="Host:${host}"
stateheader[6]="Connection:Keep-Alive"
stateheader[7]="Accept-Encoding:gzip"
local headers=""
for var in "${stateheader[@]}"; 
do
    headers="${headers} -H ${var}"
done
echo "state headers: ${headers} -H ${length}" 1>&2
local resp=$(curl -s -k ${headers} -H ${length} -d "${statereq}" "${stateurl}")

后面会将这个拼接后的字符串直接放在 curl 的参数列表中,为了防止 shell 通过空格自动切分参数,构建的 header 也不能存在空格,所以这里 Key 和 Value 之前是没有空格的,这一点需要注意。

另外因为 Content-Length 字段是随请求变化的,为了可以重复使用这个 header 数组,没有将它包含在内,而是成为一个独立的请求头:-H ${length}

不检查证书

给 curl 添加 -k 选项,这对于某些通过代理访问服务器的环境来说至关重要,没有这个选项可能导致 curl 就直接失败了:

curl: (60) SSL certificate problem: self signed certificate in certificate chain
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

相当于 wget 的 --no-check-certificate 选项。

配置脚本

与用户相关的配置都存放在 config.ini 文件中:

% cat config.ini
# idcard who own the car
userid=150121198603226428
# car number to indicate  which car should I issue permit for,
# especially when you have many cars
vehicle=津ADY1951
# user login tokens, get by network package capture..
authorization=f36abdfa-8878-46bf-91d9-5666f808e9a4
source=8724a2428c3f47358741f978fd082810
# name & idcard who drive the car
# can be same with the car owner
drivername=云海
driverid=150121198603226428

使用时需要定制自己的配置,下面逐个字段说明:

  • userid:账户对应的身份证号,这个仅用来检验,如果和通过用户凭证查出来的身份证不匹配,将导致脚本中断执行,以防误操作
  • vehicle:车牌号,用于区分名下有多辆车时为哪个办理。如果只有一辆,也需要指定一下
  • authorization & source:用户凭证,通过前面介绍的 VNET 抓包获取
  • drivername & driverid:申请进京证的驾驶员信息,可以和账户不同,但必需"查有此人"

申办日期不在配置中,而是按最近原则确定:如果申请时间是当天中午 12 点前,那就申请当天进京证;否则申请第二天进京证。这样便于周期性执行。

定时执行

有了上面的脚本和配置,定时执行就是小菜一碟了,在 unix 系统上,使用 crontab 加入定时调用:

> crontab -e
0 1 * * 1 cd /home/users/yunhai01/code/jinjing365; date >> jinjing.log; sh jinjing.sh >> jinjing.log 2>>verbose.log

每周一凌晨一点执行。在 windows 上也可以加入计划任务来实现定时调用,命令部分可以这样写:

批处理 jinjing.bat 将直接调用 jinjing.sh:

cd /d %~dp0
bash.exe jinjing.sh >> jinjing.log 2>>verbose.log

其中 %~dp0 表示脚本所在的目录。

能这样写的前提是已经安装 git bash 和 jq for windows,并且将它们所在的路径 (如 C:\Program Files\Git\bin) 放在 PATH 环境变量中。

安装 git bash 时如果指定 "Use Git and optional Unix tools from the Command Prompt“ 选项 ,可由安装包自动设置 PATH 环境变量。

结语

其实在写好这篇文章的时候,脚本运行还是有些问题的,总是返回 500 错误 (目前办理业务人数较多,请稍后再试),如果直接拷贝文中的脚本,大概率是跑不通的。

针对这个问题,追踪的过程也是颇具戏剧性,限于篇幅我打算将它记录为一篇单独的文章,以后有时间再分享出来。

因此最好直接 clone 代码库 (jinjing365),那里会包括最新的补丁,问题修复后会第一时间 push 到 github。

欢迎提交 bug 反馈、特性 patch、小额赞赏,如果觉得它还不错的话。

后记

在实现这个工具的过程中,参考了 github 上 woodheader/jjz 项目的一些思路,例如 VNET 的使用就是从这里 get 的。

jjz 这个项目有很多过人之处,社交软件通知功能就是其一,办理结果时能第一时间得到通知,避免耽误正事。

因为不想安装企业微信、盯盯等,目前没有在 jinjing365 中添加类似功能。如果没有自己的服务器可用,直接借用这个项目的通知群不失为一个简便的办法。

jjz 的一个缺点是不支持一个人名下有多辆车的场景,而这个正是 jinjing365 的长处 (顺便打个广告)。

另外在遇到 500 错误时,通过向 woodheader 提问得到了启发,在此一并表示感谢。

最后,也可以开发其它语言的进京证自动申请方案,python、lua、javascript...,没有 shell 能做它们做不到的道理,选哪种语言主要考虑熟悉度和便捷度就行,这也是本文将进京证申请规则和报文分析安排在前面的原因。

另外需要注意的一点是控制请求的频率,一周一次足矣,最多不要超过一天一次。万一太频繁把服务器打挂了,可能就会被反作弊打击,轻则加校验参数,重则改接口、封帐号,那样大家都跟着遭殃。

最后:如果你不想再体验一次学习时找不到资料,没人解答问题,坚持几天便放弃的感受的话,在这里我给大家分享一些自动化测试的学习资源,希望能给你前进的路上带来帮助,朋友们如果需要可以自行免费领取 【保证100%免费】

软件测试面试题合集

我们进阶学习自动化测试必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。

视频文档获取方式:

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐