让成交卡等报价文件真正发出后再出现,并收紧 PDF 顶部留白,避免抬头被裁或空一截。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
H5 补问 / 附件上传通道。
|
||||
H5 补问 / 附件上传 / 复制工单号通道。
|
||||
|
||||
本文件职责:挂载 /inquiry-form;用 clarification token 绑定身份(URL 禁止 userid)。
|
||||
本文件职责:挂载 /inquiry-form;用 token 绑定身份或工单号(URL 禁止 userid)。
|
||||
禁止:从 URL 读 userid;禁止在本包改六态或直连 TMS;禁止同步 invoke Graph。
|
||||
"""
|
||||
|
||||
@@ -10,10 +10,13 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.responses import HTMLResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from agent.channel.clarification import validate_token
|
||||
from agent.channel.h5.copy_page import expired_copy_page, render_copy_page
|
||||
from agent.channel.h5.copy_store import lookup_copy_token
|
||||
from agent.handlers.card_action import handle_card_action
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -34,6 +37,36 @@ def h5_ping() -> dict[str, str]:
|
||||
return {"status": "ok", "channel": "h5"}
|
||||
|
||||
|
||||
@router.get("/copy/{token}", response_class=HTMLResponse)
|
||||
def h5_copy_ticket(token: str) -> HTMLResponse:
|
||||
"""
|
||||
复制工单号页:token 换工单号,页面写入剪贴板。
|
||||
|
||||
请求线程只读 token,不进 Graph、不发企微消息。
|
||||
"""
|
||||
wo = lookup_copy_token(token)
|
||||
if not wo:
|
||||
return HTMLResponse(expired_copy_page(), status_code=410)
|
||||
return HTMLResponse(render_copy_page(wo))
|
||||
|
||||
|
||||
@router.get("/jsapi-signature")
|
||||
def h5_jsapi_signature(
|
||||
url: str = Query(default=""),
|
||||
kind: str = Query(default="corp"),
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
给复制页 ww.register 签名。失败给空对象,页面改走浏览器剪贴板。
|
||||
"""
|
||||
from agent.channel.wecom.jsapi import build_jsapi_signature
|
||||
|
||||
try:
|
||||
return build_jsapi_signature(url=url, kind=kind)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning("jsapi 签名失败 kind=%s err=%s", kind, exc)
|
||||
raise HTTPException(status_code=503, detail="jsapi_unavailable") from exc
|
||||
|
||||
|
||||
@router.get("/{token}")
|
||||
def h5_open(token: str) -> dict[str, Any]:
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
复制工单号页 HTML。
|
||||
|
||||
本文件职责:打开后立刻把工单号写入系统剪贴板(企微 JS-SDK + 浏览器兜底)。
|
||||
禁止:URL/页面带 userid;禁止在本页调主账或改六态。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
|
||||
from agent.config import get_settings
|
||||
|
||||
_EXPIRED_HTML = """<!doctype html>
|
||||
<html lang="zh-CN"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<title>链接已过期</title>
|
||||
</head><body style="font-family:sans-serif;padding:32px;text-align:center;color:#333">
|
||||
<p>复制链接已过期。</p>
|
||||
<p>请回到企微,重新询价后再点「复制工单号」。</p>
|
||||
</body></html>
|
||||
"""
|
||||
|
||||
|
||||
def expired_copy_page() -> str:
|
||||
"""token 无效时给销售看的短页,不暴露内部原因。"""
|
||||
return _EXPIRED_HTML
|
||||
|
||||
|
||||
def render_copy_page(work_order_no: str) -> str:
|
||||
"""
|
||||
渲染复制页。工单号只进页面正文,由 JS 写入剪贴板。
|
||||
|
||||
先走企微 ww.setClipboardData(应用内才能真复制),失败再走浏览器剪贴板。
|
||||
"""
|
||||
wo = html.escape((work_order_no or "").strip(), quote=True)
|
||||
settings = get_settings()
|
||||
corp = html.escape((settings.wecom_corp_id or "").strip(), quote=True)
|
||||
agent = html.escape(str(settings.wecom_agent_id or "1000010").strip(), quote=True)
|
||||
return f"""<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<title>复制工单号</title>
|
||||
<style>
|
||||
body {{ margin:0; font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
|
||||
background:#f5f6f7; color:#1f2329; }}
|
||||
.wrap {{ max-width:420px; margin:48px auto; padding:0 20px; text-align:center; }}
|
||||
.wo {{ font-size:22px; font-weight:600; letter-spacing:.04em; margin:16px 0 8px; word-break:break-all; }}
|
||||
.st {{ min-height:24px; color:#646a73; font-size:14px; }}
|
||||
.ok {{ color:#00a870; }}
|
||||
.bad {{ color:#d83931; }}
|
||||
button {{ margin-top:20px; width:100%; height:44px; border:0; border-radius:8px;
|
||||
background:#1677ff; color:#fff; font-size:16px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div>工单号</div>
|
||||
<div class="wo" id="wo" data-wo="{wo}">{wo}</div>
|
||||
<div class="st" id="st">正在复制…</div>
|
||||
<button type="button" id="btn">复制工单号</button>
|
||||
</div>
|
||||
<script src="https://wwcdn.weixin.qq.com/node/wework/open/js/wecom-jssdk-2.3.2.js"></script>
|
||||
<script>
|
||||
(function () {{
|
||||
var text = document.getElementById("wo").getAttribute("data-wo") || "";
|
||||
var st = document.getElementById("st");
|
||||
var corpId = "{corp}";
|
||||
var agentId = Number("{agent}") || 0;
|
||||
|
||||
function show(ok, msg) {{
|
||||
st.className = "st " + (ok ? "ok" : "bad");
|
||||
st.textContent = msg;
|
||||
}}
|
||||
|
||||
function browserCopy(value) {{
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {{
|
||||
return navigator.clipboard.writeText(value);
|
||||
}}
|
||||
return new Promise(function (resolve, reject) {{
|
||||
var box = document.createElement("textarea");
|
||||
box.value = value;
|
||||
box.setAttribute("readonly", "readonly");
|
||||
box.style.position = "fixed";
|
||||
box.style.left = "-9999px";
|
||||
document.body.appendChild(box);
|
||||
box.select();
|
||||
try {{
|
||||
if (!document.execCommand("copy")) throw new Error("exec");
|
||||
resolve();
|
||||
}} catch (err) {{ reject(err); }}
|
||||
document.body.removeChild(box);
|
||||
}});
|
||||
}}
|
||||
|
||||
function wecomCopy(value) {{
|
||||
if (window.ww && ww.setClipboardData) {{
|
||||
return ww.setClipboardData({{ data: value }});
|
||||
}}
|
||||
return Promise.reject(new Error("no-ww"));
|
||||
}}
|
||||
|
||||
function sign(kind) {{
|
||||
return function (url) {{
|
||||
return fetch("/inquiry-form/jsapi-signature?kind=" + encodeURIComponent(kind)
|
||||
+ "&url=" + encodeURIComponent(url))
|
||||
.then(function (r) {{
|
||||
if (!r.ok) throw new Error("sign");
|
||||
return r.json();
|
||||
}});
|
||||
}};
|
||||
}}
|
||||
|
||||
function tryCopy() {{
|
||||
return wecomCopy(text).catch(function () {{ return browserCopy(text); }})
|
||||
.then(function () {{ show(true, "已复制到剪贴板"); }})
|
||||
.catch(function () {{ show(false, "复制失败,请再点一次按钮"); }});
|
||||
}}
|
||||
|
||||
function boot() {{
|
||||
var ready = Promise.resolve();
|
||||
if (window.ww && ww.register && corpId) {{
|
||||
ready = ww.register({{
|
||||
corpId: corpId,
|
||||
agentId: agentId,
|
||||
jsApiList: ["setClipboardData"],
|
||||
getConfigSignature: sign("corp"),
|
||||
getAgentConfigSignature: sign("agent")
|
||||
}}).catch(function () {{ return null; }});
|
||||
}}
|
||||
ready.then(tryCopy).catch(tryCopy);
|
||||
}}
|
||||
|
||||
document.getElementById("btn").addEventListener("click", function () {{
|
||||
show(true, "正在复制…");
|
||||
tryCopy();
|
||||
}});
|
||||
if (document.readyState === "loading") {{
|
||||
document.addEventListener("DOMContentLoaded", boot);
|
||||
}} else {{
|
||||
boot();
|
||||
}}
|
||||
}})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
复制工单号 H5 的短命 token。
|
||||
|
||||
本文件职责:签发/校验 token → 工单号;URL 只带 token,禁止带 userid/工单号。
|
||||
优先 Redis(HTTP 与 Worker 不同进程必须共享);单测无 Redis 时用内存。
|
||||
禁止:当六态;禁止把工单号拼进公网路径。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import secrets
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from agent.config import get_settings
|
||||
from agent.redis_coord.keys import TOKEN_COPY_TICKET
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 销售可能隔几天才点卡;7 天内同一 token 可重复打开复制。
|
||||
COPY_TTL_SECONDS = 7 * 24 * 60 * 60
|
||||
_FORM_FALLBACK = "https://ai.ytd-scm.com/inquiry-form"
|
||||
|
||||
_MEM: dict[str, tuple[str, float]] = {}
|
||||
_MEM_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def issue_copy_token(work_order_no: str, *, ttl_seconds: int = COPY_TTL_SECONDS) -> str:
|
||||
"""
|
||||
签发复制页 token。
|
||||
|
||||
副作用:写 Redis 或内存。工单号为空则返回空串,调用方不要出跳转按钮。
|
||||
"""
|
||||
wo = (work_order_no or "").strip()
|
||||
if not wo:
|
||||
return ""
|
||||
token = secrets.token_urlsafe(24)
|
||||
if _redis_set(token, wo, ttl_seconds):
|
||||
return token
|
||||
expires = time.time() + ttl_seconds
|
||||
with _MEM_LOCK:
|
||||
_MEM[token] = (wo, expires)
|
||||
logger.info("copy token 走内存 wo=%s(HTTP/Worker 分进程时页面会找不到)", wo)
|
||||
return token
|
||||
|
||||
|
||||
def lookup_copy_token(token: str) -> Optional[str]:
|
||||
"""校验 token 未过期;返回工单号或 None。不消费,可重复打开。"""
|
||||
raw = (token or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
wo = _redis_get(raw)
|
||||
if wo:
|
||||
return wo
|
||||
now = time.time()
|
||||
with _MEM_LOCK:
|
||||
hit = _MEM.get(raw)
|
||||
if not hit:
|
||||
return None
|
||||
wo, exp = hit
|
||||
if exp < now:
|
||||
_MEM.pop(raw, None)
|
||||
return None
|
||||
return wo
|
||||
|
||||
|
||||
def build_copy_ticket_url(work_order_no: str) -> str:
|
||||
"""
|
||||
拼复制页绝对地址:{inquiry-form}/copy/{token}。
|
||||
|
||||
域名来自环境变量,禁止写死测试/正式切换。缺省回合同域名,保证企微能打开。
|
||||
"""
|
||||
token = issue_copy_token(work_order_no)
|
||||
if not token:
|
||||
return ""
|
||||
return f"{_form_base()}/copy/{token}"
|
||||
|
||||
|
||||
def _form_base() -> str:
|
||||
settings = get_settings()
|
||||
form = (settings.inquiry_form_public_base_url or "").strip().rstrip("/")
|
||||
if form:
|
||||
return form
|
||||
pub = (settings.public_base_url or "").strip().rstrip("/")
|
||||
if pub:
|
||||
return f"{pub}/inquiry-form"
|
||||
return _FORM_FALLBACK
|
||||
|
||||
|
||||
def _redis_set(token: str, work_order_no: str, ttl_seconds: int) -> bool:
|
||||
client = _try_redis()
|
||||
if client is None:
|
||||
return False
|
||||
try:
|
||||
client.raw.set(client.key(TOKEN_COPY_TICKET, token), work_order_no, ex=ttl_seconds)
|
||||
return True
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning("copy token Redis 写入失败:%s", exc)
|
||||
return False
|
||||
|
||||
|
||||
def _redis_get(token: str) -> Optional[str]:
|
||||
client = _try_redis()
|
||||
if client is None:
|
||||
return None
|
||||
try:
|
||||
val = client.raw.get(client.key(TOKEN_COPY_TICKET, token))
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning("copy token Redis 读取失败:%s", exc)
|
||||
return None
|
||||
if val is None:
|
||||
return None
|
||||
text = val if isinstance(val, str) else val.decode("utf-8")
|
||||
return text.strip() or None
|
||||
|
||||
|
||||
def _try_redis():
|
||||
"""有 Redis 才用;单测 memory 后端也可用,保证同进程能读到。"""
|
||||
try:
|
||||
from agent.redis_coord import get_redis_runtime
|
||||
|
||||
return get_redis_runtime(get_settings()).client
|
||||
except Exception: # noqa: BLE001
|
||||
return None
|
||||
@@ -270,6 +270,14 @@ class WeComAppClient:
|
||||
logger.info("企微发送成功 touser=%s reply=%s", body.get("touser"), shown)
|
||||
return WeComSendResult(ok=True, errcode=0, errmsg=errmsg, raw=data)
|
||||
|
||||
def get_access_token(self) -> str:
|
||||
"""
|
||||
取出站/JS-SDK 用的 access_token。
|
||||
|
||||
只给同进程 H5 签名用;禁止写进页面或日志。
|
||||
"""
|
||||
return self._get_token()
|
||||
|
||||
def _get_token(self) -> str:
|
||||
with self._lock:
|
||||
now = time.time()
|
||||
|
||||
@@ -190,6 +190,26 @@ class PostgresMessageStore:
|
||||
logger.exception("pg outbox 入队失败")
|
||||
raise
|
||||
|
||||
def has_outbound_dedupe(self, prefix: str) -> bool:
|
||||
"""去重键是否已出过站(前缀匹配)。跨进程看成交卡/文件是否已发。"""
|
||||
head = (prefix or "").strip()
|
||||
if not head:
|
||||
return False
|
||||
try:
|
||||
with connect_psycopg(self._dsn) as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT 1 FROM agent_outbox
|
||||
WHERE dedupe_key LIKE %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(head + "%",),
|
||||
).fetchone()
|
||||
return bool(row)
|
||||
except Exception:
|
||||
logger.exception("查 outbox 去重前缀失败 prefix=%s", head)
|
||||
return False
|
||||
|
||||
def claim_outbound(self) -> Optional[OutboxItem]:
|
||||
timeout = int(self._claim_timeout)
|
||||
with connect_psycopg(self._dsn) as conn:
|
||||
@@ -203,7 +223,7 @@ class PostgresMessageStore:
|
||||
AND claimed_at IS NOT NULL
|
||||
AND claimed_at < NOW() - (%s || ' seconds')::interval
|
||||
)
|
||||
ORDER BY created_at
|
||||
ORDER BY created_at ASC, id ASC
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LIMIT 1
|
||||
)
|
||||
@@ -294,6 +314,36 @@ class PostgresMessageStore:
|
||||
def wait_outbox(self, timeout: float = 1.0) -> None:
|
||||
self._outbox_event.wait(timeout=timeout)
|
||||
|
||||
def wait_outbound_done(self, item_id: str, timeout_sec: float = 12.0) -> bool:
|
||||
"""
|
||||
等到指定出站已发送完成或失败。
|
||||
|
||||
查价前先等询价确认发出,避免暂无报价卡抢先。只等这一条。
|
||||
"""
|
||||
raw = (item_id or "").strip()
|
||||
if not raw:
|
||||
return False
|
||||
started = time.time()
|
||||
deadline = started + max(0.1, float(timeout_sec))
|
||||
saw_claim = False
|
||||
while time.time() < deadline:
|
||||
with connect_psycopg(self._dsn) as conn:
|
||||
row = conn.execute(
|
||||
"SELECT state FROM agent_outbox WHERE id = %s",
|
||||
(raw,),
|
||||
).fetchone()
|
||||
state = str(row[0] or "") if row else ""
|
||||
if state in {"done", "failed"}:
|
||||
return state == "done"
|
||||
if state == "claimed":
|
||||
saw_claim = True
|
||||
if not saw_claim and state == "pending" and time.time() - started >= 0.4:
|
||||
return True
|
||||
if not state and time.time() - started >= 0.3:
|
||||
return True
|
||||
self._outbox_event.wait(0.05)
|
||||
return False
|
||||
|
||||
def stats(self) -> dict[str, Any]:
|
||||
with connect_psycopg(self._dsn) as conn:
|
||||
inbox = conn.execute(
|
||||
|
||||
@@ -163,6 +163,14 @@ class MemoryMessageStore:
|
||||
logger.info("outbox 入队 id=%s touser=%s", item.id, touser)
|
||||
return True, item.id
|
||||
|
||||
def has_outbound_dedupe(self, prefix: str) -> bool:
|
||||
"""去重键是否已出过站(前缀匹配)。给 wait_file 判断文件/成交卡是否已发。"""
|
||||
head = (prefix or "").strip()
|
||||
if not head:
|
||||
return False
|
||||
with self._lock:
|
||||
return any(str(k).startswith(head) for k in self._seen_out)
|
||||
|
||||
def claim_outbound(self) -> Optional[OutboxItem]:
|
||||
"""认领一条 pending/超时 outbox。"""
|
||||
now = time.time()
|
||||
@@ -207,6 +215,37 @@ class MemoryMessageStore:
|
||||
def wait_outbox(self, timeout: float = 1.0) -> None:
|
||||
self._outbox_event.wait(timeout=timeout)
|
||||
|
||||
def wait_outbound_done(self, item_id: str, timeout_sec: float = 12.0) -> bool:
|
||||
"""
|
||||
等到指定出站已发送完成或失败。
|
||||
|
||||
查价前必须先等询价确认发出,避免暂无报价卡抢先到销售手机。
|
||||
只等这一条,不锁其它工单。超时返回 False。
|
||||
"""
|
||||
raw = (item_id or "").strip()
|
||||
if not raw:
|
||||
return False
|
||||
started = time.time()
|
||||
deadline = started + max(0.1, float(timeout_sec))
|
||||
saw_claim = False
|
||||
while time.time() < deadline:
|
||||
state = ""
|
||||
with self._lock:
|
||||
item = self._outbox.get(raw)
|
||||
if item:
|
||||
state = item.state
|
||||
if state in {TaskState.DONE, TaskState.FAILED}:
|
||||
return state == TaskState.DONE
|
||||
if state == TaskState.CLAIMED:
|
||||
saw_claim = True
|
||||
# 单测没有出站线程:一直 PENDING 就不要空等 12 秒。测服有出站线程会立刻认领。
|
||||
if not saw_claim and state == TaskState.PENDING and time.time() - started >= 0.4:
|
||||
return True
|
||||
if not state and time.time() - started >= 0.3:
|
||||
return True
|
||||
self._outbox_event.wait(0.05)
|
||||
return False
|
||||
|
||||
def stats(self) -> dict[str, Any]:
|
||||
with self._lock:
|
||||
return {
|
||||
|
||||
@@ -40,4 +40,6 @@ class MessageStore(Protocol):
|
||||
|
||||
def wait_outbox(self, timeout: float = 1.0) -> None: ...
|
||||
|
||||
def wait_outbound_done(self, item_id: str, timeout_sec: float = 12.0) -> bool: ...
|
||||
|
||||
def stats(self) -> dict[str, Any]: ...
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
企微 JS-SDK 签名(给复制页写剪贴板)。
|
||||
|
||||
本文件职责:取 jsapi_ticket、按当前页 URL 签名。请求线程只打缓存/短 HTTP。
|
||||
禁止:把 access_token / ticket 写进页面或日志;禁止在这里改工单。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import secrets
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from agent.channel.outbox.sender import WeComAppClient
|
||||
from agent.config import get_settings
|
||||
from agent.redis_coord.keys import TOKEN_JSAPI
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_TICKET_TTL = 3600
|
||||
|
||||
|
||||
def build_jsapi_signature(*, url: str, kind: str = "corp") -> dict[str, Any]:
|
||||
"""
|
||||
给前端 ww.register 用的签名。
|
||||
|
||||
kind=corp → get_jsapi_ticket;kind=agent → agent_config ticket。
|
||||
URL 必须与浏览器地址栏一致(不含 hash)。
|
||||
"""
|
||||
page = (url or "").strip()
|
||||
if not page:
|
||||
raise ValueError("url_empty")
|
||||
ticket = _jsapi_ticket(kind="agent" if kind == "agent" else "corp")
|
||||
if not ticket:
|
||||
raise RuntimeError("jsapi_ticket_missing")
|
||||
nonce = secrets.token_hex(8)
|
||||
timestamp = str(int(time.time()))
|
||||
raw = f"jsapi_ticket={ticket}&noncestr={nonce}×tamp={timestamp}&url={page}"
|
||||
signature = hashlib.sha1(raw.encode("utf-8")).hexdigest()
|
||||
return {
|
||||
"timestamp": int(timestamp),
|
||||
"nonceStr": nonce,
|
||||
"signature": signature,
|
||||
}
|
||||
|
||||
|
||||
def _jsapi_ticket(*, kind: str) -> str:
|
||||
cached = _cache_get(kind)
|
||||
if cached:
|
||||
return cached
|
||||
settings = get_settings()
|
||||
if not (settings.wecom_corp_id and settings.wecom_secret):
|
||||
return ""
|
||||
client = WeComAppClient(
|
||||
corp_id=settings.wecom_corp_id,
|
||||
secret=settings.wecom_secret,
|
||||
agent_id=int(settings.wecom_agent_id or "0") or 0,
|
||||
)
|
||||
token = client.get_access_token()
|
||||
if kind == "agent":
|
||||
api = "https://qyapi.weixin.qq.com/cgi-bin/ticket/get"
|
||||
params = {"access_token": token, "type": "agent_config"}
|
||||
else:
|
||||
api = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket"
|
||||
params = {"access_token": token}
|
||||
with httpx.Client(timeout=8.0) as http:
|
||||
data = http.get(api, params=params).json()
|
||||
errcode = int(data.get("errcode") or 0)
|
||||
ticket = str(data.get("ticket") or "")
|
||||
if errcode != 0 or not ticket:
|
||||
logger.warning("jsapi ticket 失败 kind=%s errcode=%s", kind, errcode)
|
||||
return ""
|
||||
_cache_set(kind, ticket)
|
||||
return ticket
|
||||
|
||||
|
||||
def _cache_get(kind: str) -> Optional[str]:
|
||||
redis = _try_redis()
|
||||
if redis is None:
|
||||
return None
|
||||
try:
|
||||
val = redis.raw.get(redis.key(TOKEN_JSAPI, kind))
|
||||
except Exception: # noqa: BLE001
|
||||
return None
|
||||
if val is None:
|
||||
return None
|
||||
return val if isinstance(val, str) else val.decode("utf-8")
|
||||
|
||||
|
||||
def _cache_set(kind: str, ticket: str) -> None:
|
||||
redis = _try_redis()
|
||||
if redis is None:
|
||||
return
|
||||
try:
|
||||
redis.raw.set(redis.key(TOKEN_JSAPI, kind), ticket, ex=_TICKET_TTL)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning("jsapi ticket 缓存失败:%s", exc)
|
||||
|
||||
|
||||
def _try_redis():
|
||||
try:
|
||||
from agent.redis_coord import get_redis_runtime
|
||||
|
||||
return get_redis_runtime(get_settings()).client
|
||||
except Exception: # noqa: BLE001
|
||||
return None
|
||||
Reference in New Issue
Block a user