Implement student roster login gating
This commit is contained in:
@@ -13,6 +13,7 @@ import type {
|
|||||||
DashboardStats,
|
DashboardStats,
|
||||||
KnowledgeItem,
|
KnowledgeItem,
|
||||||
ModelItem,
|
ModelItem,
|
||||||
|
UserImportResult,
|
||||||
} from "./types/api";
|
} from "./types/api";
|
||||||
|
|
||||||
const admin = ref<AdminProfile | null>(null);
|
const admin = ref<AdminProfile | null>(null);
|
||||||
@@ -40,6 +41,17 @@ const agentPreviewMessages = ref<{ role: "user" | "assistant" | "system"; conten
|
|||||||
{ role: "assistant", content: "选择模型和知识库后,可以在这里调试 Agent 的真实问答效果。" },
|
{ role: "assistant", content: "选择模型和知识库后,可以在这里调试 Agent 的真实问答效果。" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const studentForm = reactive({
|
||||||
|
phone: "",
|
||||||
|
name: "",
|
||||||
|
nickname: "",
|
||||||
|
status: 1,
|
||||||
|
dailyChatLimit: 100,
|
||||||
|
expiredAt: "",
|
||||||
|
});
|
||||||
|
const studentImportText = ref("手机号,姓名,昵称\n13800000001,张三,三三\n13800000002,李四,");
|
||||||
|
const studentImportResult = ref<UserImportResult | null>(null);
|
||||||
|
|
||||||
const knowledgeForm = reactive({
|
const knowledgeForm = reactive({
|
||||||
name: "",
|
name: "",
|
||||||
feishuSpaceId: "",
|
feishuSpaceId: "",
|
||||||
@@ -241,6 +253,59 @@ async function saveUser(row: AdminUser) {
|
|||||||
await loadCurrentMenu();
|
await loadCurrentMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createStudent() {
|
||||||
|
if (!studentForm.phone || !studentForm.name) {
|
||||||
|
ElMessage.error("请填写手机号和姓名");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.createUser({
|
||||||
|
phone: studentForm.phone,
|
||||||
|
name: studentForm.name,
|
||||||
|
nickname: studentForm.nickname || null,
|
||||||
|
status: studentForm.status,
|
||||||
|
dailyChatLimit: studentForm.dailyChatLimit,
|
||||||
|
expiredAt: studentForm.expiredAt || null,
|
||||||
|
});
|
||||||
|
ElMessage.success("学员已添加");
|
||||||
|
resetStudentForm();
|
||||||
|
await loadCurrentMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importStudents() {
|
||||||
|
const students = parseStudentImportText(studentImportText.value);
|
||||||
|
if (!students.length) {
|
||||||
|
ElMessage.error("请粘贴学员数据");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
studentImportResult.value = await api.importUsers(students);
|
||||||
|
ElMessage.success(
|
||||||
|
`导入完成:新增 ${studentImportResult.value.created},更新 ${studentImportResult.value.updated},失败 ${studentImportResult.value.failed}`,
|
||||||
|
);
|
||||||
|
await loadCurrentMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStudentImportText(text: string) {
|
||||||
|
return text
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((line, index) => !(index === 0 && line.includes("手机号")))
|
||||||
|
.map((line) => {
|
||||||
|
const [phone = "", name = "", nickname = ""] = line.split(/,|\t/).map((item) => item.trim());
|
||||||
|
return {
|
||||||
|
phone,
|
||||||
|
name,
|
||||||
|
nickname: nickname || null,
|
||||||
|
status: 1,
|
||||||
|
dailyChatLimit: 100,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetStudentForm() {
|
||||||
|
Object.assign(studentForm, { phone: "", name: "", nickname: "", status: 1, dailyChatLimit: 100, expiredAt: "" });
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteUser(row: AdminUser) {
|
async function deleteUser(row: AdminUser) {
|
||||||
if (!window.confirm(`确认删除用户 ${row.phone} 吗?删除后前台将无法继续使用该账号。`)) return;
|
if (!window.confirm(`确认删除用户 ${row.phone} 吗?删除后前台将无法继续使用该账号。`)) return;
|
||||||
await api.deleteUser(row.id);
|
await api.deleteUser(row.id);
|
||||||
@@ -659,9 +724,55 @@ function buildChatQuery() {
|
|||||||
|
|
||||||
<template v-if="activeMenu === 'users'">
|
<template v-if="activeMenu === 'users'">
|
||||||
<div class="page-head inline">
|
<div class="page-head inline">
|
||||||
<div><h2>用户管理</h2><p>查询用户、启用禁用和调整额度。</p></div>
|
<div><h2>用户管理</h2><p>维护学员名单;只有名单内启用学员可以登录用户端。</p></div>
|
||||||
<el-input v-model="userKeyword" placeholder="手机号或姓名" clearable @change="loadCurrentMenu" />
|
<el-input v-model="userKeyword" placeholder="手机号或姓名" clearable @change="loadCurrentMenu" />
|
||||||
</div>
|
</div>
|
||||||
|
<section class="student-tools">
|
||||||
|
<div class="student-tool-panel">
|
||||||
|
<h3>手动添加学员</h3>
|
||||||
|
<div class="student-form-grid">
|
||||||
|
<el-input v-model="studentForm.phone" placeholder="手机号" />
|
||||||
|
<el-input v-model="studentForm.name" placeholder="姓名" />
|
||||||
|
<el-input v-model="studentForm.nickname" placeholder="昵称(可选)" />
|
||||||
|
<el-input-number v-model="studentForm.dailyChatLimit" :min="0" />
|
||||||
|
<el-date-picker
|
||||||
|
v-model="studentForm.expiredAt"
|
||||||
|
type="datetime"
|
||||||
|
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||||
|
placeholder="有效期(可选)"
|
||||||
|
/>
|
||||||
|
<div class="form-switch-row">
|
||||||
|
<span>启用</span>
|
||||||
|
<el-switch v-model="studentForm.status" :active-value="1" :inactive-value="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions compact">
|
||||||
|
<el-button type="primary" @click="createStudent">添加学员</el-button>
|
||||||
|
<el-button @click="resetStudentForm">清空</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="student-tool-panel">
|
||||||
|
<h3>批量导入学员</h3>
|
||||||
|
<el-input
|
||||||
|
v-model="studentImportText"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
resize="none"
|
||||||
|
placeholder="每行一个学员:手机号,姓名,昵称"
|
||||||
|
/>
|
||||||
|
<div class="actions compact">
|
||||||
|
<el-button type="primary" @click="importStudents">导入学员</el-button>
|
||||||
|
<span v-if="studentImportResult" class="import-result">
|
||||||
|
新增 {{ studentImportResult.created }},更新 {{ studentImportResult.updated }},失败 {{ studentImportResult.failed }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="studentImportResult?.failures.length" class="import-failures">
|
||||||
|
<span v-for="failure in studentImportResult.failures.slice(0, 5)" :key="`${failure.row}-${failure.phone}`">
|
||||||
|
第 {{ failure.row }} 行 {{ failure.phone || '-' }}:{{ failure.reason }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<el-table :data="users" stripe>
|
<el-table :data="users" stripe>
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
<el-table-column prop="phone" label="手机号" width="150" />
|
<el-table-column prop="phone" label="手机号" width="150" />
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
DashboardStats,
|
DashboardStats,
|
||||||
KnowledgeItem,
|
KnowledgeItem,
|
||||||
ModelItem,
|
ModelItem,
|
||||||
|
UserImportResult,
|
||||||
} from "../types/api";
|
} from "../types/api";
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "http://127.0.0.1:8100/api";
|
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "http://127.0.0.1:8100/api";
|
||||||
@@ -73,6 +74,10 @@ export const api = {
|
|||||||
profile: () => request<AdminProfile>("/admin/profile"),
|
profile: () => request<AdminProfile>("/admin/profile"),
|
||||||
dashboard: () => request<DashboardStats>("/admin/dashboard"),
|
dashboard: () => request<DashboardStats>("/admin/dashboard"),
|
||||||
users: (keyword = "") => request<AdminUser[]>(`/admin/user/list?keyword=${encodeURIComponent(keyword)}`),
|
users: (keyword = "") => request<AdminUser[]>(`/admin/user/list?keyword=${encodeURIComponent(keyword)}`),
|
||||||
|
createUser: (payload: Record<string, unknown>) =>
|
||||||
|
request<AdminUser>("/admin/user", { method: "POST", body: JSON.stringify(payload) }),
|
||||||
|
importUsers: (students: Record<string, unknown>[]) =>
|
||||||
|
request<UserImportResult>("/admin/user/import", { method: "POST", body: JSON.stringify({ students }) }),
|
||||||
updateUser: (id: number, payload: Record<string, unknown>) =>
|
updateUser: (id: number, payload: Record<string, unknown>) =>
|
||||||
request<AdminUser>(`/admin/user/${id}`, { method: "PUT", body: JSON.stringify(payload) }),
|
request<AdminUser>(`/admin/user/${id}`, { method: "PUT", body: JSON.stringify(payload) }),
|
||||||
deleteUser: (id: number) => request<null>(`/admin/user/${id}`, { method: "DELETE" }),
|
deleteUser: (id: number) => request<null>(`/admin/user/${id}`, { method: "DELETE" }),
|
||||||
|
|||||||
@@ -185,6 +185,58 @@ textarea {
|
|||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.student-tools {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(520px, 1fr) minmax(420px, 0.9fr);
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-tool-panel {
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #dfe8e5;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-tool-panel h3 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-form-grid .el-input,
|
||||||
|
.student-form-grid .el-input-number,
|
||||||
|
.student-form-grid .el-date-editor {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions.compact {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-result {
|
||||||
|
color: #40524b;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-failures {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #b42318;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.knowledge-form {
|
.knowledge-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(160px, 1.1fr) minmax(180px, 1fr) minmax(180px, 1fr) minmax(180px, 1.2fr) 100px 150px;
|
grid-template-columns: minmax(160px, 1.1fr) minmax(180px, 1fr) minmax(180px, 1fr) minmax(180px, 1.2fr) 100px 150px;
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ export interface AdminUser {
|
|||||||
createdAt?: string | null;
|
createdAt?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserImportFailure {
|
||||||
|
row: number;
|
||||||
|
phone: string;
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserImportResult {
|
||||||
|
created: number;
|
||||||
|
updated: number;
|
||||||
|
failed: number;
|
||||||
|
failures: UserImportFailure[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface KnowledgeItem {
|
export interface KnowledgeItem {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -9,7 +10,7 @@ from app.core.dependencies import get_current_admin
|
|||||||
from app.core.responses import api_success
|
from app.core.responses import api_success
|
||||||
from app.models.admin import Admin
|
from app.models.admin import Admin
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.schemas.admin import AdminUserUpdateRequest
|
from app.schemas.admin import AdminUserCreateRequest, AdminUserImportRequest, AdminUserUpdateRequest
|
||||||
from app.services.admin_service import OperationLogService
|
from app.services.admin_service import OperationLogService
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -29,6 +30,86 @@ def list_users(
|
|||||||
return api_success([_user_dict(user) for user in users])
|
return api_success([_user_dict(user) for user in users])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/user")
|
||||||
|
def create_user(
|
||||||
|
payload: AdminUserCreateRequest,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_admin: Admin = Depends(get_current_admin),
|
||||||
|
) -> dict:
|
||||||
|
phone = _normalize_phone(payload.phone)
|
||||||
|
_validate_phone(phone)
|
||||||
|
existing = db.scalar(select(User).where(User.phone == phone))
|
||||||
|
if existing is not None and not existing.is_deleted:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="手机号已存在")
|
||||||
|
|
||||||
|
if existing is not None:
|
||||||
|
user = existing
|
||||||
|
user.is_deleted = 0
|
||||||
|
user.daily_chat_used = 0
|
||||||
|
else:
|
||||||
|
user = User(phone=phone, daily_chat_used=0)
|
||||||
|
|
||||||
|
_apply_user_payload(user, payload)
|
||||||
|
db.add(user)
|
||||||
|
db.flush()
|
||||||
|
OperationLogService.write(db, admin_id=current_admin.id, module="user", action="create", target_id=user.id)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
return api_success(_user_dict(user))
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/user/import")
|
||||||
|
def import_users(
|
||||||
|
payload: AdminUserImportRequest,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_admin: Admin = Depends(get_current_admin),
|
||||||
|
) -> dict:
|
||||||
|
if not payload.students:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="请提供学员数据")
|
||||||
|
|
||||||
|
created = 0
|
||||||
|
updated = 0
|
||||||
|
failed: list[dict] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
|
||||||
|
for index, item in enumerate(payload.students, start=1):
|
||||||
|
phone = _normalize_phone(item.phone)
|
||||||
|
name = item.name.strip()
|
||||||
|
try:
|
||||||
|
_validate_phone(phone)
|
||||||
|
if not name:
|
||||||
|
raise ValueError("姓名不能为空")
|
||||||
|
if phone in seen:
|
||||||
|
raise ValueError("手机号在本次导入中重复")
|
||||||
|
seen.add(phone)
|
||||||
|
|
||||||
|
user = db.scalar(select(User).where(User.phone == phone))
|
||||||
|
if user is None:
|
||||||
|
user = User(phone=phone, daily_chat_used=0)
|
||||||
|
created += 1
|
||||||
|
else:
|
||||||
|
user.is_deleted = 0
|
||||||
|
updated += 1
|
||||||
|
_apply_user_payload(user, item)
|
||||||
|
db.add(user)
|
||||||
|
except ValueError as exc:
|
||||||
|
failed.append({"row": index, "phone": item.phone, "reason": str(exc)})
|
||||||
|
|
||||||
|
if created or updated:
|
||||||
|
db.flush()
|
||||||
|
OperationLogService.write(
|
||||||
|
db,
|
||||||
|
admin_id=current_admin.id,
|
||||||
|
module="user",
|
||||||
|
action="import",
|
||||||
|
target_id=None,
|
||||||
|
result="SUCCESS",
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return api_success({"created": created, "updated": updated, "failed": len(failed), "failures": failed})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/user/{user_id}")
|
@router.get("/user/{user_id}")
|
||||||
def user_detail(
|
def user_detail(
|
||||||
user_id: int,
|
user_id: int,
|
||||||
@@ -96,3 +177,27 @@ def _user_dict(user: User) -> dict:
|
|||||||
"lastLoginAt": user.last_login_at,
|
"lastLoginAt": user.last_login_at,
|
||||||
"createdAt": user.created_at,
|
"createdAt": user.created_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_user_payload(user: User, payload: AdminUserCreateRequest) -> None:
|
||||||
|
settings_limit = 100
|
||||||
|
try:
|
||||||
|
from app.core.config import get_settings
|
||||||
|
|
||||||
|
settings_limit = get_settings().default_daily_chat_limit
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
user.name = payload.name.strip()
|
||||||
|
user.nickname = payload.nickname.strip() if payload.nickname else None
|
||||||
|
user.status = payload.status
|
||||||
|
user.daily_chat_limit = payload.dailyChatLimit if payload.dailyChatLimit is not None else settings_limit
|
||||||
|
user.expired_at = payload.expiredAt.replace(tzinfo=None) if payload.expiredAt is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_phone(phone: str) -> str:
|
||||||
|
return re.sub(r"\D", "", phone or "")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_phone(phone: str) -> None:
|
||||||
|
if not re.fullmatch(r"1[3-9]\d{9}", phone):
|
||||||
|
raise ValueError("手机号格式不正确")
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/sms/send")
|
@router.post("/sms/send")
|
||||||
def send_sms(payload: SendSmsRequest) -> dict:
|
def send_sms(payload: SendSmsRequest, db: Session = Depends(get_db)) -> dict:
|
||||||
AuthService.send_sms_code(payload.phone)
|
AuthService.send_sms_code(db, payload.phone)
|
||||||
return api_success(message="发送成功")
|
return api_success(message="发送成功")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,28 @@ class AdminUserUpdateRequest(BaseModel):
|
|||||||
expiredAt: datetime | None = None
|
expiredAt: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class AdminUserCreateRequest(BaseModel):
|
||||||
|
phone: str = Field(min_length=11, max_length=20)
|
||||||
|
name: str = Field(min_length=1, max_length=50)
|
||||||
|
nickname: str | None = Field(default=None, max_length=50)
|
||||||
|
status: int = Field(default=1, ge=0, le=1)
|
||||||
|
dailyChatLimit: int | None = Field(default=None, ge=0, le=100000)
|
||||||
|
expiredAt: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class AdminUserImportItem(BaseModel):
|
||||||
|
phone: str = Field(default="", max_length=20)
|
||||||
|
name: str = Field(default="", max_length=50)
|
||||||
|
nickname: str | None = Field(default=None, max_length=50)
|
||||||
|
status: int = Field(default=1, ge=0, le=1)
|
||||||
|
dailyChatLimit: int | None = Field(default=None, ge=0, le=100000)
|
||||||
|
expiredAt: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class AdminUserImportRequest(BaseModel):
|
||||||
|
students: list[AdminUserImportItem] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
class KnowledgeSaveRequest(BaseModel):
|
class KnowledgeSaveRequest(BaseModel):
|
||||||
name: str = Field(min_length=1, max_length=100)
|
name: str = Field(min_length=1, max_length=100)
|
||||||
feishuSpaceId: str = Field(min_length=1, max_length=100)
|
feishuSpaceId: str = Field(min_length=1, max_length=100)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from fastapi import HTTPException, status
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.config import get_settings
|
|
||||||
from app.core.security import create_access_token
|
from app.core.security import create_access_token
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.services.sms_code_service import SmsCodeService
|
from app.services.sms_code_service import SmsCodeService
|
||||||
@@ -16,13 +15,15 @@ class AuthService:
|
|||||||
_revoked_token_jti: set[str] = set()
|
_revoked_token_jti: set[str] = set()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def send_sms_code(cls, phone: str) -> None:
|
def send_sms_code(cls, db: Session, phone: str) -> None:
|
||||||
|
user = cls._get_existing_user(db, phone)
|
||||||
|
cls._ensure_user_can_login(user)
|
||||||
SmsCodeService.send_code(phone)
|
SmsCodeService.send_code(phone)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def login_with_sms(cls, db: Session, phone: str, code: str) -> dict:
|
def login_with_sms(cls, db: Session, phone: str, code: str) -> dict:
|
||||||
SmsCodeService.verify_code(phone, code)
|
SmsCodeService.verify_code(phone, code)
|
||||||
user = cls._get_or_create_user(db, phone)
|
user = cls._get_existing_user(db, phone)
|
||||||
cls._ensure_user_can_login(user)
|
cls._ensure_user_can_login(user)
|
||||||
|
|
||||||
now = datetime.now(UTC).replace(tzinfo=None)
|
now = datetime.now(UTC).replace(tzinfo=None)
|
||||||
@@ -46,24 +47,12 @@ class AuthService:
|
|||||||
return bool(jti and str(jti) in cls._revoked_token_jti)
|
return bool(jti and str(jti) in cls._revoked_token_jti)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_or_create_user(cls, db: Session, phone: str) -> User:
|
def _get_existing_user(cls, db: Session, phone: str) -> User:
|
||||||
user = db.scalar(select(User).where(User.phone == phone, User.is_deleted == 0))
|
user = db.scalar(select(User).where(User.phone == phone, User.is_deleted == 0))
|
||||||
if user is not None:
|
if user is not None:
|
||||||
return user
|
return user
|
||||||
|
|
||||||
settings = get_settings()
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="手机号不在学员名单中,请联系管理员")
|
||||||
user = User(
|
|
||||||
phone=phone,
|
|
||||||
name=f"{settings.default_user_name_prefix}{phone[-4:]}",
|
|
||||||
daily_chat_limit=settings.default_daily_chat_limit,
|
|
||||||
daily_chat_used=0,
|
|
||||||
status=1,
|
|
||||||
is_deleted=0,
|
|
||||||
)
|
|
||||||
db.add(user)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(user)
|
|
||||||
return user
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _ensure_user_can_login(user: User) -> None:
|
def _ensure_user_can_login(user: User) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user