feat: add scoped cookie operations for support
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
店铺ID:A20260813141939
|
||||||
|
默认账号
|
||||||
|
B178660196602145988
|
||||||
|
默认密码
|
||||||
|
afac3645
|
||||||
|
AppKey: 2AJjbWmZ28cu723Q
|
||||||
|
AppSecret: 5SDFbYEMgDITOKYzDUhBOQAPyTYBQ4UK
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# 接口规范
|
||||||
|
|
||||||
|
# 一、请求格式
|
||||||
|
|
||||||
|
# 二、AppId、AppKey 和 AppSercet
|
||||||
|
|
||||||
|
- 由双方协定,或由我平台提供
|
||||||
|
|
||||||
|
- AppId:商户号
|
||||||
|
|
||||||
|
- AppKey:长度为16位
|
||||||
|
|
||||||
|
- AppSecret:长度为32位
|
||||||
|
|
||||||
|
- 用于
|
||||||
|
|
||||||
|
- 卡券、卡密类订单的数据的加密
|
||||||
|
|
||||||
|
- 签名时的补充数据
|
||||||
|
|
||||||
|
- *AppSercet需考虑定期更换*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 三、请求接口公共参数
|
||||||
|
|
||||||
|
***可选参数在对接时,请联系对接方,非特殊说明情况下,默认不传递***
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 四、请求签名规则
|
||||||
|
|
||||||
|
**注意:签名前,需双方协定或由厂商提供AppSecret**
|
||||||
|
|
||||||
|
sign签名时
|
||||||
|
|
||||||
|
1. 去掉空格:剔除参数名和参数值的前后空格,包括\\n和\\r等空值
|
||||||
|
|
||||||
|
2. 剔除空值:如果参数没有值,不参与签名
|
||||||
|
|
||||||
|
3. **sign不参与签名,其他字段均参与签名**
|
||||||
|
|
||||||
|
4. 排序生成待签名串:
|
||||||
|
|
||||||
|
1. 所有参数名**按照ascii顺序**进行排序
|
||||||
|
|
||||||
|
2. 并且参数名与参数值**通过a=1\&b=2的方式**将所有参数进行组合
|
||||||
|
|
||||||
|
3. 生成新的字符串后在字符串后**加上请求的方式**
|
||||||
|
|
||||||
|
4. 再**加上AppSecret**后获得待签名字符串
|
||||||
|
|
||||||
|
5. 进行加密
|
||||||
|
|
||||||
|
1. 默认进行MD5加密
|
||||||
|
|
||||||
|
2. 将第三步的待签名字符串进行utf8编码
|
||||||
|
|
||||||
|
3. 再进行md5操作后得到字符串
|
||||||
|
|
||||||
|
4. 最后转小写后为签名字符串
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
// 假定 AppSercet 为 ac9fa79db418fd2c61d9bd4c60956f2d5db3a6d781d6f91f2e96327c74dfaccd
|
||||||
|
// 假定是下订单的接口 POST
|
||||||
|
// 待签名参数
|
||||||
|
{
|
||||||
|
// 公共参数
|
||||||
|
app_id: '15945681 ',
|
||||||
|
timestamp: 1740916504,
|
||||||
|
|
||||||
|
// 业务参数
|
||||||
|
charge_account: '18888888888',
|
||||||
|
buy_num: 2,
|
||||||
|
customer_price: 1.21,
|
||||||
|
customer_order_no: 'DD202507010010208888',
|
||||||
|
product_id: '66668888'
|
||||||
|
}
|
||||||
|
// 上述参数的签名字符串(去掉空值、去掉空格;最后加上请求方法和AppSecret)
|
||||||
|
app_id=15945681&buy_num=2&charge_account=18888888888&
|
||||||
|
customer_order_no=DD202507010010208888&customer_price=1.21&
|
||||||
|
product_id=66668888×tamp=1740916504
|
||||||
|
POSTac9fa79db418fd2c61d9bd4c60956f2d5db3a6d781d6f91f2e96327c74dfaccd
|
||||||
|
|
||||||
|
// MD5后转小写,签名字符串为:
|
||||||
|
4087a959f3488ecb13efe6ef58e3bc67
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 五、响应公共信息
|
||||||
|
|
||||||
|
**响应签名规则请参考“请求签名规则”**
|
||||||
|
|
||||||
|
**sign不参与签名,其他字段均参与签名**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# 创建订单【必要】
|
||||||
|
|
||||||
|
# 接口说明
|
||||||
|
|
||||||
|
- 下单接口
|
||||||
|
|
||||||
|
- POST 请求
|
||||||
|
|
||||||
|
- application/json
|
||||||
|
|
||||||
|
- 接口地址 ***adapter\-apiaccess/open/api/createOrderV2***
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 请求参数
|
||||||
|
|
||||||
|
**notify\_url**:
|
||||||
|
|
||||||
|
- 一些直充类的订单时间比较久,但不能持续占用连接
|
||||||
|
|
||||||
|
- 不传就不回调,订单的状态在查询时处理
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**recharge\_arg**:(此处数据为用户填写和对应模板)
|
||||||
|
|
||||||
|
```Plain Text
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"templateName": "模板名,一般是中文,如手机号,只是标识用,调用方可自行决定",
|
||||||
|
"templateVal": "用户填写的充值账号,比如:19876543210",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
这里是用户填写的信息,比如:充值手机号:19876543210
|
||||||
|
|
||||||
|
templateName:充值手机号
|
||||||
|
|
||||||
|
templateVal:19876543210
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**ext\_arg**:(此处数据均为双方约定)
|
||||||
|
|
||||||
|
```Plain Text
|
||||||
|
{
|
||||||
|
"skuid": 243241231238888888,
|
||||||
|
"skuname: "电商平台的商品名称"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 响应信息
|
||||||
|
|
||||||
|
## code数据对业务流程的影响
|
||||||
|
|
||||||
|
- 如果返回 200 的状态码,则下单成功,后续通过【查询订单】或者【订单异步通知】来获取订单结果状态
|
||||||
|
|
||||||
|
- 如果返回非 200 的状态码,订单失败,流程结束
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## order\_status数据对业务流程的影响
|
||||||
|
|
||||||
|
- 0,待处理;1,处理中;2,成功;3,失败; 4,异常
|
||||||
|
|
||||||
|
- 当为 0 和 1 时,订单没有结束,
|
||||||
|
|
||||||
|
- 需要调用方自轮询查询订单;
|
||||||
|
|
||||||
|
- 或本平台订单结束后,异步通知订单结果
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# 查询订单【必要】
|
||||||
|
|
||||||
|
# 接口说明
|
||||||
|
|
||||||
|
- 订单查询接口
|
||||||
|
|
||||||
|
- GET 请求
|
||||||
|
|
||||||
|
- application/x\-www\-form\-urlencoded
|
||||||
|
|
||||||
|
- 接口地址 ***adapter\-apiaccess/open/api/queryOrderV2***
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 请求参数
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 响应信息
|
||||||
|
|
||||||
|
## code数据对业务流程的影响
|
||||||
|
|
||||||
|
- 查询订单时如果返回非 200 的状态码,订单失败,流程结束
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## order\_status数据对业务流程的影响
|
||||||
|
|
||||||
|
- 当为 0 和 1 时,订单没有结束,
|
||||||
|
|
||||||
|
- 需要调用方自轮询查询订单;
|
||||||
|
|
||||||
|
- 或本平台订单结束后,异步通知订单结果
|
||||||
|
|
||||||
|
- 当为 2 和 3 、4 时,订单结束
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## cards解密后的结构如下
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
[
|
||||||
|
{
|
||||||
|
card_no: 'CN88889999',(无卡号就填""空串)
|
||||||
|
card_pwd: 'ABCD-DEFG-GHIJ-JKLM',
|
||||||
|
expired_time: '2026-09-27 08:36:58',(过期时间,可不传)
|
||||||
|
price: '8.8',(卡密单价,可不传)
|
||||||
|
card_type: '0'(不传时,默认为0。0.普通卡密 1.短链)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# 订单异步通知【推荐】
|
||||||
|
|
||||||
|
# 接口说明
|
||||||
|
|
||||||
|
- 下单后,如订单流程、时间较长,需要第一时间返回下单成功状态,之后的流程中再去通知外部平台订单的状态
|
||||||
|
|
||||||
|
- POST 请求
|
||||||
|
|
||||||
|
- 接口地址一般为 ***http://IP******\[:PORT\]/网关/业务/callBackV2***
|
||||||
|
|
||||||
|
- 具体地址,请双方开发协定,或联系运营获取
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 请求参数
|
||||||
|
|
||||||
|
## cards解密后的结构如下:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
[
|
||||||
|
{
|
||||||
|
card_no: 'CN88889999',(无卡号就填""空串)
|
||||||
|
card_pwd: 'ABCD-DEFG-GHIJ-JKLM',
|
||||||
|
expired_time: '2026-09-27 08:36:58',(过期时间,可不传)
|
||||||
|
price: '8.8',(卡密单价,可不传)
|
||||||
|
card_type: '0'(不传时,默认为0。0.普通卡密 1.短链)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
# 响应信息
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 账户信息查询
|
||||||
|
|
||||||
|
# 接口说明
|
||||||
|
|
||||||
|
- 下单接口
|
||||||
|
|
||||||
|
- GET 请求
|
||||||
|
|
||||||
|
- 接口地址 ***adapter\-apiaccess/open/api/userInfoV2***
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 请求参数
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 响应信息
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 账户信息查询
|
||||||
|
|
||||||
|
# 接口说明
|
||||||
|
|
||||||
|
- 下单接口
|
||||||
|
|
||||||
|
- GET 请求
|
||||||
|
|
||||||
|
- 接口地址 ***adapter\-apiaccess/open/api/userInfoV2***
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 请求参数
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 响应信息
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
os.environ.setdefault("DATABASE_URL", "sqlite://")
|
||||||
|
os.environ.setdefault("APP_ENCRYPTION_KEY", "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=")
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from web.backend.database import Base
|
||||||
|
from web.backend.models import Account, LoginTask, User
|
||||||
|
from web.backend.routers.cookies import (
|
||||||
|
check_cookie_operations,
|
||||||
|
get_cookie,
|
||||||
|
list_cookie_operations,
|
||||||
|
list_cookies,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CookieOperationTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.engine = create_engine("sqlite://")
|
||||||
|
Base.metadata.create_all(self.engine)
|
||||||
|
self.session = sessionmaker(bind=self.engine)()
|
||||||
|
|
||||||
|
self.support = User(username="support", password_hash="hash", role="support")
|
||||||
|
self.other_support = User(username="other", password_hash="hash", role="support")
|
||||||
|
self.session.add_all([self.support, self.other_support])
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
owned = Account(
|
||||||
|
username="owned-account",
|
||||||
|
password="owned-password",
|
||||||
|
email="owned@example.com",
|
||||||
|
email_password="mail-password",
|
||||||
|
assigned_to=self.support.id,
|
||||||
|
)
|
||||||
|
other = Account(
|
||||||
|
username="other-account",
|
||||||
|
password="other-password",
|
||||||
|
email="other@example.com",
|
||||||
|
email_password="mail-password",
|
||||||
|
assigned_to=self.other_support.id,
|
||||||
|
)
|
||||||
|
self.session.add_all([owned, other])
|
||||||
|
self.session.commit()
|
||||||
|
self.session.add_all([
|
||||||
|
LoginTask(batch_id="owned", account_id=owned.id, created_by=self.support.id, status="success", cookie="owned-secret"),
|
||||||
|
LoginTask(batch_id="other", account_id=other.id, created_by=self.other_support.id, status="success", cookie="other-secret"),
|
||||||
|
])
|
||||||
|
self.session.commit()
|
||||||
|
self.owned_task = self.session.query(LoginTask).filter(LoginTask.batch_id == "owned").one()
|
||||||
|
self.other_task = self.session.query(LoginTask).filter(LoginTask.batch_id == "other").one()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.session.close()
|
||||||
|
Base.metadata.drop_all(self.engine)
|
||||||
|
self.engine.dispose()
|
||||||
|
|
||||||
|
def test_support_operation_list_is_scoped_and_never_returns_credentials(self):
|
||||||
|
result = list_cookie_operations(
|
||||||
|
search="",
|
||||||
|
page=1,
|
||||||
|
page_size=20,
|
||||||
|
db=self.session,
|
||||||
|
current=self.support,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result["total"], 1)
|
||||||
|
self.assertEqual(result["items"], [{
|
||||||
|
"id": self.owned_task.id,
|
||||||
|
"account_username": "owned-account",
|
||||||
|
"ck_check_status": "",
|
||||||
|
"ck_checked_at": None,
|
||||||
|
"created_at": None,
|
||||||
|
}])
|
||||||
|
self.assertNotIn("owned-secret", str(result))
|
||||||
|
self.assertNotIn("owned-password", str(result))
|
||||||
|
|
||||||
|
def test_support_cannot_use_cookie_management_endpoints(self):
|
||||||
|
with self.assertRaisesRegex(Exception, "cookie:view"):
|
||||||
|
list_cookies(db=self.session, current=self.support)
|
||||||
|
with self.assertRaisesRegex(Exception, "cookie:view"):
|
||||||
|
get_cookie(self.owned_task.id, db=self.session, current=self.support)
|
||||||
|
|
||||||
|
@patch("web.backend.routers.cookies._check_one_cookie")
|
||||||
|
def test_support_can_check_only_assigned_cookie(self, check_one_cookie):
|
||||||
|
check_one_cookie.return_value = {
|
||||||
|
"id": self.owned_task.id,
|
||||||
|
"valid": True,
|
||||||
|
"message": "有效",
|
||||||
|
"fish_ball": 9,
|
||||||
|
"nickname": "tester",
|
||||||
|
"level": 1,
|
||||||
|
"checked_at": "2026-01-01T00:00:00+00:00",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = check_cookie_operations(
|
||||||
|
ids=f"{self.owned_task.id},{self.other_task.id}",
|
||||||
|
db=self.session,
|
||||||
|
current=self.support,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual([item["id"] for item in result["results"]], [self.owned_task.id])
|
||||||
|
self.session.refresh(self.owned_task)
|
||||||
|
self.session.refresh(self.other_task)
|
||||||
|
self.assertEqual(self.owned_task.ck_check_status, "valid")
|
||||||
|
self.assertEqual(self.other_task.ck_check_status, "")
|
||||||
|
|
||||||
|
def test_operation_permission_does_not_allow_unrelated_users(self):
|
||||||
|
no_permission_user = SimpleNamespace(
|
||||||
|
id=999,
|
||||||
|
username="no-permission",
|
||||||
|
role="support",
|
||||||
|
custom_permissions=[],
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(Exception, "cookie:operate"):
|
||||||
|
list_cookie_operations(
|
||||||
|
search="",
|
||||||
|
page=1,
|
||||||
|
page_size=20,
|
||||||
|
db=self.session,
|
||||||
|
current=no_permission_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -23,6 +23,7 @@ PERMISSIONS = {
|
|||||||
"login:view_assigned": "查看自己账号的登录任务",
|
"login:view_assigned": "查看自己账号的登录任务",
|
||||||
# Cookie
|
# Cookie
|
||||||
"cookie:view": "查看 Cookie",
|
"cookie:view": "查看 Cookie",
|
||||||
|
"cookie:operate": "检测/重登已分配账号 Cookie(不含查看)",
|
||||||
"cookie:export": "导出 Cookie",
|
"cookie:export": "导出 Cookie",
|
||||||
# 斗鱼活动
|
# 斗鱼活动
|
||||||
"douyu:task": "斗鱼任务管理",
|
"douyu:task": "斗鱼任务管理",
|
||||||
@@ -88,6 +89,7 @@ ROLE_PERMISSIONS = {
|
|||||||
],
|
],
|
||||||
"support": [
|
"support": [
|
||||||
"account:view_assigned",
|
"account:view_assigned",
|
||||||
|
"cookie:operate",
|
||||||
"douyu:task",
|
"douyu:task",
|
||||||
"yyb:session",
|
"yyb:session",
|
||||||
"yyb:history",
|
"yyb:history",
|
||||||
|
|||||||
+124
-13
@@ -12,7 +12,7 @@ import io
|
|||||||
import csv
|
import csv
|
||||||
|
|
||||||
from ..database import get_db, SessionLocal
|
from ..database import get_db, SessionLocal
|
||||||
from ..models import User, LoginTask, Account, ProxyConfig as ProxyConfigModel
|
from ..models import User, LoginTask, Account, AuditLog, ProxyConfig as ProxyConfigModel
|
||||||
from ..deps import get_current_user, require_permission
|
from ..deps import get_current_user, require_permission
|
||||||
from ..permissions import user_has_permission, get_user_permissions
|
from ..permissions import user_has_permission, get_user_permissions
|
||||||
from ..schemas import CookieReloginRequest
|
from ..schemas import CookieReloginRequest
|
||||||
@@ -30,6 +30,15 @@ def _fmt_dt(dt) -> str | None:
|
|||||||
router = APIRouter(prefix="/api/cookies", tags=["Cookie管理"])
|
router = APIRouter(prefix="/api/cookies", tags=["Cookie管理"])
|
||||||
|
|
||||||
|
|
||||||
|
def _require_cookie_operation_perm(current: User) -> None:
|
||||||
|
"""允许脱敏的 CK 检测与重登,不授予 CK 查看或导出能力。"""
|
||||||
|
if not (
|
||||||
|
user_has_permission(current, "cookie:operate")
|
||||||
|
or user_has_permission(current, "cookie:view")
|
||||||
|
):
|
||||||
|
raise HTTPException(status_code=403, detail="无权限: cookie:operate")
|
||||||
|
|
||||||
|
|
||||||
def _parse_account_names(raw_names: str) -> list[str]:
|
def _parse_account_names(raw_names: str) -> list[str]:
|
||||||
"""解析前端粘贴的 Excel 账号名,每行一个并去重。"""
|
"""解析前端粘贴的 Excel 账号名,每行一个并去重。"""
|
||||||
names = []
|
names = []
|
||||||
@@ -162,6 +171,8 @@ def list_cookies(
|
|||||||
current: User = Depends(get_current_user),
|
current: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""查看登录成功的 Cookie 列表。"""
|
"""查看登录成功的 Cookie 列表。"""
|
||||||
|
if not user_has_permission(current, "cookie:view"):
|
||||||
|
raise HTTPException(status_code=403, detail="无权限: cookie:view")
|
||||||
if not include_cookie:
|
if not include_cookie:
|
||||||
query = _visible_cookie_tasks_query(db, current).options(defer(LoginTask.cookie))
|
query = _visible_cookie_tasks_query(db, current).options(defer(LoginTask.cookie))
|
||||||
else:
|
else:
|
||||||
@@ -248,6 +259,8 @@ def cookies_summary(
|
|||||||
current: User = Depends(get_current_user),
|
current: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""Cookie 管理统计,避免前端为了卡片统计拉全量 Cookie。"""
|
"""Cookie 管理统计,避免前端为了卡片统计拉全量 Cookie。"""
|
||||||
|
if not user_has_permission(current, "cookie:view"):
|
||||||
|
raise HTTPException(status_code=403, detail="无权限: cookie:view")
|
||||||
query = _visible_cookie_tasks_query(db, current)
|
query = _visible_cookie_tasks_query(db, current)
|
||||||
if user_has_permission(current, "login:view_all"):
|
if user_has_permission(current, "login:view_all"):
|
||||||
query = query.join(Account, LoginTask.account_id == Account.id)
|
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||||
@@ -260,6 +273,47 @@ def cookies_summary(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/operations")
|
||||||
|
def list_cookie_operations(
|
||||||
|
search: str = Query(""),
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(20, ge=1, le=200),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""脱敏的 CK 操作列表,只用于检测与重登。"""
|
||||||
|
_require_cookie_operation_perm(current)
|
||||||
|
query = _visible_cookie_tasks_query(db, current)
|
||||||
|
if user_has_permission(current, "login:view_all"):
|
||||||
|
query = query.join(Account, LoginTask.account_id == Account.id)
|
||||||
|
search_text = (search or "").strip()
|
||||||
|
if search_text:
|
||||||
|
query = query.filter(Account.username.ilike(f"%{search_text}%"))
|
||||||
|
|
||||||
|
total = query.order_by(None).count()
|
||||||
|
tasks = (
|
||||||
|
query.order_by(LoginTask.finished_at.desc(), LoginTask.id.desc())
|
||||||
|
.offset((page - 1) * page_size)
|
||||||
|
.limit(page_size)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": task.id,
|
||||||
|
"account_username": task.account.username if task.account else "",
|
||||||
|
"ck_check_status": task.ck_check_status or "",
|
||||||
|
"ck_checked_at": _fmt_dt(task.ck_checked_at),
|
||||||
|
"created_at": _fmt_dt(task.finished_at),
|
||||||
|
}
|
||||||
|
for task in tasks
|
||||||
|
],
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/duplicates")
|
@router.get("/duplicates")
|
||||||
def find_duplicate_cookies(
|
def find_duplicate_cookies(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
@@ -390,13 +444,8 @@ def export_cookies(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/check")
|
def _check_cookies(ids: str, db: Session, current: User, *, detailed: bool) -> dict:
|
||||||
def check_cookies(
|
"""执行 CK 检测;客服操作页仅返回状态,不返回账号衍生信息。"""
|
||||||
ids: str = "",
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current: User = Depends(get_current_user),
|
|
||||||
):
|
|
||||||
"""批量检测斗鱼 Cookie 有效性(鱼丸接口),返回每条有效性与鱼丸数/昵称。"""
|
|
||||||
if not ids:
|
if not ids:
|
||||||
raise HTTPException(status_code=400, detail="请指定记录ID")
|
raise HTTPException(status_code=400, detail="请指定记录ID")
|
||||||
id_list = [int(x) for x in ids.split(",") if x.strip().isdigit()]
|
id_list = [int(x) for x in ids.split(",") if x.strip().isdigit()]
|
||||||
@@ -439,16 +488,48 @@ def check_cookies(
|
|||||||
"message": item.get("message", ""),
|
"message": item.get("message", ""),
|
||||||
}
|
}
|
||||||
task.ck_checked_at = datetime.now(timezone.utc)
|
task.ck_checked_at = datetime.now(timezone.utc)
|
||||||
|
db.add(AuditLog(
|
||||||
|
user_id=current.id,
|
||||||
|
username=current.username,
|
||||||
|
action="cookie:check",
|
||||||
|
target=f"检测 {len(results)} 条已分配账号 CK",
|
||||||
|
))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
if not detailed:
|
||||||
|
results = [
|
||||||
|
{"id": item["id"], "valid": item["valid"], "checked_at": item["checked_at"]}
|
||||||
|
for item in results
|
||||||
|
]
|
||||||
return {"results": results, "success": True}
|
return {"results": results, "success": True}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/relogin")
|
@router.post("/check")
|
||||||
def relogin_cookies(
|
def check_cookies(
|
||||||
req: CookieReloginRequest,
|
ids: str = "",
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current: User = Depends(require_permission("login:batch")),
|
current: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""批量检测斗鱼 Cookie 有效性,供 Cookie 管理页查看详细结果。"""
|
||||||
|
if not user_has_permission(current, "cookie:view"):
|
||||||
|
raise HTTPException(status_code=403, detail="无权限: cookie:view")
|
||||||
|
return _check_cookies(ids, db, current, detailed=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/operations/check")
|
||||||
|
def check_cookie_operations(
|
||||||
|
ids: str = "",
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""脱敏 CK 检测,只返回有效性与检测时间。"""
|
||||||
|
_require_cookie_operation_perm(current)
|
||||||
|
return _check_cookies(ids, db, current, detailed=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _start_relogin(
|
||||||
|
req: CookieReloginRequest,
|
||||||
|
db: Session,
|
||||||
|
current: User,
|
||||||
):
|
):
|
||||||
"""重新登录失效的 Cookie:用账号信息重新登录,成功后替换旧 Cookie。
|
"""重新登录失效的 Cookie:用账号信息重新登录,成功后替换旧 Cookie。
|
||||||
|
|
||||||
@@ -498,6 +579,13 @@ def relogin_cookies(
|
|||||||
thread = threading.Thread(target=runner.run, daemon=True)
|
thread = threading.Thread(target=runner.run, daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
db.add(AuditLog(
|
||||||
|
user_id=current.id,
|
||||||
|
username=current.username,
|
||||||
|
action="cookie:relogin",
|
||||||
|
target=f"重登 {len(task_ids)} 条已分配账号 CK",
|
||||||
|
))
|
||||||
|
db.commit()
|
||||||
return {
|
return {
|
||||||
"batch_id": batch_id,
|
"batch_id": batch_id,
|
||||||
"count": len(task_ids),
|
"count": len(task_ids),
|
||||||
@@ -506,6 +594,27 @@ def relogin_cookies(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/relogin")
|
||||||
|
def relogin_cookies(
|
||||||
|
req: CookieReloginRequest,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current: User = Depends(require_permission("login:batch")),
|
||||||
|
):
|
||||||
|
"""Cookie 管理页的重登入口,保留既有 login:batch 权限。"""
|
||||||
|
return _start_relogin(req, db, current)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/operations/relogin")
|
||||||
|
def relogin_cookie_operations(
|
||||||
|
req: CookieReloginRequest,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current: User = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""脱敏 CK 操作页的重登入口。"""
|
||||||
|
_require_cookie_operation_perm(current)
|
||||||
|
return _start_relogin(req, db, current)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{task_id}")
|
@router.get("/{task_id}")
|
||||||
def get_cookie(
|
def get_cookie(
|
||||||
task_id: int,
|
task_id: int,
|
||||||
@@ -513,6 +622,8 @@ def get_cookie(
|
|||||||
current: User = Depends(get_current_user),
|
current: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""获取单条 Cookie 详情,供复制操作按需读取完整敏感字段。"""
|
"""获取单条 Cookie 详情,供复制操作按需读取完整敏感字段。"""
|
||||||
|
if not user_has_permission(current, "cookie:view"):
|
||||||
|
raise HTTPException(status_code=403, detail="无权限: cookie:view")
|
||||||
task = _visible_cookie_tasks_query(db, current).filter(LoginTask.id == task_id).first()
|
task = _visible_cookie_tasks_query(db, current).filter(LoginTask.id == task_id).first()
|
||||||
if not task:
|
if not task:
|
||||||
raise HTTPException(status_code=404, detail="记录不存在")
|
raise HTTPException(status_code=404, detail="记录不存在")
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const LoginTasksPage = lazy(() => import('./pages/LoginTasksPage'));
|
|||||||
const ProxyPage = lazy(() => import('./pages/ProxyPage'));
|
const ProxyPage = lazy(() => import('./pages/ProxyPage'));
|
||||||
const UsersPage = lazy(() => import('./pages/UsersPage'));
|
const UsersPage = lazy(() => import('./pages/UsersPage'));
|
||||||
const CookiePage = lazy(() => import('./pages/CookiePage'));
|
const CookiePage = lazy(() => import('./pages/CookiePage'));
|
||||||
|
const CookieOperationsPage = lazy(() => import('./pages/CookieOperationsPage'));
|
||||||
const DouyuTasksPage = lazy(() => import('./pages/DouyuTasksPage'));
|
const DouyuTasksPage = lazy(() => import('./pages/DouyuTasksPage'));
|
||||||
const HuyaAccountsPage = lazy(() => import('./pages/HuyaAccountsPage'));
|
const HuyaAccountsPage = lazy(() => import('./pages/HuyaAccountsPage'));
|
||||||
const HuyaAssignmentsPage = lazy(() => import('./pages/HuyaAssignmentsPage'));
|
const HuyaAssignmentsPage = lazy(() => import('./pages/HuyaAssignmentsPage'));
|
||||||
@@ -79,6 +80,7 @@ function AppContent() {
|
|||||||
<Route path="assignments" element={lazyRoute(<AssignmentsPage />)} />
|
<Route path="assignments" element={lazyRoute(<AssignmentsPage />)} />
|
||||||
<Route path="login-tasks" element={lazyRoute(<LoginTasksPage />)} />
|
<Route path="login-tasks" element={lazyRoute(<LoginTasksPage />)} />
|
||||||
<Route path="cookies" element={lazyRoute(<CookiePage />)} />
|
<Route path="cookies" element={lazyRoute(<CookiePage />)} />
|
||||||
|
<Route path="cookie-operations" element={lazyRoute(<CookieOperationsPage />)} />
|
||||||
<Route path="douyu/tasks" element={<Navigate to="/douyu/elite" replace />} />
|
<Route path="douyu/tasks" element={<Navigate to="/douyu/elite" replace />} />
|
||||||
<Route path="douyu/elite" element={lazyRoute(<DouyuTasksPage key="elite" handbook="elite" />)} />
|
<Route path="douyu/elite" element={lazyRoute(<DouyuTasksPage key="elite" handbook="elite" />)} />
|
||||||
<Route path="douyu/esports" element={lazyRoute(<DouyuTasksPage key="esports" handbook="esports" />)} />
|
<Route path="douyu/esports" element={lazyRoute(<DouyuTasksPage key="esports" handbook="esports" />)} />
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import api from './client';
|
import api from './client';
|
||||||
import type { BasicSummary, CookieCheckResult, CookieDuplicateResponse, CookieItem, LoginTaskItem, PageParams, PaginatedResponse, MessageDeletedResponse, MessageResponse } from './types';
|
import type { BasicSummary, CookieCheckResult, CookieDuplicateResponse, CookieItem, CookieOperationCheckResult, CookieOperationItem, LoginTaskItem, PageParams, PaginatedResponse, MessageDeletedResponse, MessageResponse } from './types';
|
||||||
|
|
||||||
export const cookieApi = {
|
export const cookieApi = {
|
||||||
list: () => api.get<CookieItem[], CookieItem[]>('/cookies'),
|
list: () => api.get<CookieItem[], CookieItem[]>('/cookies'),
|
||||||
listPaged: (params: PageParams & { include_cookie?: boolean; account_names?: string }) =>
|
listPaged: (params: PageParams & { include_cookie?: boolean; account_names?: string }) =>
|
||||||
api.get<PaginatedResponse<CookieItem>, PaginatedResponse<CookieItem>>('/cookies', { params }),
|
api.get<PaginatedResponse<CookieItem>, PaginatedResponse<CookieItem>>('/cookies', { params }),
|
||||||
summary: () => api.get<BasicSummary, BasicSummary>('/cookies/summary'),
|
summary: () => api.get<BasicSummary, BasicSummary>('/cookies/summary'),
|
||||||
|
listOperations: (params: PageParams) =>
|
||||||
|
api.get<PaginatedResponse<CookieOperationItem>, PaginatedResponse<CookieOperationItem>>('/cookies/operations', { params }),
|
||||||
duplicates: () => api.get<CookieDuplicateResponse, CookieDuplicateResponse>('/cookies/duplicates'),
|
duplicates: () => api.get<CookieDuplicateResponse, CookieDuplicateResponse>('/cookies/duplicates'),
|
||||||
get: (id: number) => api.get<CookieItem, CookieItem>(`/cookies/${id}`),
|
get: (id: number) => api.get<CookieItem, CookieItem>(`/cookies/${id}`),
|
||||||
exportCsv: (format?: string, accountNames?: string) => api.get<Blob, Blob>('/cookies/export', {
|
exportCsv: (format?: string, accountNames?: string) => api.get<Blob, Blob>('/cookies/export', {
|
||||||
@@ -17,8 +19,12 @@ export const cookieApi = {
|
|||||||
}),
|
}),
|
||||||
check: (ids: number[]) =>
|
check: (ids: number[]) =>
|
||||||
api.post<{ results: CookieCheckResult[] }, { results: CookieCheckResult[] }>('/cookies/check', null, { params: { ids: ids.join(',') } }),
|
api.post<{ results: CookieCheckResult[] }, { results: CookieCheckResult[] }>('/cookies/check', null, { params: { ids: ids.join(',') } }),
|
||||||
|
checkOperations: (ids: number[]) =>
|
||||||
|
api.post<{ results: CookieOperationCheckResult[] }, { results: CookieOperationCheckResult[] }>('/cookies/operations/check', null, { params: { ids: ids.join(',') } }),
|
||||||
relogin: (ids: number[]) =>
|
relogin: (ids: number[]) =>
|
||||||
api.post<{ batch_id: string; count: number; skipped: number; success: boolean }, { batch_id: string; count: number; skipped: number; success: boolean }>('/cookies/relogin', { ids }),
|
api.post<{ batch_id: string; count: number; skipped: number; success: boolean }, { batch_id: string; count: number; skipped: number; success: boolean }>('/cookies/relogin', { ids }),
|
||||||
|
reloginOperations: (ids: number[]) =>
|
||||||
|
api.post<{ batch_id: string; count: number; skipped: number; success: boolean }, { batch_id: string; count: number; skipped: number; success: boolean }>('/cookies/operations/relogin', { ids }),
|
||||||
loginTasks: (batchId: string) =>
|
loginTasks: (batchId: string) =>
|
||||||
api.get<LoginTaskItem[], LoginTaskItem[]>('/login/tasks', { params: { batch_id: batchId } }),
|
api.get<LoginTaskItem[], LoginTaskItem[]>('/login/tasks', { params: { batch_id: batchId } }),
|
||||||
delete: (id: number) => api.delete<MessageResponse, MessageResponse>(`/cookies/${id}`),
|
delete: (id: number) => api.delete<MessageResponse, MessageResponse>(`/cookies/${id}`),
|
||||||
|
|||||||
@@ -253,6 +253,20 @@ export interface CookieItem {
|
|||||||
ck_checked_at: string | null;
|
ck_checked_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CookieOperationItem {
|
||||||
|
id: number;
|
||||||
|
account_username: string;
|
||||||
|
ck_check_status: string;
|
||||||
|
ck_checked_at: string | null;
|
||||||
|
created_at: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CookieOperationCheckResult {
|
||||||
|
id: number;
|
||||||
|
valid: boolean;
|
||||||
|
checked_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CookieCheckResult {
|
export interface CookieCheckResult {
|
||||||
id: number;
|
id: number;
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
|||||||
if (can('cookie:view')) {
|
if (can('cookie:view')) {
|
||||||
douyuItems.push({ key: '/cookies', label: 'Cookie 管理', icon: <KeyOutlined /> });
|
douyuItems.push({ key: '/cookies', label: 'Cookie 管理', icon: <KeyOutlined /> });
|
||||||
}
|
}
|
||||||
|
if (can('cookie:operate')) {
|
||||||
|
douyuItems.push({ key: '/cookie-operations', label: 'CK 检测与重登', icon: <SafetyCertificateOutlined /> });
|
||||||
|
}
|
||||||
if (can('douyu:task')) {
|
if (can('douyu:task')) {
|
||||||
douyuItems.push({ key: '/douyu/elite', label: '精英宝典', icon: <BookOutlined /> });
|
douyuItems.push({ key: '/douyu/elite', label: '精英宝典', icon: <BookOutlined /> });
|
||||||
douyuItems.push({ key: '/douyu/esports', label: '电竞手册', icon: <TrophyOutlined /> });
|
douyuItems.push({ key: '/douyu/esports', label: '电竞手册', icon: <TrophyOutlined /> });
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { Button, Input, Popconfirm, Space, Table, Tag, Typography } from 'antd';
|
||||||
|
import { message } from '../utils/antdMessage';
|
||||||
|
import { LoadingOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
|
||||||
|
import { cookieApi, type CookieOperationCheckResult, type CookieOperationItem } from '../api/modules';
|
||||||
|
import { formatTime } from '../utils/time';
|
||||||
|
import { getErrorMessage } from '../utils/error';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
export default function CookieOperationsPage() {
|
||||||
|
const [items, setItems] = useState<CookieOperationItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
|
const [checkingIds, setCheckingIds] = useState<Set<number>>(new Set());
|
||||||
|
const [reloginIds, setReloginIds] = useState<Set<number>>(new Set());
|
||||||
|
const [checkResults, setCheckResults] = useState<Map<number, CookieOperationCheckResult>>(new Map());
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [pageSize, setPageSize] = useState(() => {
|
||||||
|
const value = localStorage.getItem('cookie_operations_page_size');
|
||||||
|
return value ? Number(value) || 20 : 20;
|
||||||
|
});
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
const loadItems = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await cookieApi.listOperations({
|
||||||
|
page: currentPage,
|
||||||
|
page_size: pageSize,
|
||||||
|
search: search.trim() || undefined,
|
||||||
|
});
|
||||||
|
setItems(data.items);
|
||||||
|
setTotal(data.total);
|
||||||
|
setCheckResults((previous) => {
|
||||||
|
const next = new Map(previous);
|
||||||
|
for (const item of data.items) {
|
||||||
|
if (!item.ck_check_status) continue;
|
||||||
|
next.set(item.id, {
|
||||||
|
id: item.id,
|
||||||
|
valid: item.ck_check_status === 'valid',
|
||||||
|
checked_at: item.ck_checked_at ?? '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
message.error(getErrorMessage(error));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [currentPage, pageSize, search]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadItems();
|
||||||
|
}, [loadItems]);
|
||||||
|
|
||||||
|
const handleCheck = async (ids: number[]) => {
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
setCheckingIds((previous) => new Set([...previous, ...ids]));
|
||||||
|
try {
|
||||||
|
const response = await cookieApi.checkOperations(ids);
|
||||||
|
setCheckResults((previous) => {
|
||||||
|
const next = new Map(previous);
|
||||||
|
response.results.forEach((item) => next.set(item.id, item));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
message.success(`检测完成:${response.results.filter((item) => item.valid).length}/${response.results.length} 条有效`);
|
||||||
|
void loadItems();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
message.error(getErrorMessage(error));
|
||||||
|
} finally {
|
||||||
|
setCheckingIds((previous) => {
|
||||||
|
const next = new Set(previous);
|
||||||
|
ids.forEach((id) => next.delete(id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRelogin = async (ids: number[]) => {
|
||||||
|
if (ids.length === 0) return;
|
||||||
|
setReloginIds((previous) => new Set([...previous, ...ids]));
|
||||||
|
try {
|
||||||
|
const response = await cookieApi.reloginOperations(ids);
|
||||||
|
const skipped = response.skipped ? `,${response.skipped} 条因缺少登录凭据跳过` : '';
|
||||||
|
message.success(`已开始重登 ${response.count} 个账号${skipped}`);
|
||||||
|
setSelectedRowKeys([]);
|
||||||
|
window.setTimeout(() => void loadItems(), 1000);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
message.error(getErrorMessage(error));
|
||||||
|
} finally {
|
||||||
|
setReloginIds((previous) => {
|
||||||
|
const next = new Set(previous);
|
||||||
|
ids.forEach((id) => next.delete(id));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resultFor = (item: CookieOperationItem) => checkResults.get(item.id);
|
||||||
|
const selectedIds = selectedRowKeys.map((key) => Number(key));
|
||||||
|
const columns = [
|
||||||
|
{ title: '账号', dataIndex: 'account_username', ellipsis: true },
|
||||||
|
{
|
||||||
|
title: '有效性',
|
||||||
|
width: 110,
|
||||||
|
render: (_: unknown, item: CookieOperationItem) => {
|
||||||
|
if (checkingIds.has(item.id)) return <Tag color="processing" icon={<LoadingOutlined />}>检测中</Tag>;
|
||||||
|
const result = resultFor(item);
|
||||||
|
if (!result) return <Text type="secondary">未检测</Text>;
|
||||||
|
return <Tag color={result.valid ? 'success' : 'error'}>{result.valid ? '有效' : '无效'}</Tag>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '检测时间',
|
||||||
|
width: 180,
|
||||||
|
render: (_: unknown, item: CookieOperationItem) => {
|
||||||
|
const checkedAt = resultFor(item)?.checked_at || item.ck_checked_at;
|
||||||
|
return checkedAt ? formatTime(checkedAt) : <Text type="secondary">-</Text>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 190,
|
||||||
|
render: (_: unknown, item: CookieOperationItem) => (
|
||||||
|
<Space size={4}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
loading={checkingIds.has(item.id)}
|
||||||
|
disabled={checkingIds.has(item.id)}
|
||||||
|
onClick={() => void handleCheck([item.id])}
|
||||||
|
>
|
||||||
|
检测
|
||||||
|
</Button>
|
||||||
|
<Popconfirm title="将使用已保存的账号信息重新登录并替换 CK,确认继续?" onConfirm={() => void handleRelogin([item.id])}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
loading={reloginIds.has(item.id)}
|
||||||
|
disabled={reloginIds.has(item.id)}
|
||||||
|
>
|
||||||
|
重登
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
||||||
|
<h2 style={{ margin: 0 }}>CK 检测与重登</h2>
|
||||||
|
<Space wrap>
|
||||||
|
<Button loading={checkingIds.size > 0} disabled={selectedIds.length === 0} onClick={() => void handleCheck(selectedIds)}>
|
||||||
|
检测选中 {selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title={`将重新登录选中的 ${selectedIds.length} 个账号并替换 CK,确认继续?`}
|
||||||
|
disabled={selectedIds.length === 0}
|
||||||
|
onConfirm={() => void handleRelogin(selectedIds)}
|
||||||
|
>
|
||||||
|
<Button icon={<ReloadOutlined />} disabled={selectedIds.length === 0}>
|
||||||
|
重登选中 {selectedIds.length > 0 ? `(${selectedIds.length})` : ''}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<Input.Search
|
||||||
|
allowClear
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
placeholder="搜索账号"
|
||||||
|
style={{ width: 280, marginBottom: 12 }}
|
||||||
|
value={search}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSearch(event.target.value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSelectedRowKeys([]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
size="small"
|
||||||
|
loading={loading}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={items}
|
||||||
|
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys, preserveSelectedRowKeys: true }}
|
||||||
|
pagination={{
|
||||||
|
current: currentPage,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (count) => `共 ${count} 条`,
|
||||||
|
onChange: (page, size) => {
|
||||||
|
setCurrentPage(size === pageSize ? page : 1);
|
||||||
|
if (size !== pageSize) {
|
||||||
|
setPageSize(size);
|
||||||
|
localStorage.setItem('cookie_operations_page_size', String(size));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user