"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:
1 parent
0ad5ce77c8
commit
d2de2301a4
12 files changed
+760
No files matched your search
+14
@@ -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.
@@ -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.
@@ -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):
|
||||
|
||||
@@ -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.
@@ -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
@@ -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}'
|
||||
Reference in new issue
Block a user