Innovation Port Sports Facilities Automatic Reservation.

This is an automatically translated post by LLM. The original post is in Chinese. If you find any translation errors, please leave a comment to help me improve the translation. Thanks!

Project Background

The Innovation Port badminton venue has a high demand for reservations every day. The online reservation system opens around 8:40 am every morning, and the opening time is not fixed. These factors have caused a lot of trouble for manual venue reservations, so it is considered to automate the venue reservation process with a script.

Project Concept

The sports venue reservation system needs to confirm the identity through the SJTU authentication system. Consider carrying the identity information after logging in to access the sports venue reservation system. The identity authentication here refers to the library seat reservation script by Guoguo:

XJTU Library Seat Reservation Script - Requests Library | Guoguo's Blog (gwyxjtu.github.io)

In addition, the sports venue reservation system is open from 8:40 to 21:40, not all day. Therefore, connectivity detection is required for reservation control.

The project process architecture is shown below:

  1. Check connectivity to confirm whether the sports venue reservation system is open.
  2. Use the sports venue query API to obtain all venue information.
  3. Set certain conditions to filter out the venues that need to be reserved.
  4. Obtain identity information through SJTU authentication.
  5. Send a reservation POST request to the venue reservation system to make a reservation.

Project Implementation

1. Connectivity Detection

Use the urlopen library in the requests library to perform connectivity detection:

1
2
3
4
5
6
def check_net(testserver):
try:
ret = request.urlopen(url=testserver, timeout=3.0)
except:
return False
return True

The waiting code for the detection in the main function is as follows:

1
2
3
while True:
if check_net('http://202.117.17.144/'):
break

2. Query Venue Information

First, create a session to perform subsequent access. Access through session ensures continuity of access.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter

MAX_RETRIES = 10

SLEEP_INTERVAL=0.1

retries=Retry(
total=MAX_RETRIES,
backoff_factor=SLEEP_INTERVAL,
status_forcelist=[403, 500, 502, 503, 504],
)

session = requests.Session()
session.mount("http://", HTTPAdapter(max_retries=retries))
session.mount("https://", HTTPAdapter(max_retries=retries))

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.62",
"Accept-Encoding": "gzip, deflate",
}

Next is to query the venue information. Taking the Innovation Port table tennis venue as an example (because there are many unreserved venues, which is convenient for testing ^_^).

By observing the link and conducting tests, it can be inferred that each venue is identified by an id, which is used for subsequent venue information queries and reservation applications.

By checking the page request, you can find that sending a GET request to /findtime.html can query the required information.

However, the information returned by this request is incomplete, and an event id is not returned. This event id is needed for subsequent reservations. After multiple searches, the following api was finally found on the tennis court reservation page:

From this api, three key pieces of information can be found:

  • id: the identifier of each time slot for each venue
  • status: venue status, 1 for available, 2 for reserved
  • stockid: identifier of each venue

Therefore, you can use this api to query the venue information for each venue. The implementation is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
def get_avaliable_seats(court_id,date):

params={'s_date':date,
'serviceid':court_id}
response=session.get('http://202.117.17.144/product/findOkArea.html',params=params,headers=headers).json()

available_court_list=[]
for item in response['object']:
if item['status']==1:
available_court_list.append(item)

return available_court_list

3. Filter the Required Venues

This step is relatively easy, just filter the sessions obtained in the previous step.

1
2
3
4
5
6
def get_suitable_seats(seat_list,time):
result=[]
for item in seat_list:
if item['stock']['time_no']==time:
result.append(item)
return result

4. Obtain Identity Information through SJTU Authentication

Here, Guoguo's library seat reservation script is referenced XJTU Library Seat Reservation Script - Requests Library | Guoguo's Blog (gwyxjtu.github.io). The only difference is that the application to which it jumps after logging in to the authentication system is different. In the browser, jump from the sports venue reservation system to the authentication page to query the page cookie to obtain the corresponding appid.

Fill in the corresponding appid in the appropriate location to perform login and jump. The login part of the code is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def encrypt_pwd(raw_pwd, publicKey='0725@pwdorgopenp'):
''' AES-ECB encrypt '''
publicKey = publicKey.encode('utf-8')
# pkcs7 padding
BS = AES.block_size
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
pwd = pad(raw_pwd)
# zero padding
'''
pwd = raw_pwd
while len(raw_pwd.encode('utf-8')) % 16 != 0:
pwd += '\0'
'''
cipher = AES.new(publicKey, AES.MODE_ECB)
pwd = cipher.encrypt(pwd.encode('utf-8'))
return str(base64.b64encode(pwd), encoding='utf-8')

def login():
# get cookie route
session.get('https://org.xjtu.edu.cn/openplatform/login.html')

# get JcaptchaCode and cookie JSESSIONID & sid_code
r_JcaptchaCode = session.post('https://org.xjtu.edu.cn/openplatform/g/admin/getJcaptchaCode',
headers=headers)

# is_JcaptchaCode_show
url = 'https://org.xjtu.edu.cn/openplatform/g/admin/getIsShowJcaptchaCode'
params = {
'userName': config['username'],
'_': str(int(time.time() * 1000))
}
r = session.get(url, params=params, headers=headers)
# print(r.text)
# login
url = 'https://org.xjtu.edu.cn/openplatform/g/admin/login'
cookie = {
'cur_appId_':'n0C/SQT28fY='
}
data = {
"loginType": 1,
"username": config['username'],
"pwd": encrypt_pwd(config['password']),
"jcaptchaCode": ""
}
headers['Content-Type'] = 'application/json;charset=UTF-8'
r = session.post(url, data=json.dumps(data), headers=headers,cookies=cookie)
print(r.text)
print('Authentication successful, redirecting...')
token = json.loads(r.text)['data']['tokenKey']
memberId=json.loads(r.text)['data']['orgInfo']['memberId']

cookie = {
'cur_appId_':'n0C/SQT28fY=',
'open_Platform_User' : token,
'memberId': str(memberId)
}
r=session.get('http://org.xjtu.edu.cn/openplatform/oauth/auth/getRedirectUrl?userType=1&personNo=3121154016&_=1590998261976',cookies = cookie)
r=session.get(json.loads(r.text)['data'])

5. Reserve the Venue

The last step is to reserve the venue. The manual reservation process is as follows:

  1. Select the venue.
  2. Click the confirm venue button.
  3. Jump to a new page to confirm the venue information.
  4. Click the continue reservation button to display the verification code.
  5. Enter the verification code and confirm.
  6. Return the reservation status information (success or failure).

By examining and analyzing the page requests in these processes, it is found that only two processes are related to successful reservations: ① obtaining and recognizing the verification code, and ② submitting a reservation post with the verification code.

Taking the table tennis venue reservation page as an example, the verification code generation method is as follows:

That is, access the address http://202.117.17.144/login/yzm.html?+(any float random number) to obtain the verification code image.

The post request can be located step by step through nested functions:

It can be seen that the final submitted post request address is /order/book, with two parameters, one is the string converted from the reservation information, and the other is the verification code. Below are the methods to obtain these two parameters.

There are many parameters in the reservation information, but many parameters are null and do not need to be considered. The parameters that need to be constructed are as follows:

1
2
3
4
5
6
7
8
param={"activityPrice":0,	//Price information, keep it unchanged
"address":'102', //Venue id
"extend":{}, //Extra information, keep it unchanged
"flag":"0", //Keep it unchanged
"stock":{'169111':'1'}, //The first number is the stockid of the venue mentioned above, and the second number is filled with 1
"stockdetail":{'169111':'1711756'}, //The second number is the event id
"stockdetailids":'1711756' //Event id
}

The verification code part can be verified online through some online recognition api, and the verification code is relatively simple and has a high success rate. The api address used in this project is: https://aicode.my-youth.cn

At this point, the venue can be successfully reserved. The reservation code is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def order(seat_info):

param={"activityPrice":0,
"address":seat_info['stock']['serviceid'],
"extend":{},
"flag":"0",
"stock":{str(seat_info['stockid']):'1'},
"stockdetail":{str(seat_info['stockid']):str(seat_info['id'])},
"stockdetailids":str(seat_info['id'])
}

# Get the verification code image
img=session.get('http://202.117.17.144/login/yzm.html?0.16003635332777866')
with open('yzm.jpg?x-oss-process=style/webp','wb') as f:
f.write(img.content)
f.close()
# Verification code recognition

with open('yzm.jpg?x-oss-process=style/webp','rb') as f:
img_base64=str(base64.b64encode(f.read()))[2:-1]
f.close()

headers['Content-Type']='application/x-www-form-urlencoded'
headers['Origin']='https://aicode.my-youth.cn'
response=session.post('https://aicode.my-youth.cn/base64img',data={'data':"image/jpeg;base64,"+img_base64},headers=headers)
yzm=response.json()['data']

# Reserve the venue
headers['Content-Type']='application/x-www-form-urlencoded; charset=UTF-8'
headers['Origin']='http://202.117.17.144'
response=session.post('http://202.117.17.144/order/book.html',params={'id':seat_info['stock']['serviceid']},data={'param':str(param),'yzm':yzm},headers=headers).json()

return response

Project Summary

During the development process, I also took some detours. For example, in the implementation of the reservation stage, I studied the logic of page jumping for a long time. But in fact, only the last request sent by the last page is effective. When writing the script, pay attention to the result-oriented rather than the process-oriented. This avoids many unnecessary developments.