security.py:

修改了获取用户登录状态的bug
settings.py:
预设云端数据库的端口
others:
完成串口测试,实现基本功能,修复
This commit is contained in:
Miyamizu-MitsuhaSang 2025-08-28 21:27:29 +08:00
parent f295457441
commit 6efd72a596
10 changed files with 201 additions and 31 deletions

View File

@ -1,16 +1,111 @@
from typing import Literal, List
import jaconv
import pykakasi
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request
from app.models import DefinitionJp
from app.models.fr import DefinitionFr from app.models.fr import DefinitionFr
from app.schemas.search_schemas import SearchRequest, SearchResponse, SearchItemFr, SearchItemJp
from app.utils.security import get_current_user from app.utils.security import get_current_user
from app.utils.textnorm import normalize_text
from scripts.update_jp import normalize_jp_text
dict_search = APIRouter() dict_search = APIRouter()
kks = pykakasi.kakasi()
kks.setMode("H", "a") # 平假名 -> ascii (罗马字)
kks.setMode("K", "a") # 片假名 -> ascii
kks.setMode("J", "a") # 汉字 -> ascii
kks.setMode("r", "Hepburn") # 转换成 Hepburn 罗马字
conv = kks.getConverter()
@dict_search.get("/search")
async def search(request: Request, lang_pref: str, query_word: str, user= Depends(get_current_user)): def all_in_kana(text: str) -> str:
word_content = await DefinitionFr.filter( """
word__icontains=query_word, lang_pref=lang_pref 将输入统一转换为平假名支持
).values("word", "part_of_speech", "meaning", "example") - 平假名
if not word_content: - 片假名
raise HTTPException(status_code=404, detail="Word not found") - 罗马字 (Hepburn 转写)
return word_content
返回平假名字符串
"""
if not text:
return ""
# 1. 片假名 → 平假名
normalized = jaconv.kata2hira(text)
# 2. 如果里面含有罗马字字符,就先转成假名
if any("a" <= ch.lower() <= "z" for ch in normalized):
hira = conv.do(normalized) # 罗马字 -> 平假名
normalized = jaconv.kata2hira(hira)
# 3. 再次片假名 -> 平假名保险
normalized = jaconv.kata2hira(normalized)
return normalized
@dict_search.post("/search", response_model=SearchResponse)
async def search(request: Request, body: SearchRequest, user=Depends(get_current_user)):
query = body.query
if body.language == 'fr':
query = normalize_text(query)
word_contents = await (
DefinitionFr
.filter(word__text=query)
.prefetch_related("word")
)
if not word_contents:
raise HTTPException(status_code=404, detail="Word not found")
pos_seen = set()
pos_contents = []
contents: List[SearchItemFr] = []
for wc in word_contents:
if wc.pos not in pos_seen:
pos_seen.add(wc.pos)
pos_contents.append(wc.pos)
contents.append(
SearchItemFr(
pos=wc.pos,
chi_exp=wc.meaning,
example=wc.example,
eng_explanation=wc.eng_explanation,
)
)
return SearchResponse(
query=query,
pos=pos_contents,
contents=contents,
)
else:
query = all_in_kana(query)
print(query)
word_content = await DefinitionJp.filter(
word__text=query
).prefetch_related("word", "pos")
if not word_content:
raise HTTPException(status_code=404, detail="Word not found")
first_def = word_content[0]
pos_list = await first_def.pos.all()
pos_contents = [p.pos_type for p in pos_list]
contents: List[SearchItemJp] = []
for wc in word_content:
contents.append(
SearchItemJp(
chi_exp=wc.meaning,
example=wc.example,
)
)
return SearchResponse(
query=query,
pos=pos_contents,
contents=contents,
)
# TODO 相关度排序(转换为模糊匹配)
# TODO 输入搜索框时反馈内容

View File

@ -15,7 +15,7 @@ class WordlistFr(Model):
text = fields.CharField(max_length=40, unique=True, description="单词") text = fields.CharField(max_length=40, unique=True, description="单词")
definitions: fields.ReverseRelation["DefinitionFr"] definitions: fields.ReverseRelation["DefinitionFr"]
attachments: fields.ReverseRelation["AttachmentFr"] attachments: fields.ReverseRelation["AttachmentFr"]
freq = fields.IntField() # 词频排序用 freq = fields.IntField(default=0) # 词频排序用
search_text = fields.CharField(max_length=255, index=True) # 检索字段 search_text = fields.CharField(max_length=255, index=True) # 检索字段
# attachment = fields.ForeignKeyField("models.Attachment", related_name="wordlists", on_delete=fields.CASCADE) # attachment = fields.ForeignKeyField("models.Attachment", related_name="wordlists", on_delete=fields.CASCADE)
@ -44,4 +44,4 @@ class DefinitionFr(Model):
eng_explanation = fields.TextField(null=True, description="English explanation") eng_explanation = fields.TextField(null=True, description="English explanation")
example_varification = fields.BooleanField(default=False, description="例句是否审核") example_varification = fields.BooleanField(default=False, description="例句是否审核")
class Meta: class Meta:
table = "definition_fr" table = "definitions_fr"

View File

@ -16,6 +16,8 @@ sheet_name_jp = "日汉释义"
class WordlistJp(Model): class WordlistJp(Model):
id = fields.IntField(pk=True) id = fields.IntField(pk=True)
text = fields.CharField(max_length=40, description="单词") text = fields.CharField(max_length=40, description="单词")
hiragana = fields.CharField(max_length=60, description="假名", null=False)
freq = fields.IntField(default=0)
definitions : fields.ReverseRelation["DefinitionJp"] definitions : fields.ReverseRelation["DefinitionJp"]
attachments : fields.ReverseRelation["AttachmentJp"] attachments : fields.ReverseRelation["AttachmentJp"]

View File

@ -0,0 +1,31 @@
from typing import Literal, List, Union
from pydantic import BaseModel
from app.models import PosType
from app.schemas.admin_schemas import PosEnumFr
class SearchRequest(BaseModel):
query: str
language: Literal['fr', 'jp']
sort: Literal['relevance', 'date'] = 'date'
order: Literal['asc', 'des'] = 'des'
class SearchItemJp(BaseModel):
chi_exp: str
example: str
class SearchItemFr(BaseModel):
pos: PosEnumFr
chi_exp: str
eng_explanation: str
example: str
class SearchResponse(BaseModel):
query: str
pos: list
contents: Union[List[SearchItemFr], List[SearchItemJp]]

View File

@ -130,29 +130,19 @@ async def get_current_user_with_oauth(
return await _decode_and_load_user(token) return await _decode_and_load_user(token)
async def get_current_user(*args, **kwargs) -> Tuple[User, Dict]: async def get_current_user(
request: Request,
token: Annotated[str, Depends(oauth2_scheme)] = None
) -> Tuple[User, Dict]:
if settings.USE_OAUTH: if settings.USE_OAUTH:
return await get_current_user_with_oauth(*args, **kwargs) return await get_current_user_with_oauth(token)
return await get_current_user_with_oauth(*args, **kwargs) return await get_current_user_basic(request)
async def is_admin_user_basic(user_payload: Tuple[User, Dict] = Depends(get_current_user)) -> Tuple[User, Dict]: async def is_admin_user(
user, payload = user_payload user_payload: Tuple[User, Dict] = Depends(get_current_user),
if not getattr(user, "is_admin", False):
raise HTTPException(status_code=403, detail="Access denied")
return user, payload
async def is_admin_user_oauth(
user_payload: Tuple[User, Dict] = Depends(get_current_user_with_oauth)
) -> Tuple[User, Dict]: ) -> Tuple[User, Dict]:
user, payload = user_payload user, payload = user_payload
if not getattr(user, "is_admin", False): if not getattr(user, "is_admin", False):
raise HTTPException(status_code=403, detail="Access denied") raise HTTPException(status_code=403, detail="Access denied")
return user, payload return user, payload
async def is_admin_user(*args, **kwargs) -> Tuple[User, Dict]:
if settings.USE_OAUTH:
return await is_admin_user_basic(*args, **kwargs)
return await is_admin_user_oauth(*args, **kwargs)

0
debug/__init__.py Normal file
View File

12
debug/httpdebugger.py Normal file
View File

@ -0,0 +1,12 @@
# 临时加个调试中间件(或异常处理器)
from fastapi.responses import JSONResponse
from fastapi.requests import Request
from fastapi.exceptions import RequestValidationError
from fastapi import FastAPI
from main import app
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
print("422 detail:", exc.errors()) # 在控制台打印
return JSONResponse(status_code=422, content={"detail": exc.errors()})

View File

@ -23,6 +23,8 @@ async def lifespan(app: FastAPI):
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
import debug.httpdebugger
# 添加CORS中间件 # 添加CORS中间件
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,

View File

@ -146,8 +146,10 @@ async def import_def_jp(path: Path = xlsx_path, sheet_name: str = "日汉释义"
print(f"❌ 查找单词 {word} 出错: {e}") print(f"❌ 查找单词 {word} 出错: {e}")
continue continue
if pd.isna(row[6]):
continue
# 字段处理 # 字段处理
example = None if pd.isna(row.日语例句1) else normalize_jp_text(str(row.日语例句1)) example = None if pd.isna(row.日语例句2) else normalize_jp_text(str(row.日语例句2))
if not pd.isna(row.词性): if not pd.isna(row.词性):
pos_obj, jump = await pos_process(str(row.词性)) pos_obj, jump = await pos_process(str(row.词性))
if jump: if jump:
@ -155,7 +157,7 @@ async def import_def_jp(path: Path = xlsx_path, sheet_name: str = "日汉释义"
else: else:
print(f"{word} 的词性为空,跳过") print(f"{word} 的词性为空,跳过")
continue continue
chi_exp = str(row[4]).strip() chi_exp = str(row[6]).strip() # 读取第二个释义
exists = await DefinitionJp.filter( exists = await DefinitionJp.filter(
word=cls_word, word=cls_word,
@ -209,6 +211,21 @@ async def import_attachment(path: Path = xlsx_path, sheet_name: str = "日汉释
) )
async def set_hiragana(xlsx_path: Path = xlsx_path, sheet_name : str="日汉释义"):
df = pd.read_excel(xlsx_path)
df.columns = [col.strip() for col in df.columns]
for row in df.itertuples():
word = normalize_jp_text(str(row[1]).strip())
if pd.isna(word):
break
hiragana = normalize_jp_text(jaconv.kata2hira(str(row[1]))) if pd.isna(row[2]) else normalize_jp_text(str(row[2]))
romaji = row[3]
await WordlistJp.filter(text=word).update(hiragana=hiragana)
async def main(): async def main():
await Tortoise.init(config=TORTOISE_ORM) await Tortoise.init(config=TORTOISE_ORM)
# await DefinitionJp.all().delete() # TRUNCATE TABLE definitions_fr; # await DefinitionJp.all().delete() # TRUNCATE TABLE definitions_fr;
@ -216,7 +233,8 @@ async def main():
# await AttachmentJp.all().delete() # await AttachmentJp.all().delete()
# await import_wordlist_jp() # await import_wordlist_jp()
# await import_def_jp() # await import_def_jp()
await import_attachment() # await import_attachment()
await set_hiragana()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -21,6 +21,26 @@ TORTOISE_ORM = {
'timezone': 'Asia/Shanghai' 'timezone': 'Asia/Shanghai'
} }
ONLINE_SETTINGS = {
'connections': {
'default': 'mysql://root:@124.221.145.135:3306/test_db',
},
'apps': {
'models': {
'models': [
'app.models.base',
'app.models.fr',
'app.models.jp',
'aerich.models' # aerich自带模型类必须填入
],
'default_connection': 'default',
}
},
'use_tz': False,
'timezone': 'Asia/Shanghai'
}
class Settings(BaseSettings): class Settings(BaseSettings):
USE_OAUTH = False USE_OAUTH = False