├── .gitignore ├── README.md ├── azure-pipelines.yml ├── config.py ├── encrypt.py ├── license.txt ├── login.py ├── main.py ├── process.py ├── requirements.txt └── test ├── __init__.py └── test_logger.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/config.cpython-310.pyc 2 | __pycache__/encrypt.cpython-310.pyc 3 | __pycache__/login.cpython-310.pyc 4 | __pycache__/process.cpython-310.pyc 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # i茅台预约脚本 3 | ## 原理: 4 | ### 1、登录获取验证码 5 | ### 2、输入验证码获取TOKEN 6 | ### 3、获取当日SESSION ID 7 | ### 4、根据配置文件预约CONFIG文件中,所在城市的i茅台商品 8 | 9 | ## 使用: 10 | 11 | ### 1、安装依赖 12 | ```shell 13 | pip3 install --no-cache-dir -r requirements.txt 14 | ``` 15 | 16 | ### 2、(可选)修改config.py 17 | ```python 18 | ITEM_MAP = { 19 | "10213": "53%vol 500ml贵州茅台酒(癸卯兔年)", 20 | "10214": "53%vol 375ml×2贵州茅台酒(癸卯兔年)", 21 | "10056": "53%vol 500ml茅台1935", 22 | "2478": "53%vol 500ml贵州茅台酒(珍品)" 23 | } 24 | 25 | # 需要预约的商品(默认只预约2个兔茅) 26 | ######################## 27 | ITEM_CODES = ['10213', '10214'] 28 | 29 | # 弃用掉了邮箱推送,很多人不会使用 30 | # push plus 微信推送,具体使用参考 https://www.pushplus.plus 31 | # 例如: PUSH_TOKEN = '123456' 32 | ######################## 33 | # 不填不推送消息,一对一发送 34 | PUSH_TOKEN = '123456' 35 | ######################## 36 | 37 | # credentials 路径,例如:CREDENTIALS_PATH = /home/user/.imoutai/credentials 38 | # 不配置,使用默认路径,在宿主目录 39 | # 例如: CREDENTIALS_PATH = '/home/user/.imautai/credentials' 40 | ######################## 41 | CREDENTIALS_PATH = None 42 | ######################## 43 | 44 | # 预约规则配置 45 | ######################## 46 | # 预约本市出货量最大的门店 47 | MAX_ENABLED = True 48 | # 预约你的位置附近门店 49 | DISTANCE_ENABLED = False 50 | ######################## 51 | 52 | ``` 53 | 54 | ### 3、按提示输入 预约位置、手机号、验证码 等,生成的token等 配置文件会保存在 $HOME/.imaotai/credentials, 很长时间不再需要登录。支持多账号 55 | ```shell 56 | mobian@mobian:~/app/imaotai$ python3 login.py 57 | 58 | 请输入你的位置,例如[小区名称],为你预约本市门店商店: 军安家园 59 | 0 : [地区:内蒙古自治区,位置:内蒙古自治区赤峰市红山区军安家园] 60 | 请选择位置序号,重新输入请输入[-]:0 61 | 已选择 地区:北京市,[北京市海淀区上地十街]附近的门店 62 | 输入手机号[13812341234]:1861164**** 63 | 输入 [1861164****] 验证码[1234]:1234 64 | 是否继续添加账号[Y/N]:n 65 | 66 | ``` 67 | ```shell 68 | mobian@mobian:~/app/imaotai$ cat ~/.imaotai/credentials 69 | [1850006****] 70 | city = 西安市 71 | token = zF3viZiQyUeYb5i4dxAhcBWguXS5VFYUPS5Di7BdsLs 72 | userid = 106944**** 73 | province = 陕西省 74 | lat = 45.042259 75 | lng = 115.344116 76 | 77 | [1863637****] 78 | city = 北京市 79 | token = eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 80 | userid = 1102514**** 81 | province = 北京市 82 | lat = 45.042259 83 | lng = 115.344116 84 | 85 | [1861164****] 86 | city = 太原市 87 | token = 6INvrtyGOTdpsvFmiw0I4FoFNDyG-ekt2WFsQsU9nBU 88 | userid = 10677**** 89 | province = 山西省 90 | lat = 45.042259 91 | lng = 115.344116 92 | ``` 93 | 94 | ### 4、python3 main.py ,执行预约操作 95 | ```shell 96 | python3 main.py 97 | ``` 98 | 99 | ## 注意: 100 | ### 1、可以配置一个定时任务,执行每日自动预约,建议每天多执行2次 101 | ### 2、注意服务器的时区是UTC+8,中国区域 102 | ```shell 103 | # imaotai 104 | 10,40,50 9 * * * root python3 /home/mobian/app/imaotai/main.py >> /var/log/imaotai.log 105 | ``` 106 | ##### 感谢提供的文档:https://blog.csdn.net/weixin_47481826/article/details/128893239 107 | 108 | ## Thank you to JetBrains for supporting open source projects: 109 | 110 | JetBrains 111 | 112 | 113 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Python package 2 | # Create and test a Python package on multiple Python versions. 3 | # Add steps that analyze code, save the dist with the build record, publish to a PyPI-compatible index, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/python 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: ubuntu-latest 11 | strategy: 12 | matrix: 13 | Python311: 14 | python.version: '3.11' 15 | 16 | steps: 17 | - task: UsePythonVersion@0 18 | inputs: 19 | versionSpec: '$(python.version)' 20 | displayName: 'Use Python $(python.version)' 21 | 22 | - script: | 23 | python -m pip install --upgrade pip 24 | pip install -r requirements.txt 25 | displayName: 'Install dependencies' 26 | 27 | - script: | 28 | pip install pytest pytest-azurepipelines 29 | python main.py 30 | displayName: 'main' 31 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | ITEM_MAP = { 2 | "10213": "53%vol 500ml贵州茅台酒(癸卯兔年)", 3 | "10214": "53%vol 375ml×2贵州茅台酒(癸卯兔年)", 4 | "10056": "53%vol 500ml茅台1935", 5 | "2478": "53%vol 500ml贵州茅台酒(珍品)" 6 | } 7 | 8 | # 需要预约的商品(默认只预约2个兔茅) 9 | ######################## 10 | ITEM_CODES = ['10213', '10214'] 11 | 12 | # push plus 微信推送,具体使用参考 https://www.pushplus.plus 13 | # 例如: PUSH_TOKEN = '123456' 14 | ######################## 15 | # 不填不推送消息,一对一发送 16 | PUSH_TOKEN = None 17 | ######################## 18 | 19 | # credentials 路径,例如:CREDENTIALS_PATH = /home/user/.imoutai/credentials 20 | # 不配置,使用默认路径,在宿主目录 21 | # 例如: CREDENTIALS_PATH = '/home/user/.imautai/credentials' 22 | ######################## 23 | CREDENTIALS_PATH = None 24 | ######################## 25 | 26 | # 预约规则配置 27 | ######################## 28 | # 预约本市出货量最大的门店 29 | MAX_ENABLED = True 30 | # 预约你的位置附近门店 31 | DISTANCE_ENABLED = False 32 | ######################## 33 | -------------------------------------------------------------------------------- /encrypt.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import AES 2 | import base64 3 | 4 | 5 | class Encrypt: 6 | def __init__(self, key, iv): 7 | self.key = key.encode('utf-8') 8 | self.iv = iv.encode('utf-8') 9 | 10 | # @staticmethod 11 | def pkcs7padding(self, text): 12 | """明文使用PKCS7填充 """ 13 | bs = 16 14 | length = len(text) 15 | bytes_length = len(text.encode('utf-8')) 16 | padding_size = length if (bytes_length == length) else bytes_length 17 | padding = bs - padding_size % bs 18 | padding_text = chr(padding) * padding 19 | self.coding = chr(padding) 20 | return text + padding_text 21 | 22 | def aes_encrypt(self, content): 23 | """ AES加密 """ 24 | cipher = AES.new(self.key, AES.MODE_CBC, self.iv) 25 | # 处理明文 26 | content_padding = self.pkcs7padding(content) 27 | # 加密 28 | encrypt_bytes = cipher.encrypt(content_padding.encode('utf-8')) 29 | # 重新编码 30 | result = str(base64.b64encode(encrypt_bytes), encoding='utf-8') 31 | return result 32 | 33 | def aes_decrypt(self, content): 34 | """AES解密 """ 35 | cipher = AES.new(self.key, AES.MODE_CBC, self.iv) 36 | content = base64.b64decode(content) 37 | text = cipher.decrypt(content).decode('utf-8') 38 | return text.rstrip(self.coding) 39 | 40 | # if __name__ == '__main__': 41 | # key = 'ONxYDyNaCoyTzsp83JoQ3YYuMPHxk3j7' 42 | # iv = 'yNaCoyTzsp83JoQ3' 43 | # 44 | # ts = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 45 | # p_json = { 46 | # "CompanyName": "testmall", 47 | # "UserId": "test", 48 | # "Password": "grasp@101", 49 | # "TimeStamp": "2019-05-05 10:59:26" 50 | # } 51 | # a = Encrypt(key=key, iv=iv) 52 | # e = a.aes_encrypt(json.dumps(p_json)) 53 | # d = a.aes_decrypt(e) 54 | # print("加密:", e) 55 | # print("解密:", d) 56 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /login.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | 3 | import os 4 | import config as cf 5 | import process 6 | 7 | config = configparser.ConfigParser() # 类实例化 8 | 9 | 10 | def get_credentials_path(): 11 | if cf.CREDENTIALS_PATH is not None: 12 | return cf.CREDENTIALS_PATH 13 | else: 14 | home_path = os.path.expanduser("~") 15 | path = os.path.join(home_path, '.imaotai', 'credentials') 16 | # 尝试创建目录 17 | try: 18 | os.mkdir(os.path.join(home_path, '.imaotai')) 19 | except OSError: 20 | pass 21 | return path 22 | 23 | 24 | path = get_credentials_path() 25 | config.read(get_credentials_path()) 26 | sections = config.sections() 27 | 28 | 29 | def get_location(): 30 | while 1: 31 | 32 | location = input(f"请输入你的位置,例如[小区名称],为你自动预约附近的门店:").lstrip().rstrip() 33 | selects = process.select_geo(location) 34 | 35 | a = 0 36 | for item in selects: 37 | formatted_address = item['formatted_address'] 38 | province = item['province'] 39 | print(f'{a} : [地区:{province},位置:{formatted_address}]') 40 | a += 1 41 | user_select = input(f"请选择位置序号,重新输入请输入[-]:").lstrip().rstrip() 42 | if user_select == '-': 43 | continue 44 | select = selects[int(user_select)] 45 | formatted_address = select['formatted_address'] 46 | province = select['province'] 47 | print(f'已选择 地区:{province},[{formatted_address}]附近的门店') 48 | return select 49 | 50 | 51 | if __name__ == '__main__': 52 | 53 | while 1: 54 | process.init_headers() 55 | location_select: dict = get_location() 56 | province = location_select['province'] 57 | city = location_select['city'] 58 | location: str = location_select['location'] 59 | 60 | mobile = input("输入手机号[13812341234]:").lstrip().rstrip() 61 | process.get_vcode(mobile) 62 | code = input(f"输入 [{mobile}] 验证码[1234]:").lstrip().rstrip() 63 | token, userId = process.login(mobile, code) 64 | if mobile not in sections: 65 | config.add_section(mobile) # 首先添加一个新的section 66 | 67 | config.set(mobile, 'province', str(province)) 68 | config.set(mobile, 'city', str(city)) 69 | config.set(mobile, 'token', str(token)) 70 | config.set(mobile, 'userId', str(userId)) 71 | config.set(mobile, 'lat', location.split(',')[1]) 72 | config.set(mobile, 'lng', location.split(',')[0]) 73 | config.write(open(path, 'w+')) # 保存数据 74 | condition = input(f"是否继续添加账号[Y/N]:").lstrip().rstrip() 75 | condition = condition.lower() 76 | if condition == 'n': 77 | break 78 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import logging 3 | import sys 4 | 5 | import config 6 | import login 7 | import process 8 | 9 | DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p" 10 | logging.basicConfig(level=logging.INFO, 11 | format='%(asctime)s %(filename)s : %(levelname)s %(message)s', # 定义输出log的格式 12 | stream=sys.stdout, 13 | datefmt=DATE_FORMAT) 14 | 15 | # 获取当日session id 16 | process.get_current_session_id() 17 | 18 | configs = login.config 19 | if len(configs.sections()) == 0: 20 | logging.error("配置文件未找到配置") 21 | sys.exit(1) 22 | 23 | for section in configs.sections(): 24 | mobile = section 25 | province = configs.get(section, 'province') 26 | city = configs.get(section, 'city') 27 | token = configs.get(section, 'token') 28 | userId = configs.get(section, 'userid') 29 | lat = configs.get(mobile, 'lat') 30 | lng = configs.get(mobile, 'lng') 31 | 32 | p_c_map, source_data = process.get_map(lat=lat, lng=lng) 33 | 34 | process.UserId = userId 35 | process.TOKEN = token 36 | process.init_headers(user_id=userId, token=token, lng=lng, lat=lat) 37 | # 根据配置中,要预约的商品ID,城市 进行自动预约 38 | try: 39 | for item in config.ITEM_CODES: 40 | max_shop_id = process.get_location_count(province=province, 41 | city=city, 42 | item_code=item, 43 | p_c_map=p_c_map, 44 | source_data=source_data, 45 | lat=lat, 46 | lng=lng) 47 | print(f'max shop id : {max_shop_id}') 48 | if max_shop_id == '0': 49 | continue 50 | shop_info = source_data.get(str(max_shop_id)) 51 | title = config.ITEM_MAP.get(item) 52 | logging.info(f'商品:{title}, 门店:{shop_info["name"]}') 53 | reservation_params = process.act_params(max_shop_id, item) 54 | process.reservation(reservation_params, mobile) 55 | process.getUserEnergyAward(mobile) 56 | except BaseException as e: 57 | print(e) 58 | logging.error(e) 59 | -------------------------------------------------------------------------------- /process.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import math 4 | import random 5 | import re 6 | import time 7 | import config 8 | from encrypt import Encrypt 9 | import requests 10 | import hashlib 11 | import logging 12 | import pytz 13 | 14 | AES_KEY = 'qbhajinldepmucsonaaaccgypwuvcjaa' 15 | AES_IV = '2018534749963515' 16 | SALT = '2af72f100c356273d46284f6fd1dfc08' 17 | 18 | # AMAP_KEY = '9449339b6c4aee04d69481e6e6c84a84' 19 | 20 | current_time = str(int(time.time() * 1000)) 21 | headers = {} 22 | mt_version = "".join(re.findall('latest__version">(.*?)

', 23 | requests.get('https://apps.apple.com/cn/app/i%E8%8C%85%E5%8F%B0/id1600482450').text, 24 | re.S)).split(" ")[1] 25 | 26 | header_context = f''' 27 | MT-Lat: 28.499562 28 | MT-K: 1675213490331 29 | MT-Lng: 102.182324 30 | Host: app.moutai519.com.cn 31 | MT-User-Tag: 0 32 | Accept: */* 33 | MT-Network-Type: WIFI 34 | MT-Token: 1 35 | MT-Team-ID: 36 | MT-Info: 028e7f96f6369cafe1d105579c5b9377 37 | MT-Device-ID: 2F2075D0-B66C-4287-A903-DBFF6358342A 38 | MT-Bundle-ID: com.moutai.mall 39 | Accept-Language: en-CN;q=1, zh-Hans-CN;q=0.9 40 | MT-Request-ID: 167560018873318465 41 | MT-APP-Version: 1.3.7 42 | User-Agent: iOS;16.3;Apple;?unrecognized? 43 | MT-R: clips_OlU6TmFRag5rCXwbNAQ/Tz1SKlN8THcecBp/HGhHdw== 44 | Content-Length: 93 45 | Accept-Encoding: gzip, deflate, br 46 | Connection: keep-alive 47 | Content-Type: application/json 48 | userId: 2 49 | ''' 50 | 51 | 52 | def init_headers(user_id: str = '1', token: str = '2', lat: str = '28.499562', lng: str = '102.182324'): 53 | for k in header_context.rstrip().lstrip().split("\n"): 54 | temp_l = k.split(': ') 55 | dict.update(headers, {temp_l[0]: temp_l[1]}) 56 | dict.update(headers, {"userId": user_id}) 57 | dict.update(headers, {"MT-Token": token}) 58 | dict.update(headers, {"MT-Lat": lat}) 59 | dict.update(headers, {"MT-Lng": lng}) 60 | dict.update(headers, {"MT-APP-Version": mt_version}) 61 | 62 | 63 | def signature(data: dict): 64 | keys = sorted(data.keys()) 65 | temp_v = '' 66 | for item in keys: 67 | temp_v += data[item] 68 | text = SALT + temp_v + current_time 69 | hl = hashlib.md5() 70 | hl.update(text.encode(encoding='utf8')) 71 | md5 = hl.hexdigest() 72 | return md5 73 | 74 | 75 | print() 76 | 77 | 78 | def get_vcode(mobile: str): 79 | params = {'mobile': mobile} 80 | md5 = signature(params) 81 | dict.update(params, {'md5': md5, "timestamp": current_time, 'MT-APP-Version': mt_version}) 82 | responses = requests.post("https://app.moutai519.com.cn/xhr/front/user/register/vcode", json=params, 83 | headers=headers) 84 | 85 | logging.info( 86 | f'get v_code : params : {params}, response code : {responses.status_code}, response body : {responses.text}') 87 | 88 | 89 | def login(mobile: str, v_code: str): 90 | params = {'mobile': mobile, 'vCode': v_code, 'ydToken': '', 'ydLogId': ''} 91 | md5 = signature(params) 92 | dict.update(params, {'md5': md5, "timestamp": current_time, 'MT-APP-Version': mt_version}) 93 | responses = requests.post("https://app.moutai519.com.cn/xhr/front/user/register/login", json=params, 94 | headers=headers) 95 | if responses.status_code != 200: 96 | logging.info( 97 | f'login : params : {params}, response code : {responses.status_code}, response body : {responses.text}') 98 | dict.update(headers, {'MT-Token': responses.json()['data']['token']}) 99 | dict.update(headers, {'userId': responses.json()['data']['userId']}) 100 | return responses.json()['data']['token'], responses.json()['data']['userId'] 101 | 102 | 103 | def get_current_session_id(): 104 | day_time = get_day_time() 105 | responses = requests.get(f"https://static.moutai519.com.cn/mt-backend/xhr/front/mall/index/session/get/{day_time}") 106 | if responses.status_code != 200: 107 | logging.warning( 108 | f'get_current_session_id : params : {day_time}, response code : {responses.status_code}, response body : {responses.text}') 109 | current_session_id = responses.json()['data']['sessionId'] 110 | dict.update(headers, {'current_session_id': str(current_session_id)}) 111 | 112 | 113 | def get_day_time(): 114 | 115 | # 创建一个东八区(北京时间)的时区对象 116 | beijing_tz = pytz.timezone('Asia/Shanghai') 117 | 118 | # 获取当前北京时间的日期和时间对象 119 | beijing_dt = datetime.datetime.now(beijing_tz) 120 | 121 | # 设置时间为0点 122 | beijing_dt = beijing_dt.replace(hour=0, minute=0, second=0, microsecond=0) 123 | 124 | # 获取时间戳(以秒为单位) 125 | timestamp = int(beijing_dt.timestamp()) * 1000 126 | return timestamp 127 | 128 | 129 | def get_location_count(province: str, 130 | city: str, 131 | item_code: str, 132 | p_c_map: dict, 133 | source_data: dict, 134 | lat: str = '28.499562', 135 | lng: str = '102.182324'): 136 | day_time = get_day_time() 137 | session_id = headers['current_session_id'] 138 | responses = requests.get( 139 | f"https://static.moutai519.com.cn/mt-backend/xhr/front/mall/shop/list/slim/v3/{session_id}/{province}/{item_code}/{day_time}") 140 | if responses.status_code != 200: 141 | logging.warning( 142 | f'get_location_count : params : {day_time}, response code : {responses.status_code}, response body : {responses.text}') 143 | shops = responses.json()['data']['shops'] 144 | 145 | if config.MAX_ENABLED: 146 | return max_shop(city, item_code, p_c_map, province, shops) 147 | if config.DISTANCE_ENABLED: 148 | return distance_shop(city, item_code, p_c_map, province, shops, source_data, lat, lng) 149 | 150 | 151 | def distance_shop(city, 152 | item_code, 153 | p_c_map, 154 | province, 155 | shops, 156 | source_data, 157 | lat: str = '28.499562', 158 | lng: str = '102.182324'): 159 | # shop_ids = p_c_map[province][city] 160 | temp_list = [] 161 | for shop in shops: 162 | shopId = shop['shopId'] 163 | items = shop['items'] 164 | item_ids = [i['itemId'] for i in items] 165 | # if shopId not in shop_ids: 166 | # continue 167 | if str(item_code) not in item_ids: 168 | continue 169 | shop_info = source_data.get(shopId) 170 | # d = geodesic((lat, lng), (shop_info['lat'], shop_info['lng'])).km 171 | d = math.sqrt((float(lat) - shop_info['lat']) ** 2 + (float(lng) - shop_info['lng']) ** 2) 172 | # print(f"距离:{d}") 173 | temp_list.append((d, shopId)) 174 | 175 | # sorted(a,key=lambda x:x[0]) 176 | temp_list = sorted(temp_list, key=lambda x: x[0]) 177 | # logging.info(f"所有门店距离:{temp_list}") 178 | if len(temp_list) > 0: 179 | return temp_list[0][1] 180 | else: 181 | return '0' 182 | 183 | 184 | def max_shop(city, item_code, p_c_map, province, shops): 185 | max_count = 0 186 | max_shop_id = '0' 187 | shop_ids = p_c_map[province][city] 188 | for shop in shops: 189 | shopId = shop['shopId'] 190 | items = shop['items'] 191 | 192 | if shopId not in shop_ids: 193 | continue 194 | for item in items: 195 | if item['itemId'] != str(item_code): 196 | continue 197 | if item['inventory'] > max_count: 198 | max_count = item['inventory'] 199 | max_shop_id = shopId 200 | logging.debug(f'item code {item_code}, max shop id : {max_shop_id}, max count : {max_count}') 201 | return max_shop_id 202 | 203 | 204 | encrypt = Encrypt(key=AES_KEY, iv=AES_IV) 205 | 206 | 207 | def act_params(shop_id: str, item_id: str): 208 | # { 209 | # "actParam": "a/v0XjWK/a/a+ZyaSlKKZViJHuh8tLw==", 210 | # "itemInfoList": [ 211 | # { 212 | # "count": 1, 213 | # "itemId": "2478" 214 | # } 215 | # ], 216 | # "shopId": "151510100019", 217 | # "sessionId": 508 218 | # } 219 | session_id = headers['current_session_id'] 220 | userId = headers['userId'] 221 | params = {"itemInfoList": [{"count": 1, "itemId": item_id}], 222 | "sessionId": int(session_id), 223 | "userId": userId, 224 | "shopId": shop_id 225 | } 226 | s = json.dumps(params) 227 | act = encrypt.aes_encrypt(s) 228 | params.update({"actParam": act}) 229 | return params 230 | 231 | 232 | def send_email(msg: str): 233 | if config.PUSH_TOKEN is None: 234 | return 235 | title = 'imoutai预约失败' # 改成你要的标题内容 236 | content = msg # 改成你要的正文内容 237 | url = 'http://www.pushplus.plus/send' 238 | r = requests.get(url, params={'token': config.PUSH_TOKEN, 239 | 'title': title, 240 | 'content': content}) 241 | logging.info(f'通知推送结果:{r.status_code, r.text}') 242 | 243 | 244 | def reservation(params: dict, mobile: str): 245 | params.pop('userId') 246 | responses = requests.post("https://app.moutai519.com.cn/xhr/front/mall/reservation/add", json=params, 247 | headers=headers) 248 | if responses.status_code == 401: 249 | send_email(f'[{mobile}],登录token失效,需要重新登录') 250 | raise RuntimeError 251 | if '您的实名信息未完善或未通过认证' in responses.text: 252 | send_email(f'[{mobile}],{responses.text}') 253 | raise RuntimeError 254 | logging.info( 255 | f'预约 : mobile:{mobile} : response code : {responses.status_code}, response body : {responses.text}') 256 | 257 | 258 | def select_geo(i: str): 259 | # https://www.piliang.tech/geocoding-amap 260 | resp = requests.get(f"https://www.piliang.tech/api/amap/geocode?address={i}") 261 | geocodes: list = resp.json()['geocodes'] 262 | return geocodes 263 | 264 | 265 | def get_map(lat: str = '28.499562', lng: str = '102.182324'): 266 | p_c_map = {} 267 | url = 'https://static.moutai519.com.cn/mt-backend/xhr/front/mall/resource/get' 268 | headers = { 269 | 'X-Requested-With': 'XMLHttpRequest', 270 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_0_1 like Mac OS X)', 271 | 'Referer': 'https://h5.moutai519.com.cn/gux/game/main?appConfig=2_1_2', 272 | 'Client-User-Agent': 'iOS;16.0.1;Apple;iPhone 14 ProMax', 273 | 'MT-R': 'clips_OlU6TmFRag5rCXwbNAQ/Tz1SKlN8THcecBp/HGhHdw==', 274 | 'Origin': 'https://h5.moutai519.com.cn', 275 | 'MT-APP-Version': mt_version, 276 | 'MT-Request-ID': f'{int(time.time() * 1000)}{random.randint(1111111, 999999999)}{int(time.time() * 1000)}', 277 | 'Accept-Language': 'zh-CN,zh-Hans;q=1', 278 | 'MT-Device-ID': f'{int(time.time() * 1000)}{random.randint(1111111, 999999999)}{int(time.time() * 1000)}', 279 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 280 | 'mt-lng': f'{lng}', 281 | 'mt-lat': f'{lat}' 282 | } 283 | res = requests.get(url, headers=headers, ) 284 | mtshops = res.json().get('data', {}).get('mtshops_pc', {}) 285 | urls = mtshops.get('url') 286 | r = requests.get(urls) 287 | for k, v in dict(r.json()).items(): 288 | provinceName = v.get('provinceName') 289 | cityName = v.get('cityName') 290 | if not p_c_map.get(provinceName): 291 | p_c_map[provinceName] = {} 292 | if not p_c_map[provinceName].get(cityName, None): 293 | p_c_map[provinceName][cityName] = [k] 294 | else: 295 | p_c_map[provinceName][cityName].append(k) 296 | 297 | return p_c_map, dict(r.json()) 298 | 299 | 300 | def getUserEnergyAward(mobile: str): 301 | """ 302 | 领取耐力 303 | """ 304 | cookies = { 305 | 'MT-Device-ID-Wap': headers['MT-Device-ID'], 306 | 'MT-Token-Wap': headers['MT-Token'], 307 | 'YX_SUPPORT_WEBP': '1', 308 | } 309 | response = requests.post('https://h5.moutai519.com.cn/game/isolationPage/getUserEnergyAward', cookies=cookies, 310 | headers=headers, json={}) 311 | # response.json().get('message') if '无法领取奖励' in response.text else "领取奖励成功" 312 | logging.info( 313 | f'领取耐力 : mobile:{mobile} : response code : {response.status_code}, response body : {response.text}') 314 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests~=2.30.0 2 | urllib3<=1.25 3 | pycryptodome==3.17 4 | pytz -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dongyazhuo/imaotai/4d41d6af447f17d0aa412458d6e82544f98b2be6/test/__init__.py -------------------------------------------------------------------------------- /test/test_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | print() 5 | DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p" 6 | logging.basicConfig(level=logging.DEBUG, 7 | format='%(asctime)s %(filename)s : %(levelname)s %(message)s', # 定义输出log的格式 8 | stream=sys.stdout, 9 | datefmt=DATE_FORMAT) 10 | logging.info(f'hehe{1}') 11 | --------------------------------------------------------------------------------