File size: 10,500 Bytes
b4f2238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
"""Module for cache."""

from __future__ import annotations

"""
缓存配置模块
提供 Django 内置本地内存缓存配置
"""

import os
from typing import Any


def _safe_get_config(key: str, default: Any | None = None) -> Any:
    """安全获取配置,避免循环导入"""
    try:
        from apps.core.config import get_config

        return get_config(key, default)
    except (ImportError, AttributeError, KeyError):
        return default


def _hash_key_component(value: str) -> str:
    import hashlib
    import hmac

    from django.conf import settings

    v = (value or "").strip().encode("utf-8")
    secret = (getattr(settings, "SECRET_KEY", "") or "").encode("utf-8")
    return hmac.new(secret, v, hashlib.sha256).hexdigest()[:32]


def _normalize_key_component(value: str, *, max_len: int = 64) -> str:
    import re

    raw = (value or "").strip()
    if not raw:
        return "empty"

    normalized = raw.lower()
    if re.fullmatch(r"[a-z0-9._-]+", normalized) and len(normalized) <= max_len:
        return normalized

    cleaned = re.sub(r"[^a-z0-9._-]+", "-", normalized).strip("-._")
    if not cleaned:
        cleaned = "x"

    if len(cleaned) > max_len:
        cleaned = cleaned[:max_len].rstrip("-._") or "x"

    return f"{cleaned}-{_hash_key_component(raw)}"


def get_cache_config() -> dict[str, Any]:
    """
    获取缓存配置

    使用 Django 内置的本地内存缓存,无需外部依赖.

    Returns:
        Django CACHES 配置字典
    """
    default_timeout = _safe_get_config("performance.cache.default_timeout", 300)

    redis_url = (os.environ.get("DJANGO_CACHE_REDIS_URL", "") or "").strip()
    if redis_url:
        return {
            "default": {
                "BACKEND": "django.core.cache.backends.redis.RedisCache",
                "LOCATION": redis_url,
                "KEY_PREFIX": _safe_get_config("performance.cache.key_prefix", "lawfirm"),
                "TIMEOUT": default_timeout,
            }
        }

    return {
        "default": {
            "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
            "LOCATION": "unique-lawfirm-cache",
            "OPTIONS": {
                "MAX_ENTRIES": 1000,
            },
            "KEY_PREFIX": _safe_get_config("performance.cache.key_prefix", "lawfirm"),
            "TIMEOUT": default_timeout,
        }
    }


# 缓存 key 常量
class CacheKeys:
    """缓存 key 定义"""

    # 用户相关
    USER_ORG_ACCESS = "user:org_access:{user_id}"  # 用户组织访问权限
    USER_TEAMS = "user:teams:{user_id}"  # 用户团队列表

    # 案件相关
    CASE_ACCESS_GRANTS = "case:access_grants:{user_id}"  # 用户案件访问授权

    # 自动化相关
    AUTOMATION_COURT_SMS_RECOVERY_SCHEDULED = "automation:court_sms_recovery_scheduled"

    # 配置相关
    CASE_STAGES_CONFIG = "config:case_stages"  # 案件阶段配置
    LEGAL_STATUS_CONFIG = "config:legal_status"  # 诉讼地位配置
    SYSTEM_CONFIG = "system_config:{key}"

    # 法院系统 Token 相关
    COURT_TOKEN = "court_token:{site_name}:{account}"  # 法院系统 Token
    # 自动化 Token 性能监控
    AUTOMATION_TOKEN_PERF_ACQUISITION = "automation:token:perf:acquisition:{acquisition_id}"
    AUTOMATION_TOKEN_PERF_CONCURRENT = "automation:token:perf:concurrent:{site_name}"
    AUTOMATION_TOKEN_PERF_COUNTER = "automation:token:perf:counter:{date}:{site_name}:{metric}"

    # Prompt 相关
    PROMPT_TEMPLATE = "prompt_template:{name}"
    PROMPT_VERSION_ACTIVE = "prompt_version:active:{name}"

    # Documents 模板匹配相关(包含版本号以便模板变更自动失效)
    DOCUMENTS_MATCH_CONTRACT_TEMPLATES = "documents:matching:contract_templates:{case_type}:{version}"
    DOCUMENTS_MATCH_FOLDER_TEMPLATES = "documents:matching:folder_templates:{template_type}:{case_type}:{version}"
    DOCUMENTS_MATCH_CASE_FILE_TEMPLATES = (
        "documents:matching:case_file_templates:{case_type}:{case_stage}:{institutions}:{version}"
    )
    DOCUMENTS_MATCHING_VERSION_DOCUMENT_TEMPLATES = "documents:matching:version:document_templates"
    DOCUMENTS_MATCHING_VERSION_FOLDER_TEMPLATES = "documents:matching:version:folder_templates"

    @classmethod
    def user_org_access(cls, user_id: int) -> str:
        return cls.USER_ORG_ACCESS.format(user_id=user_id)

    @classmethod
    def user_teams(cls, user_id: int) -> str:
        return cls.USER_TEAMS.format(user_id=user_id)

    @classmethod
    def case_access_grants(cls, user_id: int) -> str:
        return cls.CASE_ACCESS_GRANTS.format(user_id=user_id)

    @classmethod
    def automation_court_sms_recovery_scheduled(cls) -> str:
        return cls.AUTOMATION_COURT_SMS_RECOVERY_SCHEDULED

    @classmethod
    def court_token(cls, site_name: str, account: str) -> str:
        return cls.COURT_TOKEN.format(
            site_name=_normalize_key_component(site_name, max_len=64),
            account=_hash_key_component(account),
        )

    @classmethod
    def automation_token_perf_acquisition(cls, acquisition_id: str) -> str:
        return cls.AUTOMATION_TOKEN_PERF_ACQUISITION.format(acquisition_id=acquisition_id)

    @classmethod
    def automation_token_perf_concurrent(cls, *, site_name: str = "all") -> str:
        return cls.AUTOMATION_TOKEN_PERF_CONCURRENT.format(site_name=site_name or "all")

    @classmethod
    def automation_token_perf_counter(cls, *, date: str, site_name: str, metric: str) -> str:
        return cls.AUTOMATION_TOKEN_PERF_COUNTER.format(date=date, site_name=site_name or "all", metric=metric)

    @classmethod
    def prompt_template(cls, name: str) -> str:
        return cls.PROMPT_TEMPLATE.format(name=name)

    @classmethod
    def prompt_version_active(cls, name: str) -> str:
        return cls.PROMPT_VERSION_ACTIVE.format(name=name)

    @classmethod
    def system_config(cls, key: str) -> str:
        return cls.SYSTEM_CONFIG.format(key=key)

    @classmethod
    def documents_matching_contract_templates(cls, *, case_type: str, version: int) -> str:
        return cls.DOCUMENTS_MATCH_CONTRACT_TEMPLATES.format(case_type=case_type or "", version=version or 1)

    @classmethod
    def documents_matching_folder_templates(cls, *, template_type: str, case_type: str, version: int) -> str:
        return cls.DOCUMENTS_MATCH_FOLDER_TEMPLATES.format(
            template_type=template_type or "",
            case_type=case_type or "",
            version=version or 1,
        )

    @classmethod
    def documents_matching_case_file_templates(
        cls, *, case_type: str, case_stage: str, institutions: str = "", version: int
    ) -> str:
        return cls.DOCUMENTS_MATCH_CASE_FILE_TEMPLATES.format(
            case_type=case_type or "",
            case_stage=case_stage or "",
            institutions=institutions or "all",
            version=version or 1,
        )

    @classmethod
    def documents_matching_version_document_templates(cls) -> str:
        return cls.DOCUMENTS_MATCHING_VERSION_DOCUMENT_TEMPLATES

    @classmethod
    def documents_matching_version_folder_templates(cls) -> str:
        return cls.DOCUMENTS_MATCHING_VERSION_FOLDER_TEMPLATES


# 缓存超时时间(秒)
class _CacheTimeoutMeta(type):
    def __getattribute__(cls, name: str) -> int:
        if name == "SHORT":
            return int(_safe_get_config("performance.cache.timeout_short", 60))
        if name == "MEDIUM":
            return int(_safe_get_config("performance.cache.timeout_medium", 300))
        if name == "LONG":
            return int(_safe_get_config("performance.cache.timeout_long", 3600))
        if name == "DAY":
            return int(_safe_get_config("performance.cache.timeout_day", 86400))
        return super().__getattribute__(name)  # type: ignore[no-any-return]  # metaclass __getattribute__ 返回 Any


class CacheTimeout(metaclass=_CacheTimeoutMeta):
    """缓存超时时间定义"""

    @classmethod
    def get_short(cls) -> int:
        """短期缓存(1分钟)"""
        return int(cls.SHORT)

    @classmethod
    def get_medium(cls) -> int:
        """中期缓存(5分钟)"""
        return int(cls.MEDIUM)

    @classmethod
    def get_long(cls) -> int:
        """长期缓存(1小时)"""
        return int(cls.LONG)

    @classmethod
    def get_day(cls) -> int:
        """日缓存(1天)"""
        return int(cls.DAY)

    @classmethod
    def until_end_of_day(cls, *, now: Any | None = None, buffer_seconds: int = 3600) -> int:
        from datetime import datetime, time, timedelta

        from django.utils import timezone

        if now is None:
            now = timezone.now()

        if timezone.is_naive(now):
            now = timezone.make_aware(now, timezone.get_current_timezone())

        end = timezone.make_aware(
            datetime.combine(now.date() + timedelta(days=1), time.min),
            timezone.get_current_timezone(),
        )
        seconds = int((end - now).total_seconds()) + int(buffer_seconds or 0)
        return max(1, seconds)

    # 保持向后兼容的常量
    SHORT = 60
    MEDIUM = 300
    LONG = 3600
    DAY = 86400


def invalidate_user_access_context(user_id: int, *, org_access: bool = True, case_grants: bool = True) -> None:
    from django.core.cache import cache

    keys: list[Any] = []
    if org_access:
        keys.append(CacheKeys.user_org_access(user_id))
    if case_grants:
        keys.append(CacheKeys.case_access_grants(user_id))
    if keys:
        cache.delete_many(keys)


def invalidate_users_access_context(user_ids: list[int], *, org_access: bool = True, case_grants: bool = True) -> None:
    from django.core.cache import cache

    keys: list[str] = []
    for user_id in user_ids or []:
        if org_access:
            keys.append(CacheKeys.user_org_access(user_id))
        if case_grants:
            keys.append(CacheKeys.case_access_grants(user_id))
    if keys:
        cache.delete_many(keys)


def bump_cache_version(key: str, *, timeout: int) -> int:
    from django.core.cache import cache

    cache.add(key, 1, timeout=timeout)
    try:
        return int(cache.incr(key))
    except (ConnectionError, TimeoutError, OSError, ValueError, TypeError):
        current = int(cache.get(key) or 1)
        new_val = current + 1
        cache.set(key, new_val, timeout=timeout)
        return new_val


def delete_cache_key(key: str) -> None:
    from django.core.cache import cache

    cache.delete(key)


import logging

logger = logging.getLogger(__name__)