This commit is contained in:
ktianc 2022-10-09 21:38:03 +08:00
parent df40bebbfd
commit 5f8dc3c361
21 changed files with 357 additions and 62 deletions

View File

@ -10,19 +10,19 @@
kinit 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。 kinit 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
- 🧑‍🤝‍🧑前端采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) 、[Vue3](https://cn.vuejs.org/guide/introduction.html)、[Element Plus](https://element-plus.gitee.io/zh-CN/guide/design.html)[TypeScript](https://www.tslang.cn/),等主流技术开发。 - 🧑‍🤝‍🧑前端采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) 、[Vue3](https://cn.vuejs.org/guide/introduction.html)、[Element Plus](https://element-plus.gitee.io/zh-CN/guide/design.html)[TypeScript](https://www.tslang.cn/),等主流技术开发。
- 👭后端采用 Python 语言高性能 [FastAPI](https://fastapi.tiangolo.com/zh/) 框架以及强大的 Mysql 数据库。 - 👭后端采用 Python 语言高性能 [FastAPI](https://fastapi.tiangolo.com/zh/) 框架以及强大的 Mysql 数据库。
- 👫权限认证使用[使用(哈希)密码和 JWT Bearer 令牌的 OAuth2](https://fastapi.tiangolo.com/zh/tutorial/security/oauth2-jwt/),支持多终端认证系统。 - 👫权限认证使用[(哈希)密码和 JWT Bearer 令牌的 OAuth2](https://fastapi.tiangolo.com/zh/tutorial/security/oauth2-jwt/),支持多终端认证系统。
- 👬支持加载动态权限菜单,多方式轻松权限控制。 - 👬支持加载动态权限菜单,多方式轻松权限控制。
- 💏特别鸣谢:[django-vue-admin](https://gitee.com/liqianglog/django-vue-admin) 、 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin)。 - 💏特别鸣谢:[django-vue-admin](https://gitee.com/liqianglog/django-vue-admin) 、 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin)。
- 开箱即用的中后台解决方案,可以用来作为项目的启动模版,也可用于学习参考。并且时刻关注着最新技术动向,尽可能的第一时间更新。 - 开箱即用的中后台解决方案,可以用来作为项目的启动模版,也可用于学习参考。并且时刻关注着最新技术动向,尽可能的第一时间更新。
## 在线体验 ## 在线体验
👩‍👧‍👦演示地址:[http://demo.django-vue-admin.com](https://gitee.com/link?target=http%3A%2F%2Fdemo.django-vue-admin.com) 👩‍👧‍👦演示地址:正在部署中。。。。。
- 账号:superadmin - 账号admin
- 密码:admin123456 - 密码123456
## 源码地址 ## 源码地址
@ -33,13 +33,27 @@ github地址https://gitee.com/ktianc/kinit👩👦👦
## 内置功能 ## 内置功能
- [x] 👨‍⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。 - [x] 👨‍⚕️菜单管理:配置系统菜单,操作权限,按钮权限标识、后端接口权限等。
- [x] 👩‍⚕️角色管理:角色菜单权限分配。 - [x] 👩‍⚕️角色管理:角色菜单权限分配。
- [x] 👨‍🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置。 - [x] 👨‍🎓用户管理:用户是系统操作者,该功能主要完成系统用户配置。
- [x] 🧑‍🔧字典管理:对系统中经常使用的一些较为固定的数据进行维护。
- [ ] 🏡个人主页:配置用户个人信息,密码修改等。
- [x] 📚字典管理:对系统中经常使用的一些较为固定的数据进行维护。
- [ ] 📁附件管理:对平台上所有文件、图片等进行统一管理。 - [ ] 📁附件管理:对平台上所有文件、图片等进行统一管理。
- [ ] 🗓️登录日志:用户登录日志记录和查询。 - [ ] 🗓️登录日志:用户登录日志记录和查询。
- [ ] 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。 - [ ] 🗓️操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
- [ ] 🔒登录认证:目前支持用户使用手机号+密码方式登录。
说明:新建用户密码默认为手机号后六位;
说明:用户在第一次登录时,必须修改当前用户密码。
## 前序准备 ## 前序准备
- [FastAPI](https://fastapi.tiangolo.com/zh/) - 熟悉后台接口 Web 框架 - [FastAPI](https://fastapi.tiangolo.com/zh/) - 熟悉后台接口 Web 框架
@ -104,8 +118,19 @@ else:
3. 迁移数据库 3. 迁移数据库
``` ```shell
# 初次生成迁移文件
alembic revision -m "生成迁移文件"
# 通过该命令可以将模型迁移到数据库
alembic upgrade head
# 如果有更新,则可以使用这个命令再次生成迁移文件,初次也可以使用
alembic revision --autogenerate -m "update"
# --autogenerate自动将当前模型的修改生成迁移脚本。
# 通过该命令可以将模型迁移到数据库
alembic upgrade head
``` ```
4. 数据化数据库数据 4. 数据化数据库数据
@ -147,12 +172,35 @@ pnpm run build:pro
- 访问地址http://localhost:5000 (默认为此地址,如有修改请按照配置文件) - 访问地址http://localhost:5000 (默认为此地址,如有修改请按照配置文件)
- 账号:`superadmin` 密码:`admin123456` - 账号:`superadmin` 密码:`admin123456`
## 参与贡献 ## 如何贡献
1. Fork 本仓库 你可以[提一个 issue](https://gitee.com/link?target=https%3A%2F%2Fgithub.com%2Fkailong321200875%2Fvue-element-plus-admin%2Fissues%2Fnew) 或者提交一个 Pull Request。
2. 新建 Feat_xxx 分支
3. 提交代码 **Pull Request:**
4. 新建 Pull Request
1. Fork 代码
2. 创建自己的分支: `git checkout -b feat/xxxx`
3. 提交你的修改: `git commit -am 'feat(function): add xxxxx'`
4. 推送您的分支: `git push origin feat/xxxx`
5. 提交 `pull request`
## Git 贡献提交规范
- `feat` 新功能
- `fix` 修补 bug
- `docs` 文档
- `style` 格式、样式(不影响代码运行的变动)
- `refactor` 重构(即不是新增功能,也不是修改 BUG 的代码)
- `perf` 优化相关,比如提升性能、体验
- `test` 添加测试
- `build` 编译相关的修改,对项目构建或者依赖的改动
- `ci` 持续集成修改
- `chore` 构建过程或辅助工具的变动
- `revert` 回滚到上一个版本
- `workflow` 工作流改进
- `mod` 不确定分类的修改
- `wip` 开发中
- `types` 类型
## 浏览器支持 ## 浏览器支持
@ -166,4 +214,18 @@ pnpm run build:pro
## 许可证 ## 许可证
[MIT](https://gitee.com/kailong110120130/vue-element-plus-admin/blob/master/LICENSE) [MIT](https://gitee.com/kailong110120130/vue-element-plus-admin/blob/master/LICENSE)
## 演示图
![image-20221009145928678](images\image-20221009145928678.png)
![image-20221009150108827](images\image-20221009150108827.png)
![image-20221009150256166](images\image-20221009150256166.png)
![image-20221009150311662](images\image-20221009150311662.png)
![image-20221009150349965](images\image-20221009150349965.png)
![image-20221009150414100](images\image-20221009150414100.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@ -8,4 +8,4 @@ VITE_API_BASEPATH=base
VITE_BASE_PATH=/ VITE_BASE_PATH=/
# 标题 # 标题
VITE_APP_TITLE=ElementAdmin VITE_APP_TITLE=KInit

View File

@ -20,4 +20,4 @@ VITE_SOURCEMAP=true
VITE_OUT_DIR=dist-dev VITE_OUT_DIR=dist-dev
# 标题 # 标题
VITE_APP_TITLE=ElementAdmin VITE_APP_TITLE=KInit

View File

@ -5,7 +5,7 @@ NODE_ENV=production
VITE_API_BASEPATH=pro VITE_API_BASEPATH=pro
# 打包路径 # 打包路径
VITE_BASE_PATH=/vue-element-plus-admin/ VITE_BASE_PATH=/kinit/
# 是否删除debugger # 是否删除debugger
VITE_DROP_DEBUGGER=true VITE_DROP_DEBUGGER=true
@ -20,4 +20,4 @@ VITE_SOURCEMAP=false
VITE_OUT_DIR=dist-pro VITE_OUT_DIR=dist-pro
# 标题 # 标题
VITE_APP_TITLE=ElementAdmin VITE_APP_TITLE=KInit

View File

@ -19,3 +19,7 @@ export const putUserListApi = (data: any): Promise<IResponse> => {
export const getUserApi = (dataId: number): Promise<IResponse> => { export const getUserApi = (dataId: number): Promise<IResponse> => {
return request.get({ url: `/vadmin/auth/users/${dataId}/` }) return request.get({ url: `/vadmin/auth/users/${dataId}/` })
} }
export const postCurrentUserResetPassword = (data: any): Promise<IResponse> => {
return request.get({ url: `/vadmin/auth/user/current/reset/password/`, data })
}

View File

@ -28,13 +28,10 @@ const loginOut = () => {
type: 'warning' type: 'warning'
}) })
.then(async () => { .then(async () => {
const res = await loginOutApi().catch(() => {}) wsCache.clear()
if (res) { tagsViewStore.delAllViews()
wsCache.clear() resetRouter() //
tagsViewStore.delAllViews() replace('/login')
resetRouter() //
replace('/login')
}
}) })
.catch(() => {}) .catch(() => {})
} }

View File

@ -43,6 +43,16 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
noTagsView: true noTagsView: true
} }
}, },
{
path: '/reset/password',
component: () => import('@/views/Reset/Reset.vue'),
name: 'Reset',
meta: {
hidden: true,
title: '重置密码',
noTagsView: true
}
},
{ {
path: '/404', path: '/404',
component: () => import('@/views/Error/404.vue'), component: () => import('@/views/Error/404.vue'),

View File

@ -125,8 +125,14 @@ const signIn = async () => {
const authStore = useAuthStoreWithOut() const authStore = useAuthStoreWithOut()
const res = await authStore.login(formData) const res = await authStore.login(formData)
if (res) { if (res) {
// 使 console.log(res)
getMenu() if (!res.data.is_reset_password) {
//
push({ path: '/reset/password' })
} else {
// 使
getMenu()
}
} }
} finally { } finally {
loading.value = false loading.value = false

View File

@ -0,0 +1,192 @@
<script setup lang="ts">
import { reactive, unref, ref, watch } from 'vue'
import { Form } from '@/components/Form'
import { ElButton, ElCheckbox, ElLink } from 'element-plus'
import { useForm } from '@/hooks/web/useForm'
import { getRoleMenusApi } from '@/api/login'
import { useAuthStoreWithOut } from '@/store/modules/auth'
import { usePermissionStore } from '@/store/modules/permission'
import { useRouter } from 'vue-router'
import type { RouteRecordRaw, RouteLocationNormalizedLoaded } from 'vue-router'
import { UserLoginType } from '@/api/login/types'
import { useValidator } from '@/hooks/web/useValidator'
import { useCache } from '@/hooks/web/useCache'
import { useAppStore } from '@/store/modules/app'
import { Footer } from '@/components/Footer'
import { computed } from 'vue'
const appStore = useAppStore()
const { required } = useValidator()
const footer = computed(() => appStore.getFooter)
const permissionStore = usePermissionStore()
const { addRoute, push, currentRoute } = useRouter()
const rules = {
password_one: [required()],
password_two: [required()]
}
const schema = reactive<FormSchema[]>([
{
field: 'title',
colProps: {
span: 24
}
},
{
field: 'password_one',
label: '新密码',
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
style: {
width: '100%'
},
placeholder: '请输入新密码'
}
},
{
field: 'password_two',
label: '再次输入新密码',
component: 'InputPassword',
colProps: {
span: 24
},
componentProps: {
style: {
width: '100%'
},
placeholder: '请再次输入新密码'
}
},
{
field: 'reset',
colProps: {
span: 24
}
}
])
const { register, elFormRef, methods } = useForm()
const loading = ref(false)
const redirect = ref<string>('')
watch(
() => currentRoute.value,
(route: RouteLocationNormalizedLoaded) => {
redirect.value = route?.query?.redirect as string
},
{
immediate: true
}
)
//
const save = async () => {
const formRef = unref(elFormRef)
await formRef?.validate(async (isValid) => {
if (isValid) {
loading.value = true
const { getFormData } = methods
const formData = await getFormData<UserLoginType>()
try {
const authStore = useAuthStoreWithOut()
const res = await authStore.login(formData)
if (res) {
// 使
getMenu()
}
} finally {
loading.value = false
}
}
})
}
//
const getMenu = async () => {
const res = await getRoleMenusApi()
if (res) {
const { wsCache } = useCache()
const routers = res.data || []
wsCache.set('roleRouters', routers)
await permissionStore.generateRoutes(routers).catch(() => {})
permissionStore.getAddRouters.forEach((route) => {
addRoute(route as RouteRecordRaw) // 访
})
permissionStore.setIsAddRouters(true)
push({ path: redirect.value || permissionStore.addRouters[0].path })
}
}
</script>
<template>
<div class="main-container">
<div class="form-container">
<Form
:schema="schema"
:rules="rules"
label-position="top"
hide-required-asterisk
size="large"
class="dark:(border-1 border-[var(--el-border-color)] border-solid)"
@register="register"
>
<template #title>
<h2 class="text-2xl font-bold text-center w-[100%]">第一次登录系统需先重置密码</h2>
</template>
<template #reset>
<div class="w-[100%]">
<ElButton :loading="loading" type="primary" class="w-[100%]" @click="save">
重置密码
</ElButton>
</div>
</template>
</Form>
</div>
<div class="footer-container">
<Footer v-if="footer" />
</div>
</div>
</template>
<style lang="less" scoped>
:deep(.anticon) {
&:hover {
color: var(--el-color-primary) !important;
}
}
.main-container {
display: flex;
flex-direction: column;
padding-top: 20px;
width: 100%;
height: 100%;
background-color: var(--app-content-bg-color);
position: relative;
}
.main-container .form-container {
width: 500px;
align-self: center;
padding: 30px;
background-color: #fff;
border-radius: 30px;
}
.footer-container {
position: absolute;
bottom: 0;
width: 100%;
}
</style>

View File

@ -65,14 +65,14 @@ ALLOW_HEADERS = ["*"]
""" """
if DEBUG: if DEBUG:
# 测试库 # 测试库
SQLALCHEMY_DATABASE_URL = "mysql+asyncmy://root:123456@127.0.0.1:3306/kinit" SQLALCHEMY_DATABASE_URL = "mysql+asyncmy://root:Ktianc123@rm-bp181adf0phw2o0r05o.mysql.rds.aliyuncs.com:3306/kinit"
# SQLALCHEMY_DATABASE_URL = "mysql+asyncmy://root:123456@127.0.0.1:3306/kinit"
SQLALCHEMY_DATABASE_TYPE = "mysql" SQLALCHEMY_DATABASE_TYPE = "mysql"
else: else:
# 正式库 # 正式库
SQLALCHEMY_DATABASE_URL = "mysql+asyncmy://root:123456@127.0.0.1:3306/kinit" SQLALCHEMY_DATABASE_URL = "mysql+asyncmy://root:123456@127.0.0.1:3306/kinit"
SQLALCHEMY_DATABASE_TYPE = "mysql" SQLALCHEMY_DATABASE_TYPE = "mysql"
""" """
中间件配置 中间件配置
""" """

View File

@ -6,11 +6,13 @@
# @IDE : PyCharm # @IDE : PyCharm
# @desc : 增删改查 # @desc : 增删改查
from typing import List from typing import List
from core.exception import CustomException
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from sqlalchemy import select from sqlalchemy import select
from core.crud import DalBase from core.crud import DalBase
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from utils.tools import test_password
from . import models, schemas from . import models, schemas
from application import settings from application import settings
@ -40,6 +42,19 @@ class UserDal(DalBase):
return schema.from_orm(obj).dict() return schema.from_orm(obj).dict()
return self.out_dict(obj) return self.out_dict(obj)
async def reset_current_password(self, user: models.VadminUser, data: schemas.ResetPwd):
"""
重置密码
"""
if data.password != data.password_two:
raise CustomException(msg="两次密码不一致", code=400)
result = test_password(data.password)
if isinstance(result, str):
raise CustomException(msg=result, code=400)
self.db.add(user)
await self.db.flush()
return True
class RoleDal(DalBase): class RoleDal(DalBase):
@ -203,7 +218,3 @@ class MenuDal(DalBase):
router["children"] = self.generate_tree_options(menus, sons) router["children"] = self.generate_tree_options(menus, sons)
data.append(router) data.append(router)
return data return data

View File

@ -50,6 +50,11 @@ async def get_user(data_id: int, auth: Auth = Depends(login_auth)):
return SuccessResponse(await crud.UserDal(auth.db).get_data(data_id, options, schema)) return SuccessResponse(await crud.UserDal(auth.db).get_data(data_id, options, schema))
@app.post("/user/current/reset/password/", summary="重置当前用户密码")
async def user_current_reset_password(data: schemas.ResetPwd, auth: Auth = Depends(login_auth)):
return SuccessResponse(await crud.UserDal(auth.db).reset_current_password(auth.user, data))
########################################################### ###########################################################
# 角色管理 # 角色管理
########################################################### ###########################################################

View File

@ -1,27 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Creaet Time : 2022/2/24 17:02
# @File : crawle.py
# @IDE : PyCharm
# @desc : 爬虫
import requests
from core.logger import logger
def get_schedule_data():
"""
获取足球赛程表
"""
logger.info("开始获取足球赛程")
url = "https://webapi.sporttery.cn/gateway/jc/football/getMatchCalculatorV1.qry?poolCode=hhad,had&channel=c"
res = requests.get(url)
if res.status_code != 200:
logger.error("获取足球赛程失败!")
return False
data = res.json()
if data.get("errorCode") != "0":
logger.error("获取足球赛程失败!")
return False
return data.get("value").get("matchInfoList")

View File

@ -8,9 +8,10 @@
from datetime import datetime from datetime import datetime
import os import os
from application.settings import TEMP_DIR from application.settings import TEMP_DIR
from fastapi import UploadFile
def save_tmp_file(file, data): def save_tmp_file(file: UploadFile, data):
""" """
保存临时文件 保存临时文件
""" """
@ -19,4 +20,4 @@ def save_tmp_file(file, data):
if not os.path.exists(file_dir): if not os.path.exists(file_dir):
os.mkdir(file_dir) os.mkdir(file_dir)
with open(os.path.join(file_dir, file.filename), "wb") as f: with open(os.path.join(file_dir, file.filename), "wb") as f:
f.write(data) f.write(data)

34
kinit-api/utils/tools.py Normal file
View File

@ -0,0 +1,34 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Creaet Time : 2022/10/9 17:09
# @File : tools.py
# @IDE : PyCharm
# @desc : 工具类
import re
def test_password(password: str):
"""
检测密码强度
"""
if len(password) < 8 or len(password) > 16:
return '长度需为8-16个字符,请重新输入。'
else:
for i in password:
if 0x4e00 <= ord(i) <= 0x9fa5 or ord(i) == 0x20: # Ox4e00等十六进制数分别为中文字符和空格的Unicode编码
return '不能使用空格、中文,请重新输入。'
else:
key = 0
key += 1 if bool(re.search(r'\d', password)) else 0
key += 1 if bool(re.search(r'[A-Za-z]', password)) else 0
key += 1 if bool(re.search(r"\W", password)) else 0
if key >= 2:
return True
else:
return '至少含数字/字母/字符2种组合请重新输入。'
if __name__ == '__main__':
print(test_password("123456121a"))