ToB企服应用市场:ToB评测及商务社交产业平台

标题: 使用 shell 脚本自动申请进京证 (六环外) [打印本页]

作者: 我爱普洱茶    时间: 2023-3-6 21:26
标题: 使用 shell 脚本自动申请进京证 (六环外)
问题背景

外地车辆进入北京,需要办理《进京证》,不办理证件驶入后会被执法设备抓拍,一次罚 100 扣 1 分,目前唯一的线上办理通道是下载《北京交警》App,注册后添加车辆,就可以为自己的爱车随时随地办理进京证了。注意如果有违法记录,需要缴纳罚款后才可以办理,缴纳罚款的线上办理通道是《交管12123》。
最早的时候,进京证只限制进五环的外地车辆,一年可以办理 12 次,一次 7 天。后来扩大范围到了六环,刚开始监控设备没健全还能进一下,目前是不行了,基本一进六环就会被拍。那六环外是不是随便跑呢?也不是的,像一些远郊县城如昌平、延庆、顺义、密云、怀柔、平谷,虽然地处六环之外,但是县城城区也有一个环,凡进此环者,也要办理进京证,对我这种混迹在县城周边的外地车主就非常不友好了,周末去县城里的购物中心、电影院也得办理进京证。也许是倾听了民意,后来改革了,进京证变为六环内与六环外两种:六环内还是一年 12 次,每次 7 天;六环外是一次 7 天但不限次数。一个例外是通州,虽然在六环外,也得按六环内办理,毕竟是城市副中心嘛!
虽然不限次数了,但也不是随意办理的,主要有以下规则:
线上办理速度还是比较快的,基本一分钟左右就能出结果。但对我这种健忘症患者,往往临出门才想起来要办进京证,一看时间,已经过中午 12 点了,就比较尴尬了。
这个时候就想,有一个定时办理进京证的工具就好了,例如每周一凌晨自动办理这一周的进京证。
解决方案

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

首先要搞明白服务端的接口,需要通过抓包对《北京交警》进行报文分析。
之前抓包一直是用 mac 上的 Charles,后来偶然看到有个 VNET App,不需要安装证书、不需要启动代理就能抓包,拿来试了下,果然好用:

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


这个 stateList 接口可以获取用户账户下所有车辆的进京证信息,有些字段对于下一步申请进京证是必需的,另外了解当前进京证状态也有利于决定是否申请新的进京证。
header
  1. > POST /pro//applyRecordController/stateList HTTP/1.1
  2. > Host:jjz.jtgl.beijing.gov.cn
  3. > Accept: */*
  4. > Accept-Language:zh-CN,zh;q=0.8
  5. > User-Agent:okhttp-okgo/jeasonlzy
  6. > source:8724a2428c3f47358741f978fd082810
  7. > authorization:f36abdfa-8878-46bf-91d9-5666f808e9a4
  8. > Content-Type:application/json;charset=utf-8
  9. > Connection:Keep-Alive
  10. > Accept-Encoding:gzip
  11. > Content-Length:97
复制代码
比较重要的是 source 和 authorization 字段,前者标识了设备,后者标识了用户 token,也是主要的抓取对象之一,会用于之后的请求中。
data
  1. {
  2.   "v": "3.4.1",
  3.   "sfzmhm": "150121198603226428",
  4.   "s-source": "bjjj-android",
  5.   "timestamp": "1676016273000"
  6. }
复制代码
请求时发送的 json 数据,亲测没什么用,发个空也能得到响应,看起来身份信息通过 http 头的 authorization 字段获取的。
响应

查看代码
  1. {
  2.   "code": 200,
  3.   "msg": "用户办证信息查询成功!",
  4.   "data": {
  5.     "sfzmhm": "150121198603226428",
  6.     "ylzqyms": "市界到二环 (不含二环路)+客车全年可办理12次,每次限通行7天",
  7.     "ylzmc": "进京证(六环内)",
  8.     "elzqyms": "市界到六环 (含六环路、不含通州全域)+客车全年不限办理次数,每次限通行7天",
  9.     "elzmc": "进京证(六环外)",
  10.     "bzclxx": [
  11.       {
  12.         "vId": "1479816562371952600",
  13.         "hpzl": "52",
  14.         "hphm": "津ADY1951",
  15.         "ybcs": 0,
  16.         "bzts": 0,
  17.         "kjts": 0,
  18.         "sycs": "12",
  19.         "syts": "84",
  20.         "ylzsfkb": true,
  21.         "elzsfkb": true,
  22.         "bnbzyy": "审核未通过,该车辆存在有效或待生效的进京证,请不要重复申请。",
  23.         "qyzt": 1,
  24.         "cllx": "01",
  25.         "bzxx": [
  26.           {
  27.             "vId": "1479816562371952600",
  28.             "applyId": "827854158244610048",
  29.             "blzt": 4,
  30.             "blztmc": "失败(审核不通过)",
  31.             "sxrqmc": "02月02日",
  32.             "yxqs": "2023-02-02",
  33.             "yxqz": null,
  34.             "sxsyts": null,
  35.             "jjzzl": "02",
  36.             "jjzzlmc": "进京证(六环外)",
  37.             "jjzh": null,
  38.             "sqsj": "2023-02-02 10:36:52",
  39.             "jsrxm": "云海",
  40.             "jszh": "150121198603226428",
  41.             "sfzmhm": null,
  42.             "shsbyy": "10037",
  43.             "shsbyyms": "审核未通过,该车辆存在有效或待生效的进京证,请不要重复申请。",
  44.             "tphtml": null,
  45.             "hphm": "津ADY1951",
  46.             "hpzl": "52",
  47.             "vid": 1479816562371952600
  48.           }
  49.         ],
  50.         "ecbzxx": [],
  51.         "sfyecbzxx": false,
  52.         "ecztbz": null
  53.       },
  54.       {
  55.         "vId": "664980198424313900",
  56.         "hpzl": "02",
  57.         "hphm": "蒙AC728G",
  58.         "ybcs": 0,
  59.         "bzts": 0,
  60.         "kjts": 0,
  61.         "sycs": "12",
  62.         "syts": "84",
  63.         "ylzsfkb": true,
  64.         "elzsfkb": true,
  65.         "bnbzyy": null,
  66.         "qyzt": 1,
  67.         "cllx": "01",
  68.         "bzxx": [
  69.           {
  70.             "vId": "664980198424313900",
  71.             "applyId": "830460363400019969",
  72.             "blzt": 2,
  73.             "blztmc": "已取消",
  74.             "sxrqmc": "02月12日",
  75.             "yxqs": "2023-02-12",
  76.             "yxqz": "2023-02-18",
  77.             "sxsyts": null,
  78.             "jjzzl": "02",
  79.             "jjzzlmc": "进京证(六环外)",
  80.             "jjzh": "A23020958343645",
  81.             "sqsj": "2023-02-09 15:12:59",
  82.             "jsrxm": "云海",
  83.             "jszh": "150121198603226428",
  84.             "sfzmhm": null,
  85.             "shsbyy": null,
  86.             "shsbyyms": null,
  87.             "tphtml": null,
  88.             "hphm": "蒙AC728G",
  89.             "hpzl": "02",
  90.             "vid": 664980198424313900
  91.           }
  92.         ],
  93.         "ecbzxx": [],
  94.         "sfyecbzxx": false,
  95.         "ecztbz": null
  96.       }
  97.     ]
  98.   },
  99.   "from": "v2"
  100. }
复制代码
返回的 json 比较大,基本按 data->bzclxx[]->bzxx[] 的结构组织,其中 data 存储用户信息;bzclxx 是车辆数组,存储与车相关的信息;bzxx 是进京证数组,存储与证相关的信息。
下面对重点的字段个简单说明:
其中后三个字段仅在某些状态下才会有,例如 yxqs 和  yxqz 在生效中、待生效、已取消状态有效,szsyts 在生效中、待生效状态有效,无效值就是 null。如果车辆压根没有进京证,整个 bzxx[] 就是 null。
理论上一个车辆最多只能有一个进京证存在,或者六环内,或者六环外,但在某些场景下,两个证可以短暂的同时存在,这是我一开始认为 bzxx 要以数组形式存在的原因。这种场景主要分为两种
下面看第一种场景下一个实际的例子:
查看代码
  1. {
  2.   "code": 200,
  3.   "msg": "用户办证信息查询成功!",
  4.   "data": {
  5.     "sfzmhm": "150121198603226428",
  6.     "ylzqyms": "市界到二环 (不含二环路)+客车全年可办理12次,每次限通行7天",
  7.     "ylzmc": "进京证(六环内)",
  8.     "elzqyms": "市界到六环 (含六环路、不含通州全域)+客车全年不限办理次数,每次限通行7天",
  9.     "elzmc": "进京证(六环外)",
  10.     "bzclxx": [
  11.       {
  12.         "vId": "1479816562371952642",
  13.         "hpzl": "52",
  14.         "hphm": "津ADY1951",
  15.         "ybcs": 0,
  16.         "bzts": 0,
  17.         "kjts": 0,
  18.         "sycs": "12",
  19.         "syts": "84",
  20.         "ylzsfkb": false,
  21.         "elzsfkb": false,
  22.         "bnbzyy": "每个用户同一时间只能为一辆机动车申请办理进京证。",
  23.         "qyzt": 1,
  24.         "cllx": "01",
  25.         "bzxx": [
  26.           {
  27.             "vId": "1479816562371952600",
  28.             "applyId": "827854158244610048",
  29.             "blzt": 4,
  30.             "blztmc": "失败(审核不通过)",
  31.             "sxrqmc": "02月02日",
  32.             "yxqs": "2023-02-02",
  33.             "yxqz": null,
  34.             "sxsyts": null,
  35.             "jjzzl": "02",
  36.             "jjzzlmc": "进京证(六环外)",
  37.             "jjzh": null,
  38.             "sqsj": "2023-02-02 10:36:52",
  39.             "jsrxm": "云海",
  40.             "jszh": "150121198603226428",
  41.             "sfzmhm": null,
  42.             "shsbyy": "10037",
  43.             "shsbyyms": "审核未通过,该车辆存在有效或待生效的进京证,请不要重复申请。",
  44.             "tphtml": null,
  45.             "hphm": "津ADY1951",
  46.             "hpzl": "52",
  47.             "vid": 1479816562371952600
  48.           }
  49.         ],
  50.         "ecbzxx": [],
  51.         "sfyecbzxx": false,
  52.         "ecztbz": null
  53.       },
  54.       {
  55.         "vId": "664980198424313900",
  56.         "hpzl": "02",
  57.         "hphm": "蒙AC728G",
  58.         "ybcs": 1,
  59.         "bzts": 7,
  60.         "kjts": 0,
  61.         "sycs": "11",
  62.         "syts": "77",
  63.         "ylzsfkb": false,
  64.         "elzsfkb": false,
  65.         "bnbzyy": null,
  66.         "qyzt": 1,
  67.         "cllx": "01",
  68.         "bzxx": [
  69.           {
  70.             "vId": "664980198424313900",
  71.             "applyId": "835681535133745153",
  72.             "blzt": 1,
  73.             "blztmc": "审核通过(生效中)",
  74.             "sxrqmc": "02月25日",
  75.             "yxqs": "2023-02-25",
  76.             "yxqz": "2023-03-03",
  77.             "sxsyts": 1,
  78.             "jjzzl": "02",
  79.             "jjzzlmc": "进京证(六环外)",
  80.             "jjzh": "A23022460625295",
  81.             "sqsj": "2023-02-24 01:00:04",
  82.             "jsrxm": "云海",
  83.             "jszh": "150121198603226428",
  84.             "sfzmhm": null,
  85.             "shsbyy": null,
  86.             "shsbyyms": null,
  87.             "tphtml": "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",
  88.             "hphm": "蒙AC728G",
  89.             "hpzl": "02",
  90.             "vid": 664980198424313900
  91.           }
  92.         ],
  93.         "ecbzxx": [
  94.           {
  95.             "vId": "664980198424313900",
  96.             "applyId": "838218246271270912",
  97.             "blzt": 6,
  98.             "blztmc": "审核通过(待生效)",
  99.             "sxrqmc": "03月04日",
  100.             "yxqs": "2023-03-04",
  101.             "yxqz": "2023-03-10",
  102.             "sxsyts": null,
  103.             "jjzzl": "02",
  104.             "jjzzlmc": "进京证(六环外)",
  105.             "jjzh": "A23030361735625",
  106.             "sqsj": "2023-03-03 01:00:03",
  107.             "jsrxm": "云海",
  108.             "jszh": "150121198603226428",
  109.             "sfzmhm": null,
  110.             "shsbyy": null,
  111.             "shsbyyms": null,
  112.             "tphtml": null,
  113.             "hphm": "蒙AC728G",
  114.             "hpzl": "02",
  115.             "vid": 664980198424313900
  116.           }
  117.         ],
  118.         "sfyecbzxx": true,
  119.         "ecztbz": "1"
  120.       }
  121.     ]
  122.   },
  123.   "from": "v2"
  124. }
复制代码
可以看到待生效的进京证实际上是放在了和 bzxx 同级的 ecbzxx 中,而不是放在 bzxx 数组中,可见之前的猜测是错误的,虽然 bzxx 和 ecbzxx 都被设计为 json 数组,实际上它们最多只有一个元素,如果没有对应的信息,保持 null。
说句题外话,政务的接口特别钟意用拼音缩写为字段起名,光看字面意思费半天劲儿也猜不出,必需得结合取值情况,取值是字符串的还好些,遇上那种整数枚举,根本就猜不透。不过换个角度看,起到了混淆加固的作用,哈哈。
申请新的进京证


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

从抓包数据看,http 头和 stateList 请求完全一样,参考上一节。
data
  1. {
  2.   "dabh": "null",
  3.   "hphm": "津ADY1951",
  4.   "hpzl": "52",
  5.   "vId": "1479816562371952600",
  6.   "jjdq": "海淀区",
  7.   "jjlk": "00401",
  8.   "jjlkmc": "京藏高速",
  9.   "jjmd": "01",
  10.   "jjmdmc": "自驾旅游",
  11.   "jjrq": "2023-02-13",
  12.   "jjzzl": "02",
  13.   "jsrxm": "云海",
  14.   "jszh": "150121198603226428",
  15.   "sfzmhm": "150121198603226428",
  16.   "xxdz": "百度大厦",
  17.   "sqdzbdjd": 116.307393,
  18.   "sqdzbdwd": 40.057771
  19. }
复制代码
这里的请求数据就不是可有可无的了,其中比较关键的是 hphm / hpzl / vId / jjrq / jsrxm / jszh / sfzmhm 等几个字段,大部分在前一节介绍过了,新出现的只有 jjrq 一个,表示办理进京证的起始时间。
响应
  1. {
  2.   "code": 200,
  3.   "msg": "信息已提交,正在审核!",
  4.   "data": [
  5.     "温馨提示",
  6.     "1、请务必在进京之前,查看进京通行证是否审核通过;",
  7.     "2、若审核未通过,请按提示信息调整并重新申请;",
  8.     "3、若审核通过,可在证件生效之前申请取消,每位注册用户每天仅有1次取消机会;",
  9.     "4、在进京通行证未生效的情况下,外埠机动车禁止在限行区域内行驶;"
  10.   ],
  11.   "from": "v2"
  12. }
复制代码
成功时返回上面的结果,再次调用 stateList 接口,进京证状态将变为审核中。下面列几种常见的错误响应:
  1. {
  2.   "code": 500,
  3.   "msg": "审核未通过,申请时间已超过中午12时,无法申请当日生效的进京证。",
  4.   "data": "20015",
  5.   "from": "v2"
  6. }
复制代码
时间不正确,过了中午 12 点申请当天进京证,或者 jjrq 使用了一个过去的时间。
  1. {
  2.   "code": 500,
  3.   "msg": "每个用户同一时间只能为一辆机动车申请办理进京证。",
  4.   "data": "20005",
  5.   "from": "v2"
  6. }
复制代码
已办理了名下另一辆车的进京证时不能继续办理。
注意身份证不匹配、车辆进京证已被其它帐户办理等错误情况,并不会立刻返回,而是在一段时间后再次调用 stateList 接口才会返回。
  1. "bzxx": [
  2.   {
  3.     "vId": "1479816562371952600",
  4.     "applyId": "831666307626696704",
  5.     "blzt": 4,
  6.     "blztmc": "失败(审核不通过)",
  7.     "sxrqmc": "02月13日",
  8.     "yxqs": "2023-02-13",
  9.     "yxqz": null,
  10.     "sxsyts": null,
  11.     "jjzzl": "02",
  12.     "jjzzlmc": "进京证(六环外)",
  13.     "jjzh": null,
  14.     "sqsj": "2023-02-12 23:04:59",
  15.     "jsrxm": "云海",
  16.     "jszh": "150121198603226428",
  17.     "sfzmhm": null,
  18.     "shsbyy": "10014",
  19.     "shsbyyms": "审核未通过,驾驶人信息不正确,请核实后再次提交或到进京证办证窗口办理。",
  20.     "tphtml": null,
  21.     "hphm": "津ADY1951",
  22.     "hpzl": "52",
  23.     "vid": 1479816562371952600
  24.   }
  25. ]
复制代码
像上面这种就是因为身份证与名称不匹配出错。
  1. {
  2.   "msg": "目前办理业务人数较多,请稍后再试。",
  3.   "code": 500
  4. }
复制代码
如果返回上面的信息,说明是程序有问题了,一般是 header 或 data 没设置对。

模拟申请

报文摸清楚后就可以用 shell 脚本模拟了,下面是脚本代码:
查看代码
  1.  #! /bin/sh
  2. # @brief: is MacOS platform
  3. #         mac date a little different with other
  4. # @retval: 0 - no
  5. # @retval: 1 - yes
  6. function is_macos()
  7. {
  8.     local os="${OSTYPE/"darwin"//}"
  9.     if [ "$os" != "$OSTYPE" ]; then
  10.         # darwin: macos
  11.         return 1
  12.     else
  13.         return 0
  14.     fi
  15. }
  16. is_macos
  17. IS_MAC=$?
  18. # @brief: check command existent before run our script
  19. #         useful especially in msys2 on windows
  20. # @param: cmd to check
  21. # @retval: 0 - exist
  22. # @retval: 1 - not exist
  23. function check_cmd()
  24. {
  25.     local cmd=$1
  26.     type "${cmd}" >/dev/null 2>&1
  27.     if [ $? -ne 0 ]; then
  28.         echo "please install ${cmd} before run this script, fatal error!"
  29.         exit -1
  30.     else
  31.         echo "check ${cmd} ok"
  32.     fi
  33. }
  34. # do environment check first
  35. check_cmd "jq"
  36. check_cmd "curl"
  37. check_cmd "head"
  38. check_cmd "cat"
  39. check_cmd "awk"
  40. check_cmd "grep"
  41. check_cmd "date"
  42. function main()
  43. {
  44.     # constant
  45.     local stateurl="https://jjz.jtgl.beijing.gov.cn/pro//applyRecordController/stateList"
  46.     local issueurl="https://jjz.jtgl.beijing.gov.cn/pro//applyRecordController/insertApplyRecord"
  47.     local agent="okhttp-okgo/jeasonlzy"
  48.     local host="jjz.jtgl.beijing.gov.cn"
  49.     local content="application/json;charset=utf-8"
  50.     local lang="zh-CN,zh;q=0.8"
  51.    
  52.     # read config
  53.     local userid=$(grep '^userid=' config.ini | head -1 | awk -F'=' '{print $2}')
  54.     local vehicle=$(grep '^vehicle=' config.ini | head -1 | awk -F'=' '{print $2}')
  55.     local auth=$(grep '^authorization=' config.ini | head -1 | awk -F'=' '{print $2}')
  56.     local source=$(grep '^source=' config.ini | head -1 | awk -F'=' '{print $2}')
  57.     local drivername=$(grep '^drivername=' config.ini | head -1 | awk -F'=' '{print $2}')
  58.     local driverid=$(grep '^driverid=' config.ini | head -1 | awk -F'=' '{print $2}')
  59.     # query current status
  60.     # note: s-source should be quoted to prevent jq complain:
  61.     # jq: error: syntax error, unexpected '-', expecting '}' (Unix shell quoting issues?) at <top-level>, line 1:
  62.     local statereq=$(cat statereq.json | jq --arg sfzmhm "${userid}" --arg timestamp $(date "+%s000") -c '{ v, sfzmhm: $sfzmhm, "s-source", timestamp: $timestamp }')
  63.     echo "state req: ${statereq}" 1>&2
  64.     local stateheader #=() adb shell not support =() initialize an array..
  65.     stateheader[0]="Accept-Language:${lang}"
  66.     stateheader[1]="User-Agent:${agent}"
  67.     stateheader[2]="source:${source}"
  68.     stateheader[3]="authorization:${auth}"
  69.     stateheader[4]="Content-Type:${content}"
  70.     stateheader[5]="Host:${host}"
  71.     stateheader[6]="Connection:Keep-Alive"
  72.     stateheader[7]="Accept-Encoding:gzip"
  73.     # prevent whole time be truncated to only date
  74.     # add time alone here..
  75.     # stateheader[10]="time:$(date '+%Y-%m-%d %H:%M:%S')"
  76.     # local time="time:$(date '+%Y-%m-%d %H:%M:%S')"
  77.     # for reuse, add content-length alone here..
  78.     local length="Content-Length:${#statereq}"
  79.     # size, what does it mean? seem to be optional..
  80.     # stateheader[11]="size:459"
  81.     local headers=""
  82.     for var in "${stateheader[@]}";
  83.     do
  84.         headers="${headers} -H ${var}"
  85.     done
  86.     echo "state headers: ${headers} -H ${length}" 1>&2
  87.     local resp=$(curl -s -k ${headers} -H ${length} -d "${statereq}" "${stateurl}")
  88.     echo "${resp}" | jq  '.'  1>&2
  89.     # for debug purpose
  90.     # resp=$(cat demo.txt)
  91.     local ret=$(echo "${resp}" | jq -r '.code')
  92.     local msg=$(echo "${resp}" | jq -r '.msg')
  93.     if [ -z "${ret}" -o "${ret}" = "null" -o ${ret} -ne 200 ]; then
  94.         echo "query permits status failed, code: ${ret}, msg: ${msg}"
  95.         exit 1
  96.     fi
  97.     echo "query permits status ok: ${msg}"
  98.     # check cardid
  99.     local id=$(echo "${resp}" | jq -r '.data.sfzmhm')
  100.     if [ -z "${id}" -o "${id}" = "null" -o "${id}" != "${userid}" ]; then
  101.         echo "id [${id}] from user token does not match given [${userid}], fatal error!"
  102.         exit 1
  103.     fi
  104.     local outside6ring="进京证(六环外)"
  105.     # get permit type form response differs with above result: 进京证(六环外)
  106.     # mainly different is the brackets..
  107.     #
  108.     # local outside6ring=$(echo "${resp}" | jq -r '.data.elzmc')
  109.     # if [ -z "${outside6ring}" -o "${outside6ring}" = "null" -o "${outside6ring}" != "进京证(六环外)" ]; then
  110.     #     echo "permit type [${outside6ring}] incorrect, fatal error!"
  111.     #     exit 1
  112.     # fi
  113.     # check if any permits there
  114.     local vsize=$(echo "${resp}" | jq -r '.data.bzclxx|length')
  115.     if [ -z "${vsize}" -o "${vsize}" = "null" -o ${vsize} -eq 0 ]; then
  116.         echo "no vehicle (${vsize}) under [${userid}], please add vehicle first!"
  117.         exit 0
  118.     fi
  119.     local vehicles=$(echo "${resp}" | jq -r '.data.bzclxx[].hphm')
  120.     local find=0
  121.     local index=0
  122.     # echo "${#vehicles}"
  123.     for var in ${vehicles}
  124.     do
  125.         echo "try ${var} "
  126.         if [ "${var}" = "${vehicle}" ]; then
  127.             # match
  128.             find=1
  129.             break;
  130.         fi
  131.         index=$((index+1))
  132.     done
  133.     if [ ${find} -eq 0 ]; then
  134.         # match reach end
  135.         echo "no vehicle named <${vehicle}> under [${userid}], fatal error!"
  136.         exit 1
  137.     fi
  138.     echo "find match vehicle <${vehicle}> at index: ${index}"
  139.     # vehicle info needed later in permit issue request..
  140.     local vid=$(echo "${resp}" | jq -r ".data.bzclxx[${index}].vId")
  141.     local hpzl=$(echo "${resp}" | jq -r ".data.bzclxx[${index}].hpzl")
  142.     if [ -z "${vid}" -o "${vid}" = "null" -o \
  143.          -z "${hpzl}" -o "${hpzl}" = "null" ]; then
  144.         echo "some vehicle fields in state response null, fatal error!"
  145.         exit 1
  146.     fi
  147.     local hour_now=$(date '+%H')
  148.     local issuedate=$(date '+%Y-%m-%d')
  149.     if [ ${hour_now} -ge 12 ]; then
  150.         # can NOT issue new permit for today if afternoon
  151.         if [ ${IS_MAC} -eq 1 ]; then
  152.             issuedate=$(date -v+1d '+%Y-%m-%d')
  153.         else
  154.             issuedate=$(date '+%Y-%m-%d' -d "+1 days")
  155.         fi
  156.     fi
  157.     local psize=$(echo "${resp}" | jq -r ".data.bzclxx[${index}].bzxx|length")
  158.     # echo "psize: ${psize}"
  159.     if [ -n "${psize}" -a "${psize}" != "null" -a ${psize} -gt 0 ]; then
  160.         # if have more than one permit, one of them must be inside 6th ring
  161.         # in that case, we can not issue new permit with type outside 6th ring..
  162.         if [ ${psize} -gt 1 ]; then
  163.             echo "have more than 1 permits, can not issue new permit!"
  164.             exit 1
  165.         fi
  166.         # has permits, check if in effect
  167.         local status=$(echo "${resp}" | jq -r ".data.bzclxx[${index}].bzxx[0].blztmc")
  168.         local man=$(echo "${resp}" | jq -r ".data.bzclxx[${index}].bzxx[0].jsrxm")
  169.         local card=$(echo "${resp}" | jq -r ".data.bzclxx[${index}].bzxx[0].jszh")
  170.         local type=$(echo "${resp}" | jq -r ".data.bzclxx[${index}].bzxx[0].jjzzlmc")
  171.         local v=$(echo "${resp}" | jq -r ".data.bzclxx[${index}].bzxx[0].hphm")
  172.         if [ -z "${status}" -o "${status}" = "null" -o \
  173.              -z "${man}" -o "${man}" = "null" -o \
  174.              -z "${card}" -o "${card}" = "null" -o \
  175.              -z "${type}" -o "${type}" = "null" -o \
  176.              -z "$v" -o "$v" = "null" -o "$v" != "${vehicle}" ]; then
  177.              echo "some permit fields in state response null, fatal error!"
  178.              exit 1
  179.         fi
  180.         if [ "${type}" != "${outside6ring}" ]; then
  181.             echo "have permits with type <${type}> != <${outside6ring}>, can not issue new permit!"
  182.             exit 1
  183.         fi
  184.    
  185.         echo "${man} [${card}] issue permits on <${vehicle}> with type '${type}' status: ${status}"
  186.         # status may 审核通过(生效中) or 审核通过(待生效) or 审核通过(已失效) or 审核中 or 失败(审核不通过) or 取消办理中 or 已取消
  187.         #if [ "${status:0:4}" = "审核通过" ]; then
  188.         case ${status} in
  189.             审核通过*)
  190.                 local daybeg=$(echo "${resp}" | jq -r  ".data.bzclxx[${index}].bzxx[0].yxqs")
  191.                 local dayend=$(echo "${resp}" | jq -r  ".data.bzclxx[${index}].bzxx[0].yxqz")
  192.                 if [ -z "${daybeg}" -o "${daybeg}" = "null" -o \
  193.                      -z "${dayend}" -o "${dayend}" = "null" ]; then
  194.                     echo "some permit fields(valid/invalid) in state response null, fatal error!"
  195.                     exit 1
  196.                 fi
  197.    
  198.                 if [ "${status}" = "审核通过(已失效)" ]; then
  199.                     # treate invalid permit as no permit
  200.                     echo "invalid permit find under <${vehicle}>, try issue new.."
  201.                 else
  202.                     local expire=$(echo "${resp}" | jq -r  ".data.bzclxx[${index}].bzxx[0].sxsyts")
  203.                     if [ -z "${expire}" -o "${expire}" = "null" ]; then
  204.                         echo "some permit fields(valid) in state response null, fatal error!"
  205.                         exit 1
  206.                     fi
  207.                     echo "in effect from ${daybeg} to ${dayend}"
  208.                     # can issue new permits in last day
  209.                     if [ ${expire} -gt 1 ]; then
  210.                         echo "still in effect, try ${expire} days later .."
  211.                         exit 0
  212.                     fi
  213.                     # mac date performs differs with other unix..
  214.                     if [ ${IS_MAC} -eq 1 ]; then
  215.                         issuedate=$(date "-v+${expire}d" '+%Y-%m-%d')
  216.                     else
  217.                         issuedate=$(date '+%Y-%m-%d' -d "+${expire} days")
  218.                     fi
  219.                 fi
  220.                 ;;
  221.             审核中)
  222.                 echo "still in verify, try later.."
  223.                 exit 0
  224.                 ;;
  225.             取消办理中)
  226.                 echo "still in cancel progress, try later.."
  227.                 exit 0
  228.                 ;;
  229.             "失败(审核不通过)")
  230.                 echo "previous issue rejected, try new permit"
  231.                 ;;
  232.             已取消)
  233.                 echo "previous issue cancelled, try new permit"
  234.                 ;;
  235.             *)
  236.                 echo "unknown status ${status}, fatal error!"
  237.                 exit 1
  238.                 ;;
  239.         esac
  240.     else
  241.         local bnbzyy=$(echo "${resp}" | jq -r ".data.bzclxx[${index}].bnbzyy")
  242.         if [ "${bnbzyy}" = "每个用户同一时间只能为一辆机动车申请办理进京证。" ]; then
  243.             echo "no permit(${psize}) under <${vehicle}>, but some permits under other vehicles exist.."
  244.             echo "can only issue new permit when no permits exists under [${userid}], do a check"
  245.             exit 1
  246.         fi
  247.         echo "no permit(${psize}) under <${vehicle}>, and no permits under [${userid}], try issue new.."
  248.     fi
  249.     # issue new permit request
  250.     echo "new permit will start from ${issuedate}"
  251.     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") -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 }')
  252.     echo "issue req: ${issuereq}" 1>&2
  253.     # time="time:$(date '+%Y-%m-%d %H:%M:%S')"
  254.     length="Content-Length:${#issuereq}"
  255.     echo "issue headers: ${headers} -H ${length}" 1>&2
  256.     resp=$(curl -s -k ${headers} -H ${length} -d "${issuereq}" "${issueurl}")
  257.     echo "${resp}" | jq  '.'  1>&2
  258.     # resp=$(cat demo.txt)
  259.     ret=$(echo "${resp}" | jq -r '.code')
  260.     msg=$(echo "${resp}" | jq -r '.msg')
  261.     if [ -z "${ret}" -o "${ret}" = "null" -o ${ret} -ne 200 ]; then
  262.         echo "issue new permit failed, code: ${ret}, msg: ${msg}"
  263.         exit 1
  264.     fi
  265.     echo "issue new permit status ok: ${msg}"
  266.     exit 0
  267. }
  268. main "$@"
复制代码
完整的代码放在了 github 上:
https://github.com/goodpaperman/jinjing365
代码仓库中除了脚本,还包含了配置文件 (config.ini) 与请求模板 (*.json)。
脚本不到 300 行,不太难读,这里就不逐行解说了,捡其中的几个关键点说明一下
jq

因为要解析 json,jq 是必不可少的,如果你的系统上缺少它,执行脚本会报一行错误:
  1. please install jq before run this script, fatal error!
复制代码
其它用到的命令如 curl、awk 也都做了检查,防止在一些特殊的场合下依赖缺失。
jq 在这里主要有两种用法,一种是解析响应内容;一种是生成请求内容。
解析

解析比较简单了,例如想取 data.sfzmhm 字段,直接用一行代码搞定:
  1. local cardid=$(echo "${resp}" | jq -r '.data.sfzmhm')
复制代码
脚本中大量使用,其中 -r 选项可以除去字符串的双引号。
内置管道线

需要注意的是 jq 支持内置管道线,在某些场景中会很有用,例如:
  1. local vsize=$(echo "${resp}" | jq -r '.data.bzclxx|length')
  2. local psize=$(echo "${resp}" | jq -r ".data.bzclxx[${index}].bzxx|length")
复制代码
分别获取车辆个数和某个车辆下的进京证个数,其中 length 是 jq 内置函数,可以放在内置管道线右侧使用。
注意第二个例子中,直接在 jq 语句中嵌入了 shell 变量,此时要使用双引号而不是单引号,否则 shell 变量无法展开。
如果需要获取数组中所有的值,光使用内置管道线就不够了:
  1. local vehicles=$(echo "${resp}" | jq -r '.data.bzclxx[].hphm')
  2. local find=0
  3. local index=0
  4. # echo "${#vehicles}"
  5. for var in ${vehicles}
  6. do
  7.     echo "try ${var} "
  8.     if [ "${var}" = "${vehicle}" ]; then
  9.         # match
  10.         find=1
  11.         break;
  12.     fi
  13.     index=$((index+1))
  14. done
复制代码
上面的例子中获取所有车辆的车牌号到 shell 变量 vehicles,然后用 for..in 进行遍历,以寻找指定车牌的车辆索引。
内置变量

jq 的第二种用法是生成请求内容,这里主要使用了 jq 内置变量:
  1. 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 的内容,看看这句代码到底做了什么:
  1. {
  2.   "v": "3.4.1",
  3.   "sfzmhm": "",
  4.   "s-source": "bjjj-android",
  5.   "timestamp": ""
  6. }
复制代码
将 json 模板读入,并对指定了值的字段 (sfzmhm/timestamp) 进行设置,指定了字段名没指定值的 (v/s-source) 延用模板中的值,没指定字段名的不会出现在最终结果。
这样替换的好处是全交给 jq 处理,避免手动构造的字符串不符合 json 语法。有几点需要注意:
内置变量 vs shell 变量

有的读者比较细心,可能会问了,“内置管道线”第二个例子中不是可以直接在 jq 中使用 shell 变量吗,那能否在构造请求时也直接使用 shell 变量?当然可以,就拿上一节的例子来说,用下面的 shell 脚本代替也是没问题的:
  1. # local statereq=$(cat statereq.json | jq --arg sfzmhm "${userid}" --arg timestamp $(date "+%s000") -c '{ v, sfzmhm: $sfzmhm, "s-source", timestamp: $timestamp }')
  2. local statereq="{"v":"3.4.1","sfzmhm":"${userid}","s-source":"bjjj-android","timestamp":"$(date +%s000)"}"
复制代码
得到的内容是一样的。
可以看到,因为要包含 shell 变量,整个 json 字符串需要被双引号包围,而其中大量的 json 字段名本身就有双引号,不得不使用反斜杠进行转义,
这样一来手工修改工作会特别多,可读性也比较差。可能这个例子还不怎么直观,换 issuereq 的构造过程对比一下:
  1. # 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}')
  2. 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 充任),那么两种方式还会一致吗?
  1. 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 }')
  2. echo "issue req: ${issuereq}"
  3. 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}"}"
  4. echo "issue req: ${issuereq}"
复制代码
运行上面的脚本片段会得到如下的内容:
  1. 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"}"}
  2. 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 的区别:
  1. # mac date performs differs with other unix..
  2. if [ ${IS_MAC} -eq 1 ]; then
  3.     issuedate=$(date "-v+${expire}d" '+%Y-%m-%d')
  4. else
  5.     issuedate=$(date '+%Y-%m-%d' -d "+${expire} days")
  6. 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 数组中拼成一整个字符串的:
  1. local stateheader #=() adb shell not support =() initialize an array..
  2. stateheader[0]="Accept-Language:${lang}"
  3. stateheader[1]="User-Agent:${agent}"
  4. stateheader[2]="source:${source}"
  5. stateheader[3]="authorization:${auth}"
  6. stateheader[4]="Content-Type:${content}"
  7. stateheader[5]="Host:${host}"
  8. stateheader[6]="Connection:Keep-Alive"
  9. stateheader[7]="Accept-Encoding:gzip"
  10. local headers=""
  11. for var in "${stateheader[@]}";
  12. do
  13.     headers="${headers} -H ${var}"
  14. done
  15. echo "state headers: ${headers} -H ${length}" 1>&2
  16. local resp=$(curl -s -k ${headers} -H ${length} -d "${statereq}" "${stateurl}")
复制代码
后面会将这个拼接后的字符串直接放在 curl 的参数列表中,为了防止 shell 通过空格自动切分参数,构建的 header 也不能存在空格,所以这里 Key 和 Value 之前是没有空格的,这一点需要注意。
另外因为 Content-Length 字段是随请求变化的,为了可以重复使用这个 header 数组,没有将它包含在内,而是成为一个独立的请求头:-H ${length}。
不检查证书

给 curl 添加 -k 选项,这对于某些通过代理访问服务器的环境来说至关重要,没有这个选项可能导致 curl 就直接失败了:
  1. curl: (60) SSL certificate problem: self signed certificate in certificate chain
  2. More details here: https://curl.haxx.se/docs/sslcerts.html
  3. curl failed to verify the legitimacy of the server and therefore could not
  4. establish a secure connection to it. To learn more about this situation and
  5. how to fix it, please visit the web page mentioned above.
复制代码
相当于 wget 的 --no-check-certificate 选项。
配置脚本

与用户相关的配置都存放在 config.ini 文件中:
  1. % cat config.ini
  2. # idcard who own the car
  3. userid=150121198603226428
  4. # car number to indicate  which car should I issue permit for,
  5. # especially when you have many cars
  6. vehicle=津ADY1951
  7. # user login tokens, get by network package capture..
  8. authorization=f36abdfa-8878-46bf-91d9-5666f808e9a4
  9. source=8724a2428c3f47358741f978fd082810
  10. # name & idcard who drive the car
  11. # can be same with the car owner
  12. drivername=云海
  13. driverid=150121198603226428
复制代码
使用时需要定制自己的配置,下面逐个字段说明:
申办日期不在配置中,而是按最近原则确定:如果申请时间是当天中午 12 点前,那就申请当天进京证;否则申请第二天进京证。这样便于周期性执行。
定时执行

有了上面的脚本和配置,定时执行就是小菜一碟了,在 unix 系统上,使用 crontab 加入定时调用:
  1. > crontab -e
  2. 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:
  1. cd /d %~dp0
  2. 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 能做它们做不到的道理,选哪种语言主要考虑熟悉度和便捷度就行,这也是本文将进京证申请规则和报文分析安排在前面的原因。
另外需要注意的一点是控制请求的频率,一周一次足矣,最多不要超过一天一次。万一太频繁把服务器打挂了,可能就会被反作弊打击,少则加校验参数,多则改接口,那样大家都跟着遭殃。
参考

[1]. woodheader/jjz
[2]. Shell下解析Json之jq
[3]. shell 获取n天前和n天后日期
[4]. mac date命令
[5]. Markdown 图片语法
[6]. jq 中文手册(v1.5)
[7]. linux定时任务crontab设置每分钟、每小时、每天、每周、每月、每年定时执行 --- 2020-03-06

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4