版本更新:
1. 新增(kinit-api,kinit-admin):支持发送邮箱通知 2. 优化(kinit-api):core/crud.py 优化
This commit is contained in:
parent
0152dc9c70
commit
2ba2738107
@ -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 })
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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({
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -11,7 +11,7 @@ from fastapi.security import OAuth2PasswordBearer
|
|||||||
"""
|
"""
|
||||||
系统版本
|
系统版本
|
||||||
"""
|
"""
|
||||||
VERSION = "1.7.1"
|
VERSION = "1.7.2"
|
||||||
|
|
||||||
"""安全警告: 不要在生产中打开调试运行!"""
|
"""安全警告: 不要在生产中打开调试运行!"""
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
更新当前用户头像
|
更新当前用户头像
|
||||||
|
@ -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="密码")
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
刷新到数据库
|
刷新到数据库
|
||||||
|
@ -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
|
||||||
|
@ -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},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Binary file not shown.
@ -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:
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user