"feat: add RoleManager tree/search, OrderManager.CreateGeneric, StatsManager

- RoleManager: GetPermissionsTree / MatchUserRoleIds
- OrderManager.CreateGeneric: auto order-no, forced PENDING, transactional
- gvsdsdk.stats: StatsManager (8 methods) + 5 model mappings (app_label=gvsdsdk)"
This commit is contained in:
TermiNexus committed 2026-08-07 04:08:10 +08:00
1 parent 0ad5ce77c8
commit d2de2301a4
12 files changed
+760

No files matched your search

+14
View File
@@ -150,6 +150,11 @@ def _load_order():
return _mod
def _load_stats():
from gvsdsdk import stats as _mod
return _mod
def _load_models():
from gvsdsdk import models as _mod
return _mod
@@ -262,6 +267,14 @@ _LAZY_SYMBOLS = {
'Order': ('order', 'Order'),
'OrderItem': ('order', 'OrderItem'),
# ── 统计引擎 ──────────────────────────────────────────
'StatsManager': ('stats', 'StatsManager'),
'StatMetric': ('stats', 'StatMetric'),
'StatDimension': ('stats', 'StatDimension'),
'StatRecord': ('stats', 'StatRecord'),
'StatRanking': ('stats', 'StatRanking'),
'StatConfig': ('stats', 'StatConfig'),
# ── 数据模型 ──────────────────────────────────────────
'User': ('models', 'User'),
'Role': ('models', 'Role'),
@@ -304,6 +317,7 @@ _LOADERS = {
'commission': _load_commission,
'payment': _load_payment,
'order': _load_order,
'stats': _load_stats,
'models': _load_models,
'fluent': _load_fluent,
'model_base': _load_model_base,
Binary file not shown.
Binary file not shown.
+78
View File
@@ -261,6 +261,84 @@ class RoleManager:
perm.save()
return perm, True
@staticmethod
def GetPermissionsTree(RoleUUID, CompanyUUID, TenantUUID=None):
"""获取角色权限树——按 PermCategory 分组,标记角色+公司下已勾选权限
Args:
RoleUUID: 角色 UUID
CompanyUUID: 公司 UUID
TenantUUID: 可选,租户 UUID(过滤权限范围;传 None 表示跨租户全量)
Returns:
list[dict]:分组列表,每组含 ``Category`` 与 ``Permissions``
(每项含 PermUUID/PermName/PermCode/PermDesc/Checked);未分类组排在最后
"""
perm_qs = Permission.objects.filter(PermStatus=1)
if TenantUUID is not None:
perm_qs = perm_qs.filter(TenantUUID=TenantUUID)
perm_qs = perm_qs.order_by('PermCategory', 'PermName')
checked_perm_uuids = set(
RolePermission.objects.filter(
RoleUUID=RoleUUID, CompanyUUID=CompanyUUID,
).values_list('PermUUID', flat=True)
)
groups = {}
for perm in perm_qs:
category = perm.PermCategory or '未分类'
if category not in groups:
groups[category] = {'Category': category, 'Permissions': []}
groups[category]['Permissions'].append({
'PermUUID': UUIDObj(perm.PermUUID).str if perm.PermUUID else '',
'PermName': perm.PermName,
'PermCode': perm.PermCode,
'PermDesc': perm.PermDesc or '',
'Checked': perm.PermUUID in checked_perm_uuids,
})
result = [g for k, g in groups.items() if k != '未分类']
if '未分类' in groups:
result.append(groups['未分类'])
return result
@staticmethod
def MatchUserRoleIds(Keyword):
"""按用户名或角色名匹配关键字,返回对应的 UserUUID/RoleUUID 集合
供用户-角色关联搜索使用:先解析名称得到 UUID 集合,再在已隔离的
queryset 上按集合过滤(隔离语义由调用方 queryset 保留)。
Args:
Keyword: 用户名/角色名关键字
Returns:
(user_uuids: set[bytes], role_uuids: set[bytes])
"""
if not Keyword:
return set(), set()
User = _GetUserModel()
matched_users = User.objects.filter(
UserName__icontains=Keyword,
).values_list('UserUUID', flat=True)
matched_roles = Role.objects.filter(
RoleName__icontains=Keyword,
).values_list('RoleUUID', flat=True)
user_uuids = set()
role_uuids = set()
for u in matched_users:
try:
user_uuids.add(UUIDObj(u).bytes)
except (ValueError, AttributeError):
pass
for r in matched_roles:
try:
role_uuids.add(UUIDObj(r).bytes)
except (ValueError, AttributeError):
pass
return user_uuids, role_uuids
class PositionManager:
"""职务管理器——提供人事职务标签创建、岗位模板管理"""
Binary file not shown.
+94
View File
@@ -246,6 +246,100 @@ class OrderManager:
order_no, template.TemplateName, buyer_user_uuid)
return order
@transaction.atomic
def CreateGeneric(self, order_title, buyer_user_uuid, order_type='default',
order_description='', seller_user_uuid=None,
total_amount=None, discount_amount=None, pay_amount=None,
currency='CNY', expire_time=None, remark='',
items=None, order_metadata=None):
"""通用创建订单——自动生成单号、强制 PENDING 状态
供通用 CRUD(form_service)接入,替代裸 ``model.objects.create``,
保证订单受状态机保护(OrderStatus 恒为 PENDING,后续流转必须走状态机端点)。
Args:
order_title: 订单标题(必填)
buyer_user_uuid: 买家用户 UUID(必填)
order_type: 订单类型(默认 'default')
order_description: 订单描述
seller_user_uuid: 卖家用户 UUID(可选)
total_amount: 总金额(Decimal/str;不传按 items 汇总,无 items 则为 0)
discount_amount: 优惠金额
pay_amount: 应付金额(不传 = total - discount,最低 0)
currency: 币种
expire_time: 过期时间
remark: 备注
items: 明细列表,每项 dict:
{ItemCode, ItemName, ItemDescription, ItemType, UnitPrice, Quantity, SubTotal, ItemMetadata}
order_metadata: 附加元数据
Returns:
Order 实例
Raises:
OrderError: order_title 或 buyer_user_uuid 缺失时
"""
if not order_title:
raise OrderError('订单标题不能为空')
if not buyer_user_uuid:
raise OrderError('买家用户 UUID 不能为空')
buyer_bytes = self._to_bytes(buyer_user_uuid)
seller_bytes = self._to_bytes(seller_user_uuid) if seller_user_uuid else None
order_no = self._generate_order_no()
# 金额计算:优先显式传值,否则按 items 汇总
computed_total = Decimal('0')
for item in items or []:
computed_total += Decimal(str(item.get('UnitPrice', Decimal('0')))) * Decimal(str(item.get('Quantity', 1)))
total_amount = total_amount if total_amount is not None else (computed_total or Decimal('0'))
discount_amount = discount_amount or Decimal('0')
pay_amount = pay_amount if pay_amount is not None else max(total_amount - discount_amount, Decimal('0'))
order = Order.objects.create(
OrderUUID=_uuid.uuid4().bytes,
TenantUUID=self.TenantUUID,
CompanyUUID=self.CompanyUUID,
OrderNO=order_no,
OrderTitle=order_title,
OrderDescription=order_description,
OrderType=order_type,
BuyerUserUUID=buyer_bytes,
SellerUserUUID=seller_bytes,
TotalAmount=total_amount,
DiscountAmount=discount_amount,
PayAmount=pay_amount,
Currency=currency,
OrderStatus=Order.ORD_PENDING,
ExpireTime=expire_time,
TemplateSnapshot={},
LevelSnapshot={},
OrderMetadata=order_metadata or {},
Remark=remark,
)
item_list = []
for item_data in items or []:
unit_price = Decimal(str(item_data.get('UnitPrice', Decimal('0'))))
quantity = int(item_data.get('Quantity', 1))
item_list.append(OrderItem(
ItemUUID=_uuid.uuid4().bytes,
Order=order,
ItemCode=item_data.get('ItemCode', ''),
ItemName=item_data.get('ItemName', ''),
ItemDescription=item_data.get('ItemDescription', ''),
ItemType=item_data.get('ItemType', 'default'),
UnitPrice=unit_price,
Quantity=quantity,
Subtotal=item_data.get('SubTotal') or (unit_price * quantity),
ItemMetadata=item_data.get('ItemMetadata', {}),
))
if item_list:
OrderItem.objects.bulk_create(item_list)
logger.info('[OrderManager] 通用创建订单: %s (buyer=%s)', order_no, buyer_user_uuid)
return order
# ========== 状态流转方法 ==========
def Confirm(self, order_uuid):
+24
View File
@@ -0,0 +1,24 @@
"""gvsdsdk.stats — 统计引擎
子服务器通过 StatsManager 直接调用统计能力:
原子增量、周期查询、按维度聚合、排行榜生成、周期重置、过期清理。
"""
__all__ = [
'StatsManager',
'StatMetric', 'StatDimension', 'StatRecord', 'StatRanking', 'StatConfig',
]
def __getattr__(name):
if name == 'StatsManager':
from gvsdsdk.stats import manager as _m
val = getattr(_m, name)
globals()[name] = val
return val
if name in ('StatMetric', 'StatDimension', 'StatRecord', 'StatRanking', 'StatConfig'):
from gvsdsdk.stats import models as _m
val = getattr(_m, name)
globals()[name] = val
return val
raise AttributeError(f"module 'gvsdsdk.stats' has no attribute {name!r}")
Binary file not shown.
Binary file not shown.
Binary file not shown.
+315
View File
@@ -0,0 +1,315 @@
"""模块:统计引擎(gvsdsdk 副本)
提供原子增量、周期查询、排行榜生成的底层能力。
所有数据库操作使用 select_for_update + F() 保证并发安全。
与 gvsds/apps/stats/engine.py 零行为差异复制——
8 个模块级函数包装为 StatsManager 的静态方法。"""
from django.db import transaction, connection
from django.db.models import F, Sum, Count
from gvsdsdk.stats.models import StatMetric, StatRecord
class StatsManager:
"""统计能力管理器——子服务器直接调用统计能力的统一入口
用法:
record, created = StatsManager.AtomicIncrement(
tenant_uuid=tenant_uuid,
period_type='daily',
period_key='2026-06-11',
metric_uuid=metric_uuid,
dimension_values={'role': 'dashou', 'user_id': '123'},
delta=1,
)
"""
@staticmethod
def AtomicIncrement(tenant_uuid, period_type, period_key, metric_uuid,
dimension_values, delta):
"""原子增量更新——保证并发安全的单条统计记录累加
使用 SELECT ... FOR UPDATE + INSERT ... ON DUPLICATE KEY UPDATE
在 MySQL 上实现真正的原子 UPSERT。
Args:
tenant_uuid: 租户UUID (bytes)
period_type: 周期类型 ('daily', 'hourly', 'monthly')
period_key: 周期键 ('2026-06-11' 或 '2026-06-11T14')
metric_uuid: 指标UUID (bytes)
dimension_values: 维度值字典
delta: 增量 (Decimal 或 int)
Returns:
(StatRecord, created) 元组
"""
import json
from decimal import Decimal
delta = Decimal(str(delta))
dim_json = json.dumps(dimension_values, sort_keys=True, ensure_ascii=False)
with transaction.atomic():
existing = list(
StatRecord.objects.select_for_update().filter(
TenantUUID=tenant_uuid,
PeriodType=period_type,
PeriodKey=period_key,
Metric_id=metric_uuid,
DimensionValues=dimension_values,
)[:1]
)
if existing:
record = existing[0]
record.Value = F('Value') + delta
record.save(update_fields=['Value'])
record.refresh_from_db(fields=['Value', 'UpdateTime'])
return record, False
else:
record = StatRecord.objects.create(
TenantUUID=tenant_uuid,
PeriodType=period_type,
PeriodKey=period_key,
Metric_id=metric_uuid,
DimensionValues=dimension_values,
Value=delta,
)
return record, True
@staticmethod
def AtomicIncrementMulti(tenant_uuid, period_type, period_key,
increments):
"""批量原子增量——一次事务中更新多条统计
Args:
tenant_uuid: 租户UUID
period_type: 周期类型
period_key: 周期键
increments: [(metric_uuid, dimension_values, delta), ...]
Returns:
List[StatRecord]
"""
results = []
with transaction.atomic():
for metric_uuid, dim_values, delta in increments:
record, _ = StatsManager.AtomicIncrement(
tenant_uuid, period_type, period_key,
metric_uuid, dim_values, delta,
)
results.append(record)
return results
@staticmethod
def QueryPeriod(tenant_uuid, metric_uuid, period_type, period_key,
dimension_filter=None):
"""查询特定周期的统计数据
Args:
tenant_uuid: 租户UUID
metric_uuid: 指标UUID
period_type: 周期类型
period_key: 周期键 ('2026-06-11')
dimension_filter: 维度过滤 (JSON contains)
Returns:
QuerySet[StatRecord]
"""
qs = StatRecord.objects.filter(
TenantUUID=tenant_uuid,
Metric_id=metric_uuid,
PeriodType=period_type,
PeriodKey=period_key,
)
if dimension_filter:
qs = qs.filter(DimensionValues__contains=dimension_filter)
return qs
@staticmethod
def QueryRange(tenant_uuid, metric_uuid, period_type,
start_key, end_key, dimension_filter=None):
"""查询时间范围内的统计数据
Args:
tenant_uuid: 租户UUID
metric_uuid: 指标UUID
period_type: 周期类型
start_key: 起始周期键
end_key: 截止周期键
dimension_filter: 维度过滤
Returns:
QuerySet[StatRecord]
"""
qs = StatRecord.objects.filter(
TenantUUID=tenant_uuid,
Metric_id=metric_uuid,
PeriodType=period_type,
PeriodKey__gte=start_key,
PeriodKey__lte=end_key,
)
if dimension_filter:
qs = qs.filter(DimensionValues__contains=dimension_filter)
return qs
@staticmethod
def AggregateByDimension(tenant_uuid, metric_uuid, period_type,
start_key, end_key, dimension_key=None):
"""按维度聚合——对指定维度求和排名
Args:
tenant_uuid: 租户UUID
metric_uuid: 指标UUID
period_type: 周期类型
start_key: 起始周期键
end_key: 截止周期键
dimension_key: 聚合的维度键(如 'user_id', 'role')
Returns:
list[{dim_value, total}], 按 total 降序
"""
qs = StatRecord.objects.filter(
TenantUUID=tenant_uuid,
Metric_id=metric_uuid,
PeriodType=period_type,
PeriodKey__gte=start_key,
PeriodKey__lte=end_key,
)
results = qs.values('DimensionValues').annotate(
total=Sum('Value'),
).order_by('-total')
aggregated = {}
for row in results:
dims = row['DimensionValues']
if dimension_key:
key = dims.get(dimension_key, '_unknown')
aggregated[key] = aggregated.get(key, 0) + row['total']
else:
for k, v in dims.items():
aggregated[f'{k}={v}'] = aggregated.get(f'{k}={v}', 0) + row['total']
return sorted(
[{'dim_value': k, 'total': v} for k, v in aggregated.items()],
key=lambda x: x['total'],
reverse=True,
)
@staticmethod
def GenerateRanking(tenant_uuid, metric_uuid, ranking_period, ranking_date,
dimension_filter=None, top_n=50, sort_order='desc'):
"""生成排行榜快照
从 StatRecord 中按维度聚合后排序生成 StatRanking 记录。
Args:
tenant_uuid: 租户UUID
metric_uuid: 指标UUID
ranking_period: 'daily' | 'monthly'
ranking_date: 排行日期
dimension_filter: 过滤条件(如 {'role': 'dashou'})
top_n: 取前N名
sort_order: 'desc' | 'asc'
Returns:
List[StatRanking]
"""
import datetime
from gvsdsdk.stats.models import StatRanking
period_type = ranking_period
if ranking_period == 'daily':
period_key = ranking_date.strftime('%Y-%m-%d') if hasattr(ranking_date, 'strftime') else str(ranking_date)
elif ranking_period == 'monthly':
period_key = ranking_date.strftime('%Y-%m') if hasattr(ranking_date, 'strftime') else str(ranking_date)
else:
period_key = str(ranking_date)
qs = StatRecord.objects.filter(
TenantUUID=tenant_uuid,
Metric_id=metric_uuid,
PeriodType=period_type,
PeriodKey=period_key,
)
if dimension_filter:
for k, v in dimension_filter.items():
qs = qs.filter(**{f'DimensionValues__{k}': v})
items = qs.values('DimensionValues').annotate(
total=Sum('Value'),
).order_by(
'-total' if sort_order == 'desc' else 'total',
)[:top_n]
rankings = []
target_dim_key = (dimension_filter or {}).get('_target_dim', 'user_id')
for rank, item in enumerate(items, 1):
dims = item['DimensionValues']
target_id = str(dims.get(target_dim_key, '_unknown'))
rk = StatRanking.objects.create(
TenantUUID=tenant_uuid,
RankingPeriod=ranking_period,
RankingDate=ranking_date,
Metric_id=metric_uuid,
DimensionFilter=dimension_filter or {},
TargetID=target_id,
Rank=rank,
RankValue=item['total'],
TargetName=dims.get('name', target_id),
TargetAvatar=dims.get('avatar', ''),
)
rankings.append(rk)
return rankings
@staticmethod
def ResetPeriod(tenant_uuid, metric_uuid, period_type, period_key,
dimension_filter=None):
"""重置指定周期的统计数据(用于每日/每月清零)
Args:
tenant_uuid: 租户UUID
metric_uuid: 指标UUID
period_type: 周期类型
period_key: 周期键
dimension_filter: 可选,仅重置特定维度
Returns:
int 删除的记录数
"""
qs = StatRecord.objects.filter(
TenantUUID=tenant_uuid,
Metric_id=metric_uuid,
PeriodType=period_type,
PeriodKey=period_key,
)
if dimension_filter:
qs = qs.filter(DimensionValues__contains=dimension_filter)
count, _ = qs.delete()
return count
@staticmethod
def PurgeOld(tenant_uuid, before_date, period_type='daily'):
"""清理过期统计数据
Args:
tenant_uuid: 租户UUID
before_date: 在此之前的数据将被删除
period_type: 周期类型
Returns:
int 删除的记录数
"""
before_key = before_date.strftime('%Y-%m-%d') if hasattr(before_date, 'strftime') else str(before_date)
qs = StatRecord.objects.filter(
TenantUUID=tenant_uuid,
PeriodType=period_type,
PeriodKey__lt=before_key,
)
count, _ = qs.delete()
return count
+235
View File
@@ -0,0 +1,235 @@
"""模块:每日数据统计引擎 — SAAS 数据模型(gvsdsdk 副本)
抽象统计层:不绑定具体业务,由租户定义统计指标和维度。
与 gvsds/apps/stats/models.py 字段、db_table 完全一致,映射 SAAS 主库同名表。
核心设计:
StatMetric — 统计指标定义(计数/金额/自定义)
StatDimension — 统计维度定义(按角色/按类型/按来源)
StatRecord — 统计流水表(按维度+时间窗口聚合的统计数据)
StatRanking — 排行榜快照(日榜/月榜)
StatConfig — 统计配置(周期重置、排行榜规则)"""
import uuid
import datetime
from django.db import models
from gvsdsdk.model_base import QModel
from gvsdsdk.models import UUIDField
class StatMetric(QModel):
"""统计指标定义——租户自定义要统计什么
每个租户可定义多个指标,例如:
· dashou_jiedan_count — 打手接单数
· dashou_shouru_amount — 打手收入金额
· guanshi_chongzhi_count — 管事充值人数
· platform_total_income — 平台总收入
"""
MET_COUNT = 'count'
MET_AMOUNT = 'amount'
MET_DURATION = 'duration'
MET_RATIO = 'ratio'
MET_CUSTOM = 'custom'
MET_TYPE_CHOICES = [
(MET_COUNT, '计数'),
(MET_AMOUNT, '金额/数值'),
(MET_DURATION, '时长'),
(MET_RATIO, '比率'),
(MET_CUSTOM, '自定义'),
]
MetricUUID = UUIDField(primary_key=True, verbose_name='指标UUID')
TenantUUID = UUIDField(verbose_name='租户UUID')
MetricCode = models.CharField(max_length=64, verbose_name='指标编码')
MetricName = models.CharField(max_length=128, verbose_name='指标名称')
MetricType = models.CharField(max_length=16, choices=MET_TYPE_CHOICES, default=MET_COUNT, verbose_name='指标类型')
Unit = models.CharField(max_length=16, default='次', verbose_name='单位')
DefaultValue = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='默认值')
MetricStatus = models.SmallIntegerField(default=1, verbose_name='指标状态')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'StatMetric'
app_label = 'gvsdsdk'
verbose_name = '统计指标'
verbose_name_plural = '统计指标'
unique_together = [('TenantUUID', 'MetricCode')]
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'MetricStatus'], name='idx_statmet_tenant_status'),
]
def __str__(self):
return f'{self.MetricCode}:{self.MetricName}'
class StatDimension(QModel):
"""统计维度定义——按什么切分统计
每个指标可以有多个维度,例如:
· role — 按角色(打手/管事/组长/商家)
· platform — 按平台(微信/支付宝/银行卡)
· order_type — 按订单类型(平台订单/商家订单)
· source — 按来源渠道
"""
DimensionUUID = UUIDField(primary_key=True, verbose_name='维度UUID')
TenantUUID = UUIDField(verbose_name='租户UUID')
DimensionCode = models.CharField(max_length=64, verbose_name='维度编码')
DimensionName = models.CharField(max_length=128, verbose_name='维度名称')
DimensionStatus = models.SmallIntegerField(default=1, verbose_name='维度状态')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'StatDimension'
app_label = 'gvsdsdk'
verbose_name = '统计维度'
verbose_name_plural = '统计维度'
unique_together = [('TenantUUID', 'DimensionCode')]
managed = False
def __str__(self):
return f'{self.DimensionCode}:{self.DimensionName}'
class StatRecord(QModel):
"""统计记录表——按维度+时间窗口聚合的统计数据
核心结构: 租户 + 日期 + 指标 + 维度值 → 数值
用法:
租户 'al_long',日期 '2026-06-11',
指标 'dashou_jiedan_count',维度值 {'role': 'dashou', 'user_id': '123'}
→ value = 5(今天打手123接了5单)
"""
PERIOD_DAILY = 'daily'
PERIOD_HOURLY = 'hourly'
PERIOD_MONTHLY = 'monthly'
PERIOD_CUSTOM = 'custom'
PERIOD_CHOICES = [
(PERIOD_DAILY, '每日'),
(PERIOD_HOURLY, '每小时'),
(PERIOD_MONTHLY, '每月'),
(PERIOD_CUSTOM, '自定义'),
]
RecordUUID = UUIDField(primary_key=True, verbose_name='记录UUID')
TenantUUID = UUIDField(verbose_name='租户UUID')
PeriodType = models.CharField(max_length=16, choices=PERIOD_CHOICES, default=PERIOD_DAILY, verbose_name='周期类型')
PeriodKey = models.CharField(max_length=32, verbose_name='周期键')
Metric = models.ForeignKey(
StatMetric, on_delete=models.CASCADE,
related_name='records',
verbose_name='统计指标',
)
DimensionValues = models.JSONField(default=dict, verbose_name='维度值')
Value = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='数值')
UpdateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='更新时间')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'StatRecord'
app_label = 'gvsdsdk'
verbose_name = '统计记录'
verbose_name_plural = '统计记录'
unique_together = [('TenantUUID', 'PeriodType', 'PeriodKey', 'Metric', 'DimensionValues')]
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'PeriodType', 'PeriodKey'], name='idx_statreq_tenant_period'),
models.Index(fields=['Metric', 'PeriodKey'], name='idx_statreq_metric_period'),
models.Index(fields=['CreateTime'], name='idx_statreq_create_time'),
]
def __str__(self):
dim_str = ','.join(f'{k}={v}' for k, v in self.DimensionValues.items())
return f'{self.PeriodKey}/{self.Metric.MetricCode}/{dim_str}={self.Value}'
class StatRanking(QModel):
"""排行榜快照——周期性的排名记录
按指标+维度分组,在指定周期内的排名快照。
通常由定时任务生成,前端直接查询即可。
"""
RNK_DAILY = 'daily'
RNK_MONTHLY = 'monthly'
RNK_PERIOD_CHOICES = [
(RNK_DAILY, '日榜'),
(RNK_MONTHLY, '月榜'),
]
RankingUUID = UUIDField(primary_key=True, verbose_name='排行UUID')
TenantUUID = UUIDField(verbose_name='租户UUID')
RankingPeriod = models.CharField(max_length=16, choices=RNK_PERIOD_CHOICES, verbose_name='排行周期')
RankingDate = models.DateField(verbose_name='排行日期')
Metric = models.ForeignKey(
StatMetric, on_delete=models.CASCADE,
related_name='rankings',
verbose_name='统计指标',
)
DimensionFilter = models.JSONField(default=dict, verbose_name='维度筛选')
TargetID = models.CharField(max_length=128, verbose_name='目标ID')
Rank = models.IntegerField(verbose_name='排名')
RankValue = models.DecimalField(max_digits=18, decimal_places=4, verbose_name='排名数值')
TargetName = models.CharField(max_length=128, default='', verbose_name='目标名称')
TargetAvatar = models.CharField(max_length=512, default='', verbose_name='目标头像')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'StatRanking'
app_label = 'gvsdsdk'
verbose_name = '排行榜'
verbose_name_plural = '排行榜'
unique_together = [('TenantUUID', 'RankingPeriod', 'RankingDate', 'Metric', 'DimensionFilter', 'Rank')]
managed = False
indexes = [
models.Index(fields=['TenantUUID', 'RankingPeriod', 'RankingDate', 'Metric'], name='idx_statrnk_composite'),
models.Index(fields=['TargetID', 'RankingPeriod'], name='idx_statrnk_target'),
]
def __str__(self):
return f'{self.RankingDate} {self.RankingPeriod} #{self.Rank} {self.TargetID}'
class StatConfig(QModel):
"""统计配置——租户级统计行为配置
定义:
· 哪些指标需要周期性重置(日清零/月清零)
· 排行榜生成规则
· 数据保留天数
"""
ConfigUUID = UUIDField(primary_key=True, verbose_name='配置UUID')
TenantUUID = UUIDField(verbose_name='租户UUID')
Metric = models.ForeignKey(
StatMetric, on_delete=models.CASCADE,
related_name='configs',
verbose_name='统计指标',
)
AutoReset = models.BooleanField(default=False, verbose_name='自动重置')
ResetCron = models.CharField(max_length=64, default='0 0 * * *', verbose_name='重置Cron表达式')
EnableRanking = models.BooleanField(default=False, verbose_name='启用排行榜')
RankingDimensionFilter = models.JSONField(default=dict, verbose_name='排行维度筛选')
RankingTopN = models.IntegerField(default=50, verbose_name='排行TopN')
RankingSortOrder = models.CharField(max_length=8, default='desc', verbose_name='排行排序方式')
RetentionDays = models.IntegerField(default=365, verbose_name='数据保留天数')
ConfigStatus = models.SmallIntegerField(default=1, verbose_name='配置状态')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'StatConfig'
app_label = 'gvsdsdk'
verbose_name = '统计配置'
verbose_name_plural = '统计配置'
unique_together = [('TenantUUID', 'Metric')]
managed = False
def __str__(self):
return f'StatConfig:{self.Metric.MetricCode}'