feat: cms users institution department manager

This commit is contained in:
2026-06-11 10:37:29 +08:00
parent 1dc9141856
commit 32915bc6b4
39 changed files with 2403 additions and 75 deletions
+58
View File
@@ -0,0 +1,58 @@
"""CMS 导入 / 导出通用工具(基于 openpyxl)。"""
import io
from django.http import HttpResponse
from openpyxl import Workbook, load_workbook
XLSX_CONTENT_TYPE = (
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
def xlsx_response(filename, headers, rows):
"""生成 .xlsx 下载响应。
Args:
filename: 下载文件名(含 .xlsx
headers: 表头列表
rows: 二维数据(list[list]);空则只导出表头(可作模板)
"""
wb = Workbook()
ws = wb.active
ws.append(list(headers))
for row in rows:
ws.append(list(row))
buf = io.BytesIO()
wb.save(buf)
buf.seek(0)
resp = HttpResponse(buf.getvalue(), content_type=XLSX_CONTENT_TYPE)
resp['Content-Disposition'] = f'attachment; filename="{filename}"'
return resp
def rows_from_xlsx(file):
"""解析上传的 .xlsx,首行为表头,返回 list[dict](表头→单元格字符串)。
空行跳过;单元格统一转为去空格字符串(None → '')。
"""
wb = load_workbook(file, read_only=True, data_only=True)
ws = wb.active
it = ws.iter_rows(values_only=True)
try:
header_cells = next(it)
except StopIteration:
return []
headers = [str(h).strip() if h is not None else '' for h in header_cells]
result = []
for raw in it:
if raw is None or all(c is None or str(c).strip() == '' for c in raw):
continue
row = {}
for i, h in enumerate(headers):
if not h:
continue
val = raw[i] if i < len(raw) else None
row[h] = '' if val is None else str(val).strip()
result.append(row)
return result
+61 -1
View File
@@ -1,10 +1,70 @@
from django.db import models
from django.utils import timezone
class BaseModel(models.Model):
"""基础模型,包含通用字段"""
"""基础模型,包含通用时间字段"""
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
abstract = True
# ─── 软删除(停用 = 逻辑删除)────────────────────────────────────────────────────
class SoftDeleteQuerySet(models.QuerySet):
"""QuerySet.delete() 改为逻辑删除;hard_delete() 才物理删除。"""
def delete(self):
return self.update(is_deleted=True, deleted_at=timezone.now())
def hard_delete(self):
return super().delete()
def alive(self):
return self.filter(is_deleted=False)
class SoftDeleteManager(models.Manager):
"""默认只返回未删除的数据。"""
def get_queryset(self):
return SoftDeleteQuerySet(self.model, using=self._db).filter(is_deleted=False)
class AllObjectsManager(models.Manager):
"""包含已删除数据(管理/恢复用)。"""
def get_queryset(self):
return SoftDeleteQuerySet(self.model, using=self._db)
class SoftDeleteModel(BaseModel):
"""软删除基类:停用/下架一律逻辑删除,绝不物理删除。
- `objects`:默认管理器,只含未删除数据。
- `all_objects`:含已删除数据。
- 实例 `.delete()` → 逻辑删除;`.hard_delete()` → 物理删除。
"""
is_deleted = models.BooleanField('是否删除', default=False, db_index=True)
deleted_at = models.DateTimeField('删除时间', null=True, blank=True)
objects = SoftDeleteManager()
all_objects = AllObjectsManager()
class Meta:
abstract = True
def delete(self, using=None, keep_parents=False):
self.is_deleted = True
self.deleted_at = timezone.now()
self.save(using=using, update_fields=['is_deleted', 'deleted_at', 'updated_at'])
def hard_delete(self, using=None, keep_parents=False):
super().delete(using=using, keep_parents=keep_parents)
def restore(self, using=None):
self.is_deleted = False
self.deleted_at = None
self.save(using=using, update_fields=['is_deleted', 'deleted_at', 'updated_at'])