feat(models): 组织、用户、角色、客户、会话与消息模型
按组织边界定义数据关系,补齐仓储层与通用工具,迁移脚本可重复执行。
This commit is contained in:
1 parent
2aee6286d6
commit
5e0615f36d
126 files changed
+17436
No files matched your search
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Copyright 2024-2026 ChatterMate
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
from app.core.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Used when an organization has never configured its hours. Weekdays 9-5 in the
|
||||
# org's timezone, weekend closed.
|
||||
DEFAULT_BUSINESS_HOURS = {
|
||||
'monday': {'start': '09:00', 'end': '17:00', 'enabled': True},
|
||||
'tuesday': {'start': '09:00', 'end': '17:00', 'enabled': True},
|
||||
'wednesday': {'start': '09:00', 'end': '17:00', 'enabled': True},
|
||||
'thursday': {'start': '09:00', 'end': '17:00', 'enabled': True},
|
||||
'friday': {'start': '09:00', 'end': '17:00', 'enabled': True},
|
||||
'saturday': {'start': '09:00', 'end': '17:00', 'enabled': False},
|
||||
'sunday': {'start': '09:00', 'end': '17:00', 'enabled': False},
|
||||
}
|
||||
|
||||
_DEFAULT_START = '09:00'
|
||||
_DEFAULT_END = '17:00'
|
||||
|
||||
|
||||
def organization_timezone(organization) -> pytz.BaseTzInfo:
|
||||
"""The org's timezone, falling back to UTC for a missing or bad value."""
|
||||
tz_name = getattr(organization, 'timezone', None) or 'UTC'
|
||||
try:
|
||||
return pytz.timezone(tz_name)
|
||||
except pytz.UnknownTimeZoneError:
|
||||
logger.warning(f"Invalid timezone {tz_name}, using UTC")
|
||||
return pytz.UTC
|
||||
|
||||
|
||||
def is_within_business_hours(organization) -> bool:
|
||||
"""Whether the organization is open right now, in its own timezone.
|
||||
|
||||
Pure and cheap — no database work and no model call — so it can answer the
|
||||
widget's "are you around?" on every page load, not just when the AI is
|
||||
deciding whether to hand a chat over.
|
||||
|
||||
Note: hours are compared as minutes-of-day, so a window that wraps midnight
|
||||
(22:00-02:00) never matches. That is the behaviour this was extracted from;
|
||||
changing it would change when the AI offers a transfer.
|
||||
"""
|
||||
business_hours = getattr(organization, 'business_hours', None) or DEFAULT_BUSINESS_HOURS
|
||||
|
||||
now = datetime.now(organization_timezone(organization))
|
||||
today = business_hours.get(now.strftime('%A').lower(), {
|
||||
'start': _DEFAULT_START,
|
||||
'end': _DEFAULT_END,
|
||||
'enabled': False,
|
||||
})
|
||||
|
||||
if not today.get('enabled', False):
|
||||
return False
|
||||
|
||||
try:
|
||||
start_hour, start_minute = map(int, today.get('start', _DEFAULT_START).split(':'))
|
||||
end_hour, end_minute = map(int, today.get('end', _DEFAULT_END).split(':'))
|
||||
except (ValueError, AttributeError) as e:
|
||||
logger.error(f"Error parsing business hours: {str(e)}")
|
||||
start_hour, start_minute = 9, 0
|
||||
end_hour, end_minute = 17, 0
|
||||
|
||||
minutes_now = now.hour * 60 + now.minute
|
||||
return (start_hour * 60 + start_minute) <= minutes_now <= (end_hour * 60 + end_minute)
|
||||
Reference in new issue
Block a user