2025-05-26 15:10:12 +08:00
|
|
|
|
import calendar
|
2025-06-06 10:56:22 +08:00
|
|
|
|
from datetime import timedelta
|
2025-06-29 01:45:24 +08:00
|
|
|
|
import random
|
2025-05-26 15:10:12 +08:00
|
|
|
|
|
|
|
|
|
|
from zhenxun.services.log import logger
|
|
|
|
|
|
from zhenxun.utils._build_image import BuildImage
|
|
|
|
|
|
|
2025-05-29 18:20:03 +08:00
|
|
|
|
from ..config import g_bIsDebug
|
2025-05-26 15:10:12 +08:00
|
|
|
|
from ..dbService import g_pDBService
|
|
|
|
|
|
from ..json import g_pJsonManager
|
2025-05-27 18:15:11 +08:00
|
|
|
|
from ..tool import g_pToolManager
|
2025-05-28 19:48:12 +08:00
|
|
|
|
from .database import CSqlManager
|
2025-05-26 15:10:12 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CUserSignDB(CSqlManager):
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def initDB(cls):
|
2025-06-06 10:56:22 +08:00
|
|
|
|
# userSignLog 表结构,每条为一次签到事件
|
2025-05-26 15:10:12 +08:00
|
|
|
|
userSignLog = {
|
2025-06-06 10:56:22 +08:00
|
|
|
|
"uid": "TEXT NOT NULL", # 用户ID
|
|
|
|
|
|
"signDate": "DATE NOT NULL", # 签到日期
|
|
|
|
|
|
"isSupplement": "TINYINT NOT NULL DEFAULT 0", # 是否补签
|
|
|
|
|
|
"exp": "INT NOT NULL DEFAULT 0", # 当天签到经验
|
|
|
|
|
|
"point": "INT NOT NULL DEFAULT 0", # 当天签到金币
|
2025-06-29 01:45:24 +08:00
|
|
|
|
"createdAt": "DATETIME NOT NULL DEFAULT (datetime(CURRENT_TIMESTAMP, 'localtime'))", # 创建时间 # noqa: E501
|
2025-06-06 10:56:22 +08:00
|
|
|
|
"PRIMARY KEY": "(uid, signDate)",
|
2025-05-26 15:10:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-06 10:56:22 +08:00
|
|
|
|
# userSignSummary 表结构,每用户一行用于缓存签到状态
|
2025-05-26 15:10:12 +08:00
|
|
|
|
userSignSummary = {
|
2025-06-06 10:56:22 +08:00
|
|
|
|
"uid": "TEXT PRIMARY KEY NOT NULL", # 用户ID
|
|
|
|
|
|
"totalSignDays": "INT NOT NULL DEFAULT 0", # 累计签到天数
|
|
|
|
|
|
"currentMonth": "CHAR(7) NOT NULL DEFAULT ''", # 当前月份(如2025-05)
|
|
|
|
|
|
"monthSignDays": "INT NOT NULL DEFAULT 0", # 本月签到次数
|
|
|
|
|
|
"lastSignDate": "DATE DEFAULT NULL", # 上次签到日期
|
|
|
|
|
|
"continuousDays": "INT NOT NULL DEFAULT 0", # 连续签到天数
|
|
|
|
|
|
"supplementCount": "INT NOT NULL DEFAULT 0", # 补签次数
|
2025-06-29 01:45:24 +08:00
|
|
|
|
"updatedAt": "DATETIME NOT NULL DEFAULT (datetime(CURRENT_TIMESTAMP, 'localtime'))", # 更新时间 # noqa: E501
|
2025-05-26 15:10:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await cls.ensureTableSchema("userSignLog", userSignLog)
|
|
|
|
|
|
await cls.ensureTableSchema("userSignSummary", userSignSummary)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-05-27 18:15:11 +08:00
|
|
|
|
async def getUserSignRewardByDate(cls, uid: str, date: str) -> tuple[int, int]:
|
|
|
|
|
|
"""根据指定日期获取用户签到随机奖励
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
uid (str): 用户Uid
|
|
|
|
|
|
date (str): 用户签到日期 示例:2025-05-27
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
tuple[int, int]: 经验、金币
|
2025-05-26 15:10:12 +08:00
|
|
|
|
"""
|
2025-05-27 18:15:11 +08:00
|
|
|
|
try:
|
|
|
|
|
|
async with cls._transaction():
|
|
|
|
|
|
async with cls.m_pDB.execute(
|
|
|
|
|
|
"SELECT exp, point FROM userSignLog WHERE uid=? AND signDate=?",
|
2025-06-06 10:56:22 +08:00
|
|
|
|
(uid, date),
|
2025-05-27 18:15:11 +08:00
|
|
|
|
) as cursor:
|
|
|
|
|
|
row = await cursor.fetchone()
|
|
|
|
|
|
|
|
|
|
|
|
if row is None:
|
|
|
|
|
|
return 0, 0
|
|
|
|
|
|
|
2025-06-06 10:56:22 +08:00
|
|
|
|
exp = row["exp"]
|
|
|
|
|
|
point = row["point"]
|
2025-05-27 18:15:11 +08:00
|
|
|
|
|
|
|
|
|
|
return exp, point
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("获取用户签到数据失败", e=e)
|
|
|
|
|
|
return 0, 0
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def getUserSignCountByDate(cls, uid: str, monthStr: str) -> int:
|
|
|
|
|
|
"""根据日期查询用户签到总天数
|
2025-05-26 15:10:12 +08:00
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
uid (str): 用户Uid
|
|
|
|
|
|
monthStr (str): 需要查询的日期 示例: 2025-05
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
2025-05-27 18:15:11 +08:00
|
|
|
|
int: 查询月总签到天数
|
2025-05-26 15:10:12 +08:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
sql = "SELECT COUNT(*) FROM userSignLog WHERE uid=? AND signDate LIKE ?"
|
|
|
|
|
|
param = f"{monthStr}-%"
|
|
|
|
|
|
async with cls.m_pDB.execute(sql, (uid, param)) as cursor:
|
|
|
|
|
|
row = await cursor.fetchone()
|
|
|
|
|
|
return row[0] if row else 0
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("统计用户月签到次数失败", e=e)
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def hasSigned(cls, uid: str, signDate: str) -> bool:
|
|
|
|
|
|
"""判断指定日期是否已签到
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
uid (int): 用户ID
|
|
|
|
|
|
signDate (str): 日期字符串 'YYYY-MM-DD'
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: True=已签到,False=未签到
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
sql = "SELECT 1 FROM userSignLog WHERE uid=? AND signDate=? LIMIT 1"
|
|
|
|
|
|
async with cls.m_pDB.execute(sql, (uid, signDate)) as cursor:
|
|
|
|
|
|
row = await cursor.fetchone()
|
|
|
|
|
|
return row is not None
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("查询是否已签到失败", e=e)
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2025-06-06 10:56:22 +08:00
|
|
|
|
async def sign(cls, uid: str, signDate: str = "") -> int:
|
2025-05-27 18:15:11 +08:00
|
|
|
|
"""签到
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
uid (int): 用户ID
|
|
|
|
|
|
signDate (str): 日期字符串 'YYYY-MM-DD' 不传默认当前系统日期
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: 0: 签到失败 1: 签到成功 2: 重复签到
|
|
|
|
|
|
"""
|
2025-05-26 15:10:12 +08:00
|
|
|
|
try:
|
|
|
|
|
|
if not signDate:
|
2025-05-27 18:15:11 +08:00
|
|
|
|
signDate = g_pToolManager.dateTime().date().today().strftime("%Y-%m-%d")
|
2025-05-26 15:10:12 +08:00
|
|
|
|
|
|
|
|
|
|
if await cls.hasSigned(uid, signDate):
|
2025-05-27 18:15:11 +08:00
|
|
|
|
return 2
|
2025-05-26 15:10:12 +08:00
|
|
|
|
|
2025-05-27 18:15:11 +08:00
|
|
|
|
todayStr = g_pToolManager.dateTime().date().today().strftime("%Y-%m-%d")
|
2025-05-26 15:10:12 +08:00
|
|
|
|
isSupplement = 0 if signDate == todayStr else 1
|
|
|
|
|
|
|
2025-05-27 18:15:11 +08:00
|
|
|
|
expMax, expMin, pointMax, pointMin = [
|
|
|
|
|
|
g_pJsonManager.m_pSign.get(key, default)
|
|
|
|
|
|
for key, default in (
|
|
|
|
|
|
("exp_max", 50),
|
|
|
|
|
|
("exp_min", 5),
|
|
|
|
|
|
("point_max", 2000),
|
|
|
|
|
|
("point_min", 200),
|
|
|
|
|
|
)
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
exp = random.randint(expMin, expMax)
|
|
|
|
|
|
point = random.randint(pointMin, pointMax)
|
2025-05-29 18:20:03 +08:00
|
|
|
|
vipPoint = 0
|
2025-05-27 18:15:11 +08:00
|
|
|
|
|
2025-05-26 15:10:12 +08:00
|
|
|
|
async with cls._transaction():
|
|
|
|
|
|
await cls.m_pDB.execute(
|
2025-05-27 18:15:11 +08:00
|
|
|
|
"INSERT INTO userSignLog (uid, signDate, isSupplement, exp, point) VALUES (?, ?, ?, ?, ?)",
|
2025-06-06 10:56:22 +08:00
|
|
|
|
(uid, signDate, isSupplement, exp, point),
|
2025-05-26 15:10:12 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-06-06 10:56:22 +08:00
|
|
|
|
cursor = await cls.m_pDB.execute(
|
|
|
|
|
|
"SELECT * FROM userSignSummary WHERE uid=?", (uid,)
|
|
|
|
|
|
)
|
2025-05-26 15:10:12 +08:00
|
|
|
|
row = await cursor.fetchone()
|
|
|
|
|
|
|
|
|
|
|
|
currentMonth = signDate[:7]
|
|
|
|
|
|
if row:
|
2025-06-06 10:56:22 +08:00
|
|
|
|
monthSignDays = (
|
|
|
|
|
|
row["monthSignDays"] + 1
|
|
|
|
|
|
if row["currentMonth"] == currentMonth
|
|
|
|
|
|
else 1
|
|
|
|
|
|
)
|
|
|
|
|
|
totalSignDays = row["totalSignDays"]
|
|
|
|
|
|
lastDate = row["lastSignDate"]
|
|
|
|
|
|
prevDate = (
|
|
|
|
|
|
g_pToolManager.dateTime().strptime(signDate, "%Y-%m-%d")
|
|
|
|
|
|
- timedelta(days=1)
|
|
|
|
|
|
).strftime("%Y-%m-%d")
|
|
|
|
|
|
continuousDays = (
|
|
|
|
|
|
row["continuousDays"] + 1 if lastDate == prevDate else 1
|
|
|
|
|
|
)
|
|
|
|
|
|
supplementCount = (
|
|
|
|
|
|
row["supplementCount"] + 1
|
|
|
|
|
|
if isSupplement
|
|
|
|
|
|
else row["supplementCount"]
|
|
|
|
|
|
)
|
2025-05-26 15:10:12 +08:00
|
|
|
|
await cls.m_pDB.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
UPDATE userSignSummary
|
|
|
|
|
|
SET totalSignDays=totalSignDays+1,
|
|
|
|
|
|
currentMonth=?,
|
|
|
|
|
|
monthSignDays=?,
|
|
|
|
|
|
lastSignDate=?,
|
|
|
|
|
|
continuousDays=?,
|
|
|
|
|
|
supplementCount=?
|
|
|
|
|
|
WHERE uid=?
|
|
|
|
|
|
""",
|
2025-06-06 10:56:22 +08:00
|
|
|
|
(
|
|
|
|
|
|
currentMonth,
|
|
|
|
|
|
monthSignDays,
|
|
|
|
|
|
signDate,
|
|
|
|
|
|
continuousDays,
|
|
|
|
|
|
supplementCount,
|
|
|
|
|
|
uid,
|
|
|
|
|
|
),
|
2025-05-26 15:10:12 +08:00
|
|
|
|
)
|
|
|
|
|
|
else:
|
2025-05-27 18:15:11 +08:00
|
|
|
|
totalSignDays = 1
|
2025-05-26 15:10:12 +08:00
|
|
|
|
await cls.m_pDB.execute(
|
|
|
|
|
|
"""
|
|
|
|
|
|
INSERT INTO userSignSummary
|
|
|
|
|
|
(uid, totalSignDays, currentMonth, monthSignDays, lastSignDate, continuousDays, supplementCount)
|
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
|
""",
|
2025-06-06 10:56:22 +08:00
|
|
|
|
(
|
|
|
|
|
|
uid,
|
|
|
|
|
|
1,
|
|
|
|
|
|
currentMonth,
|
|
|
|
|
|
1,
|
|
|
|
|
|
signDate,
|
|
|
|
|
|
1,
|
|
|
|
|
|
1 if isSupplement else 0,
|
|
|
|
|
|
),
|
2025-05-26 15:10:12 +08:00
|
|
|
|
)
|
2025-05-27 18:15:11 +08:00
|
|
|
|
|
2025-06-06 10:56:22 +08:00
|
|
|
|
# 计算累签奖励
|
|
|
|
|
|
reward = g_pJsonManager.m_pSign["continuou"].get(f"{totalSignDays}", None)
|
2025-05-27 18:15:11 +08:00
|
|
|
|
|
|
|
|
|
|
if reward:
|
2025-06-06 10:56:22 +08:00
|
|
|
|
point += reward.get("point", 0)
|
|
|
|
|
|
exp += reward.get("exp", 0)
|
|
|
|
|
|
vipPoint = reward.get("vipPoint", 0)
|
2025-05-27 18:15:11 +08:00
|
|
|
|
|
2025-06-06 10:56:22 +08:00
|
|
|
|
plant = reward.get("plant", {})
|
2025-05-27 18:15:11 +08:00
|
|
|
|
|
|
|
|
|
|
if plant:
|
|
|
|
|
|
for key, value in plant.items():
|
|
|
|
|
|
await g_pDBService.userSeed.addUserSeedByUid(uid, key, value)
|
|
|
|
|
|
|
2025-05-29 18:20:03 +08:00
|
|
|
|
if g_bIsDebug:
|
|
|
|
|
|
exp += 9999
|
|
|
|
|
|
|
2025-06-06 10:56:22 +08:00
|
|
|
|
# 向数据库更新
|
2025-05-27 18:15:11 +08:00
|
|
|
|
currentExp = await g_pDBService.user.getUserExpByUid(uid)
|
|
|
|
|
|
await g_pDBService.user.updateUserExpByUid(uid, currentExp + exp)
|
|
|
|
|
|
|
|
|
|
|
|
currentPoint = await g_pDBService.user.getUserPointByUid(uid)
|
|
|
|
|
|
await g_pDBService.user.updateUserPointByUid(uid, currentPoint + point)
|
|
|
|
|
|
|
2025-05-29 18:20:03 +08:00
|
|
|
|
if vipPoint > 0:
|
|
|
|
|
|
currentVipPoint = await g_pDBService.user.getUserVipPointByUid(uid)
|
2025-06-06 10:56:22 +08:00
|
|
|
|
await g_pDBService.user.updateUserVipPointByUid(
|
|
|
|
|
|
uid, currentVipPoint + vipPoint
|
|
|
|
|
|
)
|
2025-05-29 18:20:03 +08:00
|
|
|
|
|
2025-05-27 18:15:11 +08:00
|
|
|
|
return 1
|
2025-05-26 15:10:12 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("执行签到失败", e=e)
|
2025-05-27 18:15:11 +08:00
|
|
|
|
return 0
|
2025-05-26 15:10:12 +08:00
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
async def drawSignCalendarImage(cls, uid: str, year: int, month: int):
|
2025-06-06 10:56:22 +08:00
|
|
|
|
# 绘制签到图,自动提取数据库中该用户该月的签到天数
|
2025-05-26 15:10:12 +08:00
|
|
|
|
cellSize = 80
|
|
|
|
|
|
padding = 40
|
|
|
|
|
|
titleHeight = 80
|
|
|
|
|
|
cols = 7
|
|
|
|
|
|
rows = 6
|
|
|
|
|
|
width = cellSize * cols + padding * 2
|
|
|
|
|
|
height = cellSize * rows + padding * 2 + titleHeight
|
|
|
|
|
|
|
|
|
|
|
|
img = BuildImage(width, height, color=(255, 255, 255))
|
|
|
|
|
|
await img.text((padding, 20), f"{year}年{month}月签到表", font_size=36)
|
|
|
|
|
|
|
|
|
|
|
|
firstWeekday, totalDays = calendar.monthrange(year, month)
|
|
|
|
|
|
monthStr = f"{year:04d}-{month:02d}"
|
|
|
|
|
|
try:
|
|
|
|
|
|
sql = "SELECT signDate FROM userSignLog WHERE uid=? AND signDate LIKE ?"
|
|
|
|
|
|
async with cls.m_pDB.execute(sql, (uid, f"{monthStr}-%")) as cursor:
|
|
|
|
|
|
rows = await cursor.fetchall()
|
2025-06-30 03:43:44 +08:00
|
|
|
|
signedDays = {int(r[0][-2:]) for r in rows if r[0][-2:].isdigit()}
|
2025-05-26 15:10:12 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("绘制签到图时数据库查询失败", e=e)
|
|
|
|
|
|
signedDays = set()
|
|
|
|
|
|
|
|
|
|
|
|
for day in range(1, totalDays + 1):
|
|
|
|
|
|
index = day + firstWeekday - 1
|
|
|
|
|
|
row = index // cols
|
|
|
|
|
|
col = index % cols
|
|
|
|
|
|
x1 = padding + col * cellSize
|
|
|
|
|
|
y1 = padding + titleHeight + row * cellSize
|
|
|
|
|
|
x2 = x1 + cellSize - 10
|
|
|
|
|
|
y2 = y1 + cellSize - 10
|
|
|
|
|
|
color = (112, 196, 112) if day in signedDays else (220, 220, 220)
|
|
|
|
|
|
await img.rectangle((x1, y1, x2, y2), fill=color, outline="black", width=2)
|
|
|
|
|
|
await img.text((x1 + 10, y1 + 10), str(day), font_size=24)
|
|
|
|
|
|
|
|
|
|
|
|
return img
|