当前仅 upsert.py 可用:扫码登录 -> 上传一局成绩 -> 落库回查 -> 登出。 - api/ 下 40+ 个接口模块已实现,236 项零网络自检全绿 - 上传成绩链路已真机跑通,报文与真机抓包逐字节一致 - 开发文档见 docs 分支;source/ 与 logs/ 不入库
223 lines
7.8 KiB
Python
223 lines
7.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
userlogin.py —— 用户正式登录(UserLoginApi)
|
||
================================================================================
|
||
|
||
功能:创建登录会话,返回 loginId / loginDateTime / 登录天数统计。
|
||
loginId 与 loginDateTime 是后续结算上报与登出的**必需输入**。
|
||
|
||
对应接口:UserLoginApi
|
||
请求体 :10 个字段,见 source/Assembly-CSharp/Net.VO.Mai2/UserLoginRequestVO.cs
|
||
source/Assembly-CSharp/Net.Packet.Mai2/PacketUserLogin.cs:21-31
|
||
响应 :UserLoginResponseVO(returnCode=1 表示成功)
|
||
|
||
--------------------------------------------------------------------------------
|
||
★ 关于 loginDateTime(与后续接口强相关,务必理解)
|
||
--------------------------------------------------------------------------------
|
||
真机取的是「本地墙上时间被当作 UTC 时刻」的秒数,即
|
||
|
||
loginDateTime = 标准 Unix 时间戳 + 本地时区偏移
|
||
|
||
东八区即 now + 28800。
|
||
|
||
成因:MAI2.DateTime.cs 里 DateTime.UtcNow + LocalOffset 得到 Kind=Unspecified
|
||
的值,PacketUserLogin.cs:29 再用 new DateTimeOffset(该值) 又加一次本地偏移。
|
||
|
||
影响:PacketUserLogin.cs:43 会把请求值回填响应;
|
||
PacketUpsertUserAll.cs:31 / PacketUserLogout.cs:29 都复用该值,
|
||
与登录时不一致会被服务端拒绝。
|
||
|
||
本模块默认按真机算法生成;需要手动指定时传 login_date_time。
|
||
|
||
★ 关于 dateTime 与 loginDateTime 的区别
|
||
dateTime = ALL.Net 认证时刻(Auth.AuthTime,标准 Unix 秒)
|
||
loginDateTime= 本次登录时刻(上面的特殊算法)
|
||
两者都出现在 UserLoginRequestVO 里,但只有 loginDateTime 会被后续接口复用。
|
||
|
||
调用示例:
|
||
from api import UserLogin
|
||
login = UserLogin(12345678, "TESTTOKEN0000000000", region_id=1, place_id=1000)
|
||
print(login.loginId, login.loginDateTime)
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import time
|
||
from dataclasses import dataclass
|
||
from typing import Optional
|
||
|
||
from config import CLIENT_ID, PLACE_ID, REGION_ID, TIMEOUT_LONG_MS
|
||
|
||
from .log_setup import log_debug, log_error, log_info
|
||
from .maimai_net import (
|
||
MaimaiNetClient,
|
||
local_utc_offset_seconds,
|
||
login_date_time_now,
|
||
request_dict,
|
||
)
|
||
|
||
|
||
@dataclass
|
||
class UserLoginRequestVO:
|
||
"""请求 VO(对应 UserLoginRequestVO,字段顺序照 C# 声明)。"""
|
||
userId: int
|
||
accessCode: str = ""
|
||
regionId: int = 0
|
||
placeId: int = 0
|
||
clientId: str = ""
|
||
dateTime: int = 0
|
||
loginDateTime: int = 0
|
||
isContinue: bool = False
|
||
genericFlag: int = 0
|
||
token: str = ""
|
||
|
||
|
||
@dataclass
|
||
class UserLoginResponseVO:
|
||
"""
|
||
响应 VO(对应 UserLoginResponseVO)。
|
||
|
||
★ 后两个字段是**客户端回填**的,服务端不返回:
|
||
loginDateTime —— PacketUserLogin.cs:43 用请求值回填,
|
||
UpsertUserAll / UserLogout 都复用它
|
||
dateTime —— 本次请求发出的 ALL.Net 认证时刻,
|
||
UpsertUserAll 的 userData.dateTime 要用它
|
||
(真机 source/2026-10-05.log:123 两处都是 1791134329)
|
||
"""
|
||
returnCode: int = 0
|
||
lastLoginDate: str = ""
|
||
loginCount: int = 0
|
||
consecutiveLoginCount: int = 0
|
||
loginId: int = 0
|
||
loginDateTime: int = 0
|
||
token: str = ""
|
||
dateTime: int = 0
|
||
|
||
|
||
def UserLogin(
|
||
user_id: int,
|
||
token: str,
|
||
*,
|
||
access_code: str = "",
|
||
region_id: int = REGION_ID,
|
||
place_id: int = PLACE_ID,
|
||
client_id: str = CLIENT_ID,
|
||
is_continue: bool = False,
|
||
generic_flag: int = 0,
|
||
date_time: Optional[int] = None,
|
||
login_date_time: Optional[int] = None,
|
||
timeout_ms: Optional[int] = None,
|
||
client: Optional[MaimaiNetClient] = None,
|
||
) -> UserLoginResponseVO:
|
||
"""
|
||
用户正式登录(创建登录会话)。
|
||
|
||
参数:
|
||
user_id: 用户 ID(必填)
|
||
token: 会话令牌(必填,来自 ParseQRCode)
|
||
access_code: 访问码(可选,通常为空)
|
||
region_id: 区域 ID(可选,默认 config.REGION_ID)
|
||
place_id: 店铺 ID(可选,默认 config.PLACE_ID)
|
||
client_id: 机台短值(可选)
|
||
is_continue: 是否续玩(可选)
|
||
generic_flag: 通用标志位(可选)
|
||
date_time: ALL.Net 认证时刻(可选,默认当前 Unix 秒)
|
||
login_date_time: 本次登录时刻(可选,默认按真机算法生成)
|
||
timeout_ms: 超时毫秒(可选,默认 60 秒)
|
||
client: 可复用的 MaimaiNetClient(可选)
|
||
|
||
返回:
|
||
UserLoginResponseVO(loginDateTime 已按真机行为回填)
|
||
|
||
异常:
|
||
RuntimeError: returnCode != 1
|
||
"""
|
||
log_info(
|
||
"用户正式登录:user_id=" + str(user_id)
|
||
+ " region_id=" + str(region_id) + " place_id=" + str(place_id)
|
||
)
|
||
|
||
now = int(time.time())
|
||
if date_time is None:
|
||
date_time = now
|
||
if login_date_time is None:
|
||
login_date_time = login_date_time_now()
|
||
|
||
log_debug(
|
||
"时间戳:dateTime=" + str(date_time) + " loginDateTime=" + str(login_date_time)
|
||
+ "(本地时区偏移 " + str(local_utc_offset_seconds()) + " 秒)"
|
||
)
|
||
|
||
raw = request_dict(
|
||
"UserLoginApi",
|
||
UserLoginRequestVO(
|
||
userId=user_id,
|
||
accessCode=access_code,
|
||
regionId=region_id,
|
||
placeId=place_id,
|
||
clientId=client_id,
|
||
dateTime=date_time,
|
||
loginDateTime=login_date_time,
|
||
isContinue=is_continue,
|
||
genericFlag=generic_flag,
|
||
token=token,
|
||
),
|
||
user_id=user_id, client_id=client_id,
|
||
timeout_ms=timeout_ms or TIMEOUT_LONG_MS, client=client,
|
||
)
|
||
|
||
known = {f for f in UserLoginResponseVO.__dataclass_fields__}
|
||
resp = UserLoginResponseVO(**{k: v for k, v in raw.items() if k in known})
|
||
|
||
if resp.returnCode != 1:
|
||
msg = "登录失败:returnCode=" + str(resp.returnCode)
|
||
log_error(msg)
|
||
raise RuntimeError(msg)
|
||
|
||
# ★ 回填 loginDateTime(对应 PacketUserLogin.cs:43)
|
||
# 服务端不返回该字段,客户端用请求值回填;后续 UpsertUserAll / UserLogout 复用。
|
||
if not resp.loginDateTime:
|
||
resp.loginDateTime = login_date_time
|
||
log_debug("服务端未返回 loginDateTime,按真机行为回填为 " + str(login_date_time))
|
||
|
||
# ★ 回填 dateTime:本次请求实际发出的 ALL.Net 认证时刻。
|
||
# UpsertUserAll 的 userData.dateTime 要用它(真机两处一致)。
|
||
resp.dateTime = date_time
|
||
|
||
log_info(
|
||
"用户登录成功:loginId=" + str(resp.loginId)
|
||
+ " loginDateTime=" + str(resp.loginDateTime)
|
||
+ " dateTime=" + str(resp.dateTime)
|
||
+ " lastLoginDate=" + resp.lastLoginDate
|
||
+ " loginCount=" + str(resp.loginCount)
|
||
+ " consecutiveLoginCount=" + str(resp.consecutiveLoginCount)
|
||
)
|
||
return resp
|
||
|
||
|
||
if __name__ == "__main__":
|
||
import sys
|
||
|
||
from .log_setup import setup_logging, teardown_logging
|
||
|
||
def _main() -> int:
|
||
setup_logging("userlogin_test")
|
||
try:
|
||
if len(sys.argv) < 3:
|
||
print("用法: python -m api.userlogin <user_id> <token> [region_id] [place_id]")
|
||
return 1
|
||
rid = int(sys.argv[3]) if len(sys.argv) > 3 else REGION_ID
|
||
pid = int(sys.argv[4]) if len(sys.argv) > 4 else PLACE_ID
|
||
lg = UserLogin(int(sys.argv[1]), sys.argv[2], region_id=rid, place_id=pid)
|
||
print("")
|
||
for name in UserLoginResponseVO.__dataclass_fields__:
|
||
print(" " + name.ljust(24) + "= " + str(getattr(lg, name)))
|
||
return 0
|
||
except Exception as exc:
|
||
log_error("用户登录失败:" + str(exc))
|
||
return 1
|
||
finally:
|
||
teardown_logging()
|
||
|
||
raise SystemExit(_main())
|