"""Django × Tokin:Web 多用户接入示例(把这几个 view 抄进你的 app 即可)。
跑起来(单文件 Django,仅为演示;真实项目请拆进你的 app):
pip install tokincloud django
python examples/django_web.py runserver 127.0.0.1:8000
上线时换两处(其余不动):
APP_ID → 控制台创建应用拿到的 app_xxx
REDIRECT_URI → https://你的域名/oauth/callback(不用登记,首次授权自动绑定)
要点与 FastAPI 版一致:每个用户各自授权、各自扣自己的钱包;凭证落库、自动续期。
Django 项目里 user_id 直接用 request.user.pk 就行(见 current_user)。
"""
import os
import sys
from django.conf import settings
from django.core.wsgi import get_wsgi_application
from django.http import HttpResponse, JsonResponse
from django.shortcuts import redirect
from django.urls import path
from tokincloud import SqliteTokenStore, TokinAuthError, TokinError, TokinWebAuth
BASE_URL = "https://api.tokincloud.com"
APP_ID = "dev_myapp" # 上线换成 app_xxx
# 必须是**固定常量**,不要从 request.base_url / Host 头推导。
# 平台按「首次授权自动绑定、之后只认已绑定的」来防回调被换掉,而同一台服务器上
# http://IP:8321、https://IP:8443、以后的域名**各算一个绑定**——从请求推导的话,
# 你自己的几个变体就会把绑定位占满,之后所有人都授权不了,且现场看不出原因
# (授权页正常、回调正常,只是不认了)。上线就改这一行。
REDIRECT_URI = "http://127.0.0.1:8000/oauth/callback" # 上线换成 https://你的域名/oauth/callback
# 模块级单例:进程内共享一份(内部只有配置与存储句柄,线程安全)
auth = TokinWebAuth(
base_url=BASE_URL,
app_id=APP_ID,
redirect_uri=REDIRECT_URI,
store=SqliteTokenStore("tokin_tokens.db"), # 生产建议换成 Django ORM 的一张表,见文末
)
def current_user(request) -> str:
"""真实项目:return str(request.user.pk)(配 @login_required)。示例用 session 里的 uid。"""
if getattr(request, "user", None) and request.user.is_authenticated:
return str(request.user.pk)
return request.session.get("uid") or "demo-user"
def home(request):
uid = current_user(request)
if not auth.is_authorized(uid):
return HttpResponse(f'
用户 {uid}
连接我的 Tokin 账户
')
return HttpResponse(
f'用户 {uid} 已连接
问一个问题 · '
f'看余额 · 解除绑定
'
)
# ① 发起授权
def connect(request):
return redirect(auth.start(user_id=current_user(request)))
# ② 回调:换凭证并落库。user_id 由 state 还原(URL 里传的一律不可信)
def callback(request):
try:
uid = auth.callback(
code=request.GET.get("code"),
state=request.GET.get("state"),
error=request.GET.get("error"),
)
except TokinAuthError as e:
return HttpResponse(f"连接失败:{e}
重试
", status=400)
request.session["uid"] = uid # 示例用;真实项目这里已经是你自己的登录态
return redirect("/")
# ③ 之后随时用:过期自动续期
def ask(request):
uid = current_user(request)
try:
r = auth.client_for(uid).chat(
model="deepseek-v4-flash",
messages=[{"role": "user", "content": request.GET.get("q", "你好")}],
)
return JsonResponse({"answer": r["choices"][0]["message"]["content"], "usage": r.get("usage")})
except TokinAuthError:
return redirect("/connect") # 授权被吊销 / refresh 失效 → 重新连接
except TokinError as e:
return JsonResponse({"error": str(e), "status": e.status}, status=e.status or 500)
def balance(request):
try:
return JsonResponse({"balance": auth.balance_of(current_user(request))})
except TokinAuthError:
return redirect("/connect")
def disconnect(request):
auth.forget(current_user(request))
return redirect("/")
urlpatterns = [
path("", home),
path("connect", connect),
path("oauth/callback", callback),
path("ask", ask),
path("balance", balance),
path("disconnect", disconnect),
]
# ── 生产:把凭证存进 Django ORM(替代 SqliteTokenStore)──
#
# # models.py
# class TokinToken(models.Model):
# user = models.OneToOneField(User, on_delete=models.CASCADE)
# tokens = models.JSONField()
#
# # tokin_store.py
# from tokincloud import TokenStore
# class OrmTokenStore(TokenStore):
# def save(self, user_id, tokens):
# TokinToken.objects.update_or_create(user_id=user_id, defaults={"tokens": tokens})
# def load(self, user_id):
# row = TokinToken.objects.filter(user_id=user_id).first()
# return row.tokens if row else None
# def delete(self, user_id):
# TokinToken.objects.filter(user_id=user_id).delete()
#
# 多进程(gunicorn -w 4)还需把 pending 换成 Redis 实现,见 fastapi_web.py 文末。
# ── 以下仅为让本文件单独可跑,真实项目不需要 ──
if not settings.configured:
settings.configure(
DEBUG=True,
SECRET_KEY="demo-only-not-for-production",
ROOT_URLCONF=__name__,
ALLOWED_HOSTS=["*"],
INSTALLED_APPS=["django.contrib.sessions"],
MIDDLEWARE=["django.contrib.sessions.middleware.SessionMiddleware"],
SESSION_ENGINE="django.contrib.sessions.backends.signed_cookies",
)
application = get_wsgi_application()
if __name__ == "__main__":
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv or [os.path.basename(__file__), "runserver"])