"""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"])