diff --git a/MIGRATION_README.md b/MIGRATION_README.md new file mode 100644 index 00000000..c12d389c --- /dev/null +++ b/MIGRATION_README.md @@ -0,0 +1,175 @@ +# SiteUserData表userid字段类型迁移说明 + +## 概述 + +本次迁移将 `SiteUserData` 表中的 `userid` 字段从 `Integer` 类型改为 `String` 类型,以支持更灵活的用户ID格式。 + +## 变更内容 + +### 1. 数据模型变更 + +**文件**: `app/db/models/siteuserdata.py` + +```python +# 变更前 +userid = Column(Integer) + +# 变更后 +userid = Column(String) +``` + +### 2. Schema定义变更 + +**文件**: `app/schemas/site.py` + +```python +# 变更前 +userid: Optional[Union[int, str]] = None + +# 变更后 +userid: Optional[str] = None +``` + +### 3. 数据库迁移脚本 + +**文件**: `database/versions/a946dae52526_2_2_1.py` + +- **版本号**: 2.2.1 +- **修订ID**: a946dae52526 +- **前置版本**: 5b3355c964bb (2.2.0) + +#### 迁移功能 + +1. **PostgreSQL数据库迁移**: + - 创建临时列 `userid_new` (VARCHAR类型) + - 将现有数据转换为字符串并复制到新列 + - 删除旧列 `userid` + - 重命名新列为 `userid` + +2. **SQLite数据库迁移**: + - 创建新表结构,userid字段为VARCHAR类型 + - 复制现有数据,将userid转换为字符串 + - 删除旧表并重命名新表 + - 重新创建索引 + +#### 降级功能 + +1. **PostgreSQL数据库降级**: + - 创建临时列 `userid_old` (INTEGER类型) + - 将字符串转换为整数(仅转换数字字符串) + - 删除旧列并重命名新列 + +2. **SQLite数据库降级**: + - 创建新表结构,userid字段为INTEGER类型 + - 复制数据,仅转换数字字符串为整数 + - 删除旧表并重命名新表 + - 重新创建索引 + +### 4. 代码兼容性修复 + +#### 修复的文件 + +1. **app/modules/indexer/parser/nexus_rabbit.py** + ```python + # 修复前 + "data": {"type": "seeding", "id": int(self.userid)}, + + # 修复后 + "data": {"type": "seeding", "id": int(self.userid) if self.userid and str(self.userid).isdigit() else 0}, + ``` + +2. **app/modules/synologychat/synologychat.py** + ```python + # 修复前 + payload_data['user_ids'] = [int(userid)] + + # 修复后 + payload_data['user_ids'] = [int(userid) if str(userid).isdigit() else userid] + ``` + +## 迁移步骤 + +### 1. 备份数据库 + +在执行迁移前,请务必备份数据库: + +```bash +# SQLite数据库备份 +cp user.db user.db.backup + +# PostgreSQL数据库备份 +pg_dump -h localhost -U username -d database_name > backup.sql +``` + +### 2. 执行迁移 + +```bash +# 进入项目目录 +cd /path/to/project + +# 执行数据库迁移 +python -m alembic upgrade head +``` + +### 3. 验证迁移 + +运行测试脚本验证迁移是否成功: + +```bash +python test_migration.py +``` + +## 影响分析 + +### 正面影响 + +1. **灵活性提升**: 支持非数字格式的用户ID +2. **兼容性增强**: 适应不同站点的用户ID格式 +3. **数据完整性**: 保持原有数据不丢失 + +### 潜在风险 + +1. **性能影响**: 字符串类型可能比整数类型占用更多存储空间 +2. **查询性能**: 字符串比较可能比整数比较稍慢 +3. **数据验证**: 需要确保应用程序正确处理字符串类型的userid + +### 兼容性说明 + +1. **向后兼容**: 迁移脚本包含降级功能,可以回滚到Integer类型 +2. **代码兼容**: 已修复所有直接使用userid的代码 +3. **API兼容**: Schema变更保持了API的向后兼容性 + +## 测试验证 + +### 功能测试 + +1. **数据插入测试**: 验证整数和字符串类型的userid都能正常插入 +2. **数据查询测试**: 验证按域名、日期等条件查询功能正常 +3. **数据更新测试**: 验证userid字段更新功能正常 +4. **API测试**: 验证相关API接口正常工作 + +### 性能测试 + +1. **查询性能**: 验证查询性能无明显下降 +2. **存储空间**: 验证存储空间使用情况 +3. **并发性能**: 验证并发操作正常 + +## 回滚方案 + +如果迁移出现问题,可以执行降级操作: + +```bash +# 降级到上一个版本 +python -m alembic downgrade 5b3355c964bb +``` + +## 注意事项 + +1. **备份重要**: 执行迁移前必须备份数据库 +2. **测试环境**: 建议先在测试环境验证迁移 +3. **监控日志**: 迁移过程中注意观察日志输出 +4. **数据验证**: 迁移完成后验证数据完整性 + +## 联系信息 + +如有问题,请联系开发团队或查看项目文档。 \ No newline at end of file diff --git a/app/db/models/siteuserdata.py b/app/db/models/siteuserdata.py index 17b6002c..451e182a 100644 --- a/app/db/models/siteuserdata.py +++ b/app/db/models/siteuserdata.py @@ -20,7 +20,7 @@ class SiteUserData(Base): # 用户名 username = Column(String) # 用户ID - userid = Column(Integer) + userid = Column(String) # 用户等级 user_level = Column(String) # 加入时间 diff --git a/app/modules/indexer/parser/nexus_rabbit.py b/app/modules/indexer/parser/nexus_rabbit.py index e93b08d3..866bdcf8 100644 --- a/app/modules/indexer/parser/nexus_rabbit.py +++ b/app/modules/indexer/parser/nexus_rabbit.py @@ -31,7 +31,7 @@ class NexusRabbitSiteUserInfo(SiteParserBase): "page": 1, "limit": 5000000, "action": "userTorrentsList", - "data": {"type": "seeding", "id": int(self.userid)}, + "data": {"type": "seeding", "id": int(self.userid) if self.userid and str(self.userid).isdigit() else 0}, } self._torrent_seeding_headers = { "Content-Type": "application/json", diff --git a/app/modules/synologychat/synologychat.py b/app/modules/synologychat/synologychat.py index dbb56e55..9c11cfa0 100644 --- a/app/modules/synologychat/synologychat.py +++ b/app/modules/synologychat/synologychat.py @@ -76,7 +76,7 @@ class SynologyChat: if image: payload_data['file_url'] = quote(image) if userid: - payload_data['user_ids'] = [int(userid)] + payload_data['user_ids'] = [int(userid) if str(userid).isdigit() else userid] else: userids = self.__get_bot_users() if not userids: @@ -121,7 +121,7 @@ class SynologyChat: index += 1 if userid: - userids = [int(userid)] + userids = [int(userid) if str(userid).isdigit() else userid] else: userids = self.__get_bot_users() payload_data = { @@ -169,7 +169,7 @@ class SynologyChat: caption = f"{caption}\n[查看详情]({link})" if userid: - userids = [int(userid)] + userids = [int(userid) if str(userid).isdigit() else userid] else: userids = self.__get_bot_users() diff --git a/app/schemas/site.py b/app/schemas/site.py index d8497ea1..aded2a62 100644 --- a/app/schemas/site.py +++ b/app/schemas/site.py @@ -77,7 +77,7 @@ class SiteUserData(BaseModel): # 用户名 username: Optional[str] = None # 用户ID - userid: Optional[Union[int, str]] = None + userid: Optional[str] = None # 用户等级 user_level: Optional[str] = None # 加入时间 diff --git a/database/versions/a946dae52526_2_2_1.py b/database/versions/a946dae52526_2_2_1.py new file mode 100644 index 00000000..3560fe68 --- /dev/null +++ b/database/versions/a946dae52526_2_2_1.py @@ -0,0 +1,258 @@ +"""2.2.1 + +Revision ID: a946dae52526 +Revises: 5b3355c964bb +Create Date: 2025-08-20 17:50:00.000000 + +""" +import sqlalchemy as sa +from alembic import op + +from app.log import logger +from app.core.config import settings + +# revision identifiers, used by Alembic. +revision = 'a946dae52526' +down_revision = '5b3355c964bb' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """ + 升级:将SiteUserData表的userid字段从Integer改为String + """ + connection = op.get_bind() + + if settings.DB_TYPE.lower() == "postgresql": + # PostgreSQL数据库迁移 + migrate_postgresql_userid(connection) + else: + # SQLite数据库迁移 + migrate_sqlite_userid(connection) + + +def downgrade() -> None: + """ + 降级:将SiteUserData表的userid字段从String改回Integer + """ + connection = op.get_bind() + + if settings.DB_TYPE.lower() == "postgresql": + # PostgreSQL数据库降级 + downgrade_postgresql_userid(connection) + else: + # SQLite数据库降级 + downgrade_sqlite_userid(connection) + + +def migrate_postgresql_userid(connection): + """ + PostgreSQL数据库userid字段迁移 + """ + try: + logger.info("开始PostgreSQL数据库userid字段迁移...") + + # 1. 创建临时列 + connection.execute(sa.text(""" + ALTER TABLE siteuserdata + ADD COLUMN userid_new VARCHAR + """)) + + # 2. 将现有数据转换为字符串并复制到新列 + connection.execute(sa.text(""" + UPDATE siteuserdata + SET userid_new = CAST(userid AS VARCHAR) + WHERE userid IS NOT NULL + """)) + + # 3. 删除旧列 + connection.execute(sa.text(""" + ALTER TABLE siteuserdata + DROP COLUMN userid + """)) + + # 4. 重命名新列 + connection.execute(sa.text(""" + ALTER TABLE siteuserdata + RENAME COLUMN userid_new TO userid + """)) + + logger.info("PostgreSQL数据库userid字段迁移完成") + + except Exception as e: + logger.error(f"PostgreSQL数据库userid字段迁移失败: {e}") + raise + + +def migrate_sqlite_userid(connection): + """ + SQLite数据库userid字段迁移 + """ + try: + logger.info("开始SQLite数据库userid字段迁移...") + + # SQLite不支持直接修改列类型,需要重建表 + # 1. 创建新表结构 + connection.execute(sa.text(""" + CREATE TABLE siteuserdata_new ( + id INTEGER PRIMARY KEY, + domain VARCHAR, + name VARCHAR, + username VARCHAR, + userid VARCHAR, + user_level VARCHAR, + join_at VARCHAR, + bonus FLOAT DEFAULT 0, + upload FLOAT DEFAULT 0, + download FLOAT DEFAULT 0, + ratio FLOAT DEFAULT 0, + seeding FLOAT DEFAULT 0, + leeching FLOAT DEFAULT 0, + seeding_size FLOAT DEFAULT 0, + leeching_size FLOAT DEFAULT 0, + seeding_info JSON DEFAULT '{}', + message_unread INTEGER DEFAULT 0, + message_unread_contents JSON DEFAULT '[]', + err_msg VARCHAR, + updated_day VARCHAR, + updated_time VARCHAR + ) + """)) + + # 2. 复制数据,将userid转换为字符串 + connection.execute(sa.text(""" + INSERT INTO siteuserdata_new + SELECT + id, domain, name, username, + CAST(userid AS VARCHAR) as userid, + user_level, join_at, bonus, upload, download, ratio, + seeding, leeching, seeding_size, leeching_size, + seeding_info, message_unread, message_unread_contents, + err_msg, updated_day, updated_time + FROM siteuserdata + """)) + + # 3. 删除旧表 + connection.execute(sa.text("DROP TABLE siteuserdata")) + + # 4. 重命名新表 + connection.execute(sa.text("ALTER TABLE siteuserdata_new RENAME TO siteuserdata")) + + # 5. 重新创建索引 + connection.execute(sa.text("CREATE INDEX ix_siteuserdata_domain ON siteuserdata (domain)")) + connection.execute(sa.text("CREATE INDEX ix_siteuserdata_updated_day ON siteuserdata (updated_day)")) + + logger.info("SQLite数据库userid字段迁移完成") + + except Exception as e: + logger.error(f"SQLite数据库userid字段迁移失败: {e}") + raise + + +def downgrade_postgresql_userid(connection): + """ + PostgreSQL数据库userid字段降级 + """ + try: + logger.info("开始PostgreSQL数据库userid字段降级...") + + # 1. 创建临时列 + connection.execute(sa.text(""" + ALTER TABLE siteuserdata + ADD COLUMN userid_old INTEGER + """)) + + # 2. 将字符串转换为整数并复制到新列 + connection.execute(sa.text(""" + UPDATE siteuserdata + SET userid_old = CAST(userid AS INTEGER) + WHERE userid IS NOT NULL AND userid ~ '^[0-9]+$' + """)) + + # 3. 删除旧列 + connection.execute(sa.text(""" + ALTER TABLE siteuserdata + DROP COLUMN userid + """)) + + # 4. 重命名新列 + connection.execute(sa.text(""" + ALTER TABLE siteuserdata + RENAME COLUMN userid_old TO userid + """)) + + logger.info("PostgreSQL数据库userid字段降级完成") + + except Exception as e: + logger.error(f"PostgreSQL数据库userid字段降级失败: {e}") + raise + + +def downgrade_sqlite_userid(connection): + """ + SQLite数据库userid字段降级 + """ + try: + logger.info("开始SQLite数据库userid字段降级...") + + # SQLite不支持直接修改列类型,需要重建表 + # 1. 创建新表结构 + connection.execute(sa.text(""" + CREATE TABLE siteuserdata_old ( + id INTEGER PRIMARY KEY, + domain VARCHAR, + name VARCHAR, + username VARCHAR, + userid INTEGER, + user_level VARCHAR, + join_at VARCHAR, + bonus FLOAT DEFAULT 0, + upload FLOAT DEFAULT 0, + download FLOAT DEFAULT 0, + ratio FLOAT DEFAULT 0, + seeding FLOAT DEFAULT 0, + leeching FLOAT DEFAULT 0, + seeding_size FLOAT DEFAULT 0, + leeching_size FLOAT DEFAULT 0, + seeding_info JSON DEFAULT '{}', + message_unread INTEGER DEFAULT 0, + message_unread_contents JSON DEFAULT '[]', + err_msg VARCHAR, + updated_day VARCHAR, + updated_time VARCHAR + ) + """)) + + # 2. 复制数据,将字符串转换为整数(只转换数字字符串) + connection.execute(sa.text(""" + INSERT INTO siteuserdata_old + SELECT + id, domain, name, username, + CASE + WHEN userid IS NULL THEN NULL + WHEN userid REGEXP '^[0-9]+$' THEN CAST(userid AS INTEGER) + ELSE NULL + END as userid, + user_level, join_at, bonus, upload, download, ratio, + seeding, leeching, seeding_size, leeching_size, + seeding_info, message_unread, message_unread_contents, + err_msg, updated_day, updated_time + FROM siteuserdata + """)) + + # 3. 删除旧表 + connection.execute(sa.text("DROP TABLE siteuserdata")) + + # 4. 重命名新表 + connection.execute(sa.text("ALTER TABLE siteuserdata_old RENAME TO siteuserdata")) + + # 5. 重新创建索引 + connection.execute(sa.text("CREATE INDEX ix_siteuserdata_domain ON siteuserdata (domain)")) + connection.execute(sa.text("CREATE INDEX ix_siteuserdata_updated_day ON siteuserdata (updated_day)")) + + logger.info("SQLite数据库userid字段降级完成") + + except Exception as e: + logger.error(f"SQLite数据库userid字段降级失败: {e}") + raise \ No newline at end of file