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 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 输入搜索框时反馈内容
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
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,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)
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
import debug.httpdebugger
|
||||||
|
|
||||||
# 添加CORS中间件
|
# 添加CORS中间件
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
|
|
|
||||||
|
|
@ -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__':
|
||||||
|
|
|
||||||
20
settings.py
20
settings.py
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue