refactor: Линзы, API, Docker

- Обновлена логика фильтрации и сортировки линз
- Добавлена возможность поиска
- Добавлены переменные окружения
- Удален secrets-example.yaml
This commit is contained in:
Андрей Дувакин 2025-06-08 09:38:51 +05:00
parent 05bfec81fc
commit 914ae0528f
10 changed files with 152 additions and 109 deletions

View File

@ -1,3 +1,4 @@
.venv .venv
k8s k8s
.idea .idea
.env

View File

@ -1,6 +1,6 @@
from typing import Sequence, Optional, Tuple, Literal from typing import Sequence, Optional, Tuple, Literal
from sqlalchemy import select, desc, or_, func from sqlalchemy import select, desc, or_, func, String
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import Lens from app.domain.models import Lens
@ -11,18 +11,18 @@ class LensesRepository:
self.db = db self.db = db
async def get_all( async def get_all(
self, self,
skip: int = 0, skip: int = 0,
limit: int = 10, limit: int = 10,
search: str = None, search: str = None,
sort_order: Literal["asc", "desc"] = "desc", sort_order: Literal["asc", "desc"] = "desc",
tor: float = None, tor: float = None,
diameter: float = None, diameter: float = None,
preset_refraction: float = None, preset_refraction: float = None,
periphery_toricity: float = None, periphery_toricity: float = None,
side: str = "all", side: str = "all",
issued: bool = None, issued: bool = None,
trial: float = None, trial: float = None,
) -> Tuple[Sequence[Lens], int]: ) -> Tuple[Sequence[Lens], int]:
stmt = select(Lens) stmt = select(Lens)
count_stmt = select(func.count()).select_from(Lens) count_stmt = select(func.count()).select_from(Lens)
@ -31,24 +31,26 @@ class LensesRepository:
search = f"%{search}%" search = f"%{search}%"
stmt = stmt.filter( stmt = stmt.filter(
or_( or_(
Lens.tor.cast(str).ilike(search), Lens.tor.cast(String).ilike(search),
Lens.diameter.cast(str).ilike(search), Lens.diameter.cast(String).ilike(search),
Lens.preset_refraction.cast(str).ilike(search), Lens.preset_refraction.cast(String).ilike(search),
Lens.periphery_toricity.cast(str).ilike(search), Lens.periphery_toricity.cast(String).ilike(search),
Lens.side.ilike(search), Lens.side.cast(String).ilike(search),
Lens.fvc.cast(str).ilike(search), Lens.fvc.cast(String).ilike(search),
Lens.trial.cast(str).ilike(search), Lens.trial.cast(String).ilike(search),
Lens.esa.cast(String).ilike(search),
) )
) )
count_stmt = count_stmt.filter( count_stmt = count_stmt.filter(
or_( or_(
Lens.tor.cast(str).ilike(search), Lens.tor.cast(String).ilike(search),
Lens.diameter.cast(str).ilike(search), Lens.diameter.cast(String).ilike(search),
Lens.preset_refraction.cast(str).ilike(search), Lens.preset_refraction.cast(String).ilike(search),
Lens.periphery_toricity.cast(str).ilike(search), Lens.periphery_toricity.cast(String).ilike(search),
Lens.side.ilike(search), Lens.side.cast(String).ilike(search),
Lens.fvc.cast(str).ilike(search), Lens.fvc.cast(String).ilike(search),
Lens.trial.cast(str).ilike(search), Lens.trial.cast(String).ilike(search),
Lens.esa.cast(String).ilike(search),
) )
) )

View File

@ -3,7 +3,6 @@ import uuid
from typing import Optional from typing import Optional
import aiofiles import aiofiles
import magic
from fastapi import UploadFile, HTTPException from fastapi import UploadFile, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse from starlette.responses import FileResponse

View File

@ -24,6 +24,16 @@ spec:
secretKeyRef: secretKeyRef:
name: visus-api-secret name: visus-api-secret
key: SECRET_KEY key: SECRET_KEY
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: visus-api-secret
key: DATABASE_URL
- name: ALGORITHM
valueFrom:
secretKeyRef:
name: visus-api-secret
key: ALGORITHM
volumeMounts: volumeMounts:
- name: uploads-volume - name: uploads-volume
mountPath: {{ .Values.persistence.uploads.containerPath }} mountPath: {{ .Values.persistence.uploads.containerPath }}

View File

@ -3,4 +3,5 @@ npm-debug.log
build build
.dockerignore .dockerignore
.git .git
k8s k8s
.env

View File

@ -1,8 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: visus-api-secret
type: Opaque
data:
SECRET_KEY:
DATABASE_URL:

View File

@ -8,9 +8,30 @@ export const lensesApi = createApi({
tagTypes: ['Lens'], tagTypes: ['Lens'],
endpoints: (builder) => ({ endpoints: (builder) => ({
getLenses: builder.query({ getLenses: builder.query({
query: () => '/lenses/', query: ({page, pageSize, search, sortOrder, searchParams}) => ({
providesTags: ['Lens'], url: '/lenses/',
refetchOnMountOrArgChange: 5 params: {
page,
page_size: pageSize,
search: search || undefined,
sort_order: sortOrder || undefined,
tor: searchParams.tor || undefined,
diameter: searchParams.diameter || undefined,
preset_refraction: searchParams.preset_refraction || undefined,
periphery_toricity: searchParams.periphery_toricity || undefined,
side: searchParams.side !== 'all' ? searchParams.side : undefined,
issued: searchParams.issued !== 'all' ? searchParams.issued : undefined,
trial: searchParams.trial || undefined,
},
}),
providesTags: ['Lenses'],
transformResponse: (response) => {
if (!response || !Array.isArray(response.lenses)) {
console.warn('Unexpected lenses API response:', response);
return {lenses: [], total_count: 0};
}
return response;
},
}), }),
getNotIssuedLenses: builder.query({ getNotIssuedLenses: builder.query({
query: () => '/lenses/not_issued/', query: () => '/lenses/not_issued/',

View File

@ -148,8 +148,8 @@ const LensesTab = () => {
<Col xs={24} md={24} sm={24} xl={15}> <Col xs={24} md={24} sm={24} xl={15}>
<Input <Input
placeholder="Поиск линзы" placeholder="Поиск линзы"
value={lensesData.searchText} value={lensesData.tempSearchText}
onChange={(e) => lensesData.handleSetSearchText(e.target.value)} onChange={(e) => lensesData.handleSetTempSearchText(e.target.value)}
style={lensesData.formItemStyle} style={lensesData.formItemStyle}
allowClear allowClear
/> />

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo } from "react"; import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { notification } from "antd"; import { notification } from "antd";
import { import {
@ -16,14 +16,14 @@ import {
setSearchParams, setSearchParams,
setSearchText, setSearchText,
setShowAdvancedSearch, setShowAdvancedSearch,
setViewMode setViewMode,
setSortOrder
} from "../../../../../Redux/Slices/lensesSlice.js"; } from "../../../../../Redux/Slices/lensesSlice.js";
import { getCachedInfo } from "../../../../../Utils/cachedInfoUtils.js"; import { getCachedInfo } from "../../../../../Utils/cachedInfoUtils.js";
const useLenses = () => { const useLenses = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { const {
searchText,
viewMode, viewMode,
currentPage, currentPage,
pageSize, pageSize,
@ -31,16 +31,36 @@ const useLenses = () => {
isModalVisible, isModalVisible,
showAdvancedSearch, showAdvancedSearch,
searchParams, searchParams,
sortOrder,
} = useSelector(state => state.lensesUI); } = useSelector(state => state.lensesUI);
const { data = { lenses: [], total_count: 0 }, isLoading, isError } = useGetLensesQuery( const [tempSearchText, setTempSearchText] = useState("");
{ page: currentPage, pageSize, search: searchText || undefined },
const { data = { lenses: [], total_count: 0 }, isLoading, isError, error } = useGetLensesQuery(
{
page: currentPage,
pageSize,
search: tempSearchText || undefined,
sortOrder,
searchParams,
},
{ {
pollingInterval: 20000, pollingInterval: 20000,
refetchOnMountOrArgChange: true, refetchOnMountOrArgChange: true,
} }
); );
useEffect(() => {
if (isError) {
console.error('useGetLensesQuery error:', error);
notification.error({
message: "Ошибка загрузки данных",
description: error?.data?.detail || "Не удалось загрузить линзы",
placement: "topRight",
});
}
}, [isError, error]);
const [addLens, { isLoading: isAdding }] = useAddLensMutation(); const [addLens, { isLoading: isAdding }] = useAddLensMutation();
const [updateLens, { isLoading: isUpdating }] = useUpdateLensMutation(); const [updateLens, { isLoading: isUpdating }] = useUpdateLensMutation();
const [deleteLens, { isLoading: isDeleting }] = useDeleteLensMutation(); const [deleteLens, { isLoading: isDeleting }] = useDeleteLensMutation();
@ -58,9 +78,24 @@ const useLenses = () => {
const tableActionButtonsStyle = { display: "flex", gap: "8px" }; const tableActionButtonsStyle = { display: "flex", gap: "8px" };
const advancedSearchCardStyle = { marginBottom: 20 }; const advancedSearchCardStyle = { marginBottom: 20 };
const handleSetSearchText = (value) => { const handleSetTempSearchText = (value) => {
dispatch(setSearchText(value)); setTempSearchText(value);
dispatch(setCurrentPage(1)); // Сбрасываем на первую страницу при изменении поиска };
const handleSearch = () => {
dispatch(setSearchText(tempSearchText));
dispatch(setCurrentPage(1));
};
const handleClearSearch = () => {
setTempSearchText('');
dispatch(setSearchText(''));
dispatch(setCurrentPage(1));
};
const handleSetSortOrder = (value) => {
dispatch(setSortOrder(value));
dispatch(setCurrentPage(1));
}; };
const handleCloseModal = () => dispatch(closeModal()); const handleCloseModal = () => dispatch(closeModal());
@ -85,6 +120,7 @@ const useLenses = () => {
} else { } else {
dispatch(setSearchParams({ ...searchParams, [param]: value })); dispatch(setSearchParams({ ...searchParams, [param]: value }));
} }
dispatch(setCurrentPage(1));
}; };
const toggleAdvancedSearch = () => { const toggleAdvancedSearch = () => {
@ -140,34 +176,10 @@ const useLenses = () => {
} }
}; };
const filteredLenses = useMemo(() => {
return data.lenses.filter((lens) => {
const textMatch = Object.values(lens).some((value) =>
value?.toString().toLowerCase().includes(searchText.toLowerCase())
);
const advancedMatch = Object.entries(searchParams).every(([key, value]) => {
if (value === null || value === '') return true;
if (key === 'side') {
if (value === 'all') return true;
return lens.side === value;
}
if (key === 'issued') {
return lens.issued === value || value === "all";
}
return lens[key] === value;
});
return textMatch && advancedMatch && (searchParams.issued || lens.issued === false);
}).sort((a, b) => {
return b.id - a.id; // Сортировка по убыванию id
});
}, [data.lenses, searchText, searchParams]);
const pagination = { const pagination = {
current: currentPage, current: currentPage,
pageSize: pageSize, pageSize: pageSize,
total: data.total_count, total: data.total_count || 0,
showSizeChanger: true, showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"], pageSizeOptions: ["5", "10", "20", "50"],
onChange: (page, newPageSize) => { onChange: (page, newPageSize) => {
@ -176,12 +188,12 @@ const useLenses = () => {
}; };
return { return {
lenses: data.lenses, lenses: data.lenses || [],
filteredLenses: filteredLenses.map(lens => ({ ...lens, key: lens.id })), filteredLenses: (data.lenses || []).map(lens => ({ ...lens, key: lens.id })),
isLoading, isLoading,
isError, isError,
isProcessing: isAdding || isUpdating || isDeleting, isProcessing: isAdding || isUpdating || isDeleting,
searchText, tempSearchText,
viewMode, viewMode,
currentPage, currentPage,
pageSize, pageSize,
@ -189,6 +201,7 @@ const useLenses = () => {
isModalVisible, isModalVisible,
showAdvancedSearch, showAdvancedSearch,
searchParams, searchParams,
sortOrder,
pagination, pagination,
containerStyle, containerStyle,
filterBarStyle, filterBarStyle,
@ -196,7 +209,10 @@ const useLenses = () => {
viewModIconStyle, viewModIconStyle,
tableActionButtonsStyle, tableActionButtonsStyle,
advancedSearchCardStyle, advancedSearchCardStyle,
handleSetSearchText, handleSetTempSearchText,
handleSearch,
handleClearSearch,
handleSetSortOrder,
handleAddLens, handleAddLens,
handleEditLens, handleEditLens,
handleCloseModal, handleCloseModal,

View File

@ -1,12 +1,11 @@
import {createSlice} from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit';
import {cacheInfo} from "../../Utils/cachedInfoUtils.js";
const initialState = { const initialState = {
searchText: '', searchText: '',
viewMode: 'tile', viewMode: 'tile',
currentPage: 1, currentPage: 1,
pageSize: 10, pageSize: 5,
sortOrder: 'desc',
selectedLens: null, selectedLens: null,
isModalVisible: false, isModalVisible: false,
showAdvancedSearch: false, showAdvancedSearch: false,
@ -16,57 +15,59 @@ const initialState = {
preset_refraction: null, preset_refraction: null,
periphery_toricity: null, periphery_toricity: null,
side: 'all', side: 'all',
issued: false, issued: 'all',
trial: null trial: null,
}, },
}; };
const lensesSlice = createSlice({ const lensesSlice = createSlice({
name: 'lensesUI', name: 'lenses',
initialState, initialState,
reducers: { reducers: {
setSearchText: (state, action) => { setSearchText(state, action) {
state.searchText = action.payload; state.searchText = action.payload;
}, },
setCurrentPage: (state, action) => { setSortOrder(state, action) {
state.sortOrder = action.payload;
},
setViewMode(state, action) {
state.viewMode = action.payload;
},
setCurrentPage(state, action) {
state.currentPage = action.payload; state.currentPage = action.payload;
}, },
setPageSize: (state, action) => { setPageSize(state, action) {
state.pageSize = action.payload; state.pageSize = action.payload;
}, },
setViewMode: (state, action) => { selectLens(state, action) {
state.viewMode = action.payload;
cacheInfo("viewModeLenses", action.payload);
},
openModal: (state) => {
state.isModalVisible = true;
},
closeModal: (state) => {
state.isModalVisible = false;
state.selectedLens = null;
},
selectLens: (state, action) => {
state.selectedLens = action.payload; state.selectedLens = action.payload;
}, },
setSearchParams: (state, action) => { openModal(state) {
state.searchParams = action.payload; state.isModalVisible = true;
}, },
setShowAdvancedSearch: (state, action) => { closeModal(state) {
state.isModalVisible = false;
},
setShowAdvancedSearch(state, action) {
state.showAdvancedSearch = action.payload; state.showAdvancedSearch = action.payload;
}, },
} setSearchParams(state, action) {
state.searchParams = action.payload;
},
},
}); });
export const { export const {
setSearchText, setSearchText,
setSortOrder,
setViewMode,
setCurrentPage, setCurrentPage,
setPageSize, setPageSize,
setViewMode, selectLens,
openModal, openModal,
closeModal, closeModal,
selectLens, setShowAdvancedSearch,
setSearchParams, setSearchParams,
setShowAdvancedSearch
} = lensesSlice.actions; } = lensesSlice.actions;
export default lensesSlice.reducer; export default lensesSlice.reducer;