parent
638d9fe8f3
commit
2a96ce0a3d
|
|
@ -20,16 +20,17 @@ MAX_USAGE_PER = 100
|
|||
CHAT_TTL = 7200
|
||||
|
||||
|
||||
@ai_router.post("/exp")
|
||||
@ai_router.post("/word/exp", deprecated=False)
|
||||
async def dict_exp(
|
||||
request: Request,
|
||||
Q: AIQuestionRequest,
|
||||
user: Tuple[User, Dict] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
|
||||
:param word:
|
||||
:param question: 不允许question为空调用
|
||||
该接口仅用于查词页面且为具有MCP功能的
|
||||
:param request:
|
||||
:param Q:
|
||||
:param user:
|
||||
:return:
|
||||
"""
|
||||
if user[0].token_usage > CHAT_TTL and not user[0].is_admin:
|
||||
|
|
@ -95,6 +96,11 @@ async def dict_exp(
|
|||
raise HTTPException(status_code=500, detail=f"AI调用失败: {str(e)}")
|
||||
|
||||
|
||||
@ai_router.post("/univer")
|
||||
async def universal_main():
|
||||
pass
|
||||
|
||||
|
||||
@ai_router.post("/clear")
|
||||
async def clear_history(word: str, request: Request, user: Tuple[User, Dict] = Depends(get_current_user)):
|
||||
redis = request.app.state.redis
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ CHAT_TTL = 7200
|
|||
async def get_and_set_last_key(redis: Redis, word: str, user_id: str):
|
||||
last_key = f"last_word:{user_id}"
|
||||
last_word = await redis.get(last_key)
|
||||
print(last_word)
|
||||
|
||||
# 如果上一次查的词和这次不同,就清空旧词的记录
|
||||
if last_word and last_word.decode() != word:
|
||||
if last_word and last_word != word:
|
||||
await clear_chat_history(redis, user_id, last_word.decode())
|
||||
|
||||
# 更新当前词
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
from typing import Tuple
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.models import User, CommentFr, CommentJp
|
||||
from app.schemas.comment_schemas import CommentUpload
|
||||
from app.utils.security import get_current_user
|
||||
|
||||
comment_router = APIRouter()
|
||||
|
||||
|
||||
@comment_router.post("/make-comment")
|
||||
async def new_word_comment(
|
||||
upload: CommentUpload,
|
||||
user: Tuple[User, dict] = Depends(get_current_user)
|
||||
) -> None:
|
||||
if upload.lang == "fr":
|
||||
await CommentFr.create(
|
||||
user=user[0],
|
||||
comment_text=upload.comment_content,
|
||||
comment_word=upload.comment_word,
|
||||
)
|
||||
else:
|
||||
await CommentJp.create(
|
||||
user=user[0],
|
||||
comment_text=upload.comment_content,
|
||||
comment_word=upload.comment_word,
|
||||
)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
from pydantic import BaseModel, field_validator, ValidationError
|
||||
|
||||
|
||||
class Feedback(BaseModel):
|
||||
report_part: str
|
||||
text: str
|
||||
|
||||
@classmethod
|
||||
@field_validator("report_part")
|
||||
def report_part_validator(cls, v):
|
||||
types = (
|
||||
"ui_design",
|
||||
"dict_fr",
|
||||
"dict_jp",
|
||||
"user",
|
||||
"translate",
|
||||
"writting",
|
||||
"ai_assist",
|
||||
"pronounce",
|
||||
"comment_api_test", # 该类型仅作测试使用,不对外暴露
|
||||
)
|
||||
if v not in types:
|
||||
raise ValidationError("Invalid feedback report type")
|
||||
return v
|
||||
|
||||
@classmethod
|
||||
@field_validator("text")
|
||||
def text_validator(cls, v):
|
||||
if v is None:
|
||||
raise ValidationError("Feedback text cannot be NULL")
|
||||
return v
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
from typing import Tuple
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.api.make_comments.comment_schemas import Feedback
|
||||
from app.core.email_utils import send_email
|
||||
from app.models import User
|
||||
from app.utils.security import get_current_user
|
||||
|
||||
comment_router = APIRouter()
|
||||
|
||||
|
||||
@comment_router.post("/improvements")
|
||||
async def new_comment(
|
||||
upload: Feedback,
|
||||
user: Tuple[User, dict] = Depends(get_current_user)
|
||||
):
|
||||
user_id = user[0].id
|
||||
username = user[0].name
|
||||
type = upload.report_part
|
||||
mail_text = upload.text
|
||||
sender = "no-reply@lexiverse.com.cn"
|
||||
receivers = ["GodricTan@gmail.com"]
|
||||
|
||||
if type == "dict_fr":
|
||||
receivers.append("aurora@lexiverse.com.cn")
|
||||
|
||||
content = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>用户反馈通知</title>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {{
|
||||
body, .email-body {{ background: #0f172a !important; color: #e5e7eb !important; }}
|
||||
.card {{ background: #111827 !important; border-color: #374151 !important; }}
|
||||
.muted {{ color: #9ca3af !important; }}
|
||||
.badge {{ background: #1f2937 !important; color: #e5e7eb !important; border-color:#374151 !important; }}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f5f7fb;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f5f7fb;">
|
||||
<tr>
|
||||
<td align="center" style="padding:24px;">
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" class="email-body" style="width:600px;max-width:600px;background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e5e7eb;">
|
||||
<tr>
|
||||
<td style="background:linear-gradient(90deg,#4f46e5,#06b6d4);padding:22px 24px;">
|
||||
<h1 style="margin:0;font-size:18px;line-height:1.4;color:#ffffff;">新的用户反馈</h1>
|
||||
<p class="muted" style="margin:4px 0 0 0;font-size:12px;color:rgba(255,255,255,.85);">来自平台反馈中心</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="padding:20px 24px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td style="padding:0 0 12px 0;font-size:14px;color:#111827;">
|
||||
<strong>用户:</strong><span>{username}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:0 0 12px 0;">
|
||||
<span class="badge" style="display:inline-block;padding:6px 10px;border:1px solid #e5e7eb;border-radius:999px;font-size:12px;line-height:1;color:#374151;background:#f9fafb;">
|
||||
反馈板块:{type}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="card" style="margin-top:8px;border:1px solid #e5e7eb;border-radius:10px;background:#ffffff;">
|
||||
<div style="padding:16px 18px;">
|
||||
<div style="font-size:13px;color:#6b7280;margin-bottom:8px;">反馈内容</div>
|
||||
<div style="font-size:15px;line-height:1.7;color:#111827;white-space:pre-wrap;">
|
||||
{mail_text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="muted" style="margin:16px 0 0 0;font-size:12px;color:#6b7280;">
|
||||
您收到此邮件是因为系统检测到有新的反馈提交。请在后台查看详情并进行处理。
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="padding:16px 24px;border-top:1px solid #e5e7eb;">
|
||||
<table width="100%" role="presentation" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td align="left" class="muted" style="font-size:12px;color:#9ca3af;">
|
||||
这是一封系统通知邮件,请勿直接回复。
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
for receiver in receivers:
|
||||
send_email(to_email=receiver, subject="用户反馈", content=content)
|
||||
|
||||
return {"massages": "feedback succeed"}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from fastapi import APIRouter
|
||||
|
||||
pron_test_router = APIRouter()
|
||||
|
|
@ -2,9 +2,9 @@ from typing import Literal, List
|
|||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
from app.api.word_comment.word_comment_schemas import CommentSet
|
||||
from app.models import DefinitionJp, CommentFr, CommentJp
|
||||
from app.models.fr import DefinitionFr
|
||||
from app.schemas.comment_schemas import CommentSet
|
||||
from app.schemas.search_schemas import SearchRequest, SearchResponse, SearchItemFr, SearchItemJp
|
||||
from app.utils.all_kana import all_in_kana
|
||||
from app.utils.autocomplete import suggest_autocomplete
|
||||
|
|
@ -147,3 +147,5 @@ async def search_list(query_word: SearchRequest, user=Depends(get_current_user))
|
|||
print(query_word.query, query_word.language, query_word.sort, query_word.order)
|
||||
word_contents = await suggest_autocomplete(query=query_word)
|
||||
return {"list": word_contents}
|
||||
|
||||
#TODO 用户搜索历史
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ async def validate_password(password: str):
|
|||
detail=f"密码只能包含字母、数字和常见特殊字符 {allowed_specials}"
|
||||
)
|
||||
|
||||
|
||||
async def validate_email_exists(email: str):
|
||||
user = await User.get_or_none(email=email)
|
||||
if user:
|
||||
|
|
@ -99,28 +100,81 @@ async def send_email_code(redis: Redis, email: str, code: str, ops_type: Literal
|
|||
await save_email_code(redis, email, code)
|
||||
|
||||
ops_dict = {
|
||||
"reg" : "用户注册",
|
||||
"reset" : "密码重置",
|
||||
"reg": "用户注册",
|
||||
"reset": "密码重置",
|
||||
}
|
||||
subject = "Lexiverse 用户邮箱验证码"
|
||||
content = f"""
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; line-height:1.6;">
|
||||
<h2 style="color:#4CAF50;">Lexiverse 验证码</h2>
|
||||
<p>您好,</p>
|
||||
<p>您正在进行 <b>{ops_dict[ops_type]}</b> 操作。</p>
|
||||
<p>
|
||||
您的验证码是:
|
||||
<span style="font-size: 24px; font-weight: bold; color: #d9534f;">{code}</span>
|
||||
</p>
|
||||
<p>有效期 5 分钟,请勿泄露给他人。</p>
|
||||
<hr>
|
||||
<p style="font-size: 12px; color: #999;">
|
||||
如果这不是您本人的操作,请忽略此邮件。
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
content = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>Lexiverse 验证码</title>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {{
|
||||
body, .email-body {{ background: #0f172a !important; color: #e5e7eb !important; }}
|
||||
.card {{ background: #111827 !important; border-color: #374151 !important; }}
|
||||
.muted {{ color: #9ca3af !important; }}
|
||||
.code-box {{ background:#1f2937 !important; color:#fff !important; border-color:#374151 !important; }}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f5f7fb;font-family:'Microsoft Yahei','Arial',sans-serif;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f5f7fb;">
|
||||
<tr>
|
||||
<td align="center" style="padding:24px;">
|
||||
<table role="presentation" width="600" cellpadding="0" cellspacing="0" class="email-body" style="width:600px;max-width:600px;background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e5e7eb;">
|
||||
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background:linear-gradient(90deg,#4f46e5,#06b6d4);padding:22px 24px;">
|
||||
<h1 style="margin:0;font-size:18px;line-height:1.4;color:#ffffff;">Lexiverse 验证码</h1>
|
||||
<p class="muted" style="margin:4px 0 0;font-size:12px;color:rgba(255,255,255,.85);">安全身份校验</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Body -->
|
||||
<tr>
|
||||
<td style="padding:20px 24px;">
|
||||
|
||||
<p style="margin-top:0;font-size:15px;color:#111827;">您好,</p>
|
||||
<p style="font-size:15px;color:#111827;">
|
||||
您正在进行 <strong>{ops_dict[ops_type]}</strong> 操作
|
||||
</p>
|
||||
|
||||
<div class="card" style="margin:18px 0;padding:18px;border:1px solid #e5e7eb;border-radius:10px;background:#ffffff;text-align:center;">
|
||||
<div style="font-size:13px;color:#6b7280;margin-bottom:6px;">您的验证码</div>
|
||||
|
||||
<div class="code-box" style="display:inline-block;padding:12px 24px;border:1px solid #e5e7eb;border-radius:8px;background:#f9fafb;font-size:26px;font-weight:bold;color:#d9534f;letter-spacing:4px;">
|
||||
{code}
|
||||
</div>
|
||||
|
||||
<div class="muted" style="margin-top:10px;font-size:12px;color:#6b7280;">
|
||||
有效期 5 分钟,请勿泄露给他人
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="muted" style="margin-top:16px;font-size:12px;color:#9ca3af;">
|
||||
如果这不是您本人的操作,请忽略此邮件
|
||||
</p>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="padding:16px 24px;border-top:1px solid #e5e7eb;">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td align="left" class="muted" style="font-size:12px;color:#9ca3af;">
|
||||
这是一封系统通知邮件,请勿直接回复
|
||||
</td>
|
||||
<td align="right" class="muted" style="font-size:12px;color:#9ca3af;">
|
||||
Lexiverse 安全中心
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
send_email(email, subject, content)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ from typing import Literal, Tuple
|
|||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.api.word_comment.word_comment_schemas import CommentUpload
|
||||
from app.models import User, CommentFr, CommentJp
|
||||
from app.schemas.comment_schemas import CommentUpload
|
||||
from app.utils.security import get_current_user
|
||||
|
||||
word_comment_router = APIRouter()
|
||||
|
|
|
|||
|
|
@ -22,6 +22,3 @@ class CommentUpload(BaseModel):
|
|||
comment_word: str
|
||||
comment_content: str
|
||||
# lang: Literal["fr", "jp"]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
|
@ -11,7 +11,7 @@ from app.utils.textnorm import normalize_text
|
|||
from settings import TORTOISE_ORM
|
||||
|
||||
|
||||
async def suggest_autocomplete(query: SearchRequest, limit: int = 10) -> List[Tuple[str, str]]:
|
||||
async def suggest_autocomplete(query: SearchRequest, limit: int = 10):
|
||||
"""
|
||||
|
||||
:param query: 当前用户输入的内容
|
||||
|
|
@ -22,7 +22,7 @@ async def suggest_autocomplete(query: SearchRequest, limit: int = 10) -> List[Tu
|
|||
query_word = normalize_text(query.query)
|
||||
exact = await (
|
||||
WordlistFr
|
||||
.get_or_none(text=query.query)
|
||||
.get_or_none(search_text=query.query)
|
||||
.values("text", "freq")
|
||||
)
|
||||
if exact:
|
||||
|
|
@ -33,7 +33,7 @@ async def suggest_autocomplete(query: SearchRequest, limit: int = 10) -> List[Tu
|
|||
qs_prefix = (
|
||||
WordlistFr
|
||||
.filter(Q(search_text__startswith=query_word) | Q(text__startswith=query.query))
|
||||
.exclude(text=query.query)
|
||||
.exclude(search_text=query.query)
|
||||
.only("text", "freq")
|
||||
)
|
||||
prefix_objs = await qs_prefix[:limit]
|
||||
|
|
@ -53,6 +53,17 @@ async def suggest_autocomplete(query: SearchRequest, limit: int = 10) -> List[Tu
|
|||
contains_objs = await qs_contain[: need * 2]
|
||||
contains = [(o.text, o.freq) for o in contains_objs]
|
||||
|
||||
seen_text, out = set(), []
|
||||
for text, freq in list(exact_word) + list(prefix) + list(contains):
|
||||
key = text
|
||||
if key not in seen_text:
|
||||
seen_text.add(key)
|
||||
out.append((text, freq))
|
||||
if len(out) >= limit:
|
||||
break
|
||||
out = sorted(out, key=lambda w: (-w[2], len(w[0]), w[0]))
|
||||
return [text for text, _ in out]
|
||||
|
||||
else:
|
||||
query_word = all_in_kana(query.query)
|
||||
exact = await (
|
||||
|
|
@ -89,16 +100,16 @@ async def suggest_autocomplete(query: SearchRequest, limit: int = 10) -> List[Tu
|
|||
contains_objs = qs_contain[:need * 2]
|
||||
contains: List[Tuple[str, str, int]] = [(o.text, o.hiragana, o.freq) for o in contains_objs]
|
||||
|
||||
seen_text, out = set(), []
|
||||
for text, hiragana, freq in list(exact_word) + list(prefix) + list(contains):
|
||||
key = (text, hiragana)
|
||||
if key not in seen_text:
|
||||
seen_text.add(key)
|
||||
out.append((text, hiragana, freq))
|
||||
if len(out) >= limit:
|
||||
break
|
||||
out = sorted(out, key=lambda w: (-w[2], len(w[0]), w[0]))
|
||||
return [(text, hiragana) for text, hiragana, _ in out]
|
||||
seen_text, out = set(), []
|
||||
for text, hiragana, freq in list(exact_word) + list(prefix) + list(contains):
|
||||
key = (text, hiragana)
|
||||
if key not in seen_text:
|
||||
seen_text.add(key)
|
||||
out.append((text, hiragana, freq))
|
||||
if len(out) >= limit:
|
||||
break
|
||||
out = sorted(out, key=lambda w: (-w[2], len(w[0]), w[0]))
|
||||
return [(text, hiragana) for text, hiragana, _ in out]
|
||||
|
||||
|
||||
async def __test():
|
||||
|
|
|
|||
10
main.py
10
main.py
|
|
@ -8,6 +8,8 @@ from tortoise.contrib.fastapi import register_tortoise
|
|||
import app.models.signals
|
||||
from app.api.admin.router import admin_router
|
||||
from app.api.ai_assist.routes import ai_router
|
||||
from app.api.make_comments.routes import comment_router
|
||||
from app.api.pronounciation_test.routes import pron_test_router
|
||||
from app.api.redis_test import redis_test_router
|
||||
from app.api.search import dict_search
|
||||
from app.api.translator import translator_router
|
||||
|
|
@ -15,7 +17,7 @@ from app.api.user.routes import users_router
|
|||
from app.api.word_comment.routes import word_comment_router
|
||||
from app.core.redis import init_redis, close_redis
|
||||
from app.utils.phone_encrypt import PhoneEncrypt
|
||||
from settings import ONLINE_SETTINGS
|
||||
from settings import TORTOISE_ORM
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
|
|
@ -43,7 +45,7 @@ app.add_middleware(
|
|||
|
||||
register_tortoise(
|
||||
app=app,
|
||||
config=ONLINE_SETTINGS,
|
||||
config=TORTOISE_ORM,
|
||||
)
|
||||
|
||||
app.include_router(users_router, tags=["User API"], prefix="/users")
|
||||
|
|
@ -56,7 +58,11 @@ app.include_router(translator_router, tags=["Translation API"])
|
|||
|
||||
app.include_router(ai_router, tags=["AI Assist API"], prefix="/ai_assist")
|
||||
|
||||
app.include_router(comment_router, tags=["Comment API"])
|
||||
|
||||
app.include_router(word_comment_router, tags=["Word Comment API"], prefix="/comment/word")
|
||||
|
||||
app.include_router(pron_test_router, tags=["Pron Test API"], prefix="/test")
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)
|
||||
|
|
|
|||
Loading…
Reference in New Issue