Compare commits
No commits in common. "4a7eb906d18b9f01af772c09efd9d5ee9e950bd7" and "721464975967aa70de776b2931d7bb6ae098464d" have entirely different histories.
4a7eb906d1
...
7214649759
|
|
@ -16,8 +16,7 @@ Thumbs.db
|
|||
*.sublime-*
|
||||
|
||||
# 环境变量 / 密钥
|
||||
.env
|
||||
**/.env
|
||||
# .env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.pem
|
||||
|
|
|
|||
|
|
@ -2,22 +2,12 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.security import create_access_token
|
||||
from app.config import get_settings
|
||||
from app.db import get_db
|
||||
from app.deps import get_current_user
|
||||
from app.integrations.sms_client import SmsClient
|
||||
from app.integrations.wechat_client import WechatMiniClient, WechatMiniClientError
|
||||
from app.models import Permission, User
|
||||
from app.schemas.auth import (
|
||||
CurrentUserResponse,
|
||||
LoginRequest,
|
||||
SmsCodeRequest,
|
||||
TokenResponse,
|
||||
WechatLoginRequest,
|
||||
)
|
||||
from app.schemas.auth import CurrentUserResponse, LoginRequest, SmsCodeRequest, TokenResponse
|
||||
from app.services.audit_service import write_audit
|
||||
from app.services.user_service import get_or_create_login_user
|
||||
|
||||
|
|
@ -70,51 +60,6 @@ def login(payload: LoginRequest, db: Session = Depends(get_db)) -> TokenResponse
|
|||
)
|
||||
|
||||
|
||||
@router.post("/wechat-login", response_model=TokenResponse)
|
||||
def wechat_login(payload: WechatLoginRequest, db: Session = Depends(get_db)) -> TokenResponse:
|
||||
"""微信授权登录:拿手机号在用户表中匹配,找到则登录,找不到提示联系管理员。"""
|
||||
settings = get_settings()
|
||||
client = WechatMiniClient()
|
||||
phone: str | None = None
|
||||
|
||||
if client.configured and payload.phone_code:
|
||||
try:
|
||||
phone = client.get_phone_number(payload.phone_code)
|
||||
except WechatMiniClientError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
elif payload.phone:
|
||||
# 未配置微信凭据或前端未拿到 phone_code 时的兜底(开发/联调)
|
||||
phone = payload.phone.strip()
|
||||
elif not client.configured:
|
||||
# mock 模式且未传 phone:用配置里的测试手机号,方便本地联调
|
||||
phone = settings.wx_mini_mock_phone
|
||||
|
||||
if not phone:
|
||||
raise HTTPException(status_code=400, detail="未获取到微信手机号")
|
||||
|
||||
# 可选:换 openid 做审计追踪,失败不阻断登录
|
||||
if client.configured and payload.js_code:
|
||||
try:
|
||||
client.code2session(payload.js_code)
|
||||
except WechatMiniClientError:
|
||||
pass
|
||||
|
||||
user = db.scalar(select(User).where(User.phone == phone))
|
||||
if user is None:
|
||||
raise HTTPException(status_code=403, detail="未找到匹配账号,请联系管理员添加账号后使用")
|
||||
if user.status != "active":
|
||||
raise HTTPException(status_code=403, detail="账号已禁用")
|
||||
|
||||
user.last_login_at = datetime.utcnow()
|
||||
write_audit(db, "auth.wechat_login", actor_id=user.id, target_type="user", target_id=str(user.id))
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return TokenResponse(
|
||||
access_token=create_access_token(str(user.id)),
|
||||
user=_build_current_user(db, user),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
def logout(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)) -> dict:
|
||||
write_audit(db, "auth.logout", actor_id=current_user.id, target_type="user", target_id=str(current_user.id))
|
||||
|
|
|
|||
|
|
@ -28,10 +28,6 @@ class Settings(BaseSettings):
|
|||
tencent_secret_key: str = ""
|
||||
tencent_region: str = "ap-guangzhou"
|
||||
|
||||
wx_mini_appid: str = ""
|
||||
wx_mini_secret: str = ""
|
||||
wx_mini_mock_phone: str = "13800000000"
|
||||
|
||||
@property
|
||||
def cors_origins(self) -> list[str]:
|
||||
return [origin.strip() for origin in self.allowed_origins.split(",") if origin.strip()]
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
import httpx
|
||||
|
||||
from app.config import get_settings
|
||||
|
||||
|
||||
class WechatMiniClientError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class WechatMiniClient:
|
||||
"""微信小程序 code2session / getPhoneNumber 客户端。
|
||||
|
||||
未配置 appid/secret 时视为 mock 模式,由上层跳过真实调用。
|
||||
"""
|
||||
|
||||
CODE2SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session"
|
||||
ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token"
|
||||
GET_PHONE_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber"
|
||||
|
||||
def __init__(self) -> None:
|
||||
settings = get_settings()
|
||||
self.appid = settings.wx_mini_appid
|
||||
self.secret = settings.wx_mini_secret
|
||||
self.timeout = 10.0
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
return bool(self.appid and self.secret)
|
||||
|
||||
def code2session(self, js_code: str) -> dict:
|
||||
params = {
|
||||
"appid": self.appid,
|
||||
"secret": self.secret,
|
||||
"js_code": js_code,
|
||||
"grant_type": "authorization_code",
|
||||
}
|
||||
with httpx.Client(timeout=self.timeout, trust_env=False) as client:
|
||||
resp = client.get(self.CODE2SESSION_URL, params=params)
|
||||
data = resp.json()
|
||||
if data.get("errcode"):
|
||||
raise WechatMiniClientError(f"code2session 失败: {data.get('errmsg')}")
|
||||
return data
|
||||
|
||||
def _access_token(self) -> str:
|
||||
params = {
|
||||
"grant_type": "client_credential",
|
||||
"appid": self.appid,
|
||||
"secret": self.secret,
|
||||
}
|
||||
with httpx.Client(timeout=self.timeout, trust_env=False) as client:
|
||||
resp = client.get(self.ACCESS_TOKEN_URL, params=params)
|
||||
data = resp.json()
|
||||
token = data.get("access_token")
|
||||
if not token:
|
||||
raise WechatMiniClientError(f"获取 access_token 失败: {data.get('errmsg')}")
|
||||
return token
|
||||
|
||||
def get_phone_number(self, phone_code: str) -> str:
|
||||
token = self._access_token()
|
||||
url = f"{self.GET_PHONE_URL}?access_token={token}"
|
||||
with httpx.Client(timeout=self.timeout, trust_env=False) as client:
|
||||
resp = client.post(url, json={"code": phone_code})
|
||||
data = resp.json()
|
||||
if data.get("errcode"):
|
||||
raise WechatMiniClientError(f"getPhoneNumber 失败: {data.get('errmsg')}")
|
||||
phone_info = data.get("phone_info") or {}
|
||||
phone = phone_info.get("purePhoneNumber") or phone_info.get("phoneNumber")
|
||||
if not phone:
|
||||
raise WechatMiniClientError("getPhoneNumber 未返回手机号")
|
||||
return phone
|
||||
|
|
@ -10,15 +10,6 @@ class LoginRequest(BaseModel):
|
|||
code: str = Field(min_length=4, max_length=8)
|
||||
|
||||
|
||||
class WechatLoginRequest(BaseModel):
|
||||
# wx.login() 返回的 jscode,用于换 openid(可选,仅记录用途)
|
||||
js_code: str | None = None
|
||||
# button open-type="getPhoneNumber" 事件返回的 code,用于换手机号
|
||||
phone_code: str | None = None
|
||||
# mock 模式或跳过手机号服务时,前端可直接传手机号(生产可禁用)
|
||||
phone: str | None = None
|
||||
|
||||
|
||||
class CurrentUserResponse(BaseModel):
|
||||
id: int
|
||||
phone: str
|
||||
|
|
|
|||
|
|
@ -1,9 +1,27 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import logoUrl from '@/assets/logo.png'
|
||||
|
||||
const store = useAppStore()
|
||||
const displayName = computed(() => store.currentUser?.name || '未登录')
|
||||
const avatarInitial = computed(() => (displayName.value || '?')[0].toUpperCase())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="topbar">
|
||||
<img class="topbar-logo" :src="logoUrl" alt="中国建筑材料科学研究总院有限公司" />
|
||||
<div class="top-actions">
|
||||
<span class="status-pill" :class="{ muted: !store.healthy }">
|
||||
<span class="status-dot"></span>
|
||||
{{ store.healthy ? '已连接' : '未连接' }}
|
||||
</span>
|
||||
<button class="topbar-icon" type="button" title="通知">🔔</button>
|
||||
<div class="topbar-user">
|
||||
<div class="topbar-user-avatar">{{ avatarInitial }}</div>
|
||||
<span class="topbar-user-name">{{ displayName }}</span>
|
||||
<span class="topbar-user-caret">▾</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ button {
|
|||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 0 24px;
|
||||
background: transparent;
|
||||
|
|
@ -478,7 +478,7 @@ button {
|
|||
grid-template-columns: 1fr auto;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
padding: 24px 28px;
|
||||
border-radius: 14px;
|
||||
background:
|
||||
radial-gradient(circle at 90% 50%, #ffd1d4 0%, transparent 60%),
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ App({
|
|||
globalData: {
|
||||
// 后端 API 基址:开发环境需要在开发者工具「详情 - 本地设置」中勾选
|
||||
// 「不校验合法域名、web-view (业务域名)、TLS 版本以及 HTTPS 证书」
|
||||
apiBase: 'https://finai.ctc-zc.com:8775',
|
||||
apiBase: 'http://localhost:8000',
|
||||
token: '',
|
||||
user: null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,6 @@
|
|||
"pages/home/home",
|
||||
"pages/chat/chat"
|
||||
],
|
||||
"plugins": {
|
||||
"WechatSI": {
|
||||
"version": "0.3.5",
|
||||
"provider": "wx069ba97219f66d99"
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB |
|
|
@ -8,7 +8,6 @@ Page({
|
|||
code: '',
|
||||
sending: false,
|
||||
submitting: false,
|
||||
wxSubmitting: false,
|
||||
countdown: 0
|
||||
},
|
||||
|
||||
|
|
@ -70,54 +69,6 @@ Page({
|
|||
}
|
||||
},
|
||||
|
||||
wxLoginCode() {
|
||||
return new Promise((resolve) => {
|
||||
wx.login({
|
||||
success: (res) => resolve(res && res.code ? res.code : ''),
|
||||
fail: () => resolve('')
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async onGetPhoneNumber(e) {
|
||||
if (this.data.wxSubmitting) return
|
||||
const detail = e.detail || {}
|
||||
// 用户拒绝授权
|
||||
if (!detail.code && !detail.encryptedData) {
|
||||
wx.showToast({ title: '已取消微信授权', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ wxSubmitting: true })
|
||||
try {
|
||||
const jsCode = await this.wxLoginCode()
|
||||
const payload = { js_code: jsCode, phone_code: detail.code || '' }
|
||||
const result = await request('/api/auth/wechat-login', {
|
||||
method: 'POST',
|
||||
data: payload
|
||||
})
|
||||
app.globalData.token = result.access_token
|
||||
app.globalData.user = result.user
|
||||
wx.setStorageSync('token', result.access_token)
|
||||
wx.setStorageSync('user', result.user)
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
wx.redirectTo({ url: '/pages/home/home' })
|
||||
} catch (err) {
|
||||
const msg = (err && err.message) || '微信登录失败'
|
||||
if (msg.indexOf('联系管理员') !== -1) {
|
||||
wx.showModal({
|
||||
title: '无法登录',
|
||||
content: '未找到匹配账号,请联系管理员添加账号后使用',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了'
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: msg, icon: 'none' })
|
||||
}
|
||||
} finally {
|
||||
this.setData({ wxSubmitting: false })
|
||||
}
|
||||
},
|
||||
|
||||
async login() {
|
||||
if (this.data.submitting) return
|
||||
const phone = (this.data.phone || '').trim()
|
||||
|
|
|
|||
|
|
@ -49,20 +49,6 @@
|
|||
{{submitting ? '登录中…' : '登录'}}
|
||||
</button>
|
||||
|
||||
<view class="divider"><text>或</text></view>
|
||||
|
||||
<view class="wechat-entry">
|
||||
<button
|
||||
class="wechat-icon-btn {{wxSubmitting ? 'disabled' : ''}}"
|
||||
open-type="getPhoneNumber"
|
||||
bindgetphonenumber="onGetPhoneNumber"
|
||||
disabled="{{wxSubmitting || submitting}}"
|
||||
>
|
||||
<image class="wechat-icon-img" src="/images/wchat.png" mode="aspectFit" />
|
||||
</button>
|
||||
<text class="wechat-entry-tip">{{wxSubmitting ? '授权中…' : '微信授权登录'}}</text>
|
||||
</view>
|
||||
|
||||
<view class="tip">测试账号:13800000000 / 验证码 123456</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
|
|||
|
|
@ -144,52 +144,3 @@
|
|||
font-size: 22rpx;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin: 24rpx 0 8rpx;
|
||||
color: var(--muted);
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.divider::before,
|
||||
.divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1rpx;
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
.wechat-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.wechat-icon-btn {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: normal;
|
||||
}
|
||||
.wechat-icon-btn::after {
|
||||
border: none;
|
||||
}
|
||||
.wechat-icon-btn.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.wechat-icon-img {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
}
|
||||
.wechat-entry-tip {
|
||||
font-size: 24rpx;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue