版本更新:

1. 新增(kinit-api,kinit-admin):支持发送邮箱通知
2. 优化(kinit-api):core/crud.py 优化
This commit is contained in:
ktianc 2023-03-30 15:56:47 +08:00
parent 0152dc9c70
commit 2ba2738107
20 changed files with 290 additions and 85 deletions

View File

@ -51,3 +51,7 @@ export const postImportUserApi = (data: any): Promise<IResponse> => {
export const postUsersInitPasswordSendSMSApi = (data: any): Promise<IResponse> => { export const postUsersInitPasswordSendSMSApi = (data: any): Promise<IResponse> => {
return request.post({ url: `/vadmin/auth/users/init/password/send/sms/`, data }) return request.post({ url: `/vadmin/auth/users/init/password/send/sms/`, data })
} }
export const postUsersInitPasswordSendEmailApi = (data: any): Promise<IResponse> => {
return request.post({ url: `/vadmin/auth/users/init/password/send/email/`, data })
}

View File

@ -54,11 +54,21 @@ export const useValidator = () => {
} }
} }
const isEmail = (rule: any, val: any, callback: Callback) => {
// 判断是否为邮箱地址
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) {
callback()
} else {
callback(new Error('请填写正确的邮箱地址'))
}
}
return { return {
required, required,
lengthRange, lengthRange,
notSpace, notSpace,
notSpecialCharacters, notSpecialCharacters,
isEqual isEqual,
isEmail
} }
} }

View File

@ -6,7 +6,7 @@ import { useValidator } from '@/hooks/web/useValidator'
import { schema } from './user.data' import { schema } from './user.data'
import { getRoleOptionsApi } from '@/api/vadmin/auth/role' import { getRoleOptionsApi } from '@/api/vadmin/auth/role'
const { required } = useValidator() const { required, isEmail } = useValidator()
const props = defineProps({ const props = defineProps({
currentRow: { currentRow: {
@ -20,7 +20,8 @@ const rules = reactive({
is_active: [required()], is_active: [required()],
is_staff: [required()], is_staff: [required()],
role_ids: [required()], role_ids: [required()],
telephone: [required()] telephone: [required()],
email: [required(), { validator: isEmail, trigger: 'blur' }]
}) })
const { register, methods, elFormRef } = useForm({ const { register, methods, elFormRef } = useForm({

View File

@ -27,6 +27,12 @@ export const columns = reactive<TableColumn[]>([
show: true, show: true,
disabled: true disabled: true
}, },
{
field: 'email',
label: '邮箱',
show: true,
disabled: true
},
{ {
field: 'gender', field: 'gender',
label: '性别', label: '性别',
@ -76,6 +82,19 @@ export const schema = reactive<FormSchema[]>([
} }
} }
}, },
{
field: 'nickname',
label: '用户昵称',
colProps: {
span: 12
},
component: 'Input',
componentProps: {
style: {
width: '100%'
}
}
},
{ {
field: 'telephone', field: 'telephone',
label: '手机号码', label: '手机号码',
@ -90,8 +109,8 @@ export const schema = reactive<FormSchema[]>([
} }
}, },
{ {
field: 'nickname', field: 'email',
label: '用户昵称', label: '邮箱',
colProps: { colProps: {
span: 12 span: 12
}, },

View File

@ -20,7 +20,8 @@ import { columns, searchSchema } from './components/user.data'
import { ref, unref, watch, nextTick } from 'vue' import { ref, unref, watch, nextTick } from 'vue'
import Write from './components/Write.vue' import Write from './components/Write.vue'
import Import from './components/Import.vue' import Import from './components/Import.vue'
import Password from './components/Password.vue' import PasswordSendSMS from './components/PasswordSendSMS.vue'
import PasswordSendEmail from './components/PasswordSendEmail.vue'
import { Dialog } from '@/components/Dialog' import { Dialog } from '@/components/Dialog'
import { import {
ElButton, ElButton,
@ -180,7 +181,8 @@ const importList = () => {
} }
const passwordDialogVisible = ref(false) const passwordDialogVisible = ref(false)
const passwordDialogTitle = ref('重置密码并发送短信') let passwordDialogType = 'sms'
let passwordDialogTitle = ref('重置密码并发送短信')
const selections = ref([] as any[]) const selections = ref([] as any[])
// //
@ -189,6 +191,21 @@ const sendPasswordToSMS = async () => {
selections.value = await getSelections() selections.value = await getSelections()
if (selections.value.length > 0) { if (selections.value.length > 0) {
passwordDialogVisible.value = true passwordDialogVisible.value = true
passwordDialogType = 'sms'
passwordDialogTitle.value = '重置密码并发送短信'
} else {
return ElMessage.warning('请先选择数据')
}
}
//
const sendPasswordToEmail = async () => {
const { getSelections } = methods
selections.value = await getSelections()
if (selections.value.length > 0) {
passwordDialogVisible.value = true
passwordDialogType = 'email'
passwordDialogTitle.value = '重置密码并发送邮件'
} else { } else {
return ElMessage.warning('请先选择数据') return ElMessage.warning('请先选择数据')
} }
@ -205,6 +222,8 @@ const handleCommand = (command: string) => {
} else if (command === 'c') { } else if (command === 'c') {
sendPasswordToSMS() sendPasswordToSMS()
} else if (command === 'd') { } else if (command === 'd') {
sendPasswordToEmail()
} else if (command === 'e') {
delDatas(null, true) delDatas(null, true)
} }
} }
@ -233,6 +252,9 @@ const handleCommand = (command: string) => {
<ElCol :span="1.5" v-hasPermi="['auth.user.reset']" v-if="!mobile"> <ElCol :span="1.5" v-hasPermi="['auth.user.reset']" v-if="!mobile">
<ElButton @click="sendPasswordToSMS">重置密码通知短信</ElButton> <ElButton @click="sendPasswordToSMS">重置密码通知短信</ElButton>
</ElCol> </ElCol>
<ElCol :span="1.5" v-hasPermi="['auth.user.reset']" v-if="!mobile">
<ElButton @click="sendPasswordToEmail">重置密码通知邮件</ElButton>
</ElCol>
<ElCol :span="1.5" v-hasPermi="['auth.user.delete']" v-if="!mobile"> <ElCol :span="1.5" v-hasPermi="['auth.user.delete']" v-if="!mobile">
<ElButton type="danger" @click="delDatas(null, true)">批量删除</ElButton> <ElButton type="danger" @click="delDatas(null, true)">批量删除</ElButton>
</ElCol> </ElCol>
@ -255,7 +277,10 @@ const handleCommand = (command: string) => {
<ElDropdownItem command="c" v-hasPermi="['auth.user.reset']" <ElDropdownItem command="c" v-hasPermi="['auth.user.reset']"
>重置密码通知短信</ElDropdownItem >重置密码通知短信</ElDropdownItem
> >
<ElDropdownItem command="d" v-hasPermi="['auth.user.delete']" <ElDropdownItem command="d" v-hasPermi="['auth.user.reset']"
>重置密码通知邮件</ElDropdownItem
>
<ElDropdownItem command="e" v-hasPermi="['auth.user.delete']"
>批量删除</ElDropdownItem >批量删除</ElDropdownItem
> >
</ElDropdownMenu> </ElDropdownMenu>
@ -343,10 +368,20 @@ const handleCommand = (command: string) => {
<Dialog <Dialog
v-model="passwordDialogVisible" v-model="passwordDialogVisible"
:title="passwordDialogTitle" :title="passwordDialogTitle"
width="850px" width="1000px"
maxHeight="550px" maxHeight="550px"
> >
<Password :selections="selections" @get-list="getList" /> <PasswordSendSMS
v-if="passwordDialogType === 'sms'"
:selections="selections"
@get-list="getList"
/>
<PasswordSendEmail
v-else-if="passwordDialogType === 'email'"
:selections="selections"
@get-list="getList"
/>
</Dialog> </Dialog>
</ContentWrap> </ContentWrap>
</template> </template>

View File

@ -12,6 +12,7 @@ import Baidu from './baidu.vue'
import Privacy from './privacy.vue' import Privacy from './privacy.vue'
import Agreement from './agreement.vue' import Agreement from './agreement.vue'
import WXClient from './wxServer.vue' import WXClient from './wxServer.vue'
import Email from './email.vue'
import { ContentWrap } from '@/components/ContentWrap' import { ContentWrap } from '@/components/ContentWrap'
import { getSystemSettingsTabsApi } from '@/api/vadmin/system/settings' import { getSystemSettingsTabsApi } from '@/api/vadmin/system/settings'
@ -37,6 +38,7 @@ getList()
<Privacy v-else-if="item.tab_name === 'web_privacy'" :tab-id="item.id" /> <Privacy v-else-if="item.tab_name === 'web_privacy'" :tab-id="item.id" />
<Agreement v-else-if="item.tab_name === 'web_agreement'" :tab-id="item.id" /> <Agreement v-else-if="item.tab_name === 'web_agreement'" :tab-id="item.id" />
<WXClient v-else-if="item.tab_name === 'wx_server'" :tab-id="item.id" /> <WXClient v-else-if="item.tab_name === 'wx_server'" :tab-id="item.id" />
<Email v-else-if="item.tab_name === 'web_email'" :tab-id="item.id" />
</ElTabPane> </ElTabPane>
</template> </template>
</ElTabs> </ElTabs>

View File

@ -11,7 +11,7 @@ from fastapi.security import OAuth2PasswordBearer
""" """
系统版本 系统版本
""" """
VERSION = "1.7.1" VERSION = "1.7.2"
"""安全警告: 不要在生产中打开调试运行!""" """安全警告: 不要在生产中打开调试运行!"""
DEBUG = True DEBUG = True

View File

@ -21,6 +21,7 @@ from utils.file.aliyun_oss import AliyunOSS, BucketConf
from utils.aliyun_sms import AliyunSMS from utils.aliyun_sms import AliyunSMS
from utils.excel.import_manage import ImportManage, FieldType from utils.excel.import_manage import ImportManage, FieldType
from utils.excel.write_xlsx import WriteXlsx from utils.excel.write_xlsx import WriteXlsx
from utils.send_email import EmailSender
from .params import UserParams from .params import UserParams
from utils.tools import test_password from utils.tools import test_password
from . import models, schemas from . import models, schemas
@ -205,16 +206,16 @@ class UserDal(DalBase):
"error_url": im.generate_error_url() "error_url": im.generate_error_url()
} }
async def init_password_send_sms(self, ids: List[int], rd: Redis): async def init_password(self, ids: List[int]):
""" """
初始化所选用户密码并发送通知短信 初始化所选用户密码
将用户密码改为系统默认密码并将初始化密码状态改为false 将用户密码改为系统默认密码并将初始化密码状态改为false
""" """
users = await self.get_datas(limit=0, id=("in", ids), v_return_objs=True) users = await self.get_datas(limit=0, id=("in", ids), v_return_objs=True)
result = [] result = []
for user in users: for user in users:
# 重置密码 # 重置密码
data = {"id": user.id, "telephone": user.telephone, "name": user.name} data = {"id": user.id, "telephone": user.telephone, "name": user.name, "email": user.email}
password = user.telephone[5:12] if settings.DEFAULT_PASSWORD == "0" else settings.DEFAULT_PASSWORD password = user.telephone[5:12] if settings.DEFAULT_PASSWORD == "0" else settings.DEFAULT_PASSWORD
user.password = self.model.get_password_hash(password) user.password = self.model.get_password_hash(password)
user.is_reset_password = False user.is_reset_password = False
@ -223,6 +224,14 @@ class UserDal(DalBase):
data["password"] = password data["password"] = password
result.append(data) result.append(data)
await self.db.flush() await self.db.flush()
return result
async def init_password_send_sms(self, ids: List[int], rd: Redis):
"""
初始化所选用户密码并发送通知短信
将用户密码改为系统默认密码并将初始化密码状态改为false
"""
result = await self.init_password(ids)
for user in result: for user in result:
if not user["reset_password_status"]: if not user["reset_password_status"]:
user["send_sms_status"] = False user["send_sms_status"] = False
@ -239,6 +248,35 @@ class UserDal(DalBase):
user["send_sms_msg"] = e.msg user["send_sms_msg"] = e.msg
return result return result
async def init_password_send_email(self, ids: List[int], rd: Redis):
"""
初始化所选用户密码并发送通知邮件
将用户密码改为系统默认密码并将初始化密码状态改为false
"""
result = await self.init_password(ids)
for user in result:
if not user["reset_password_status"]:
user["send_sms_status"] = False
user["send_sms_msg"] = "重置密码失败"
continue
password: str = user.pop("password")
email: str = user.get("email", None)
if email:
subject = "密码已重置"
body = f"您好,您的密码已经重置为{password},请及时登录并修改密码。"
es = EmailSender(rd)
try:
send_result = await es.send_email([email], subject, body)
user["send_sms_status"] = send_result
user["send_sms_msg"] = "" if send_result else "发送失败,请联系管理员"
except CustomException as e:
user["send_sms_status"] = False
user["send_sms_msg"] = e.msg
else:
user["send_sms_status"] = False
user["send_sms_msg"] = "未获取到邮箱地址"
return result
async def update_current_avatar(self, user: models.VadminUser, file: UploadFile): async def update_current_avatar(self, user: models.VadminUser, file: UploadFile):
""" """
更新当前用户头像 更新当前用户头像

View File

@ -23,6 +23,7 @@ class VadminUser(BaseModel):
avatar = Column(String(500), nullable=True, comment='头像') avatar = Column(String(500), nullable=True, comment='头像')
telephone = Column(String(11), nullable=False, index=True, comment="手机号", unique=False) telephone = Column(String(11), nullable=False, index=True, comment="手机号", unique=False)
email = Column(String(50), nullable=True, comment="邮箱地址")
name = Column(String(50), index=True, nullable=False, comment="姓名") name = Column(String(50), index=True, nullable=False, comment="姓名")
nickname = Column(String(50), nullable=True, comment="昵称") nickname = Column(String(50), nullable=True, comment="昵称")
password = Column(String(255), nullable=True, comment="密码") password = Column(String(255), nullable=True, comment="密码")

View File

@ -22,6 +22,7 @@ class UserParams(QueryParams):
self, self,
name: str = None, name: str = None,
telephone: str = None, telephone: str = None,
email: str = None,
is_active: bool | str = None, is_active: bool | str = None,
is_staff: bool | str = None, is_staff: bool | str = None,
params: Paging = Depends() params: Paging = Depends()
@ -29,6 +30,7 @@ class UserParams(QueryParams):
super().__init__(params) super().__init__(params)
self.name = ("like", name) self.name = ("like", name)
self.telephone = ("like", telephone) self.telephone = ("like", telephone)
self.email = ("like", email)
self.is_active = is_active self.is_active = is_active
self.is_staff = is_staff self.is_staff = is_staff

View File

@ -11,13 +11,14 @@
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel, root_validator from pydantic import BaseModel, root_validator
from core.data_types import Telephone, DatetimeStr from core.data_types import Telephone, DatetimeStr, Email
from .role import RoleSimpleOut from .role import RoleSimpleOut
class User(BaseModel): class User(BaseModel):
name: Optional[str] = None name: Optional[str] = None
telephone: Telephone telephone: Telephone
email: Optional[Email] = None
nickname: Optional[str] = None nickname: Optional[str] = None
avatar: Optional[str] = None avatar: Optional[str] = None
is_active: Optional[bool] = True is_active: Optional[bool] = True
@ -40,6 +41,7 @@ class UserUpdateBaseInfo(BaseModel):
""" """
name: str name: str
telephone: Telephone telephone: Telephone
email: Optional[Email] = None
nickname: Optional[str] = None nickname: Optional[str] = None
gender: Optional[str] = "0" gender: Optional[str] = "0"
@ -50,6 +52,7 @@ class UserUpdate(User):
""" """
name: Optional[str] = None name: Optional[str] = None
telephone: Telephone telephone: Telephone
email: Optional[Email] = None
nickname: Optional[str] = None nickname: Optional[str] = None
avatar: Optional[str] = None avatar: Optional[str] = None
is_active: Optional[bool] = True is_active: Optional[bool] = True

View File

@ -119,6 +119,15 @@ async def post_users_init_password(
return SuccessResponse(await crud.UserDal(auth.db).init_password_send_sms(ids.ids, request.app.state.redis)) return SuccessResponse(await crud.UserDal(auth.db).init_password_send_sms(ids.ids, request.app.state.redis))
@app.post("/users/init/password/send/email/", summary="初始化所选用户密码并发送通知邮件")
async def post_users_init_password_send_email(
request: Request,
ids: IdList = Depends(),
auth: Auth = Depends(FullAdminAuth(permissions=["auth.user.reset"]))
):
return SuccessResponse(await crud.UserDal(auth.db).init_password_send_email(ids.ids, request.app.state.redis))
@app.put("/users/wx/server/openid/", summary="更新当前用户服务端微信平台openid") @app.put("/users/wx/server/openid/", summary="更新当前用户服务端微信平台openid")
async def put_user_wx_server_openid(request: Request, code: str, auth: Auth = Depends(AllUserAuth())): async def put_user_wx_server_openid(request: Request, code: str, auth: Auth = Depends(AllUserAuth())):
result = await crud.UserDal(auth.db).update_wx_server_openid(code, auth.user, request.app.state.redis) result = await crud.UserDal(auth.db).update_wx_server_openid(code, auth.user, request.app.state.redis)

View File

@ -19,14 +19,14 @@
# https://www.osgeo.cn/sqlalchemy/orm/loading_relationships.html?highlight=selectinload#sqlalchemy.orm.joinedload # https://www.osgeo.cn/sqlalchemy/orm/loading_relationships.html?highlight=selectinload#sqlalchemy.orm.joinedload
import datetime import datetime
from typing import List from typing import List, Set
from fastapi import HTTPException from fastapi import HTTPException
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from sqlalchemy import func, delete, update, or_ from sqlalchemy import func, delete, update, or_
from sqlalchemy.future import select from sqlalchemy.future import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status from starlette import status
from core.logger import logger from core.exception import CustomException
from sqlalchemy.sql.selectable import Select from sqlalchemy.sql.selectable import Select
from typing import Any from typing import Any
@ -47,6 +47,7 @@ class DalBase:
data_id: int = None, data_id: int = None,
v_options: list = None, v_options: list = None,
v_join_query: dict = None, v_join_query: dict = None,
v_or: List[tuple] = None,
v_order: str = None, v_order: str = None,
v_return_none: bool = False, v_return_none: bool = False,
v_schema: Any = None, v_schema: Any = None,
@ -58,6 +59,7 @@ class DalBase:
:param data_id: 数据 ID :param data_id: 数据 ID
:param v_options: 指示应使用select在预加载中加载给定的属性 :param v_options: 指示应使用select在预加载中加载给定的属性
:param v_join_query: 外键字段查询内连接 :param v_join_query: 外键字段查询内连接
:param v_or: 或逻辑查询
:param v_order: 排序默认正序 desc 是倒叙 :param v_order: 排序默认正序 desc 是倒叙
:param v_return_none: 是否返回空 None否认 抛出异常默认抛出异常 :param v_return_none: 是否返回空 None否认 抛出异常默认抛出异常
:param v_schema: 指定使用的序列化对象 :param v_schema: 指定使用的序列化对象
@ -66,7 +68,7 @@ class DalBase:
sql = select(self.model).where(self.model.is_delete == False) sql = select(self.model).where(self.model.is_delete == False)
if data_id: if data_id:
sql = sql.where(self.model.id == data_id) sql = sql.where(self.model.id == data_id)
sql = self.add_filter_condition(sql, v_join_query, v_options, **kwargs) sql = self.add_filter_condition(sql, v_options, v_join_query, v_or, **kwargs)
if v_order and (v_order in self.ORDER_FIELD): if v_order and (v_order in self.ORDER_FIELD):
sql = sql.order_by(self.model.create_datetime.desc()) sql = sql.order_by(self.model.create_datetime.desc())
queryset = await self.db.execute(sql) queryset = await self.db.execute(sql)
@ -83,8 +85,9 @@ class DalBase:
self, self,
page: int = 1, page: int = 1,
limit: int = 10, limit: int = 10,
v_join_query: dict = None,
v_options: list = None, v_options: list = None,
v_join_query: dict = None,
v_or: List[tuple] = None,
v_order: str = None, v_order: str = None,
v_order_field: str = None, v_order_field: str = None,
v_return_objs: bool = False, v_return_objs: bool = False,
@ -96,18 +99,19 @@ class DalBase:
获取数据列表 获取数据列表
:param page: 页码 :param page: 页码
:param limit: 当前页数据量 :param limit: 当前页数据量
:param v_join_query: 外键字段查询
:param v_options: 指示应使用select在预加载中加载给定的属性 :param v_options: 指示应使用select在预加载中加载给定的属性
:param v_schema: 指定使用的序列化对象 :param v_join_query: 外键字段查询
:param v_or: 或逻辑查询
:param v_order: 排序默认正序 desc 是倒叙 :param v_order: 排序默认正序 desc 是倒叙
:param v_order_field: 排序字段 :param v_order_field: 排序字段
:param v_return_objs: 是否返回对象 :param v_return_objs: 是否返回对象
:param v_start_sql: 初始 sql :param v_start_sql: 初始 sql
:param v_schema: 指定使用的序列化对象
:param kwargs: 查询参数 :param kwargs: 查询参数
""" """
if not isinstance(v_start_sql, Select): if not isinstance(v_start_sql, Select):
v_start_sql = select(self.model).where(self.model.is_delete == False) v_start_sql = select(self.model).where(self.model.is_delete == False)
sql = self.add_filter_condition(v_start_sql, v_join_query, v_options, **kwargs) sql = self.add_filter_condition(v_start_sql, v_options, v_join_query, v_or, **kwargs)
if v_order_field and (v_order in self.ORDER_FIELD): if v_order_field and (v_order in self.ORDER_FIELD):
sql = sql.order_by(getattr(self.model, v_order_field).desc(), self.model.id.desc()) sql = sql.order_by(getattr(self.model, v_order_field).desc(), self.model.id.desc())
elif v_order_field: elif v_order_field:
@ -121,15 +125,17 @@ class DalBase:
return queryset.scalars().unique().all() return queryset.scalars().unique().all()
return [await self.out_dict(i, v_schema=v_schema) for i in queryset.scalars().unique().all()] return [await self.out_dict(i, v_schema=v_schema) for i in queryset.scalars().unique().all()]
async def get_count(self, v_join_query: dict = None, v_options: list = None, **kwargs): async def get_count(self, v_options: list = None, v_join_query: dict = None, v_or: List[tuple] = None, **kwargs):
""" """
获取数据总数 获取数据总数
:param v_join_query: 外键字段查询
:param v_options: 指示应使用select在预加载中加载给定的属性 :param v_options: 指示应使用select在预加载中加载给定的属性
:param v_join_query: 外键字段查询
:param v_or: 或逻辑查询
:param kwargs: 查询参数 :param kwargs: 查询参数
""" """
sql = select(func.count(self.model.id).label('total')).where(self.model.is_delete == False) sql = select(func.count(self.model.id).label('total')).where(self.model.is_delete == False)
sql = self.add_filter_condition(sql, v_join_query, v_options, **kwargs) sql = self.add_filter_condition(sql, v_options, v_join_query, v_or, **kwargs)
queryset = await self.db.execute(sql) queryset = await self.db.execute(sql)
return queryset.one()['total'] return queryset.one()['total']
@ -189,72 +195,118 @@ class DalBase:
else: else:
await self.db.execute(delete(self.model).where(self.model.id.in_(ids))) await self.db.execute(delete(self.model).where(self.model.id.in_(ids)))
def add_filter_condition(self, sql: select, v_join_query: dict = None, v_options: list = None, **kwargs) -> select: def add_filter_condition(
self,
sql: select,
v_options: list = None,
v_join_query: dict = None,
v_or: List[tuple] = None,
**kwargs
) -> select:
""" """
添加过滤条件以及内连接过滤条件 添加过滤条件以及内连接过滤条件
:param sql: :param sql:
:param v_join_query: 外键字段查询内连接
:param v_options: 指示应使用select在预加载中加载给定的属性 :param v_options: 指示应使用select在预加载中加载给定的属性
:param v_join_query: 外键字段查询内连接
:param v_or: 或逻辑
:param kwargs: 关键词参数 :param kwargs: 关键词参数
""" """
if v_join_query and self.key_models: v_join: Set[str] = set()
v_join_left: Set[str] = set()
if v_join_query:
for key, value in v_join_query.items(): for key, value in v_join_query.items():
foreign_key = self.key_models.get(key) foreign_key = self.key_models.get(key)
if foreign_key and foreign_key.get("model"): conditions = []
# 当外键模型在查询模型中存在多个外键时则需要添加onclause属性 self.__dict_filter(conditions, foreign_key.get("model"), **value)
sql = sql.join(foreign_key.get("model"), onclause=foreign_key.get("onclause", None)) if conditions:
for v_key, v_value in value.items(): sql = sql.where(*conditions)
if v_value is not None and v_value != "": v_join.add(key)
v_attr = getattr(foreign_key.get("model"), v_key, None) if v_or:
sql = self.filter_condition(sql, v_attr, v_value) sql = self.__or_filter(sql, v_or, v_join_left, v_join)
else: for item in v_join:
logger.error(f"外键查询报错:{key}模型不存在,无法进行下一步查询。") foreign_key = self.key_models.get(item)
elif v_join_query and not self.key_models: # 当外键模型在查询模型中存在多个外键时则需要添加onclause属性
logger.error(f"外键查询报错key_models 外键模型无配置项,无法进行下一步查询。") sql = sql.join(foreign_key.get("model"), onclause=foreign_key.get("onclause"))
for field in kwargs: for item in v_join_left:
value = kwargs.get(field) foreign_key = self.key_models.get(item)
if value is not None and value != "": # 当外键模型在查询模型中存在多个外键时则需要添加onclause属性
attr = getattr(self.model, field, None) sql = sql.outerjoin(foreign_key.get("model"), onclause=foreign_key.get("onclause"))
sql = self.filter_condition(sql, attr, value) conditions = []
self.__dict_filter(conditions, self.model, **kwargs)
if conditions:
sql = sql.where(*conditions)
if v_options: if v_options:
sql = sql.options(*[load for load in v_options]) sql = sql.options(*[load for load in v_options])
return sql return sql
@classmethod def __or_filter(self, sql: select, v_or: List[tuple], v_join_left: Set[str], v_join: Set[str]):
def filter_condition(cls, sql: Any, attr: Any, value: Any):
""" """
过滤条件 或逻辑操作
:param sql:
:param v_or: 或逻辑
:param v_join_left: 左连接
:param v_join: 内连接
""" """
if not attr: or_list = []
return sql for item in v_or:
if isinstance(value, tuple): if len(item) == 2:
if len(value) == 1: model = self.model
if value[0] == "None": condition = {item[0]: item[1]}
sql = sql.where(attr.is_(None)) self.__dict_filter(or_list, model, **condition)
elif value[0] == "not None": elif len(item) == 4 and item[0] == "fk":
sql = sql.where(attr.isnot(None)) model = self.key_models.get(item[1]).get("model")
elif len(value) == 2 and value[1] is not None: condition = {item[2]: item[3]}
if value[0] == "date": conditions = []
# 根据日期查询, 关键函数是func.time_format和func.date_format self.__dict_filter(conditions, model, **condition)
sql = sql.where(func.date_format(attr, "%Y-%m-%d") == value[1]) if conditions:
elif value[0] == "like": or_list = or_list + conditions
sql = sql.where(attr.like(f"%{value[1]}%")) v_join_left.add(item[1])
elif value[0] == "or": if item[1] in v_join:
sql = sql.where(or_(i for i in value[1])) v_join.remove(item[1])
elif value[0] == "in": else:
sql = sql.where(attr.in_(value[1])) raise CustomException(msg="v_or 获取查询属性失败,语法错误!")
elif value[0] == "between" and len(value[1]) == 2: if or_list:
sql = sql.where(attr.between(value[1][0], value[1][1])) sql = sql.where(or_(i for i in or_list))
elif value[0] == "month":
sql = sql.where(func.date_format(attr, "%Y-%m") == value[1])
elif value[0] == "!=":
sql = sql.where(attr != value[1])
elif value[0] == ">":
sql = sql.where(attr > value[1])
else:
sql = sql.where(attr == value)
return sql return sql
def __dict_filter(self, conditions: list, model, **kwargs):
"""
字典过滤
:param model:
:param kwargs:
"""
for field, value in kwargs.items():
if value is not None and value != "":
attr = getattr(model, field)
if isinstance(value, tuple):
if len(value) == 1:
if value[0] == "None":
conditions.append(attr.is_(None))
elif value[0] == "not None":
conditions.append(attr.isnot(None))
else:
raise CustomException("SQL查询语法错误")
elif len(value) == 2 and value[1] not in [None, [], ""]:
if value[0] == "date":
# 根据日期查询, 关键函数是func.time_format和func.date_format
conditions.append(func.date_format(attr, "%Y-%m-%d") == value[1])
elif value[0] == "like":
conditions.append(attr.like(f"%{value[1]}%"))
elif value[0] == "in":
conditions.append(attr.in_(value[1]))
elif value[0] == "between" and len(value[1]) == 2:
conditions.append(attr.between(value[1][0], value[1][1]))
elif value[0] == "month":
conditions.append(func.date_format(attr, "%Y-%m") == value[1])
elif value[0] == "!=":
conditions.append(attr != value[1])
elif value[0] == ">":
conditions.append(attr > value[1])
else:
raise CustomException("SQL查询语法错误")
else:
conditions.append(attr == value)
async def flush(self, obj: Any = None): async def flush(self, obj: Any = None):
""" """
刷新到数据库 刷新到数据库

View File

@ -37,6 +37,17 @@ class Telephone(str):
return vali_telephone(v) return vali_telephone(v)
class Email(str):
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, v):
return vali_email(v)
class DateStr(str): class DateStr(str):
@classmethod @classmethod

View File

@ -16,7 +16,8 @@ from core.logger import logger
class CustomException(Exception): class CustomException(Exception):
def __init__(self, msg: str, code: int, status_code: int = status.HTTP_200_OK):
def __init__(self, msg: str, code: int = status.HTTP_400_BAD_REQUEST, status_code: int = status.HTTP_200_OK):
self.msg = msg self.msg = msg
self.code = code self.code = code
self.status_code = status_code self.status_code = status_code
@ -33,7 +34,7 @@ def register_exception(app: FastAPI):
自定义异常 自定义异常
""" """
return JSONResponse( return JSONResponse(
status_code=200, status_code=exc.status_code,
content={"message": exc.msg, "code": exc.code}, content={"message": exc.msg, "code": exc.code},
) )

View File

@ -24,5 +24,6 @@ error = logger.add(log_path_error, rotation="00:00", retention="3 days", enqueue
if __name__ == '__main__': if __name__ == '__main__':
print(BASE_DIR) print(BASE_DIR)
logger.info("hell") # logger.info("1")
logger.error("hell") retry: int = 1
logger.error("未从Redis中获取到配置信息正在重新更新配置信息重试次数{}".format(retry))

View File

@ -22,13 +22,30 @@ def vali_telephone(value: str) -> str:
if not value or len(value) != 11 or not value.isdigit(): if not value or len(value) != 11 or not value.isdigit():
raise ValueError("请输入正确手机号") raise ValueError("请输入正确手机号")
REGEX_TELEPHONE = r'^1(3\d|4[4-9]|5[0-35-9]|6[67]|7[013-8]|8[0-9]|9[0-9])\d{8}$' regex = r'^1(3\d|4[4-9]|5[0-35-9]|6[67]|7[013-8]|8[0-9]|9[0-9])\d{8}$'
if not re.match(REGEX_TELEPHONE, value): if not re.match(regex, value):
raise ValueError("请输入正确手机号") raise ValueError("请输入正确手机号")
return value return value
def vali_email(value: str) -> str:
"""
邮箱地址验证器
:param value: 邮箱
:return: 邮箱
"""
if not value:
raise ValueError("请输入邮箱地址")
regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(regex, value):
raise ValueError("请输入正确邮箱地址")
return value

View File

@ -42,13 +42,13 @@ class Cache:
""" """
获取系统配置 获取系统配置
:params tab_name: 配置表标签名称 :params tab_name: 配置表标签名称
:params retry: 重试次数 :params retry_num: 重试次数
""" """
result = await self.rd.get(tab_name) result = await self.rd.get(tab_name)
if not result and retry > 0: if not result and retry > 0:
logger.error(f"未从Redis中获取到{tab_name}配置信息,正在重新更新配置信息,重试次数:{retry}") logger.error(f"未从Redis中获取到{tab_name}配置信息,正在重新更新配置信息,重试次数:{retry}")
await self.cache_tab_names([tab_name]) await self.cache_tab_names([tab_name])
await self.get_tab_name(tab_name, retry - 1) return await self.get_tab_name(tab_name, retry - 1)
elif not result and retry == 0: elif not result and retry == 0:
raise CustomException(f"获取{tab_name}配置信息失败,请联系管理员!", code=status.HTTP_ERROR) raise CustomException(f"获取{tab_name}配置信息失败,请联系管理员!", code=status.HTTP_ERROR)
else: else:

View File

@ -6,7 +6,6 @@
# @IDE : PyCharm # @IDE : PyCharm
# @desc : 简要说明 # @desc : 简要说明
import json
import requests import requests
from core.logger import logger from core.logger import logger
from utils.cache import Cache from utils.cache import Cache