优化斗鱼充值日志与配置
This commit is contained in:
+4
-1
@@ -18,6 +18,9 @@ ADMIN_PASSWORD=admin123
|
|||||||
# Docker 对外端口(容器内仍监听 8800;服务器当前使用 8000)
|
# Docker 对外端口(容器内仍监听 8800;服务器当前使用 8000)
|
||||||
APP_PORT=8000
|
APP_PORT=8000
|
||||||
|
|
||||||
|
# 应用日志等级:DEBUG、INFO、WARNING、ERROR、CRITICAL;生产环境建议保持 INFO。
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
|
||||||
# Docker 构建镜像源(网络可访问官方源时保持 false;国内服务器可改为 true)
|
# Docker 构建镜像源(网络可访问官方源时保持 false;国内服务器可改为 true)
|
||||||
USE_CHINA_MIRRORS=false
|
USE_CHINA_MIRRORS=false
|
||||||
|
|
||||||
@@ -79,7 +82,7 @@ FISH_FIN_RECHARGE_APP_SECRET=
|
|||||||
# 公网回调地址;不填则只通过 queryOrderV2 轮询订单状态。
|
# 公网回调地址;不填则只通过 queryOrderV2 轮询订单状态。
|
||||||
FISH_FIN_RECHARGE_NOTIFY_URL=
|
FISH_FIN_RECHARGE_NOTIFY_URL=
|
||||||
# FISH_FIN_RECHARGE_TIMEOUT=20
|
# FISH_FIN_RECHARGE_TIMEOUT=20
|
||||||
# 排查供应商协议时临时开启;会输出完整请求、签名、AppSecret 和响应,排查后必须关闭。
|
# 排查供应商协议时临时开启;协议参数会按 DEBUG 级别脱敏输出,排查后建议关闭。
|
||||||
FISH_FIN_RECHARGE_DEBUG=false
|
FISH_FIN_RECHARGE_DEBUG=false
|
||||||
|
|
||||||
# Roundcube 邮件验证码读取服务地址(可选;不填则使用默认服务)
|
# Roundcube 邮件验证码读取服务地址(可选;不填则使用默认服务)
|
||||||
|
|||||||
@@ -116,6 +116,28 @@ class FishFinRechargeClient:
|
|||||||
"""生成单行 JSON 调试文本,避免日志被控制字符截断。"""
|
"""生成单行 JSON 调试文本,避免日志被控制字符截断。"""
|
||||||
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), default=str)
|
return json.dumps(value, ensure_ascii=False, separators=(",", ":"), default=str)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _debug_payload(cls, payload: Mapping[str, Any]) -> dict[str, Any]:
|
||||||
|
"""调试记录只保留协议字段,签名值和密钥相关内容统一脱敏。"""
|
||||||
|
safe = dict(payload)
|
||||||
|
if "sign" in safe:
|
||||||
|
safe["sign"] = "<redacted>"
|
||||||
|
return safe
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _response_value(payload: Mapping[str, Any], *keys: str) -> Any:
|
||||||
|
"""兼容供应商将订单字段放在根节点、data 或 result 节点。"""
|
||||||
|
sources = [payload]
|
||||||
|
for key in ("data", "result"):
|
||||||
|
value = payload.get(key)
|
||||||
|
if isinstance(value, Mapping):
|
||||||
|
sources.append(value)
|
||||||
|
for source in sources:
|
||||||
|
for key in keys:
|
||||||
|
if source.get(key) is not None:
|
||||||
|
return source[key]
|
||||||
|
return None
|
||||||
|
|
||||||
def verify_response_sign(self, payload: Mapping[str, Any], method: str) -> bool:
|
def verify_response_sign(self, payload: Mapping[str, Any], method: str) -> bool:
|
||||||
"""校验带 sign 的响应;供应商未返回 sign 时由调用方决定是否接受。"""
|
"""校验带 sign 的响应;供应商未返回 sign 时由调用方决定是否接受。"""
|
||||||
received = str(payload.get("sign") or "").strip().lower()
|
received = str(payload.get("sign") or "").strip().lower()
|
||||||
@@ -153,10 +175,12 @@ class FishFinRechargeClient:
|
|||||||
trace_event.update({
|
trace_event.update({
|
||||||
"url": url,
|
"url": url,
|
||||||
"content_type": "application/json",
|
"content_type": "application/json",
|
||||||
# 调试模式按用户要求保留完整协议内容,包含 sign 和 AppSecret。
|
"json_body": self._debug_payload(request_params),
|
||||||
"json_body": request_params,
|
|
||||||
"sign_params": sign_params,
|
"sign_params": sign_params,
|
||||||
"sign_source": f"{sign_query}{method.upper()}{self.config.app_secret}",
|
# 不把可重放签名原文或 AppSecret 写入日志,只记录待签名串摘要。
|
||||||
|
"sign_source_digest": hashlib.sha256(
|
||||||
|
f"{sign_query}{method.upper()}".encode("utf-8")
|
||||||
|
).hexdigest()[:12],
|
||||||
})
|
})
|
||||||
self.trace(trace_event)
|
self.trace(trace_event)
|
||||||
try:
|
try:
|
||||||
@@ -179,9 +203,10 @@ class FishFinRechargeClient:
|
|||||||
"http_status": response.status_code,
|
"http_status": response.status_code,
|
||||||
"code": payload.get("code"),
|
"code": payload.get("code"),
|
||||||
"message": payload.get("msg") or payload.get("message") or "",
|
"message": payload.get("msg") or payload.get("message") or "",
|
||||||
"keys": sorted(payload.keys()),
|
"out_order_id": self._response_value(payload, "out_order_id", "outOrderId"),
|
||||||
"data_keys": sorted((payload.get("data") or {}).keys()) if isinstance(payload.get("data"), dict) else [],
|
"order_id": self._response_value(payload, "order_id", "orderId"),
|
||||||
"result_keys": sorted((payload.get("result") or {}).keys()) if isinstance(payload.get("result"), dict) else [],
|
"order_status": self._response_value(payload, "order_status", "orderStatus"),
|
||||||
|
"fail_reason": self._response_value(payload, "fail_reason", "failReason"),
|
||||||
}
|
}
|
||||||
if self.config.debug:
|
if self.config.debug:
|
||||||
trace_event.update({
|
trace_event.update({
|
||||||
@@ -190,8 +215,7 @@ class FishFinRechargeClient:
|
|||||||
for key, value in response.headers.items()
|
for key, value in response.headers.items()
|
||||||
if key.lower() in {"content-type", "x-request-id", "request-id"}
|
if key.lower() in {"content-type", "x-request-id", "request-id"}
|
||||||
},
|
},
|
||||||
"response_body": payload,
|
"response_body": self._debug_payload(payload),
|
||||||
"response_text": response.text,
|
|
||||||
})
|
})
|
||||||
self.trace(trace_event)
|
self.trace(trace_event)
|
||||||
return payload
|
return payload
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ services:
|
|||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
|
# 应用日志等级由 .env 控制;生产环境默认只记录 INFO 及以上。
|
||||||
|
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||||
# MySQL 仅在 Compose 内网开放,不映射宿主机端口。
|
# MySQL 仅在 Compose 内网开放,不映射宿主机端口。
|
||||||
- DB_HOST=mysql
|
- DB_HOST=mysql
|
||||||
- DB_PORT=3306
|
- DB_PORT=3306
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class DouyuGoldRechargeChannelTests(unittest.TestCase):
|
|||||||
email="mail@example.com",
|
email="mail@example.com",
|
||||||
email_password="mail-password",
|
email_password="mail-password",
|
||||||
uid="10001",
|
uid="10001",
|
||||||
|
nickname="罗炅729",
|
||||||
)
|
)
|
||||||
self.session.add(self.account)
|
self.session.add(self.account)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
@@ -52,7 +53,7 @@ class DouyuGoldRechargeChannelTests(unittest.TestCase):
|
|||||||
self.engine.dispose()
|
self.engine.dispose()
|
||||||
|
|
||||||
@patch("web.backend.services.douyu_runner.FishFinRechargeClient")
|
@patch("web.backend.services.douyu_runner.FishFinRechargeClient")
|
||||||
def test_supplier_channel_creates_order_using_uid_and_finishes_on_success(self, client_class):
|
def test_supplier_channel_creates_order_using_nickname_and_finishes_on_success(self, client_class):
|
||||||
supplier = Mock()
|
supplier = Mock()
|
||||||
supplier.create_order.return_value = {
|
supplier.create_order.return_value = {
|
||||||
"code": 200,
|
"code": 200,
|
||||||
@@ -72,12 +73,13 @@ class DouyuGoldRechargeChannelTests(unittest.TestCase):
|
|||||||
self.session, self.task, self.account, "acf_uid=10001", config,
|
self.session, self.task, self.account, "acf_uid=10001", config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
expected_order_id = self.runner._supplier_out_order_id(self.task)
|
||||||
supplier.create_order.assert_called_once_with(
|
supplier.create_order.assert_called_once_with(
|
||||||
buy_num=10,
|
buy_num=10,
|
||||||
pay_amount=unittest.mock.ANY,
|
pay_amount=unittest.mock.ANY,
|
||||||
out_order_id=f"DYGF{self.task.id}",
|
out_order_id=expected_order_id,
|
||||||
product_id="gold-product",
|
product_id="gold-product",
|
||||||
recharge_arg=[{"templateName": "斗鱼账号", "templateVal": "10001"}],
|
recharge_arg=[{"templateName": "斗鱼账号", "templateVal": "罗炅729"}],
|
||||||
order_type=0,
|
order_type=0,
|
||||||
notify_url=unittest.mock.ANY,
|
notify_url=unittest.mock.ANY,
|
||||||
)
|
)
|
||||||
@@ -87,12 +89,40 @@ class DouyuGoldRechargeChannelTests(unittest.TestCase):
|
|||||||
self.assertEqual(self.task.result["recharge_channel"], "supplier_api")
|
self.assertEqual(self.task.result["recharge_channel"], "supplier_api")
|
||||||
self.assertEqual(self.task.result["supplier_order_status"], 2)
|
self.assertEqual(self.task.result["supplier_order_status"], 2)
|
||||||
self.assertEqual(self.task.result["buy_num"], 10)
|
self.assertEqual(self.task.result["buy_num"], 10)
|
||||||
self.assertEqual(self.task.result["out_order_id"], f"DYGF{self.task.id}")
|
self.assertEqual(self.task.result["out_order_id"], expected_order_id)
|
||||||
|
self.assertEqual(self.task.result["recharge_account"], "罗炅729")
|
||||||
|
self.assertEqual(self.task.result["douyu_uid"], "10001")
|
||||||
self.assertEqual(self.task.result["order_id"], "supplier-001")
|
self.assertEqual(self.task.result["order_id"], "supplier-001")
|
||||||
self.assertEqual(self.task.result["pay_amount"], "10")
|
self.assertEqual(self.task.result["pay_amount"], "10")
|
||||||
self.assertNotIn("pay_url", self.task.result)
|
self.assertNotIn("pay_url", self.task.result)
|
||||||
self.assertNotIn("sign", self.task.result["supplier_order"])
|
self.assertNotIn("sign", self.task.result["supplier_order"])
|
||||||
|
|
||||||
|
@patch("web.backend.services.douyu_runner.FishFinRechargeClient")
|
||||||
|
def test_supplier_channel_rejects_account_without_nickname(self, client_class):
|
||||||
|
self.account.nickname = ""
|
||||||
|
self.session.commit()
|
||||||
|
config = {
|
||||||
|
"gold_recharge_channel": "supplier_api",
|
||||||
|
"gold_api_product_id": "gold-product",
|
||||||
|
"gold_api_account_template_name": "斗鱼昵称",
|
||||||
|
}
|
||||||
|
|
||||||
|
self.runner._execute_create_gold_qr(
|
||||||
|
self.session, self.task, self.account, "acf_uid=10001", config,
|
||||||
|
)
|
||||||
|
|
||||||
|
client_class.assert_not_called()
|
||||||
|
self.session.refresh(self.task)
|
||||||
|
self.assertEqual(self.task.status, "failed")
|
||||||
|
self.assertIn("斗鱼昵称", self.task.message)
|
||||||
|
|
||||||
|
def test_supplier_out_order_id_reuses_persisted_value(self):
|
||||||
|
self.task.supplier_out_order_id = "DYGFHISTORICAL-001"
|
||||||
|
self.assertEqual(
|
||||||
|
self.runner._supplier_out_order_id(self.task),
|
||||||
|
"DYGFHISTORICAL-001",
|
||||||
|
)
|
||||||
|
|
||||||
@patch("web.backend.services.douyu_runner.FishFinRechargeClient")
|
@patch("web.backend.services.douyu_runner.FishFinRechargeClient")
|
||||||
def test_supplier_channel_marks_non_200_create_response_as_failed(self, client_class):
|
def test_supplier_channel_marks_non_200_create_response_as_failed(self, client_class):
|
||||||
supplier = Mock()
|
supplier = Mock()
|
||||||
@@ -113,7 +143,7 @@ class DouyuGoldRechargeChannelTests(unittest.TestCase):
|
|||||||
self.assertEqual(self.task.message, "商品已下架")
|
self.assertEqual(self.task.message, "商品已下架")
|
||||||
|
|
||||||
def test_supplier_callback_verifies_signature_and_updates_terminal_task(self):
|
def test_supplier_callback_verifies_signature_and_updates_terminal_task(self):
|
||||||
self.task.supplier_out_order_id = f"DYGF{self.task.id}"
|
self.task.supplier_out_order_id = f"DYGFBATCHT{self.task.id}"
|
||||||
self.task.result = {"recharge_channel": "supplier_api", "out_order_id": self.task.supplier_out_order_id}
|
self.task.result = {"recharge_channel": "supplier_api", "out_order_id": self.task.supplier_out_order_id}
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
client = FishFinRechargeClient(FishFinRechargeConfig(
|
client = FishFinRechargeClient(FishFinRechargeConfig(
|
||||||
|
|||||||
@@ -137,12 +137,13 @@ class FishFinRechargeClientTests(unittest.TestCase):
|
|||||||
recharge_arg=[{"templateName": "斗鱼UID", "templateVal": "10001"}],
|
recharge_arg=[{"templateName": "斗鱼UID", "templateVal": "10001"}],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(events[0]["json_body"]["sign"], client.sign(events[0]["json_body"], "POST"))
|
self.assertEqual(events[0]["json_body"]["sign"], "<redacted>")
|
||||||
self.assertEqual(events[0]["json_body"]["pay_amount"], 1)
|
self.assertEqual(events[0]["json_body"]["pay_amount"], 1)
|
||||||
self.assertNotIn("sign", events[0]["sign_params"])
|
self.assertNotIn("sign", events[0]["sign_params"])
|
||||||
self.assertIn(self.config.app_secret, events[0]["sign_source"])
|
self.assertNotIn(self.config.app_secret, str(events[0]))
|
||||||
self.assertEqual(events[1]["response_body"]["sign"], "response-sign")
|
self.assertIn("sign_source_digest", events[0])
|
||||||
self.assertEqual(events[1]["response_text"], response.text)
|
self.assertEqual(events[1]["response_body"]["sign"], "<redacted>")
|
||||||
|
self.assertNotIn(self.config.app_secret, str(events[1]))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ from pathlib import Path
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
_LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_log_level(level: str | None) -> str:
|
||||||
|
"""规范化环境变量日志等级,非法值回退到 INFO。"""
|
||||||
|
normalized = str(level or "INFO").strip().upper()
|
||||||
|
return normalized if normalized in _LOG_LEVELS else "INFO"
|
||||||
|
|
||||||
|
|
||||||
class _RelevantAccessFilter(logging.Filter):
|
class _RelevantAccessFilter(logging.Filter):
|
||||||
"""仅保留异常 HTTP 请求,避免前端轮询淹没业务日志。"""
|
"""仅保留异常 HTTP 请求,避免前端轮询淹没业务日志。"""
|
||||||
|
|
||||||
@@ -67,6 +76,7 @@ def setup_logger(level: str = "INFO", log_dir: str = None, log_file: str = None)
|
|||||||
log_dir: 日志目录路径(写入 app.log,按日轮转)
|
log_dir: 日志目录路径(写入 app.log,按日轮转)
|
||||||
log_file: 日志文件路径(兼容旧接口,优先级低于 log_dir)
|
log_file: 日志文件路径(兼容旧接口,优先级低于 log_dir)
|
||||||
"""
|
"""
|
||||||
|
level = _normalize_log_level(level)
|
||||||
# 移除默认 handler,由标准 logging 统一处理控制台和文件输出。
|
# 移除默认 handler,由标准 logging 统一处理控制台和文件输出。
|
||||||
logger.remove()
|
logger.remove()
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ from utils import setup_logger
|
|||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
# 初始化日志(控制台 + 按天命名文件)
|
# 初始化日志(控制台 + 按天命名文件)
|
||||||
_log_level = os.getenv("LOG_LEVEL", "DEBUG")
|
_log_level = os.getenv("LOG_LEVEL", "INFO")
|
||||||
_log_dir = Path(__file__).resolve().parents[2] / "logs"
|
_log_dir = Path(__file__).resolve().parents[2] / "logs"
|
||||||
setup_logger(level=_log_level, log_dir=str(_log_dir))
|
setup_logger(level=_log_level, log_dir=str(_log_dir))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""将斗鱼供应商直充默认账号模板切换为昵称。
|
||||||
|
|
||||||
|
Revision ID: 20260814_0028
|
||||||
|
Revises: 20260813_0027
|
||||||
|
Create Date: 2026-08-14
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = "20260814_0028"
|
||||||
|
down_revision: Union[str, None] = "20260813_0027"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""只迁移历史默认值,保留运营人员明确配置的模板名。"""
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = sa.inspect(bind)
|
||||||
|
if not inspector.has_table("douyu_config"):
|
||||||
|
return
|
||||||
|
columns = {column["name"] for column in inspector.get_columns("douyu_config")}
|
||||||
|
if "gold_api_account_template_name" in columns:
|
||||||
|
op.execute(
|
||||||
|
sa.text(
|
||||||
|
"UPDATE douyu_config SET gold_api_account_template_name = :new "
|
||||||
|
"WHERE gold_api_account_template_name IS NULL "
|
||||||
|
"OR TRIM(gold_api_account_template_name) = '' "
|
||||||
|
"OR gold_api_account_template_name = :old"
|
||||||
|
).bindparams(new="斗鱼昵称", old="斗鱼UID")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""回滚时只回退本迁移设置的默认昵称。"""
|
||||||
|
bind = op.get_bind()
|
||||||
|
inspector = sa.inspect(bind)
|
||||||
|
if not inspector.has_table("douyu_config"):
|
||||||
|
return
|
||||||
|
columns = {column["name"] for column in inspector.get_columns("douyu_config")}
|
||||||
|
if "gold_api_account_template_name" in columns:
|
||||||
|
op.execute(
|
||||||
|
sa.text(
|
||||||
|
"UPDATE douyu_config SET gold_api_account_template_name = :old "
|
||||||
|
"WHERE gold_api_account_template_name = :new"
|
||||||
|
).bindparams(new="斗鱼昵称", old="斗鱼UID")
|
||||||
|
)
|
||||||
@@ -218,7 +218,7 @@ class DouyuConfig(Base):
|
|||||||
gold_pay_type = Column(Integer, default=1)
|
gold_pay_type = Column(Integer, default=1)
|
||||||
gold_recharge_channel = Column(String(16), default="wechat_qr")
|
gold_recharge_channel = Column(String(16), default="wechat_qr")
|
||||||
gold_api_product_id = Column(String(128), default="111570")
|
gold_api_product_id = Column(String(128), default="111570")
|
||||||
gold_api_account_template_name = Column(String(64), default="斗鱼UID")
|
gold_api_account_template_name = Column(String(64), default="斗鱼昵称")
|
||||||
gift_id = Column(String(64), default="23643")
|
gift_id = Column(String(64), default="23643")
|
||||||
skin_id = Column(String(64), default="2942")
|
skin_id = Column(String(64), default="2942")
|
||||||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
||||||
|
|||||||
@@ -587,7 +587,7 @@ class DouyuConfigOut(BaseModel):
|
|||||||
gold_pay_type: int = 1
|
gold_pay_type: int = 1
|
||||||
gold_recharge_channel: str = "wechat_qr"
|
gold_recharge_channel: str = "wechat_qr"
|
||||||
gold_api_product_id: str = "111570"
|
gold_api_product_id: str = "111570"
|
||||||
gold_api_account_template_name: str = "斗鱼UID"
|
gold_api_account_template_name: str = "斗鱼昵称"
|
||||||
gift_id: str = "23643"
|
gift_id: str = "23643"
|
||||||
skin_id: str = "2942"
|
skin_id: str = "2942"
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@@ -760,6 +761,15 @@ class DouyuBatchRunner:
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _supplier_out_order_id(task: DouyuTask) -> str:
|
||||||
|
"""生成可追踪的供应商外部订单号;已有订单号必须在重试时复用。"""
|
||||||
|
existing = str(task.supplier_out_order_id or "").strip()
|
||||||
|
if existing:
|
||||||
|
return existing
|
||||||
|
batch_token = re.sub(r"[^A-Za-z0-9]", "", str(task.batch_id or "")).upper()[:16] or "LOCAL"
|
||||||
|
return f"DYGF{batch_token}T{task.id}"
|
||||||
|
|
||||||
def _wait_supplier_gold_order(
|
def _wait_supplier_gold_order(
|
||||||
self,
|
self,
|
||||||
db: Session,
|
db: Session,
|
||||||
@@ -2456,7 +2466,7 @@ class DouyuBatchRunner:
|
|||||||
channel = str(config.get("gold_recharge_channel") or "wechat_qr")
|
channel = str(config.get("gold_recharge_channel") or "wechat_qr")
|
||||||
if channel == "supplier_api":
|
if channel == "supplier_api":
|
||||||
try:
|
try:
|
||||||
self._execute_create_gold_supplier_order(db, task, account, config, amount)
|
self._execute_create_gold_supplier_order(db, task, account, cookie, config, amount)
|
||||||
except FishFinRechargeError as exc:
|
except FishFinRechargeError as exc:
|
||||||
self._mark_task(db, task, "failed", str(exc), {"recharge_channel": "supplier_api"})
|
self._mark_task(db, task, "failed", str(exc), {"recharge_channel": "supplier_api"})
|
||||||
return
|
return
|
||||||
@@ -2500,24 +2510,28 @@ class DouyuBatchRunner:
|
|||||||
db: Session,
|
db: Session,
|
||||||
task: DouyuTask,
|
task: DouyuTask,
|
||||||
account: Account,
|
account: Account,
|
||||||
|
cookie: str,
|
||||||
config: dict,
|
config: dict,
|
||||||
amount: int,
|
amount: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""创建供应商鱼翅直充订单并轮询订单状态。"""
|
"""创建供应商鱼翅直充订单并轮询订单状态。"""
|
||||||
product_id = str(config.get("gold_api_product_id") or "").strip()
|
product_id = str(config.get("gold_api_product_id") or "").strip()
|
||||||
template_name = str(config.get("gold_api_account_template_name") or "斗鱼UID").strip()
|
template_name = str(config.get("gold_api_account_template_name") or "斗鱼昵称").strip()
|
||||||
if not product_id:
|
if not product_id:
|
||||||
raise FishFinRechargeError("请先在配置中填写供应商直充商品 ID")
|
raise FishFinRechargeError("请先在配置中填写供应商直充商品 ID")
|
||||||
charge_account = str(account.uid or "").strip()
|
# 充值商品按斗鱼昵称识别账号,UID 只能作为审计信息,不能作为充值值。
|
||||||
if not charge_account:
|
update_account_profile_from_cookie(account, cookie)
|
||||||
raise FishFinRechargeError("账号缺少斗鱼 UID,无法发起供应商直充")
|
recharge_account = str(account.nickname or "").strip()
|
||||||
|
if not recharge_account:
|
||||||
|
raise FishFinRechargeError("账号缺少斗鱼昵称,无法发起供应商直充")
|
||||||
|
|
||||||
# task.id 是唯一且稳定的外部订单号来源,重试时不会创建不同的供应商订单。
|
# 首次生成后持久化,网络重试或进程重启都继续查询同一笔订单。
|
||||||
order_no = f"DYGF{task.id}"
|
order_no = self._supplier_out_order_id(task)
|
||||||
task.supplier_out_order_id = order_no
|
task.supplier_out_order_id = order_no
|
||||||
db.commit()
|
db.commit()
|
||||||
# pay_amount 是用户选择的充值面值;goodsFaceValue=0.993 是供货成本,不能作为支付金额。
|
# pay_amount 是用户选择的充值面值;goodsFaceValue=0.993 是供货成本,不能作为支付金额。
|
||||||
pay_amount = Decimal(amount)
|
pay_amount = Decimal(amount)
|
||||||
|
|
||||||
def trace(event: dict) -> None:
|
def trace(event: dict) -> None:
|
||||||
"""将脱敏供应商协议信息输出到任务日志,便于线上联调。"""
|
"""将脱敏供应商协议信息输出到任务日志,便于线上联调。"""
|
||||||
stage = event.get("stage")
|
stage = event.get("stage")
|
||||||
@@ -2525,36 +2539,41 @@ class DouyuBatchRunner:
|
|||||||
params = event.get("params") or {}
|
params = event.get("params") or {}
|
||||||
self._push_log(
|
self._push_log(
|
||||||
"info",
|
"info",
|
||||||
"供应商直充请求 "
|
"供应商直充 | 下单 "
|
||||||
f"path={event.get('path')} out_order_id={params.get('out_order_id')} "
|
f"| 外部单号={params.get('out_order_id') or '-'} "
|
||||||
f"buy_num={params.get('buy_num')} pay_amount={params.get('pay_amount')} "
|
f"| 数量={params.get('buy_num') or '-'} "
|
||||||
f"product_id={params.get('product_id')} types={event.get('parameter_types')} "
|
f"| 金额={params.get('pay_amount') or '-'} "
|
||||||
f"sign_digest={event.get('sign_digest')}",
|
f"| 商品={params.get('product_id') or '-'}",
|
||||||
)
|
)
|
||||||
if event.get("json_body"):
|
if event.get("json_body"):
|
||||||
self._push_log(
|
self._push_log(
|
||||||
"info",
|
"debug",
|
||||||
"供应商直充完整调试请求 "
|
"供应商协议 | 请求 "
|
||||||
f"url={event.get('url')} content_type={event.get('content_type')} "
|
f"| {event.get('method')} {event.get('path')} "
|
||||||
f"body={FishFinRechargeClient._json_text(event['json_body'])} "
|
f"| 签名摘要={event.get('sign_digest')} "
|
||||||
f"sign_params={FishFinRechargeClient._json_text(event.get('sign_params') or {})} "
|
f"| 参数={FishFinRechargeClient._json_text(event['json_body'])}",
|
||||||
f"sign_source={event.get('sign_source')}",
|
|
||||||
)
|
)
|
||||||
elif stage == "response":
|
elif stage == "response":
|
||||||
|
status = self._to_int(event.get("order_status"))
|
||||||
|
status_labels = {0: "待处理", 1: "处理中", 2: "成功", 3: "失败", 4: "异常"}
|
||||||
|
status_text = status_labels.get(status, "-")
|
||||||
|
reason = str(event.get("fail_reason") or event.get("message") or "-")
|
||||||
self._push_log(
|
self._push_log(
|
||||||
"info",
|
"info",
|
||||||
"供应商直充响应 "
|
"供应商直充 | 响应 "
|
||||||
f"http={event.get('http_status')} code={event.get('code')} "
|
f"| HTTP={event.get('http_status') or '-'} "
|
||||||
f"message={event.get('message') or '-'} keys={event.get('keys')} "
|
f"| 业务码={event.get('code') or '-'} "
|
||||||
f"data_keys={event.get('data_keys')} result_keys={event.get('result_keys')}",
|
f"| 外部单号={event.get('out_order_id') or '-'} "
|
||||||
|
f"| 供应商单号={event.get('order_id') or '-'} "
|
||||||
|
f"| 状态={status_text} "
|
||||||
|
f"| 提示={reason}",
|
||||||
)
|
)
|
||||||
if event.get("response_body"):
|
if event.get("response_body"):
|
||||||
self._push_log(
|
self._push_log(
|
||||||
"info",
|
"debug",
|
||||||
"供应商直充完整调试响应 "
|
"供应商协议 | 响应 "
|
||||||
f"headers={FishFinRechargeClient._json_text(event.get('response_headers') or {})} "
|
f"| HTTP={event.get('http_status')} "
|
||||||
f"body={FishFinRechargeClient._json_text(event['response_body'])} "
|
f"| 内容={FishFinRechargeClient._json_text(event['response_body'])}",
|
||||||
f"raw={event.get('response_text')}",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
client = FishFinRechargeClient(FishFinRechargeConfig.from_env(), trace=trace)
|
client = FishFinRechargeClient(FishFinRechargeConfig.from_env(), trace=trace)
|
||||||
@@ -2563,7 +2582,7 @@ class DouyuBatchRunner:
|
|||||||
pay_amount=pay_amount,
|
pay_amount=pay_amount,
|
||||||
out_order_id=order_no,
|
out_order_id=order_no,
|
||||||
product_id=product_id,
|
product_id=product_id,
|
||||||
recharge_arg=[{"templateName": template_name, "templateVal": charge_account}],
|
recharge_arg=[{"templateName": template_name, "templateVal": recharge_account}],
|
||||||
order_type=0,
|
order_type=0,
|
||||||
notify_url=client.config.notify_url,
|
notify_url=client.config.notify_url,
|
||||||
)
|
)
|
||||||
@@ -2573,7 +2592,8 @@ class DouyuBatchRunner:
|
|||||||
"recharge_channel": "supplier_api",
|
"recharge_channel": "supplier_api",
|
||||||
"out_order_id": order_no,
|
"out_order_id": order_no,
|
||||||
"order_id": self._supplier_value(order_payload, "order_id", "orderId"),
|
"order_id": self._supplier_value(order_payload, "order_id", "orderId"),
|
||||||
"charge_account": charge_account,
|
"recharge_account": recharge_account,
|
||||||
|
"douyu_uid": str(account.uid or "").strip(),
|
||||||
"buy_num": amount,
|
"buy_num": amount,
|
||||||
"product_id": product_id,
|
"product_id": product_id,
|
||||||
"pay_amount": format(pay_amount.normalize(), "f"),
|
"pay_amount": format(pay_amount.normalize(), "f"),
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ DOUYU_CONFIG_DEFAULTS = { "manual_id": "G4KA4Qnz4LDp7",
|
|||||||
"gold_pay_type": 1,
|
"gold_pay_type": 1,
|
||||||
"gold_recharge_channel": "wechat_qr",
|
"gold_recharge_channel": "wechat_qr",
|
||||||
"gold_api_product_id": "111570",
|
"gold_api_product_id": "111570",
|
||||||
"gold_api_account_template_name": "斗鱼UID",
|
"gold_api_account_template_name": "斗鱼昵称",
|
||||||
"gift_id": "23643",
|
"gift_id": "23643",
|
||||||
"skin_id": "2942",
|
"skin_id": "2942",
|
||||||
"xpd_act_alias": "20260623KDQFH",
|
"xpd_act_alias": "20260623KDQFH",
|
||||||
|
|||||||
@@ -2227,7 +2227,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
|||||||
<>
|
<>
|
||||||
{[
|
{[
|
||||||
{ label: '供应商商品 ID', key: 'gold_api_product_id' },
|
{ label: '供应商商品 ID', key: 'gold_api_product_id' },
|
||||||
{ label: '充值账号模板名', key: 'gold_api_account_template_name' },
|
{ label: '充值账号模板名(值为斗鱼昵称)', key: 'gold_api_account_template_name' },
|
||||||
].map(({ label, key }) => (
|
].map(({ label, key }) => (
|
||||||
<Space key={key} style={{ width: '100%', justifyContent: 'space-between' }}>
|
<Space key={key} style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||||
<Text>{label}</Text>
|
<Text>{label}</Text>
|
||||||
|
|||||||
Reference in New Issue
Block a user