This commit is contained in:
ktianc 2022-12-04 21:05:01 +08:00
commit a754468f5f
25 changed files with 962 additions and 1001 deletions

121
README.md
View File

@ -21,6 +21,7 @@ Kinit 是一套全部开源的快速开发平台,毫无保留给个人及企
- 后端采用 Python 语言现代、快速(高性能) [FastAPI](https://fastapi.tiangolo.com/zh/) 异步框架 + [SQLAlchemy](https://www.sqlalchemy.org/) 异步操作 [MySQL](https://www.mysql.com/) 数据库。 - 后端采用 Python 语言现代、快速(高性能) [FastAPI](https://fastapi.tiangolo.com/zh/) 异步框架 + [SQLAlchemy](https://www.sqlalchemy.org/) 异步操作 [MySQL](https://www.mysql.com/) 数据库。
- 前端采用 [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/),等主流技术开发。
- 新加入 [Typer](https://typer.tiangolo.com/) 命令行应用,简单化数据初始化,数据表模型迁移。
- 权限认证使用[(哈希)密码和 JWT Bearer 令牌的 OAuth2](https://fastapi.tiangolo.com/zh/tutorial/security/oauth2-jwt/),支持多终端认证系统。 - 权限认证使用[(哈希)密码和 JWT Bearer 令牌的 OAuth2](https://fastapi.tiangolo.com/zh/tutorial/security/oauth2-jwt/),支持多终端认证系统。
- 支持加载动态权限菜单,多方式轻松权限控制,按钮级别权限控制。 - 支持加载动态权限菜单,多方式轻松权限控制,按钮级别权限控制。
- 已加入常见的`Redis``MYSQL``MongoDB`数据库异步操作。 - 已加入常见的`Redis``MYSQL``MongoDB`数据库异步操作。
@ -110,6 +111,8 @@ github地址https://github.com/vvandk/kinit
- [x] 已加入常见的`Redis``MYSQL``MongoDB`数据库异步操作。 - [x] 已加入常见的`Redis``MYSQL``MongoDB`数据库异步操作。
- [x] 命令行操作:新加入 `Typer` 命令行应用,简单化数据初始化,数据表模型迁移。
## TODO ## TODO
- [ ] 考虑支持多机部署方案,如果接口使用多机,那么用户是否支持统一认证 - [ ] 考虑支持多机部署方案,如果接口使用多机,那么用户是否支持统一认证
@ -121,6 +124,7 @@ github地址https://github.com/vvandk/kinit
## 前序准备 ## 前序准备
- [FastAPI](https://fastapi.tiangolo.com/zh/) - 熟悉后台接口 Web 框架 - [FastAPI](https://fastapi.tiangolo.com/zh/) - 熟悉后台接口 Web 框架
- [Typer](https://typer.tiangolo.com/) - 熟悉命令行工具的使用
- [node](https://gitee.com/link?target=http%3A%2F%2Fnodejs.org%2F) 和 [git](https://gitee.com/link?target=https%3A%2F%2Fgit-scm.com%2F) - 项目开发环境 - [node](https://gitee.com/link?target=http%3A%2F%2Fnodejs.org%2F) 和 [git](https://gitee.com/link?target=https%3A%2F%2Fgit-scm.com%2F) - 项目开发环境
- [Vite](https://gitee.com/link?target=https%3A%2F%2Fvitejs.dev%2F) - 熟悉 vite 特性 - [Vite](https://gitee.com/link?target=https%3A%2F%2Fvitejs.dev%2F) - 熟悉 vite 特性
- [Vue3](https://gitee.com/link?target=https%3A%2F%2Fv3.vuejs.org%2F) - 熟悉 Vue 基础语法 - [Vue3](https://gitee.com/link?target=https%3A%2F%2Fv3.vuejs.org%2F) - 熟悉 Vue 基础语法
@ -154,9 +158,11 @@ git clone https://gitee.com/ktianc/kinit.git
### 准备工作 ### 准备工作
``` ```
Python >= 3.8.0 (推荐3.8+版本) Python >= 3.10.0
nodejs >= 14.0 (推荐最新) nodejs >= 14.0 (推荐使用最新稳定版)
Mysql >= 8.0 Mysql >= 8.0
MongoDB (推荐使用最新稳定版)
Redis (推荐使用最新稳定版)
``` ```
### 后端 ### 后端
@ -169,31 +175,64 @@ cd kinit-api
pip3 install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ pip3 install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
``` ```
2. 修改数据库信息 2. 修改项目数据库配置信息
`application/settings.py` 文件中配置数据库信息,用于项目连接 `application/config` 目录中
- development.py开发环境
- production.py生产环境
- mysql数据库版本建议8.0
- mysql数据库字符集utf8mb4
```python ```python
""" """
数据库配置项 Mysql 数据库配置项
连接引擎官方文档https://www.osgeo.cn/sqlalchemy/core/engines.html 连接引擎官方文档https://www.osgeo.cn/sqlalchemy/core/engines.html
数据库链接配置说明mysql+asyncmy://数据库用户名:数据库密码@数据库地址:数据库端口/数据库名称 数据库链接配置说明mysql+asyncmy://数据库用户名:数据库密码@数据库地址:数据库端口/数据库名称
""" """
if DEBUG: SQLALCHEMY_DATABASE_URL = "mysql+asyncmy://数据库用户名:数据库密码@数据库地址:数据库端口/数据库名称"
# 测试库 SQLALCHEMY_DATABASE_TYPE = "mysql"
SQLALCHEMY_DATABASE_URL = "mysql+asyncmy://root:123456@127.0.0.1:3306/kinit"
SQLALCHEMY_DATABASE_TYPE = "mysql"
else: """
# 正式库 Redis 数据库配置
SQLALCHEMY_DATABASE_URL = "mysql+asyncmy://root:123456@127.0.0.1:3306/kinit" """
SQLALCHEMY_DATABASE_TYPE = "mysql" REDIS_DB_ENABLE = True
REDIS_DB_URL = "redis://:密码@地址:端口/数据库"
"""
MongoDB 数据库配置
"""
MONGO_DB_ENABLE = True
MONGO_DB_NAME = "数据库名称"
MONGO_DB_URL = f"mongodb://用户名:密码@地址:端口/?authSource={MONGO_DB_NAME}"
"""
阿里云对象存储OSS配置
阿里云账号AccessKey拥有所有API的访问权限风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维请登录RAM控制台创建RAM用户。
yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1杭州为例Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
* [accessKeyId] {String}通过阿里云控制台创建的AccessKey。
* [accessKeySecret] {String}通过阿里云控制台创建的AccessSecret。
* [bucket] {String}通过控制台或PutBucket创建的bucket。
* [endpoint] {String}bucket所在的区域 默认oss-cn-hangzhou。
"""
ALIYUN_OSS = {
"accessKeyId": "accessKeyId",
"accessKeySecret": "accessKeySecret",
"endpoint": "endpoint",
"bucket": "bucket",
"baseUrl": "baseUrl"
}
"""
获取IP地址归属地
文档https://user.ip138.com/ip/doc
"""
IP_PARSE_ENABLE = True
IP_PARSE_TOKEN = "IP_PARSE_TOKEN"
``` ```
并在`alembic.ini`文件中配置数据库信息,用于数据库映射 并在`alembic.ini`文件中配置数据库信息,用于数据库映射
```python ```python
# mysql+pymysql://数据库用户名:数据库密码@数据库地址:数据库端口/数据库名称 # mysql+pymysql://数据库用户名:数据库密码@数据库地址:数据库端口/数据库名称
sqlalchemy.url = mysql+pymysql://root:123456@127.0.0.1/kinit sqlalchemy.url = mysql+pymysql://root:123456@127.0.0.1/kinit
@ -207,39 +246,35 @@ mysql> use kinit; # 使用已创建的数据库
mysql> set names utf8; # 设置编码 mysql> set names utf8; # 设置编码
``` ```
4. 映射数据库 4. 初始化数据库数据
```shell ```shell
# 初次生成映射文件 # 进入项目根目录下执行
alembic revision -m "生成映射文件" python3 main.py init
# 通过该命令可以将模型映射到数据库
alembic upgrade head
# 如果有更新,则可以使用这个命令再次生成映射文件,初次也可以使用
alembic revision --autogenerate -m "update"
# --autogenerate自动将当前模型的修改生成映射脚本。
# 通过该命令可以将模型映射到数据库
alembic upgrade head
``` ```
5. 导入数据库数据 5. 修改项目基本配置信息
导入数据库数据前,请先保存映射后数据库中`alembic_version`表中的`version_num`数据 修改数据库表 - vadmin_system_settings 中的关键信息
导入完成后,将此数据替换到导入后的对应字段 ```python
# 阿里云短信配置
sms_access_key
sms_access_key_secret
sms_sign_name_1
sms_template_code_1
sms_sign_name_2
sms_template_code_2
# 高德地图配置
map_key
```
6. 启动
```shell ```shell
# 数据库文件地址kinit-api/static/kinit.sql # 进入项目根目录下执行
# 导入命令 python3 main.py run
mysql> source kinit-api/static/kinit.sql # 导入备份数据库
```
5. 启动
```
python3 main.py
``` ```
### 前端 ### 前端

View File

@ -26,6 +26,7 @@
"dependencies": { "dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1", "@amap/amap-jsapi-loader": "^1.0.1",
"@iconify/iconify": "^3.0.0", "@iconify/iconify": "^3.0.0",
"@kjgl77/datav-vue3": "^1.3.3",
"@vueuse/core": "^9.5.0", "@vueuse/core": "^9.5.0",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10", "@wangeditor/editor-for-vue": "^5.1.10",
@ -40,6 +41,7 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"moment": "^2.29.4",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.0.23", "pinia": "^2.0.23",
"pinia-plugin-persist": "^1.0.0", "pinia-plugin-persist": "^1.0.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

View File

@ -28,6 +28,8 @@ import { setupRouter } from './router'
// 权限 // 权限
import { setupPermission } from './directives' import { setupPermission } from './directives'
import DataVVue3 from '@kjgl77/datav-vue3'
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
@ -49,6 +51,8 @@ const setupAll = async () => {
setupPermission(app) setupPermission(app)
app.use(DataVVue3)
app.mount('#app') app.mount('#app')
} }

View File

@ -0,0 +1,63 @@
<script lang="ts" setup>
import { reactive, PropType, ref, watch } from 'vue'
const props = defineProps({
centerBottomData: {
type: Array as PropType<string[][]>,
required: true
}
})
const scrollBoardRef = ref<any>(null)
const centerBottomData = ref(props.centerBottomData)
const config = reactive({
header: ['部门名称', '甲醛', 'PM2.5', 'PM10', '温度', '湿度', '更新时间'],
data: centerBottomData.value,
index: true,
columnWidth: [50],
align: ['center'],
rowNum: 6,
waitTime: 2000,
headerHeight: 40
})
watch(
() => props.centerBottomData,
(val: string[][]) => {
scrollBoardRef.value.updateRows(val)
},
{
deep: true
}
)
</script>
<template>
<div class="center-bottom-view">
<dv-scroll-board ref="scrollBoardRef" :config="config" />
</div>
</template>
<style lang="less">
.center-bottom-view {
width: 100%;
height: 100%;
margin-top: 20px;
.dv-scroll-board {
width: 100%;
height: 100%;
color: #fff;
.header {
font-size: 18px;
}
.rows .row-item {
font-size: 18px;
}
}
}
</style>

View File

@ -0,0 +1,151 @@
<script lang="ts" setup>
import { Echart } from '@/components/Echart'
import { propTypes } from '@/utils/propTypes'
import { EChartsOption } from 'echarts'
import { PropType, ref, watch, reactive } from 'vue'
import { CenterTopPropsType } from '../typers'
const props = defineProps({
centerTopData: {
type: Object as PropType<CenterTopPropsType>,
required: true
},
activeMenuName: propTypes.string
})
const lineOptions = ref({})
watch(
() => props.centerTopData,
(val: CenterTopPropsType) => {
lineOptions.value = {
xAxis: {
data: [
'6H',
'7H',
'8H',
'9H',
'10H',
'11H',
'12H',
'13H',
'14H',
'15H',
'16H',
'17H',
'18H',
'19H'
],
type: 'category'
},
textStyle: {
fontFamily: 'Microsoft YaHei',
fontSize: 20,
fontStyle: 'normal',
fontWeight: 'normal',
color: '#ecc460'
},
grid: {
left: 20,
right: 20,
bottom: 20,
top: 80,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
padding: [5, 10]
},
yAxis: [
{
type: 'value',
name: '',
min: 0,
axisLabel: {
formatter: '{value}'
}
}
],
legend: {
data: ['PM2.5', '甲醛', '温度', '湿度'],
// top: 50,
textStyle: {
color: '#c3f19d'
}
},
series: [
{
name: 'PM2.5',
type: 'bar',
color: '#bbff67',
emphasis: {
focus: 'series'
},
data: val.pm25,
showBackground: false,
barGap: 0
},
{
name: '甲醛',
type: 'bar',
color: '#6deedf',
emphasis: {
focus: 'series'
},
data: val.hcho,
showBackground: false,
barGap: 0
},
{
name: '温度',
type: 'line',
emphasis: {
focus: 'series'
},
data: val.temp
},
{
name: '湿度',
type: 'line',
emphasis: {
focus: 'series'
},
data: val.hum
}
]
}
},
{
immediate: true,
deep: true
}
)
// const lineOptions: EChartsOption = reactive()
</script>
<template>
<div class="center-top-view">
<span class="text-3xl font-bold">{{ props.activeMenuName }}</span>
<div>
<Echart :options="lineOptions" :height="400" />
</div>
</div>
</template>
<style lang="less">
.center-top-view {
width: 100%;
height: 100%;
box-shadow: 0 0 3px blue;
display: flex;
flex-direction: column;
background-color: rgba(6, 30, 93, 0.5);
border-top: 2px solid rgba(1, 153, 209, 0.5);
box-sizing: border-box;
padding: 10px 20px;
}
</style>

View File

@ -0,0 +1,95 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { PropType } from 'vue'
import { LeftPropsType } from '../typers/index'
const props = defineProps({
leftData: {
type: Object as PropType<LeftPropsType>,
required: true
},
activeMenuName: propTypes.string
})
</script>
<template>
<div class="left-view">
<span class="text-3xl font-bold">{{ props.activeMenuName }}</span>
<div class="main-content">
<dv-border-box11 title="室内甲醛">
<div class="data-view">
<span class="data-title">{{ props.leftData.hcho }}ug/</span>
<span class="data-desc">提示低于80ug/m³适合长期居住</span>
</div>
</dv-border-box11>
<dv-border-box11 title="室内PM2.5">
<div class="data-view">
<span class="data-title">{{ props.leftData.pm25 }}ug/</span>
<span class="data-desc">提示低于75ug/m³适合长期居住</span>
</div>
</dv-border-box11>
<dv-border-box11 title="室内温度">
<div class="data-view">
<span class="data-title">{{ props.leftData.temp }}°C</span>
<span class="data-desc">提示当前室外温度为25°C</span>
</div>
</dv-border-box11>
<dv-border-box11 title="室内湿度">
<div class="data-view">
<span class="data-title">{{ props.leftData.hum }}%RH</span>
<span class="data-desc">提示当前室外湿度为38%RH</span>
</div>
</dv-border-box11>
</div>
</div>
</template>
<style lang="less">
.left-view {
width: 100%;
height: 100%;
box-shadow: 0 0 3px blue;
display: flex;
flex-direction: column;
background-color: rgba(6, 30, 93, 0.5);
border-top: 2px solid rgba(1, 153, 209, 0.5);
padding: 10px 20px;
.main-content {
flex: 1;
display: flex;
flex-direction: column;
margin-top: 10px;
.dv-border-box-11 {
.border-box-content {
justify-content: center;
align-items: center;
display: -webkit-flex;
}
.data-view {
position: relative;
height: 100%;
width: 100%;
justify-content: center;
display: -webkit-flex;
.data-title {
font-size: 35px;
display: block;
position: absolute;
top: 40%;
}
.data-desc {
font-size: 14px;
display: block;
position: absolute;
bottom: 10%;
}
}
}
}
}
</style>

View File

@ -0,0 +1,55 @@
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { PropType } from 'vue'
const props = defineProps({
menus: {
type: Array as PropType<string[]>,
required: true
},
activeIndex: propTypes.number
})
</script>
<template>
<div class="top-menu-view">
<dv-border-box10>
<div class="menu-item-view" v-for="(item, index) in props.menus" :key="index">
<dv-decoration7
class="animate__animated animate__fadeInDown"
v-if="index === props.activeIndex"
>
{{ item }}
</dv-decoration7>
<span v-else>{{ item }}</span>
</div>
</dv-border-box10>
</div>
</template>
<style lang="less">
.top-menu-view {
.dv-border-box-10 {
height: 80px;
overflow: hidden;
.menu-item-view {
float: left;
line-height: 80px;
margin: 0 30px;
font-size: 22px;
color: #909399;
font-weight: bold;
&:first-of-type {
margin-left: 50px;
}
.dv-decoration-7 {
height: 100%;
color: #fff;
}
}
}
}
</style>

View File

@ -0,0 +1,139 @@
<script lang="ts" setup>
import Left from './components/Left.vue'
import CenterTop from './components/CenterTop.vue'
import CenterBottom from './components/CenterBottom.vue'
import TopMenu from './components/TopMenu.vue'
import { LeftPropsType, CenterTopPropsType } from './typers/index'
import { ref, onBeforeUnmount } from 'vue'
import moment from 'moment'
const randomNumber = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min)) + min
}
const randomNumberStr = (min: number, max: number) => {
return randomNumber(min, max).toString()
}
const randArray = (len: number, min: number, max: number) => {
return Array(len)
.fill(1)
.map(() => randomNumber(min, max))
}
const leftData = ref({} as LeftPropsType)
const centerBottomData = ref([] as string[][])
const centerTopData = ref({} as CenterTopPropsType)
const menus = ref([] as string[])
const activeIndex = ref(-1)
const activeMenuName = ref('')
const generateData = () => {
leftData.value = {
pm25: randomNumberStr(5, 50),
temp: randomNumberStr(5, 50),
hum: randomNumberStr(5, 50),
hcho: randomNumberStr(5, 50)
}
menus.value = [
'技术1部',
'技术2部',
'运营部',
'销售部',
'人力资源部',
'技术支持部',
'客服部',
'老板办公室'
]
centerBottomData.value = []
for (let item of menus.value) {
centerBottomData.value.push([
item,
randomNumberStr(5, 50),
randomNumberStr(5, 50),
randomNumberStr(5, 50),
randomNumberStr(5, 50),
randomNumberStr(5, 50),
moment(new Date()).format('YYYY-MM-DD HH:mm:ss')
])
}
centerTopData.value = {
pm25: randArray(14, 10, 50),
temp: randArray(14, 10, 50),
hum: randArray(14, 10, 50),
hcho: randArray(14, 10, 50)
}
activeIndex.value++
if (activeIndex.value === menus.value.length) {
activeIndex.value = 0
}
activeMenuName.value = menus.value[activeIndex.value]
}
generateData()
const timer = setInterval(() => {
setTimeout(() => {
generateData()
}, 0)
}, 6000)
onBeforeUnmount(() => {
console.log('已清除定时器')
clearInterval(timer)
})
</script>
<template>
<div id="data-view">
<dv-full-screen-container>
<div class="flex justify-between">
<div small-bg>
<dv-decoration8 style="width: 500px; height: 60px" />
</div>
<div class="text-4xl leading-80px font-bold">
<span>办公室空气质量实时检测</span>
</div>
<div small-bg>
<dv-decoration8 :reverse="true" style="width: 500px; height: 60px" />
</div>
</div>
<div class="mx-10px my-15px">
<TopMenu :menus="menus" :activeIndex="activeIndex" />
</div>
<div class="h-1/1 overflow-hidden mb-10px ml-10px">
<div class="float-left w-20/100 h-1/1 mr-20px">
<Left :leftData="leftData" :activeMenuName="activeMenuName" />
</div>
<div class="float-left w-78/100 h-5/10">
<CenterTop :centerTopData="centerTopData" :activeMenuName="activeMenuName" />
</div>
<div class="float-left w-78/100 h-48/100">
<CenterBottom :centerBottomData="centerBottomData" />
</div>
</div>
</dv-full-screen-container>
</div>
</template>
<style lang="less">
#data-view {
width: 100%;
height: 100%;
background-color: #030409;
color: #fff;
#dv-full-screen-container {
background-image: url('@/assets/imgs/bg.png');
background-size: 100% 100%;
box-shadow: 0 0 3px blue;
display: flex;
flex-direction: column;
}
}
</style>

View File

@ -0,0 +1,13 @@
export type LeftPropsType = {
pm25: string
temp: string
hum: string
hcho: string
}
export type CenterTopPropsType = {
pm25: number[]
temp: number[]
hum: number[]
hcho: number[]
}

View File

@ -37,9 +37,9 @@ SQLAlchemy-Utilshttps://sqlalchemy-utils.readthedocs.io/en/latest/
## 开发环境 ## 开发环境
开发语言Python 3.8 开发语言Python 3.10
开发框架Fastapi 0.73.0 开发框架Fastapi 0.87.0
## 使用 ## 使用
@ -54,15 +54,22 @@ pip3 install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
1. 阿里源: https://mirrors.aliyun.com/pypi/simple/ 1. 阿里源: https://mirrors.aliyun.com/pypi/simple/
``` ```
### 数据初始化
```shell
# 项目根目录下执行,需提前创建好数据库
# 会自动将模型迁移到数据库,并生成初始化数据
python main.py init
```
### 运行启动 ### 运行启动
```shell
# 直接运行main文件
python main.py run
``` ```
# 命令行运行(开发模式)
uvicorn main:app --host=127.0.0.1 --port=9000 --reload
# 或者直接运行main文件 ## 其他
python main.py
```
在线文档地址(在配置文件里面设置路径或者关闭) 在线文档地址(在配置文件里面设置路径或者关闭)
@ -86,19 +93,10 @@ git commit -m "clear cached"
执行数据库迁移命令(终端执行) 执行数据库迁移命令(终端执行)
```python ```shell
# 初次生成迁移文件 # 执行命令:
alembic revision -m "生成迁移文件"
# 通过该命令可以将模型迁移到数据库 python main.py migrate
alembic upgrade head
# 如果有更新,则可以使用这个命令再次生成迁移文件,初次也可以使用
alembic revision --autogenerate -m "update"
# --autogenerate自动将当前模型的修改生成迁移脚本。
# 通过该命令可以将模型迁移到数据库
alembic upgrade head
``` ```
生成迁移文件后会在alembic迁移目录中的version目录中多个迁移文件 生成迁移文件后会在alembic迁移目录中的version目录中多个迁移文件

View File

@ -8,8 +8,13 @@
import os import os
from fastapi.security import OAuth2PasswordBearer from fastapi.security import OAuth2PasswordBearer
"""
系统版本
"""
VERSION = "1.3.0"
"""安全警告: 不要在生产中打开调试运行!""" """安全警告: 不要在生产中打开调试运行!"""
DEBUG = False DEBUG = True
"""是否开启演示功能取消所有POST,DELETE,PUT操作权限""" """是否开启演示功能取消所有POST,DELETE,PUT操作权限"""
DEMO = False DEMO = False

View File

@ -10,4 +10,4 @@
from .m2m import vadmin_user_roles, vadmin_role_menus from .m2m import vadmin_user_roles, vadmin_role_menus
from .menu import VadminMenu from .menu import VadminMenu
from .role import VadminRole from .role import VadminRole
from .user import VadminUser from .user import VadminUser

View File

@ -6,6 +6,10 @@
# @IDE : PyCharm # @IDE : PyCharm
# @desc : 关联中间表 # @desc : 关联中间表
"""
Table 操作博客http://www.ttlsa.com/python/sqlalchemy-concise-guide/
"""
from db.db_base import Model from db.db_base import Model
from sqlalchemy import Column, Table, Integer, ForeignKey, INT from sqlalchemy import Column, Table, Integer, ForeignKey, INT

View File

@ -12,7 +12,6 @@
from typing import List, Union from typing import List, Union
from sqlalchemy import select, update from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from utils.file_manage import FileManage from utils.file_manage import FileManage
from . import models, schemas from . import models, schemas
from core.crud import DalBase from core.crud import DalBase

File diff suppressed because one or more lines are too long

View File

@ -19,79 +19,90 @@ from starlette.staticfiles import StaticFiles # 依赖安装pip install aiof
import importlib import importlib
from core.logger import logger from core.logger import logger
from core.exception import register_exception from core.exception import register_exception
import typer
""" from scripts.initialize.initialize import InitializeData
其他配置 import asyncio
docs_url配置交互文档的路由地址如果禁用则为None默认为 /docs
redoc_url 配置 Redoc 文档的路由地址如果禁用则为None默认为 /redoc
openapi_url配置接口文件json数据文件路由地址如果禁用则为None默认为/openapi.json
"""
app = FastAPI(
title="KInit",
description="本项目基于Fastapi与Vue3+Typescript+Vite3+element-plus的基础项目 前端基于vue-element-plus-admin框架开发",
version="1.0.0"
)
def import_module(modules: list, desc: str): shell_app = typer.Typer()
for module in modules:
if not module:
continue
try:
# 动态导入模块
module_pag = importlib.import_module(module[0:module.rindex(".")])
getattr(module_pag, module[module.rindex(".") + 1:])(app)
except ModuleNotFoundError:
logger.error(f"AttributeError导入{desc}失败,未找到该模块:{module}")
except AttributeError:
logger.error(f"ModuleNotFoundError导入{desc}失败,未找到该模块下的方法:{module}")
""" def create_app():
添加中间件 """
""" 启动项目
import_module(settings.MIDDLEWARES, "中间件")
""" docs_url配置交互文档的路由地址如果禁用则为None默认为 /docs
添加全局事件 redoc_url 配置 Redoc 文档的路由地址如果禁用则为None默认为 /redoc
""" openapi_url配置接口文件json数据文件路由地址如果禁用则为None默认为/openapi.json
import_module(settings.EVENTS, "全局事件") """
app = FastAPI(
title="KInit",
description="本项目基于Fastapi与Vue3+Typescript+Vite3+element-plus的基础项目 前端基于vue-element-plus-admin框架开发",
version="1.0.0"
)
""" def import_module(modules: list, desc: str):
全局异常捕捉处理 for module in modules:
""" if not module:
register_exception(app) continue
try:
# 动态导入模块
module_pag = importlib.import_module(module[0:module.rindex(".")])
getattr(module_pag, module[module.rindex(".") + 1:])(app)
except ModuleNotFoundError:
logger.error(f"AttributeError导入{desc}失败,未找到该模块:{module}")
except AttributeError:
logger.error(f"ModuleNotFoundError导入{desc}失败,未找到该模块下的方法:{module}")
""" import_module(settings.MIDDLEWARES, "中间件")
跨域解决 import_module(settings.EVENTS, "全局事件")
""" # 全局异常捕捉处理
if settings.CORS_ORIGIN_ENABLE: register_exception(app)
app.add_middleware( # 跨域解决
CORSMiddleware, if settings.CORS_ORIGIN_ENABLE:
allow_origins=settings.ALLOW_ORIGINS, app.add_middleware(
allow_credentials=settings.ALLOW_CREDENTIALS, CORSMiddleware,
allow_methods=settings.ALLOW_METHODS, allow_origins=settings.ALLOW_ORIGINS,
allow_headers=settings.ALLOW_HEADERS) allow_credentials=settings.ALLOW_CREDENTIALS,
allow_methods=settings.ALLOW_METHODS,
allow_headers=settings.ALLOW_HEADERS)
# 挂在静态目录
if settings.STATIC_ENABLE:
app.mount(settings.STATIC_URL, app=StaticFiles(directory=settings.STATIC_ROOT))
if settings.TEMP_ENABLE:
app.mount(settings.TEMP_URL, app=StaticFiles(directory=settings.TEMP_DIR))
# 引入应用中的路由
for url in urls.urlpatterns:
app.include_router(url["ApiRouter"], prefix=url["prefix"], tags=url["tags"])
return app
"""
挂在静态目录
"""
if settings.STATIC_ENABLE:
app.mount(settings.STATIC_URL, app=StaticFiles(directory=settings.STATIC_ROOT))
if settings.TEMP_ENABLE:
app.mount(settings.TEMP_URL, app=StaticFiles(directory=settings.TEMP_DIR))
""" @shell_app.command()
引入应用中的路由 def run():
""" """
for url in urls.urlpatterns: 启动项目
app.include_router(url["ApiRouter"], prefix=url["prefix"], tags=url["tags"]) """
uvicorn.run(app='main:create_app', host="0.0.0.0", port=9000)
@shell_app.command()
def init():
"""
初始化数据
"""
print("开始初始化数据")
data = InitializeData()
asyncio.run(data.run())
@shell_app.command()
def migrate():
"""
将模型迁移到数据库更新数据库表结构
"""
print("开始更新数据库表")
InitializeData().migrate_model()
if __name__ == '__main__': if __name__ == '__main__':
""" shell_app()
# 启动项目
# reload自动重载项目
# debug调试
# workers启动几个进程
"""
uvicorn.run(app='main:app', host="0.0.0.0", port=9000)

View File

@ -1,82 +1,83 @@
aiohttp==3.8.3 aiohttp
aioredis==2.0.1 aioredis
aiosignal==1.2.0 aiosignal
alembic==1.7.5 alembic
alibabacloud-credentials==0.3.0 alibabacloud-credentials
alibabacloud-dysmsapi20170525==2.0.22 alibabacloud-dysmsapi20170525
alibabacloud-endpoint-util==0.0.3 alibabacloud-endpoint-util
alibabacloud-gateway-spi==0.0.1 alibabacloud-gateway-spi
alibabacloud-openapi-util==0.1.6 alibabacloud-openapi-util
alibabacloud-tea==0.3.0 alibabacloud-tea
alibabacloud-tea-openapi==0.3.5 alibabacloud-tea-openapi
alibabacloud-tea-util==0.3.7 alibabacloud-tea-util
alibabacloud-tea-xml==0.0.2 alibabacloud-tea-xml
aliyun-python-sdk-core==2.13.36 aliyun-python-sdk-core
aliyun-python-sdk-kms==2.16.0 aliyun-python-sdk-kms
anyio==3.5.0 anyio
asgiref==3.5.0 asgiref
async-timeout==4.0.2 async-timeout
asyncmy==0.2.5 asyncmy
attrs==22.1.0 attrs
bcrypt==3.2.2 bcrypt
certifi==2022.6.15 certifi
cffi==1.15.1 cffi
charset-normalizer==2.0.12 charset-normalizer
click==8.0.3 click
colorama==0.4.4 colorama
crcmod==1.7 crcmod
cryptography==37.0.4 cryptography
dnspython==2.2.1 dnspython
ecdsa==0.18.0 ecdsa
et-xmlfile==1.1.0 et-xmlfile
fastapi==0.85.1 fastapi
frozenlist==1.3.1 frozenlist
greenlet==2.0.0 greenlet
gunicorn==20.1.0 gunicorn
h11==0.13.0 h11
httptools==0.3.0 httptools
idna==3.3 idna
importlib-metadata==4.10.1 importlib-metadata
importlib-resources==5.4.0 importlib-resources
jmespath==0.10.0 jmespath
loguru==0.5.3 loguru
Mako==1.1.6 Mako
MarkupSafe==2.0.1 MarkupSafe
motor==3.1.1 motor
multidict==6.0.2 multidict
openpyxl==3.0.10 openpyxl
orjson==3.6.6 orjson
oss2==2.16.0 oss2
passlib==1.7.4 passlib
Pillow==9.2.0 Pillow
pyasn1==0.4.8 pyasn1
pycparser==2.21 pycparser
pycryptodome==3.15.0 pycryptodome
pydantic==1.9.0 pydantic
pymongo==4.3.2 pymongo
PyMySQL==1.0.2 PyMySQL
python-dotenv==0.19.2 python-dotenv
python-jose==3.3.0 python-jose
python-multipart==0.0.5 python-multipart
PyYAML==6.0 PyYAML
requests==2.28.0 requests
rsa==4.9 rsa
six==1.16.0 six
sniffio==1.2.0 sniffio
SQLAlchemy==1.4.31 SQLAlchemy
SQLAlchemy-Utils==0.38.3 SQLAlchemy-Utils
SSIM-PIL==1.0.14 SSIM-PIL
starlette==0.20.4 starlette
typing-extensions==4.0.1 typing-extensions
ua-parser==0.15.0 ua-parser
urllib3==1.26.9 urllib3
user-agents==2.2.0 user-agents
uvicorn==0.17.0.post1 uvicorn
watchgod==0.7 watchgod
websockets==10.1 websockets
win32-setctime==1.1.0 win32-setctime
XlsxWriter==3.0.3 XlsxWriter
yarl==1.8.1 yarl
zipp==3.7.0 zipp
zope.event==4.5.0 zope.event
zope.interface==5.5.0 zope.interface
typer

View File

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# @version : 1.0
# @Creaet Time : 2021/10/19 15:47
# @File : initialize.py
# @IDE : PyCharm
# @desc : 初始化数据

Binary file not shown.

View File

@ -0,0 +1,148 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Creaet Time : 2022/11/23 11:21
# @File : initialize.py
# @IDE : PyCharm
# @desc : 简要说明
from core.database import db_getter
from utils.excel.excel_manage import ExcelManage
from application.settings import BASE_DIR, VERSION
import os
from apps.vadmin.auth import models as auth_models
from apps.vadmin.system import models as system_models
from sqlalchemy.sql.schema import Table
import subprocess
class InitializeData:
"""
初始化数据
生成步骤
1. 读取数据
2. 获取数据库
3. 创建数据
"""
SCRIPT_DIR = os.path.join(BASE_DIR, 'scripts', 'initialize')
def __init__(self):
self.sheet_names = []
self.datas = {}
self.ex = None
self.db = None
self.__serializer_data()
self.__get_sheet_data()
@classmethod
def migrate_model(cls):
"""
模型迁移映射到数据库
"""
subprocess.check_call(f'alembic revision --autogenerate -m "{VERSION}"', cwd=BASE_DIR)
subprocess.check_call('alembic upgrade head', cwd=BASE_DIR)
print(f"{VERSION} 数据库表迁移完成")
def __serializer_data(self):
"""
序列化数据将excel数据转为python对象
"""
self.ex = ExcelManage()
self.ex.open_workbook(os.path.join(self.SCRIPT_DIR, 'data', 'init.xlsx'), read_only=True)
self.sheet_names = self.ex.get_sheets()
def __get_sheet_data(self):
"""
获取工作区数据
"""
for sheet in self.sheet_names:
sheet_data = []
self.ex.open_sheet(sheet)
headers = self.ex.get_header()
datas = self.ex.readlines(min_row=2, max_col=len(headers))
for row in datas:
sheet_data.append(dict(zip(headers, row)))
self.datas[sheet] = sheet_data
async def __generate_data(self, table_name: str, model):
"""
生成数据
@params table_name: 表名
@params model: 数据表模型
"""
async_session = db_getter()
db = await async_session.__anext__()
if isinstance(model, Table):
for data in self.datas.get(table_name):
await db.execute(model.insert().values(**data))
else:
for data in self.datas.get(table_name):
db.add(model(**data))
print(f"{table_name} 表数据已生成")
await db.flush()
await db.commit()
async def generate_menu(self):
"""
生成菜单数据
"""
await self.__generate_data("vadmin_auth_menu", auth_models.VadminMenu)
async def generate_role(self):
"""
生成角色
"""
await self.__generate_data("vadmin_auth_role", auth_models.VadminRole)
async def generate_user(self):
"""
生成用户
"""
await self.__generate_data("vadmin_auth_user", auth_models.VadminUser)
async def generate_user_role(self):
"""
生成用户
"""
await self.__generate_data("vadmin_auth_user_roles", auth_models.vadmin_user_roles)
async def generate_system_tab(self):
"""
生成系统配置分类数据
"""
await self.__generate_data("vadmin_system_settings_tab", system_models.VadminSystemSettingsTab)
async def generate_system_config(self):
"""
生成系统配置数据
"""
await self.__generate_data("vadmin_system_settings", system_models.VadminSystemSettings)
async def generate_dict_type(self):
"""
生成字典类型数据
"""
await self.__generate_data("vadmin_system_dict_type", system_models.VadminDictType)
async def generate_dict_details(self):
"""
生成字典详情数据
"""
await self.__generate_data("vadmin_system_dict_details", system_models.VadminDictDetails)
async def run(self):
"""
执行初始化工作
"""
self.migrate_model()
await self.generate_menu()
await self.generate_role()
await self.generate_user()
await self.generate_user_role()
await self.generate_system_tab()
await self.generate_dict_type()
await self.generate_system_config()
await self.generate_dict_details()
print(f"{VERSION} 数据已初始化完成")

View File

@ -29,21 +29,38 @@ class ExcelManage:
self.sheet = None self.sheet = None
self.wb = None self.wb = None
def open_sheet(self, file: str, sheet_name: str = None, read_only: bool = False, data_only: bool = False) -> None: def open_workbook(self, file: str, read_only: bool = False, data_only: bool = False) -> None:
""" """
初始化 excel 文件 初始化 excel 文件
@param file: 文件名称或者对象 @param file: 文件名称或者对象
@param sheet_name: 表单名称为空则默认第一个
@param read_only: 是否只读优化读取速度 @param read_only: 是否只读优化读取速度
@param data_only: 是否加载文件对象 @param data_only: 是否加载文件对象
""" """
# 加载excel文件获取表单 # 加载excel文件获取表单
self.wb = load_workbook(file, read_only=read_only, data_only=data_only) self.wb = load_workbook(file, read_only=read_only, data_only=data_only)
def open_sheet(self, sheet_name: str = None, **kwargs) -> None:
"""
初始化 excel 文件
@param sheet_name: 表单名称为空则默认第一个
"""
# 加载excel文件获取表单
if not self.wb:
self.open_workbook(kwargs.get("file"), kwargs.get("read_only", False), kwargs.get("data_only", False))
if sheet_name: if sheet_name:
self.sheet = self.wb[sheet_name] self.sheet = self.wb[sheet_name]
else: else:
self.sheet = self.wb.active self.sheet = self.wb.active
def get_sheets(self) -> list:
"""
读取所有工作区名称
@return: 一维数组
"""
return self.wb.sheetnames
def create_excel(self, sheet_name: str = None) -> None: def create_excel(self, sheet_name: str = None) -> None:
""" """
创建 excel 文件 创建 excel 文件
@ -72,7 +89,7 @@ class ExcelManage:
result.append(_row) result.append(_row)
return result return result
def get_header(self, row: int = 1, col: int = 1, asterisk: bool = False) -> list: def get_header(self, row: int = 1, col: int = None, asterisk: bool = False) -> list:
""" """
读取指定表单的表头第一行数据 读取指定表单的表头第一行数据

View File

@ -1,313 +0,0 @@
# 项目部署
## 初始化工作
### 部署目录
根目录:/opt
项目目录:/opt/project
项目启动日志目录:/opt/logs
## 生产环境要求
服务器系统版本Centos 7.2以上
| 软件名称 | 版本 |
| ---------- | ------------ |
| Python | >= 3.8 |
| Mysql | >= 8.0 |
| Nginx | >= 1.16 |
| Git | 最新,非必须 |
| Supervisor | >= 3.4.0 |
## 项目配置
### 接口项目配置
**项目主要配置信息:**
文件路径kinit-api/application/settings.py
```python
# 关闭代码调试
DEBUG = False
```
**导出项目接口依赖包:**
kinit-api 根目录下执行命令:
```
pip freeze > requirements.txt
```
### 前端项目打包
kinit-admin 打包项目根目录下执行命令:
```
pnpm run build:pro
```
### 软件安装配置
#### Mysql
安装配置请查看链接https://www.cnblogs.com/yanglang/p/10782941.html
```
scp mysql-8.0.27-1.el7.x86_64.rpm-bundle.tar root@127.0.0.1:/usr/local/pag
// 安装命令:
tar -xvf mysql-8.0.27-1.el7.x86_64.rpm-bundle.tar
rpm -ivh mysql-community-common-8.0.27-1.el7.x86_64.rpm --nodeps --force
rpm -ivh mysql-community-libs-8.0.27-1.el7.x86_64.rpm --nodeps --force
rpm -ivh mysql-community-client-8.0.27-1.el7.x86_64.rpm --nodeps --force
rpm -ivh mysql-community-server-8.0.27-1.el7.x86_64.rpm --nodeps --force
rpm -qa | grep mysql
rpm -e mysql-community-common --nodeps
rpm -e mysql-community-libs --nodeps
rpm -e mysql-community-client --nodeps
rpm -e mysql-community-server --nodeps
mysqld --initialize
chown mysql:mysql /var/lib/mysql -R
systemctl start mysqld.service
systemctl enable mysqld
cat /var/log/mysqld.log | grep password
mysql -uroot -p
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'b0Nrzh97G9lJkQMK';
create user 'root'@'%' identified with mysql_native_password by 'b0Nrzh97G9lJkQMK';
grant all privileges on *.* to 'root'@'%' with grant option;
flush privileges;
```
安装配置完成后建议使用Navicat Premium 作为 Mysql 的客户端
1. 首先需要创建 kinit 系统数据库
2. 然后将 kinit-api\static\kinit.sql 文件导入
#### Python
```shell
# 下载python安装包
wget https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tgz
# 创建安装目录
mkdir /usr/local/python3
# 解压安装包
tar -zxvf Python-3.8.2.tgz
# 进入解压目录
cd Python-3.8.2
# 配置
./configure --prefix=/usr/local/python3
#编译
make && make install
#创建软链接
ln -s /usr/local/python3/bin/python3 /usr/bin/python3
ln -s /usr/local/python3/bin/python3 /usr/local/bin/python3
#pip创建软链接
ln -s /usr/local/python3/bin/pip3 /usr/bin/pip3
# 更新pip
pip3 install --upgrade pip
```
#### Git
安装:
```
yum -y install git
```
获取项目文件:
```
mkdir -p /opt/project/
git clone -b release 项目Git仓库
```
或者也可以通过压缩包上传的方式上传到服务器,推荐使用 Git 方式
#### Python 虚拟环境
创建项目所使用的 python 虚拟环境:
```shell
# 安装 virtualenv
pip3 install virtualenv
# 找到虚拟环境的安装目录
find / -name virtualenv
# 创建软链接
ln -s /usr/local/python3/bin/virtualenv /usr/bin/
# 进入虚拟环境目录
mkdir -p /opt/env
cd /opt/env
# 新建虚拟环境,会在当前目录生成一个文件夹
virtualenv kinit
# 进入虚拟环境(命令行前面会出现虚拟环境名称)
cd kinit
source bin/activate
# 退出虚拟环境命令(之后的设置都是在虚拟环境中设置,这里不需要退出)
deactivate
```
安装接口项目依赖包:
```shell
# 在虚拟环境中执行
# 进入到 saas-api 根目录下执行
pip3 install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
```
#### Supervisor
官方文档http://supervisord.org/
安装配置请查看https://www.jianshu.com/p/0b9054b33db3
创建项目接口 supervisor 配置文件:
```shell
vim /etc/supervisord.d/kinit.api.ini
```
内容如下:
```ini
[program:kinit.api]
directory=/opt/project/kinit/kinit-api/
command=/opt/env/kinit/bin/gunicorn main:app -w 2 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:9000
startsecs=10
stopwaitsecs=10
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile_maxbytes=10MB
stdout_logfile_backups = 3
stdout_logfile=/opt/logs/kinit.api.log
```
更新 supervisor 配置:
```
supervisorctl update
```
更新后会自动启动进程
可以通过命令查看:
```
supervisorctl status
```
#### Nginx
安装:
```
yum -y install nginx
```
增加配置文件
```
vim /etc/nginx/conf.d/kinit.conf
```
内容如下:
```nginx
# 如果需要使用搭配小程序使用则需要配置api的访问域名并配置域名证书
server
{
listen 80;
listen 443 ssl http2;
server_name 127.0.0.1;
root /opt/project/kinit/kinit-api/;
keepalive_timeout 180s;
client_max_body_size 20m;
if ($server_port !~ 443){
rewrite ^(/.*)$ https://$host$1 permanent;
}
#HTTP_TO_HTTPS_END
ssl_certificate /etc/nginx/conf.d/cert/kinit/kinit.pem;
ssl_certificate_key /etc/nginx/conf.d/cert/kinit/kinit.key;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000";
error_page 497 https://$host$request_uri;
location ~ \.well-known{
allow all;
}
location /api/ {
rewrite ^/api/(.*) /$1 break;
}
location / {
proxy_pass http://127.0.0.1:9000;
}
access_log /opt/logs/kinit.api.nginx.log;
error_log /opt/logs/kinit.api.nginx.error.log;
}
server
{
listen 80;
server_name 127.0.0.1;
root /opt/project/kinit/kinit-admin/dist-pro/;
keepalive_timeout 180s;
client_max_body_size 20m;
location ~ \.well-known{
allow all;
}
location / {
try_files $uri $uri/ @router;
root /opt/project/kinit/kinit-admin/dist-pro/;
index index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:9000/;
client_max_body_size 20m;
}
location @router {
rewrite ^.*$ /index.html break;
}
access_log /opt/logs/kinit.nginx.log;
error_log /opt/logs/kinit.nginx.error.log;
}
```
配置完成后启动:
```
systemctl start nginx
# 开启自启动
systemctl enable nginx
```
查看当前状态:
```
systemctl status nginx
```