Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
1638245306
2024-11-06 01:39:28 +08:00
13 changed files with 570 additions and 5 deletions

1
backend/.gitignore vendored
View File

@@ -98,4 +98,5 @@ media/
__pypackages__/ __pypackages__/
package-lock.json package-lock.json
gunicorn.pid gunicorn.pid
plugins/*
!plugins/__init__.py !plugins/__init__.py

View File

@@ -2,6 +2,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.conf import settings from django.conf import settings
from django.db import connection from django.db import connection
from django.core.cache import cache
from dvadmin.utils.validator import CustomValidationError
dispatch_db_type = getattr(settings, 'DISPATCH_DB_TYPE', 'memory') # redis
def is_tenants_mode(): def is_tenants_mode():
@@ -68,6 +72,9 @@ def init_dictionary():
:return: :return:
""" """
try: try:
if dispatch_db_type == 'redis':
cache.set(f"init_dictionary", _get_all_dictionary())
return
if is_tenants_mode(): if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model from django_tenants.utils import tenant_context, get_tenant_model
@@ -88,7 +95,9 @@ def init_system_config():
:return: :return:
""" """
try: try:
if dispatch_db_type == 'redis':
cache.set(f"init_system_config", _get_all_system_config())
return
if is_tenants_mode(): if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model from django_tenants.utils import tenant_context, get_tenant_model
@@ -107,6 +116,9 @@ def refresh_dictionary():
刷新字典配置 刷新字典配置
:return: :return:
""" """
if dispatch_db_type == 'redis':
cache.set(f"init_dictionary", _get_all_dictionary())
return
if is_tenants_mode(): if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model from django_tenants.utils import tenant_context, get_tenant_model
@@ -122,6 +134,9 @@ def refresh_system_config():
刷新系统配置 刷新系统配置
:return: :return:
""" """
if dispatch_db_type == 'redis':
cache.set(f"init_system_config", _get_all_system_config())
return
if is_tenants_mode(): if is_tenants_mode():
from django_tenants.utils import tenant_context, get_tenant_model from django_tenants.utils import tenant_context, get_tenant_model
@@ -141,6 +156,11 @@ def get_dictionary_config(schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值 :param schema_name: 对应字典配置的租户schema_name值
:return: :return:
""" """
if dispatch_db_type == 'redis':
init_dictionary_data = cache.get(f"init_dictionary")
if not init_dictionary_data:
refresh_dictionary()
return cache.get(f"init_dictionary") or {}
if not settings.DICTIONARY_CONFIG: if not settings.DICTIONARY_CONFIG:
refresh_dictionary() refresh_dictionary()
if is_tenants_mode(): if is_tenants_mode():
@@ -157,6 +177,12 @@ def get_dictionary_values(key, schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值 :param schema_name: 对应字典配置的租户schema_name值
:return: :return:
""" """
if dispatch_db_type == 'redis':
dictionary_config = cache.get(f"init_dictionary")
if not dictionary_config:
refresh_dictionary()
dictionary_config = cache.get(f"init_dictionary")
return dictionary_config.get(key)
dictionary_config = get_dictionary_config(schema_name) dictionary_config = get_dictionary_config(schema_name)
return dictionary_config.get(key) return dictionary_config.get(key)
@@ -169,8 +195,8 @@ def get_dictionary_label(key, name, schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值 :param schema_name: 对应字典配置的租户schema_name值
:return: :return:
""" """
children = get_dictionary_values(key, schema_name) or [] res = get_dictionary_values(key, schema_name) or []
for ele in children: for ele in res.get('children'):
if ele.get("value") == str(name): if ele.get("value") == str(name):
return ele.get("label") return ele.get("label")
return "" return ""
@@ -187,6 +213,11 @@ def get_system_config(schema_name=None):
:param schema_name: 对应字典配置的租户schema_name值 :param schema_name: 对应字典配置的租户schema_name值
:return: :return:
""" """
if dispatch_db_type == 'redis':
init_dictionary_data = cache.get(f"init_system_config")
if not init_dictionary_data:
refresh_system_config()
return cache.get(f"init_system_config") or {}
if not settings.SYSTEM_CONFIG: if not settings.SYSTEM_CONFIG:
refresh_system_config() refresh_system_config()
if is_tenants_mode(): if is_tenants_mode():
@@ -203,10 +234,32 @@ def get_system_config_values(key, schema_name=None):
:param schema_name: 对应系统配置的租户schema_name值 :param schema_name: 对应系统配置的租户schema_name值
:return: :return:
""" """
if dispatch_db_type == 'redis':
system_config = cache.get(f"init_system_config")
if not system_config:
refresh_system_config()
system_config = cache.get(f"init_system_config")
return system_config.get(key)
system_config = get_system_config(schema_name) system_config = get_system_config(schema_name)
return system_config.get(key) return system_config.get(key)
def get_system_config_values_to_dict(key, schema_name=None):
"""
获取系统配置数据并转换为字典 **仅限于数组类型系统配置
:param key: 对应系统配置的key值(字典编号)
:param schema_name: 对应系统配置的租户schema_name值
:return:
"""
values_dict = {}
config_values = get_system_config_values(key, schema_name)
if not isinstance(config_values, list):
raise CustomValidationError("该方式仅限于数组类型系统配置")
for ele in get_system_config_values(key, schema_name):
values_dict[ele.get('key')] = ele.get('value')
return values_dict
def get_system_config_label(key, name, schema_name=None): def get_system_config_label(key, name, schema_name=None):
""" """
获取获取系统配置label值 获取获取系统配置label值

View File

@@ -672,6 +672,53 @@
"model": "ApiWhiteList" "model": "ApiWhiteList"
} }
] ]
},
{
"name": "下载中心",
"icon": "ele-Download",
"sort": 9,
"is_link": false,
"is_catalog": false,
"web_path": "/downloadCenter",
"component": "system/downloadCenter/index",
"component_name": "downloadCenter",
"status": true,
"cache": false,
"visible": true,
"parent": 277,
"children": [],
"menu_button": [
{
"name": "查询",
"value": "Search",
"api": "/api/system/downloadCenter/",
"method": 0
},
{
"name": "详情",
"value": "Retrieve",
"api": "/api/system/downloadCenter/{id}/",
"method": 0
},
{
"name": "新增",
"value": "Create",
"api": "/api/system/downloadCenter/",
"method": 1
},
{
"name": "编辑",
"value": "Update",
"api": "/api/system/downloadCenter/{id}/",
"method": 2
},
{
"name": "删除",
"value": "Delete",
"api": "/api/system/downloadCenter/{id}/",
"method": 3
}
]
} }
], ],
"menu_button": [], "menu_button": [],

View File

@@ -1,5 +1,7 @@
import hashlib import hashlib
import os import os
from time import time
from pathlib import PurePosixPath
from django.contrib.auth.models import AbstractUser, UserManager from django.contrib.auth.models import AbstractUser, UserManager
from django.db import models from django.db import models
@@ -596,3 +598,41 @@ class MessageCenterTargetUser(CoreModel):
db_table = table_prefix + "message_center_target_user" db_table = table_prefix + "message_center_target_user"
verbose_name = "消息中心目标用户表" verbose_name = "消息中心目标用户表"
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
def media_file_name_downloadcenter(instance:'DownloadCenter', filename):
h = instance.md5sum
basename, ext = os.path.splitext(filename)
return PurePosixPath("files", "dlct", h[:1], h[1:2], basename + '-' + str(time()).replace('.', '') + ext.lower())
class DownloadCenter(CoreModel):
TASK_STATUS_CHOICES = [
(0, '任务已创建'),
(1, '任务进行中'),
(2, '任务完成'),
(3, '任务失败'),
]
task_name = models.CharField(max_length=255, verbose_name="任务名称", help_text="任务名称")
task_status = models.SmallIntegerField(default=0, choices=TASK_STATUS_CHOICES, verbose_name='是否可下载', help_text='是否可下载')
file_name = models.CharField(max_length=255, null=True, blank=True, verbose_name="文件名", help_text="文件名")
url = models.FileField(upload_to=media_file_name_downloadcenter, null=True, blank=True)
size = models.BigIntegerField(default=0, verbose_name="文件大小", help_text="文件大小")
md5sum = models.CharField(max_length=36, null=True, blank=True, verbose_name="文件md5", help_text="文件md5")
def save(self, *args, **kwargs):
if self.url:
if not self.md5sum: # file is new
md5 = hashlib.md5()
for chunk in self.url.chunks():
md5.update(chunk)
self.md5sum = md5.hexdigest()
if not self.size:
self.size = self.url.size
super(DownloadCenter, self).save(*args, **kwargs)
class Meta:
db_table = table_prefix + "download_center"
verbose_name = "下载中心"
verbose_name_plural = verbose_name
ordering = ("-create_datetime",)

View File

@@ -0,0 +1,107 @@
from hashlib import md5
from io import BytesIO
from datetime import datetime
from time import sleep
from openpyxl import Workbook
from openpyxl.worksheet.table import Table, TableStyleInfo
from openpyxl.utils import get_column_letter
from django.core.files.base import ContentFile
from application.celery import app
from dvadmin.system.models import DownloadCenter
def is_number(num):
try:
float(num)
return True
except ValueError:
pass
try:
import unicodedata
unicodedata.numeric(num)
return True
except (TypeError, ValueError):
pass
return False
def get_string_len(string):
"""
获取字符串最大长度
:param string:
:return:
"""
length = 4
if string is None:
return length
if is_number(string):
return length
for char in string:
length += 2.1 if ord(char) > 256 else 1
return round(length, 1) if length <= 50 else 50
@app.task
def async_export_data(data: list, filename: str, dcid: int, export_field_label: dict):
instance = DownloadCenter.objects.get(pk=dcid)
instance.task_status = 1
instance.save()
sleep(2)
try:
wb = Workbook()
ws = wb.active
header_data = ["序号", *export_field_label.values()]
hidden_header = ["#", *export_field_label.keys()]
df_len_max = [get_string_len(ele) for ele in header_data]
row = get_column_letter(len(export_field_label) + 1)
column = 1
ws.append(header_data)
for index, results in enumerate(data):
results_list = []
for h_index, h_item in enumerate(hidden_header):
for key, val in results.items():
if key == h_item:
if val is None or val == "":
results_list.append("")
elif isinstance(val, datetime):
val = val.strftime("%Y-%m-%d %H:%M:%S")
results_list.append(val)
else:
results_list.append(val)
# 计算最大列宽度
result_column_width = get_string_len(val)
if h_index != 0 and result_column_width > df_len_max[h_index]:
df_len_max[h_index] = result_column_width
ws.append([index + 1, *results_list])
column += 1
#  更新列宽
for index, width in enumerate(df_len_max):
ws.column_dimensions[get_column_letter(index + 1)].width = width
tab = Table(displayName="Table", ref=f"A1:{row}{column}") # 名称管理器
style = TableStyleInfo(
name="TableStyleLight11",
showFirstColumn=True,
showLastColumn=True,
showRowStripes=True,
showColumnStripes=True,
)
tab.tableStyleInfo = style
ws.add_table(tab)
stream = BytesIO()
wb.save(stream)
stream.seek(0)
s = md5()
while True:
chunk = stream.read(1024)
if not chunk:
break
s.update(chunk)
stream.seek(0)
instance.md5sum = s.hexdigest()
instance.file_name = filename
instance.url.save(filename, ContentFile(stream.read()))
instance.task_status = 2
except Exception as e:
instance.task_status = 3
instance.description = str(e)[:250]
instance.save()

View File

@@ -18,6 +18,7 @@ from dvadmin.system.views.role_menu_button_permission import RoleMenuButtonPermi
from dvadmin.system.views.system_config import SystemConfigViewSet from dvadmin.system.views.system_config import SystemConfigViewSet
from dvadmin.system.views.user import UserViewSet from dvadmin.system.views.user import UserViewSet
from dvadmin.system.views.menu_field import MenuFieldViewSet from dvadmin.system.views.menu_field import MenuFieldViewSet
from dvadmin.system.views.download_center import DownloadCenterViewSet
system_url = routers.SimpleRouter() system_url = routers.SimpleRouter()
system_url.register(r'menu', MenuViewSet) system_url.register(r'menu', MenuViewSet)
@@ -36,6 +37,7 @@ system_url.register(r'role_menu_button_permission', RoleMenuButtonPermissionView
system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet) system_url.register(r'role_menu_permission', RoleMenuPermissionViewSet)
system_url.register(r'column', MenuFieldViewSet) system_url.register(r'column', MenuFieldViewSet)
system_url.register(r'login_log', LoginLogViewSet) system_url.register(r'login_log', LoginLogViewSet)
system_url.register(r'download_center', DownloadCenterViewSet)
urlpatterns = [ urlpatterns = [

View File

@@ -0,0 +1,46 @@
from rest_framework import serializers
from django.conf import settings
from django_filters.rest_framework import FilterSet, CharFilter
from dvadmin.utils.serializers import CustomModelSerializer
from dvadmin.utils.viewset import CustomModelViewSet
from dvadmin.system.models import DownloadCenter
class DownloadCenterSerializer(CustomModelSerializer):
url = serializers.SerializerMethodField(read_only=True)
def get_url(self, instance):
if self.request.query_params.get('prefix'):
if settings.ENVIRONMENT in ['local']:
prefix = 'http://127.0.0.1:8000'
elif settings.ENVIRONMENT in ['test']:
prefix = 'http://{host}/api'.format(host=self.request.get_host())
else:
prefix = 'https://{host}/api'.format(host=self.request.get_host())
return (f'{prefix}/media/{str(instance.url)}')
return f'media/{str(instance.url)}'
class Meta:
model = DownloadCenter
fields = "__all__"
read_only_fields = ["id"]
class DownloadCenterFilterSet(FilterSet):
task_name = CharFilter(field_name='task_name', lookup_expr='icontains')
file_name = CharFilter(field_name='file_name', lookup_expr='icontains')
class Meta:
model = DownloadCenter
fields = ['task_status', 'task_name', 'file_name']
class DownloadCenterViewSet(CustomModelViewSet):
queryset = DownloadCenter.objects.all()
serializer_class = DownloadCenterSerializer
filter_class = DownloadCenterFilterSet
permission_classes = []
def get_queryset(self):
return super().get_queryset().filter(creator=self.request.user)

View File

@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import datetime
from urllib.parse import quote from urllib.parse import quote
from django.db import transaction from django.db import transaction
@@ -11,8 +12,10 @@ from rest_framework.decorators import action
from rest_framework.request import Request from rest_framework.request import Request
from dvadmin.utils.import_export import import_to_data from dvadmin.utils.import_export import import_to_data
from dvadmin.utils.json_response import DetailResponse from dvadmin.utils.json_response import DetailResponse, SuccessResponse
from dvadmin.utils.request_util import get_verbose_name from dvadmin.utils.request_util import get_verbose_name
from dvadmin.system.tasks import async_export_data
from dvadmin.system.models import DownloadCenter
class ImportSerializerMixin: class ImportSerializerMixin:
@@ -301,6 +304,17 @@ class ExportSerializerMixin:
assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__ assert self.export_field_label, "'%s' 请配置对应的导出模板字段。" % self.__class__.__name__
assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__ assert self.export_serializer_class, "'%s' 请配置对应的导出序列化器。" % self.__class__.__name__
data = self.export_serializer_class(queryset, many=True, request=request).data data = self.export_serializer_class(queryset, many=True, request=request).data
try:
from dvadmin3_celery import settings
async_export_data.delay(
data,
str(f"导出{get_verbose_name(queryset)}-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.xlsx"),
DownloadCenter.objects.create(creator=request.user, task_name=f'{get_verbose_name(queryset)}数据导出任务').pk,
self.export_field_label
)
return SuccessResponse(msg="导入任务已创建,请前往‘下载中心’等待下载")
except:
pass
# 导出excel 表 # 导出excel 表
response = HttpResponse(content_type="application/msexcel") response = HttpResponse(content_type="application/msexcel")
response["Access-Control-Expose-Headers"] = f"Content-Disposition" response["Access-Control-Expose-Headers"] = f"Content-Disposition"

View File

@@ -28,5 +28,4 @@ uvicorn==0.30.3
gunicorn==22.0.0 gunicorn==22.0.0
gevent==24.2.1 gevent==24.2.1
Pillow==10.4.0 Pillow==10.4.0
dvadmin-celery==1.0.5
pyinstaller==6.9.0 pyinstaller==6.9.0

View File

@@ -10,6 +10,7 @@ import { errorLog, errorCreate } from './tools.ts';
import { Local, Session } from '/@/utils/storage'; import { Local, Session } from '/@/utils/storage';
import qs from 'qs'; import qs from 'qs';
import { getBaseURL } from './baseUrl'; import { getBaseURL } from './baseUrl';
import { successMessage } from './message.js';
/** /**
* @description 创建请求实例 * @description 创建请求实例
*/ */
@@ -204,6 +205,8 @@ export const requestForMock = createRequestFunction(serviceForMock);
* @param filename * @param filename
*/ */
export const downloadFile = function ({ url, params, method, filename = '文件导出' }: any) { export const downloadFile = function ({ url, params, method, filename = '文件导出' }: any) {
// return request({ url: url, method: method, params: params })
// .then((res: any) => successMessage(res.msg));
request({ request({
url: url, url: url,
method: method, method: method,
@@ -211,6 +214,8 @@ export const downloadFile = function ({ url, params, method, filename = '文件
responseType: 'blob' responseType: 'blob'
// headers: {Accept: 'application/vnd.openxmlformats-officedocument'} // headers: {Accept: 'application/vnd.openxmlformats-officedocument'}
}).then((res: any) => { }).then((res: any) => {
// console.log(res.headers['content-type']); // 根据content-type不同来判断是否异步下载
if (res.headers['content-type'] === 'application/json') return successMessage('导入任务已创建,请前往‘下载中心’等待下载');
const xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1]) const xlsxName = window.decodeURI(res.headers['content-disposition'].split('=')[1])
const fileName = xlsxName || `${filename}.xlsx` const fileName = xlsxName || `${filename}.xlsx`
if (res) { if (res) {

View File

@@ -0,0 +1,49 @@
import { request } from '/@/utils/service';
import { PageQuery, AddReq, DelReq, EditReq, InfoReq } from '@fast-crud/fast-crud';
export const apiPrefix = '/api/system/download_center/';
export function GetPermission() {
return request({
url: apiPrefix + 'field_permission/',
method: 'get',
});
}
export function GetList(query: PageQuery) {
return request({
url: apiPrefix,
method: 'get',
params: query,
});
}
export function GetObj(id: InfoReq) {
return request({
url: apiPrefix + id,
method: 'get',
});
}
export function AddObj(obj: AddReq) {
return request({
url: apiPrefix,
method: 'post',
data: obj,
});
}
export function UpdateObj(obj: EditReq) {
return request({
url: apiPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
export function DelObj(id: DelReq) {
return request({
url: apiPrefix + id + '/',
method: 'delete',
data: { id },
});
}

View File

@@ -0,0 +1,160 @@
import { CrudOptions, AddReq, DelReq, EditReq, dict, CrudExpose, compute } from '@fast-crud/fast-crud';
import * as api from './api';
import { dictionary } from '/@/utils/dictionary';
import { successMessage } from '../../../utils/message';
import { auth } from '/@/utils/authFunction'
interface CreateCrudOptionsTypes {
output: any;
crudOptions: CrudOptions;
}
//此处为crudOptions配置
export const createCrudOptions = function ({ crudExpose }: { crudExpose: CrudExpose; }): CreateCrudOptionsTypes {
const pageRequest = async (query: any) => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
//权限判定
// @ts-ignore
// @ts-ignore
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
pagination: {
show: true
},
actionbar: {
buttons: {
add: {
show: false
}
}
},
toolbar: {
buttons: {
export: {
show: false
}
}
},
rowHandle: {
//固定右侧
fixed: 'right',
width: 120,
buttons: {
view: {
show: false
},
edit: {
show: false
},
remove: {
show: false
},
download: {
show: compute(ctx => ctx.row.task_status === 2),
text: '下载文件',
type: 'warning',
click: (ctx) => window.open(ctx.row.url, '_blank')
}
},
},
form: {
col: { span: 24 },
labelWidth: '100px',
wrapper: {
is: 'el-dialog',
width: '600px',
},
},
columns: {
_index: {
title: '序号',
form: { show: false },
column: {
type: 'index',
align: 'center',
width: '70px',
columnSetDisabled: true, //禁止在列设置中选择
},
},
task_name: {
title: '任务名',
type: 'text',
column: {
minWidth: 160,
align: 'left'
},
search: {
show: true
}
},
file_name: {
title: '文件名',
type: 'text',
column: {
minWidth: 160,
align: 'left'
},
search: {
show: true
}
},
size: {
title: '文件大小(b)',
type: 'number',
column: {
width: 100
}
},
task_status: {
title: '任务状态',
type: 'dict-select',
dict: dict({
data: [
{ label: '任务已创建', value: 0 },
{ label: '任务进行中', value: 1 },
{ label: '任务完成', value: 2 },
{ label: '任务失败', value: 3 },
]
}),
column: {
width: 120
},
search: {
show: true
}
},
create_datetime: {
title: '创建时间',
column: {
width: 160
}
},
update_datetime: {
title: '创建时间',
column: {
width: 160
}
}
},
},
};
};

View File

@@ -0,0 +1,42 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #cell_url="scope">
<el-tag size="small">{{ scope.row.url }}</el-tag>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup name="downloadCenter">
import { ref, onMounted, inject, onBeforeUpdate } from 'vue';
import { GetPermission } from './api';
import { useExpose, useCrud } from '@fast-crud/fast-crud';
import { createCrudOptions } from './crud';
import PermissionComNew from './components/PermissionComNew/index.vue';
import _ from "lodash-es";
import { handleColumnPermission } from "/@/utils/columnPermission";
// crud组件的ref
const crudRef = ref();
// crud 配置的ref
const crudBinding = ref();
const { crudExpose } = useExpose({ crudRef, crudBinding });
// 你的crud配置
const { crudOptions } = createCrudOptions({ crudExpose });
// 初始化crud配置
const { resetCrudOptions } = useCrud({
crudExpose,
crudOptions,
context: {},
});
// 页面打开后获取列表数据
onMounted(async () => {
crudExpose.doRefresh();
});
</script>