security.py:
修改了获取用户登录状态的bug settings.py: 预设云端数据库的端口 others: 完成串口测试,实现基本功能,修复
This commit is contained in:
parent
f295457441
commit
6efd72a596
|
|
@ -1,16 +1,111 @@
|
|||
from typing import Literal, List
|
||||
|
||||
import jaconv
|
||||
import pykakasi
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
from app.models import DefinitionJp
|
||||
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.textnorm import normalize_text
|
||||
from scripts.update_jp import normalize_jp_text
|
||||
|
||||
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)):
|
||||
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")
|
||||
return word_content
|
||||
|
||||
def all_in_kana(text: str) -> str:
|
||||
"""
|
||||
将输入统一转换为平假名,支持:
|
||||
- 平假名
|
||||
- 片假名
|
||||
- 罗马字 (Hepburn 转写)
|
||||
|
||||
返回:平假名字符串
|
||||
"""
|
||||
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 输入搜索框时反馈内容
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class WordlistFr(Model):
|
|||
text = fields.CharField(max_length=40, unique=True, description="单词")
|
||||
definitions: fields.ReverseRelation["DefinitionFr"]
|
||||
attachments: fields.ReverseRelation["AttachmentFr"]
|
||||
freq = fields.IntField() # 词频排序用
|
||||
freq = fields.IntField(default=0) # 词频排序用
|
||||
search_text = fields.CharField(max_length=255, index=True) # 检索字段
|
||||
|
||||
# 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")
|
||||
example_varification = fields.BooleanField(default=False, description="例句是否审核")
|
||||
class Meta:
|
||||
table = "definition_fr"
|
||||
table = "definitions_fr"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ sheet_name_jp = "日汉释义"
|
|||
class WordlistJp(Model):
|
||||
id = fields.IntField(pk=True)
|
||||
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"]
|
||||
attachments : fields.ReverseRelation["AttachmentJp"]
|
||||
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
@ -130,29 +130,19 @@ async def get_current_user_with_oauth(
|
|||
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:
|
||||
return await get_current_user_with_oauth(*args, **kwargs)
|
||||
return await get_current_user_with_oauth(*args, **kwargs)
|
||||
return await get_current_user_with_oauth(token)
|
||||
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]:
|
||||
user, payload = user_payload
|
||||
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)
|
||||
async def is_admin_user(
|
||||
user_payload: Tuple[User, Dict] = Depends(get_current_user),
|
||||
) -> Tuple[User, Dict]:
|
||||
user, payload = user_payload
|
||||
if not getattr(user, "is_admin", False):
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
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,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()})
|
||||
2
main.py
2
main.py
|
|
@ -23,6 +23,8 @@ async def lifespan(app: FastAPI):
|
|||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
import debug.httpdebugger
|
||||
|
||||
# 添加CORS中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
|
|
|
|||
|
|
@ -146,8 +146,10 @@ async def import_def_jp(path: Path = xlsx_path, sheet_name: str = "日汉释义"
|
|||
print(f"❌ 查找单词 {word} 出错: {e}")
|
||||
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.词性):
|
||||
pos_obj, jump = await pos_process(str(row.词性))
|
||||
if jump:
|
||||
|
|
@ -155,7 +157,7 @@ async def import_def_jp(path: Path = xlsx_path, sheet_name: str = "日汉释义"
|
|||
else:
|
||||
print(f"❌ {word} 的词性为空,跳过")
|
||||
continue
|
||||
chi_exp = str(row[4]).strip()
|
||||
chi_exp = str(row[6]).strip() # 读取第二个释义
|
||||
|
||||
exists = await DefinitionJp.filter(
|
||||
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():
|
||||
await Tortoise.init(config=TORTOISE_ORM)
|
||||
# await DefinitionJp.all().delete() # TRUNCATE TABLE definitions_fr;
|
||||
|
|
@ -216,7 +233,8 @@ async def main():
|
|||
# await AttachmentJp.all().delete()
|
||||
# await import_wordlist_jp()
|
||||
# await import_def_jp()
|
||||
await import_attachment()
|
||||
# await import_attachment()
|
||||
await set_hiragana()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
20
settings.py
20
settings.py
|
|
@ -21,6 +21,26 @@ TORTOISE_ORM = {
|
|||
'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):
|
||||
USE_OAUTH = False
|
||||
|
|
|
|||
Loading…
Reference in New Issue