feat: implement WeChat mini login with openid bind and phone match
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
89d9471f37
commit
efd7efbc69
15 files changed
+524
-3
No files matched your search
@@ -50,6 +50,7 @@ DATABASE_URL=mysql+pymysql://root:password@localhost:3306/zkxw
|
||||
|
||||
# 微信支付配置示例
|
||||
# 小程序支付使用 JSAPI(trade_type=JSAPI)
|
||||
# 同时复用于小程序微信登录(code2session / 手机号):WXPAY_MINI_APPID + WXPAY_SECRET
|
||||
# WXPAY_MCHID=1900000109
|
||||
# WXPAY_MINI_APPID=wx1234567890abcdef
|
||||
# WXPAY_SECRET=your-miniapp-secret
|
||||
|
||||
@@ -34,6 +34,7 @@ from app.api.cleanroom_garment import router as cleanroom_garment_router
|
||||
from app.api.market import router as market_router
|
||||
from app.api.pre_transit_sample import router as pre_transit_sample_router
|
||||
from app.api.oa_approval import router as oa_approval_router
|
||||
from app.api.mini_auth import router as mini_auth_router
|
||||
|
||||
__all__ = [
|
||||
"public_router",
|
||||
@@ -71,4 +72,5 @@ __all__ = [
|
||||
"market_router",
|
||||
"pre_transit_sample_router",
|
||||
"oa_approval_router",
|
||||
"mini_auth_router",
|
||||
]
|
||||
@@ -0,0 +1,130 @@
|
||||
"""小程序鉴权:微信登录 / 绑定 openid。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.auth import get_current_user_by_token
|
||||
from app.core.database import get_db
|
||||
from app.core.response import error_response, success_response
|
||||
from app.crud.user import user_crud
|
||||
from app.services.issue_user_tokens import issue_tokens_for_user
|
||||
from app.services.wechat_mini_auth import (
|
||||
WechatMiniAuthError,
|
||||
code2session,
|
||||
get_phone_number,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/mini/auth", tags=["小程序鉴权"])
|
||||
|
||||
|
||||
async def _resolve_user_id(request: Request, db: Session) -> str:
|
||||
authorization = request.headers.get("Authorization", "")
|
||||
if not authorization.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="未授权")
|
||||
token = authorization.replace("Bearer ", "")
|
||||
current = await get_current_user_by_token(token=token, db=db)
|
||||
user_id = current.get("user_id") or current.get("sub")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="未授权")
|
||||
return user_id
|
||||
|
||||
|
||||
@router.post("/wechat-login", response_model=dict)
|
||||
async def wechat_login(body: dict, db: Session = Depends(get_db)):
|
||||
"""微信授权登录:优先 openid;未绑定则用手机号匹配员工并绑定。"""
|
||||
login_code = (body.get("loginCode") or "").strip()
|
||||
phone_code = (body.get("phoneCode") or "").strip() or None
|
||||
|
||||
try:
|
||||
sess = code2session(login_code)
|
||||
except WechatMiniAuthError as exc:
|
||||
return error_response(400, str(exc))
|
||||
|
||||
openid = sess["openid"]
|
||||
user = user_crud.get_by_wx_openid(db, openid)
|
||||
if user:
|
||||
try:
|
||||
data = await issue_tokens_for_user(db, user)
|
||||
except ValueError as exc:
|
||||
return error_response(403, str(exc))
|
||||
return success_response(data)
|
||||
|
||||
if not phone_code:
|
||||
return error_response(400, "请授权手机号登录,或使用账号密码登录")
|
||||
|
||||
try:
|
||||
phone = get_phone_number(phone_code)
|
||||
except WechatMiniAuthError as exc:
|
||||
return error_response(400, str(exc))
|
||||
|
||||
users = user_crud.list_active_by_phone_no(db, phone)
|
||||
if not users:
|
||||
return error_response(
|
||||
401, "未找到与该手机号匹配的员工账号,请使用账号密码登录"
|
||||
)
|
||||
if len(users) > 1:
|
||||
return error_response(400, "手机号对应多个账号,请使用账号密码登录")
|
||||
|
||||
user = users[0]
|
||||
other = user_crud.get_by_wx_openid(db, openid)
|
||||
if other and other.id != user.id:
|
||||
return error_response(409, "该微信已绑定其他账号")
|
||||
existing_oid = (getattr(user, "wx_openid", None) or "").strip()
|
||||
if existing_oid and existing_oid != openid:
|
||||
return error_response(409, "该账号已绑定其他微信,请联系管理员")
|
||||
|
||||
user.wx_openid = openid
|
||||
user.update_time = datetime.now()
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
try:
|
||||
data = await issue_tokens_for_user(db, user)
|
||||
except ValueError as exc:
|
||||
return error_response(403, str(exc))
|
||||
return success_response(data)
|
||||
|
||||
|
||||
@router.post("/wechat-bind", response_model=dict)
|
||||
async def wechat_bind(
|
||||
request: Request,
|
||||
body: dict,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""密码登录后绑定当前微信 openid(需 Bearer)。"""
|
||||
try:
|
||||
user_id = await _resolve_user_id(request, db)
|
||||
except HTTPException:
|
||||
return error_response(401, "未授权")
|
||||
|
||||
login_code = (body.get("loginCode") or "").strip()
|
||||
try:
|
||||
sess = code2session(login_code)
|
||||
except WechatMiniAuthError as exc:
|
||||
return error_response(400, str(exc))
|
||||
|
||||
openid = sess["openid"]
|
||||
user = user_crud.get(db, user_id)
|
||||
if not user or user.del_flag != 0:
|
||||
return error_response(401, "用户不存在")
|
||||
|
||||
other = user_crud.get_by_wx_openid(db, openid)
|
||||
if other and other.id != user.id:
|
||||
return error_response(409, "该微信已绑定其他账号")
|
||||
|
||||
existing_oid = (getattr(user, "wx_openid", None) or "").strip()
|
||||
if existing_oid == openid:
|
||||
return success_response({"bound": True, "message": "已绑定"})
|
||||
|
||||
if existing_oid and existing_oid != openid:
|
||||
return error_response(409, "该账号已绑定其他微信,请联系管理员")
|
||||
|
||||
user.wx_openid = openid
|
||||
user.update_time = datetime.now()
|
||||
db.add(user)
|
||||
db.commit()
|
||||
return success_response({"bound": True, "message": "绑定成功"})
|
||||
@@ -134,6 +134,7 @@ class Settings(BaseSettings):
|
||||
"/api/web/auth/login-rsa-public-key,"
|
||||
"/api/web/auth/login,"
|
||||
"/api/web/auth/refresh-token,"
|
||||
"/api/mini/auth/wechat-login,"
|
||||
"/api/web/admin/attachment/upload,"
|
||||
"/api/web/admin/products/upload-image,"
|
||||
"/api/web/admin/categories/upload-image,"
|
||||
|
||||
@@ -31,6 +31,30 @@ class _UserCRUDCompat(CRUDBase[SysUser]):
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_by_wx_openid(self, db: Session, wx_openid: str) -> Optional[SysUser]:
|
||||
oid = (wx_openid or "").strip()
|
||||
if not oid:
|
||||
return None
|
||||
return (
|
||||
db.query(SysUser)
|
||||
.filter(SysUser.wx_openid == oid, SysUser.del_flag == 0)
|
||||
.first()
|
||||
)
|
||||
|
||||
def list_active_by_phone_no(self, db: Session, phone_no: str) -> List[SysUser]:
|
||||
phone = (phone_no or "").strip()
|
||||
if not phone:
|
||||
return []
|
||||
return (
|
||||
db.query(SysUser)
|
||||
.filter(
|
||||
SysUser.phone_no == phone,
|
||||
SysUser.del_flag == 0,
|
||||
SysUser.status == "1",
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
user_crud = _UserCRUDCompat(SysUser)
|
||||
|
||||
|
||||
+2
-1
@@ -13,7 +13,7 @@ from app.core.database import SessionLocal
|
||||
from app.core.api_request_log_middleware import ApiRequestLogMiddleware
|
||||
from app.core.api_crypto_middleware import ApiCryptoMiddleware
|
||||
from app.models import models # noqa: F401
|
||||
from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router
|
||||
from app.api import public_router, menu_router, admin_router, workflow_router, basic_info_router, collection_protocol_router, leave_router, operations_router, ethnic_router, sample_reception_router, sample_production_reception_router, qc_router, research_donation_router, production_router, pilot_service_router, insurance_company_router, material_router, storage_release_review_router, sample_sms_notification_router, storage_entry_sms_notification_router, qc_sms_notification_router, protocol_sample_collection_router, research_donation_sample_collection_router, pilot_application_router, pilot_checkup_router, pilot_delivery_router, pilot_infusion_confirm_router, pilot_infusion_schedule_mgmt_router, outbound_release_review_router, clinical_payment_router, env_detect_router, cleanroom_garment_router, market_router, pre_transit_sample_router, oa_approval_router, mini_auth_router
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -133,6 +133,7 @@ app.include_router(cleanroom_garment_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(market_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(pre_transit_sample_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(oa_approval_router, prefix=settings.API_V1_PREFIX)
|
||||
app.include_router(mini_auth_router, prefix=settings.API_V1_PREFIX)
|
||||
|
||||
|
||||
def _mask_secret(value: str) -> str:
|
||||
|
||||
@@ -31,6 +31,7 @@ class SysUser(Base):
|
||||
nick_name = Column(String(30), nullable=False, comment="用户名")
|
||||
email = Column(String(50), nullable=False, default="", comment="用户邮箱")
|
||||
phone_no = Column(String(11), nullable=False, default="", comment="手机号码")
|
||||
wx_openid = Column(String(64), nullable=True, comment="微信小程序 openid")
|
||||
hire_date = Column(Date, nullable=True, comment="入职日期")
|
||||
probation_months = Column(Integer, nullable=True, comment="试用期月数")
|
||||
last_work_day = Column(Date, nullable=True, comment="最后工作日")
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""为已通过校验的 SysUser 签发与 web 登录一致的 JWT 会话包。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.auth import (
|
||||
create_access_token,
|
||||
create_refresh_token,
|
||||
store_refresh_token,
|
||||
store_session,
|
||||
)
|
||||
from app.core.config import settings
|
||||
from app.core.utils import generate_uuid
|
||||
from app.models.models import SysUser
|
||||
from app.services.web_auth_roles import (
|
||||
get_user_role_names,
|
||||
resolve_web_role_for_user,
|
||||
user_has_active_roles,
|
||||
)
|
||||
|
||||
|
||||
def _avatar_url_field(user: SysUser) -> Optional[str]:
|
||||
raw = getattr(user, "avatar", None)
|
||||
if not raw:
|
||||
return None
|
||||
s = str(raw).strip()
|
||||
return s if s else None
|
||||
|
||||
|
||||
def build_user_info_payload(user: SysUser, web_role: str, role_names: list = None) -> dict:
|
||||
phone = (getattr(user, "phone_no", None) or "").strip() or None
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.user_no,
|
||||
"realName": user.nick_name,
|
||||
"phone": phone,
|
||||
"email": (getattr(user, "email", None) or "").strip() or None,
|
||||
"hireDate": user.hire_date.strftime("%Y-%m-%d") if getattr(user, "hire_date", None) else None,
|
||||
"probationMonths": getattr(user, "probation_months", None),
|
||||
"lastWorkDay": user.last_work_day.strftime("%Y-%m-%d")
|
||||
if getattr(user, "last_work_day", None)
|
||||
else None,
|
||||
"sex": getattr(user, "sex", None) or "0",
|
||||
"remark": (getattr(user, "remark", None) or "").strip() or None,
|
||||
"role": web_role,
|
||||
"roleNames": role_names or [],
|
||||
"regionId": user.dept_id,
|
||||
"regionName": None,
|
||||
"avatarUrl": _avatar_url_field(user),
|
||||
}
|
||||
|
||||
|
||||
def _jwt_user_payload(user: SysUser, web_role: str) -> dict:
|
||||
return {
|
||||
"sub": user.id,
|
||||
"username": user.user_no,
|
||||
"role": web_role,
|
||||
"region_id": user.dept_id,
|
||||
"real_name": user.nick_name,
|
||||
}
|
||||
|
||||
|
||||
async def issue_tokens_for_user(db: Session, user: SysUser) -> dict:
|
||||
"""校验停用/角色后签发 token。失败抛 ValueError(中文 message)。"""
|
||||
if user.status != "1":
|
||||
raise ValueError("账号已被停用,请联系管理员")
|
||||
if not user_has_active_roles(db, user.id):
|
||||
raise ValueError("账号未分配角色,请联系管理员")
|
||||
|
||||
web_role = resolve_web_role_for_user(db, user.id)
|
||||
role_names = get_user_role_names(db, user.id)
|
||||
|
||||
session_id = generate_uuid()
|
||||
access_jti = generate_uuid()
|
||||
refresh_jti = generate_uuid()
|
||||
|
||||
access_token = create_access_token(
|
||||
data=_jwt_user_payload(user, web_role),
|
||||
session_id=session_id,
|
||||
token_jti=access_jti,
|
||||
)
|
||||
refresh_token = create_refresh_token(
|
||||
data={
|
||||
"sub": user.id,
|
||||
"username": user.user_no,
|
||||
"role": web_role,
|
||||
},
|
||||
session_id=session_id,
|
||||
refresh_jti=refresh_jti,
|
||||
)
|
||||
|
||||
await store_session(session_id=session_id, user_id=user.id)
|
||||
await store_refresh_token(refresh_jti=refresh_jti, user_id=user.id)
|
||||
|
||||
return {
|
||||
"token": access_token,
|
||||
"refreshToken": refresh_token,
|
||||
"expiresIn": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||||
"userInfo": build_user_info_payload(user, web_role, role_names),
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
"""微信小程序登录:code2session、手机号快速验证。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import httpx
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_WX_CODE2SESSION = "https://api.weixin.qq.com/sns/jscode2session"
|
||||
_WX_TOKEN = "https://api.weixin.qq.com/cgi-bin/token"
|
||||
_WX_PHONE = "https://api.weixin.qq.com/wxa/business/getuserphonenumber"
|
||||
|
||||
|
||||
class WechatMiniAuthError(Exception):
|
||||
def __init__(self, message: str, *, wx_errcode: Optional[int] = None):
|
||||
super().__init__(message)
|
||||
self.wx_errcode = wx_errcode
|
||||
|
||||
|
||||
def normalize_cn_mobile(raw: Optional[str]) -> str:
|
||||
if raw is None:
|
||||
return ""
|
||||
s = re.sub(r"\s+", "", str(raw).strip())
|
||||
if s.startswith("+86"):
|
||||
s = s[3:]
|
||||
elif s.startswith("86") and len(s) == 13:
|
||||
s = s[2:]
|
||||
return s
|
||||
|
||||
|
||||
def _require_mini_credentials() -> tuple:
|
||||
appid = (settings.WXPAY_MINI_APPID or "").strip()
|
||||
secret = (settings.WXPAY_SECRET or "").strip()
|
||||
if not appid or not secret:
|
||||
raise WechatMiniAuthError("未配置小程序凭证(WXPAY_MINI_APPID / WXPAY_SECRET)")
|
||||
return appid, secret
|
||||
|
||||
|
||||
def code2session(login_code: str) -> Dict[str, Any]:
|
||||
code = (login_code or "").strip()
|
||||
if not code:
|
||||
raise WechatMiniAuthError("loginCode 不能为空")
|
||||
appid, secret = _require_mini_credentials()
|
||||
with httpx.Client(timeout=15.0) as client:
|
||||
r = client.get(
|
||||
_WX_CODE2SESSION,
|
||||
params={
|
||||
"appid": appid,
|
||||
"secret": secret,
|
||||
"js_code": code,
|
||||
"grant_type": "authorization_code",
|
||||
},
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
if data.get("errcode"):
|
||||
logger.warning("code2session failed: %s", data)
|
||||
raise WechatMiniAuthError(
|
||||
"微信登录凭证无效或已过期", wx_errcode=data.get("errcode")
|
||||
)
|
||||
openid = data.get("openid")
|
||||
if not openid:
|
||||
raise WechatMiniAuthError("微信未返回 openid")
|
||||
return {
|
||||
"openid": openid,
|
||||
"session_key": data.get("session_key"),
|
||||
"unionid": data.get("unionid"),
|
||||
}
|
||||
|
||||
|
||||
def _get_access_token() -> str:
|
||||
appid, secret = _require_mini_credentials()
|
||||
with httpx.Client(timeout=15.0) as client:
|
||||
r = client.get(
|
||||
_WX_TOKEN,
|
||||
params={
|
||||
"grant_type": "client_credential",
|
||||
"appid": appid,
|
||||
"secret": secret,
|
||||
},
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
token = data.get("access_token")
|
||||
if not token:
|
||||
logger.warning("get access_token failed: %s", data)
|
||||
raise WechatMiniAuthError(
|
||||
"获取微信 access_token 失败", wx_errcode=data.get("errcode")
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
def get_phone_number(phone_code: str) -> str:
|
||||
code = (phone_code or "").strip()
|
||||
if not code:
|
||||
raise WechatMiniAuthError("phoneCode 不能为空")
|
||||
token = _get_access_token()
|
||||
with httpx.Client(timeout=15.0) as client:
|
||||
r = client.post(
|
||||
f"{_WX_PHONE}?access_token={token}",
|
||||
json={"code": code},
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
if data.get("errcode") not in (0, None):
|
||||
logger.warning("getuserphonenumber failed: %s", data)
|
||||
raise WechatMiniAuthError(
|
||||
"获取手机号失败,请改用账号密码登录",
|
||||
wx_errcode=data.get("errcode"),
|
||||
)
|
||||
info = data.get("phone_info") or {}
|
||||
phone = normalize_cn_mobile(
|
||||
info.get("purePhoneNumber") or info.get("phoneNumber")
|
||||
)
|
||||
if not phone:
|
||||
raise WechatMiniAuthError("微信未返回手机号")
|
||||
return phone
|
||||
@@ -0,0 +1,15 @@
|
||||
"""手机号规范化单测(微信登录匹配 sys_user.phone_no)。"""
|
||||
from app.services.wechat_mini_auth import normalize_cn_mobile
|
||||
|
||||
|
||||
def test_normalize_strips_plus_86():
|
||||
assert normalize_cn_mobile("+8613800138000") == "13800138000"
|
||||
|
||||
|
||||
def test_normalize_strips_spaces():
|
||||
assert normalize_cn_mobile(" 138 0013 8000 ") == "13800138000"
|
||||
|
||||
|
||||
def test_normalize_empty():
|
||||
assert normalize_cn_mobile("") == ""
|
||||
assert normalize_cn_mobile(None) == ""
|
||||
@@ -0,0 +1,8 @@
|
||||
-- 员工微信小程序 openid 绑定字段(微信授权登录)
|
||||
-- MySQL 5.7:唯一索引允许多个 NULL
|
||||
|
||||
ALTER TABLE `sys_user`
|
||||
ADD COLUMN `wx_openid` varchar(64) DEFAULT NULL COMMENT '微信小程序 openid' AFTER `phone_no`;
|
||||
|
||||
ALTER TABLE `sys_user`
|
||||
ADD UNIQUE INDEX `uk_sys_user_wx_openid` (`wx_openid`);
|
||||
@@ -50,7 +50,7 @@
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "",
|
||||
"appid" : "wxa261ad85f2c99b26",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
|
||||
@@ -74,7 +74,23 @@
|
||||
|
||||
<view class="login-divider">或</view>
|
||||
|
||||
<view class="login-wechat is-disabled">
|
||||
<button
|
||||
v-if="agreed"
|
||||
class="login-wechat"
|
||||
:class="{ 'is-disabled': loading }"
|
||||
open-type="getPhoneNumber"
|
||||
:disabled="loading"
|
||||
@getphonenumber="onGetPhoneNumber"
|
||||
>
|
||||
<image class="login-wechat__icon" src="/static/wechat-logo.png" mode="aspectFit" />
|
||||
<text>微信授权登录</text>
|
||||
</button>
|
||||
<view
|
||||
v-else
|
||||
class="login-wechat"
|
||||
:class="{ 'is-disabled': loading }"
|
||||
@click="onWechatGate"
|
||||
>
|
||||
<image class="login-wechat__icon" src="/static/wechat-logo.png" mode="aspectFit" />
|
||||
<text>微信授权登录</text>
|
||||
</view>
|
||||
@@ -105,6 +121,7 @@
|
||||
import {
|
||||
isLoggedIn,
|
||||
passwordLogin,
|
||||
wechatLogin,
|
||||
clearLegacyMockSessionIfNeeded
|
||||
} from '../../utils/auth.js'
|
||||
import { validateCredentials } from '../../utils/validate-credentials.js'
|
||||
@@ -199,6 +216,50 @@
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async onWechatGate() {
|
||||
if (this.loading) return
|
||||
await this.ensureAgree()
|
||||
},
|
||||
async onGetPhoneNumber(e) {
|
||||
if (this.loading) return
|
||||
if (!(await this.ensureAgree())) return
|
||||
const detail = (e && e.detail) || {}
|
||||
const errMsg = detail.errMsg || ''
|
||||
if (errMsg && errMsg.indexOf('ok') === -1) {
|
||||
uni.showToast({
|
||||
title: '需要授权手机号,或请使用账号密码登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
const phoneCode = detail.code
|
||||
if (!phoneCode) {
|
||||
uni.showToast({
|
||||
title: '需要授权手机号,或请使用账号密码登录',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
uni.login({ provider: 'weixin', success: resolve, fail: reject })
|
||||
})
|
||||
await wechatLogin({
|
||||
loginCode: loginRes.code,
|
||||
phoneCode
|
||||
})
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
setTimeout(() => this.goHome(), 400)
|
||||
} catch (err) {
|
||||
uni.showToast({
|
||||
title: (err && err.message) || '登录失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +159,15 @@
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: normal;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button.login-wechat::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.login-wechat.is-disabled {
|
||||
|
||||
@@ -87,5 +87,49 @@ export async function passwordLogin({ username, password }) {
|
||||
refreshToken: data.refreshToken,
|
||||
userInfo: data.userInfo
|
||||
})
|
||||
// 密码登录成功后静默绑定微信(失败不影响登录)
|
||||
bindWechatQuietly()
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信授权登录(openid 优先;未绑定需 phoneCode)。
|
||||
*/
|
||||
export async function wechatLogin({ loginCode, phoneCode }) {
|
||||
const payload = { loginCode }
|
||||
if (phoneCode) {
|
||||
payload.phoneCode = phoneCode
|
||||
}
|
||||
const data = await post('/mini/auth/wechat-login', payload)
|
||||
if (!data || !data.token) {
|
||||
throw new Error('登录失败')
|
||||
}
|
||||
saveSession({
|
||||
token: data.token,
|
||||
refreshToken: data.refreshToken,
|
||||
userInfo: data.userInfo
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 已登录态下用 wx.login code 绑定 openid;失败静默。
|
||||
*/
|
||||
export async function bindWechatQuietly() {
|
||||
try {
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
uni.login({ provider: 'weixin', success: resolve, fail: reject })
|
||||
})
|
||||
const loginCode = loginRes && loginRes.code
|
||||
if (!loginCode) return
|
||||
const token = getToken()
|
||||
if (!token) return
|
||||
await post(
|
||||
'/mini/auth/wechat-bind',
|
||||
{ loginCode },
|
||||
{ Authorization: 'Bearer ' + token }
|
||||
)
|
||||
} catch (e) {
|
||||
console.warn('wechat bind skipped', e && e.message)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user