|
|
|
|
@ -1,127 +1,237 @@
|
|
|
|
|
# Create your views here.
|
|
|
|
|
import datetime
|
|
|
|
|
import itertools
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
from datetime import timezone
|
|
|
|
|
from itertools import groupby
|
|
|
|
|
|
|
|
|
|
import django
|
|
|
|
|
import requests
|
|
|
|
|
from django.contrib.auth.decorators import login_required
|
|
|
|
|
from django.http import HttpResponse
|
|
|
|
|
from django.http import JsonResponse
|
|
|
|
|
from django.shortcuts import render
|
|
|
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
|
|
|
|
|
|
from .models import OwnTrackLog
|
|
|
|
|
|
|
|
|
|
# 说明:该文件为 Django 视图层核心文件,包含所有 /owntracks/ 路由对应的业务处理逻辑
|
|
|
|
|
# 视图函数职责:接收请求、处理数据(数据库读写/第三方接口调用)、返回响应(页面/JSON/状态码)
|
|
|
|
|
|
|
|
|
|
# 导入标准库模块
|
|
|
|
|
import datetime # 处理日期时间相关操作(如日期计算、格式化)
|
|
|
|
|
import itertools # 提供迭代器工具(如切片、分组,用于批量处理经纬度数据)
|
|
|
|
|
import json # 处理 JSON 数据序列化/反序列化(适配接口请求/响应)
|
|
|
|
|
import logging # 日志模块:记录业务日志(信息/错误),便于问题排查
|
|
|
|
|
from datetime import timezone # 处理时区相关(确保时间计算一致性)
|
|
|
|
|
from itertools import groupby # 分组工具:按用户标识(tid)分组轨迹数据
|
|
|
|
|
|
|
|
|
|
# 导入第三方库/框架模块
|
|
|
|
|
import django # Django 核心模块(用于时区时间处理)
|
|
|
|
|
import requests # HTTP 请求库:调用高德地图坐标转换接口
|
|
|
|
|
from django.contrib.auth.decorators import login_required # 登录验证装饰器:限制未登录用户访问
|
|
|
|
|
from django.http import HttpResponse # 基础响应类:返回文本/状态码响应
|
|
|
|
|
from django.http import JsonResponse # JSON 响应类:返回 JSON 格式数据(适配前端异步请求)
|
|
|
|
|
from django.shortcuts import render # 页面渲染函数:加载模板并返回 HTML 页面
|
|
|
|
|
from django.views.decorators.csrf import csrf_exempt # CSRF 豁免装饰器:关闭跨站请求伪造保护(适配第三方客户端提交数据)
|
|
|
|
|
|
|
|
|
|
# 导入当前应用模块
|
|
|
|
|
from .models import OwnTrackLog # 轨迹数据模型:用于数据库读写操作
|
|
|
|
|
|
|
|
|
|
# 初始化日志对象:按当前模块名创建日志实例,日志输出会携带模块标识
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@csrf_exempt
|
|
|
|
|
@csrf_exempt # 豁免 CSRF 验证:因客户端(如设备/第三方系统)可能无法提供 CSRF Token,故关闭保护
|
|
|
|
|
def manage_owntrack_log(request):
|
|
|
|
|
"""
|
|
|
|
|
轨迹数据提交接口视图:
|
|
|
|
|
功能:接收客户端 POST 提交的 JSON 格式轨迹数据(tid/经纬度),验证后写入数据库
|
|
|
|
|
请求方式:POST(仅支持 POST,其他方式会因缺少请求体报错)
|
|
|
|
|
请求体格式:{"tid": "用户标识", "lat": 纬度值, "lon": 经度值}
|
|
|
|
|
响应:
|
|
|
|
|
- 成功写入:返回 "ok"(HTTP 200)
|
|
|
|
|
- 数据不完整:返回 "data error"(HTTP 200)
|
|
|
|
|
- 异常报错:返回 "error"(HTTP 200)并记录错误日志
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# 读取请求体:将 JSON 字符串解码为 Python 字典(utf-8 编码适配中文/特殊字符)
|
|
|
|
|
s = json.loads(request.read().decode('utf-8'))
|
|
|
|
|
# 提取请求数据中的核心字段(用户标识、纬度、经度)
|
|
|
|
|
tid = s['tid']
|
|
|
|
|
lat = s['lat']
|
|
|
|
|
lon = s['lon']
|
|
|
|
|
|
|
|
|
|
# 记录信息日志:打印提交的轨迹数据(便于追踪数据流转)
|
|
|
|
|
logger.info(
|
|
|
|
|
'tid:{tid}.lat:{lat}.lon:{lon}'.format(
|
|
|
|
|
tid=tid, lat=lat, lon=lon))
|
|
|
|
|
tid=tid, lat=lat, lon=lon)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# 数据合法性校验:确保核心字段非空(避免写入无效数据)
|
|
|
|
|
if tid and lat and lon:
|
|
|
|
|
# 创建模型实例并赋值
|
|
|
|
|
m = OwnTrackLog()
|
|
|
|
|
m.tid = tid
|
|
|
|
|
m.lat = lat
|
|
|
|
|
m.lon = lon
|
|
|
|
|
m.save()
|
|
|
|
|
return HttpResponse('ok')
|
|
|
|
|
m.tid = tid # 用户标识
|
|
|
|
|
m.lat = lat # 纬度
|
|
|
|
|
m.lon = lon # 经度
|
|
|
|
|
# creation_time 字段使用默认值(当前时间),无需手动赋值
|
|
|
|
|
m.save() # 保存到数据库
|
|
|
|
|
return HttpResponse('ok') # 响应成功标识
|
|
|
|
|
else:
|
|
|
|
|
# 数据不完整:返回错误提示
|
|
|
|
|
return HttpResponse('data error')
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(e)
|
|
|
|
|
return HttpResponse('error')
|
|
|
|
|
# 捕获所有异常(如 JSON 解析失败、字段缺失、数据库报错等)
|
|
|
|
|
logger.error(e) # 记录错误日志(包含异常堆栈信息,便于排查)
|
|
|
|
|
return HttpResponse('error') # 响应错误标识
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
|
@login_required # 登录验证:仅登录用户可访问,未登录自动重定向到登录页
|
|
|
|
|
def show_maps(request):
|
|
|
|
|
"""
|
|
|
|
|
地图展示页面视图:
|
|
|
|
|
功能:渲染包含用户轨迹的地图页面(需管理员权限)
|
|
|
|
|
请求方式:GET
|
|
|
|
|
请求参数:?date=YYYY-MM-DD(可选,默认当前日期)
|
|
|
|
|
响应:
|
|
|
|
|
- 管理员登录:返回地图 HTML 页面(携带日期参数)
|
|
|
|
|
- 非管理员登录:返回 403 禁止访问
|
|
|
|
|
"""
|
|
|
|
|
# 权限二次校验:仅超级管理员(is_superuser=True)可访问,普通登录用户无权限
|
|
|
|
|
if request.user.is_superuser:
|
|
|
|
|
# 计算默认日期:当前 UTC 时间的日期(格式:YYYY-MM-DD)
|
|
|
|
|
defaultdate = str(datetime.datetime.now(timezone.utc).date())
|
|
|
|
|
# 获取请求参数中的日期(若未传则使用默认日期)
|
|
|
|
|
date = request.GET.get('date', defaultdate)
|
|
|
|
|
# 构造模板上下文:传递日期参数给前端模板(用于筛选该日期的轨迹数据)
|
|
|
|
|
context = {
|
|
|
|
|
'date': date
|
|
|
|
|
}
|
|
|
|
|
# 渲染模板:加载 show_maps.html 模板并传入上下文,返回 HTML 响应
|
|
|
|
|
return render(request, 'owntracks/show_maps.html', context)
|
|
|
|
|
else:
|
|
|
|
|
# 非管理员:导入并返回 403 禁止访问响应
|
|
|
|
|
from django.http import HttpResponseForbidden
|
|
|
|
|
return HttpResponseForbidden()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
|
@login_required # 登录验证:仅登录用户可访问
|
|
|
|
|
def show_log_dates(request):
|
|
|
|
|
"""
|
|
|
|
|
轨迹日期列表页面视图:
|
|
|
|
|
功能:查询数据库中所有轨迹数据的日期(去重),渲染日期列表页面(用于前端筛选)
|
|
|
|
|
请求方式:GET
|
|
|
|
|
响应:返回日期列表 HTML 页面(包含去重后的所有轨迹日期)
|
|
|
|
|
"""
|
|
|
|
|
# 查询所有轨迹记录的创建时间(仅取 creation_time 字段,flat=True 返回一维列表)
|
|
|
|
|
dates = OwnTrackLog.objects.values_list('creation_time', flat=True)
|
|
|
|
|
# 日期处理:
|
|
|
|
|
# 1. map 转换:将 datetime 对象格式化为 'YYYY-MM-DD' 字符串
|
|
|
|
|
# 2. set 去重:去除重复日期
|
|
|
|
|
# 3. sorted 排序:按日期升序排列
|
|
|
|
|
# 4. list 转换:转为列表用于模板渲染
|
|
|
|
|
results = list(sorted(set(map(lambda x: x.strftime('%Y-%m-%d'), dates))))
|
|
|
|
|
|
|
|
|
|
# 构造上下文:传递日期列表给模板
|
|
|
|
|
context = {
|
|
|
|
|
'results': results
|
|
|
|
|
}
|
|
|
|
|
# 渲染日期列表模板
|
|
|
|
|
return render(request, 'owntracks/show_log_dates.html', context)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def convert_to_amap(locations):
|
|
|
|
|
convert_result = []
|
|
|
|
|
it = iter(locations)
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
高德地图坐标转换工具函数:
|
|
|
|
|
功能:将 GPS 坐标系(WGS84)的经纬度转换为高德坐标系(GCJ02)
|
|
|
|
|
原因:GPS 原始坐标在高德地图上会有偏移,转换后可精准定位
|
|
|
|
|
参数:locations - OwnTrackLog 模型实例列表(包含 lon/lat 字段)
|
|
|
|
|
返回值:转换后的经纬度字符串(格式:"lon1,lat1;lon2,lat2;...")
|
|
|
|
|
限制:高德接口单次最多支持 30 个坐标,故分批次转换
|
|
|
|
|
"""
|
|
|
|
|
convert_result = [] # 存储所有批次的转换结果
|
|
|
|
|
it = iter(locations) # 将列表转为迭代器,便于分批次切片
|
|
|
|
|
|
|
|
|
|
# 循环分批次处理:每次取 30 个坐标(适配高德接口限制)
|
|
|
|
|
item = list(itertools.islice(it, 30))
|
|
|
|
|
while item:
|
|
|
|
|
# 构造坐标字符串:将每个实例的 lon/lat 拼接为 "lon,lat",再用 ";" 连接多个坐标
|
|
|
|
|
# set 去重:避免重复坐标提交(减少接口调用量)
|
|
|
|
|
datas = ';'.join(
|
|
|
|
|
set(map(lambda x: str(x.lon) + ',' + str(x.lat), item)))
|
|
|
|
|
set(map(lambda x: str(x.lon) + ',' + str(x.lat), item))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
key = '8440a376dfc9743d8924bf0ad141f28e'
|
|
|
|
|
api = 'http://restapi.amap.com/v3/assistant/coordinate/convert'
|
|
|
|
|
# 高德地图坐标转换接口配置
|
|
|
|
|
key = '8440a376dfc9743d8924bf0ad141f28e' # 高德开发者密钥(需替换为有效密钥)
|
|
|
|
|
api = 'http://restapi.amap.com/v3/assistant/coordinate/convert' # 转换接口地址
|
|
|
|
|
query = {
|
|
|
|
|
'key': key,
|
|
|
|
|
'locations': datas,
|
|
|
|
|
'coordsys': 'gps'
|
|
|
|
|
'key': key, # 开发者密钥(必填)
|
|
|
|
|
'locations': datas, # 待转换的坐标字符串
|
|
|
|
|
'coordsys': 'gps' # 源坐标系:gps(WGS84)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 调用高德接口(GET 请求)
|
|
|
|
|
rsp = requests.get(url=api, params=query)
|
|
|
|
|
# 解析接口响应(JSON 转字典)
|
|
|
|
|
result = json.loads(rsp.text)
|
|
|
|
|
|
|
|
|
|
# 若响应包含 "locations" 字段(转换成功),添加到结果列表
|
|
|
|
|
if "locations" in result:
|
|
|
|
|
convert_result.append(result['locations'])
|
|
|
|
|
|
|
|
|
|
# 处理下一批次坐标
|
|
|
|
|
item = list(itertools.islice(it, 30))
|
|
|
|
|
|
|
|
|
|
# 拼接所有批次结果,返回统一格式的坐标字符串
|
|
|
|
|
return ";".join(convert_result)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@login_required
|
|
|
|
|
@login_required # 登录验证:仅登录用户可访问
|
|
|
|
|
def get_datas(request):
|
|
|
|
|
"""
|
|
|
|
|
轨迹数据查询接口视图:
|
|
|
|
|
功能:按日期筛选轨迹数据,按用户标识(tid)分组,返回 JSON 格式的轨迹路径(经纬度列表)
|
|
|
|
|
请求方式:GET
|
|
|
|
|
请求参数:?date=YYYY-MM-DD(可选,默认当前日期)
|
|
|
|
|
响应:JSON 数组(格式:[{"name": "tid1", "path": [[lon1,lat1], [lon2,lat2], ...]}, ...])
|
|
|
|
|
"""
|
|
|
|
|
# 获取当前 UTC 时间(带时区信息,确保与数据库时间字段时区一致)
|
|
|
|
|
now = django.utils.timezone.now().replace(tzinfo=timezone.utc)
|
|
|
|
|
# 构造默认查询日期:当前日期的 00:00:00(UTC 时间)
|
|
|
|
|
querydate = django.utils.timezone.datetime(
|
|
|
|
|
now.year, now.month, now.day, 0, 0, 0)
|
|
|
|
|
now.year, now.month, now.day, 0, 0, 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# 若请求携带 date 参数,解析为指定日期的 00:00:00
|
|
|
|
|
if request.GET.get('date', None):
|
|
|
|
|
# 拆分日期字符串(YYYY-MM-DD → [年, 月, 日])并转为整数
|
|
|
|
|
date = list(map(lambda x: int(x), request.GET.get('date').split('-')))
|
|
|
|
|
querydate = django.utils.timezone.datetime(
|
|
|
|
|
date[0], date[1], date[2], 0, 0, 0)
|
|
|
|
|
date[0], date[1], date[2], 0, 0, 0
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# 构造查询结束日期:查询日期的次日 00:00:00(即筛选 [querydate, nextdate) 区间的数据)
|
|
|
|
|
nextdate = querydate + datetime.timedelta(days=1)
|
|
|
|
|
|
|
|
|
|
# 数据库查询:筛选指定日期区间内的所有轨迹记录
|
|
|
|
|
models = OwnTrackLog.objects.filter(
|
|
|
|
|
creation_time__range=(querydate, nextdate))
|
|
|
|
|
result = list()
|
|
|
|
|
creation_time__range=(querydate, nextdate)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result = list() # 存储最终返回的 JSON 数据
|
|
|
|
|
|
|
|
|
|
# 若查询到数据,按 tid 分组并构造轨迹路径
|
|
|
|
|
if models and len(models):
|
|
|
|
|
# 1. sorted:按 tid 排序(确保相同 tid 的记录连续,为 groupby 分组做准备)
|
|
|
|
|
# 2. groupby:按 tid 分组,key 为分组依据(tid)
|
|
|
|
|
for tid, item in groupby(
|
|
|
|
|
sorted(models, key=lambda k: k.tid), key=lambda k: k.tid):
|
|
|
|
|
|
|
|
|
|
# 构造单个用户的轨迹数据字典
|
|
|
|
|
d = dict()
|
|
|
|
|
d["name"] = tid
|
|
|
|
|
paths = list()
|
|
|
|
|
# 使用高德转换后的经纬度
|
|
|
|
|
d["name"] = tid # 用户标识(用于前端区分不同用户的轨迹)
|
|
|
|
|
paths = list() # 存储该用户的经纬度路径列表
|
|
|
|
|
|
|
|
|
|
# 【可选】使用高德转换后的经纬度(当前注释未启用,默认使用 GPS 原始坐标)
|
|
|
|
|
# locations = convert_to_amap(
|
|
|
|
|
# sorted(item, key=lambda x: x.creation_time))
|
|
|
|
|
# sorted(item, key=lambda x: x.creation_time) # 按创建时间排序,确保轨迹顺序正确
|
|
|
|
|
# )
|
|
|
|
|
# for i in locations.split(';'):
|
|
|
|
|
# paths.append(i.split(','))
|
|
|
|
|
# 使用GPS原始经纬度
|
|
|
|
|
# paths.append(i.split(',')) # 拆分坐标为 [lon, lat] 列表
|
|
|
|
|
|
|
|
|
|
# 使用 GPS 原始经纬度(默认启用)
|
|
|
|
|
# 按创建时间排序:确保轨迹点按时间顺序排列(避免路径错乱)
|
|
|
|
|
for location in sorted(item, key=lambda x: x.creation_time):
|
|
|
|
|
# 转为字符串格式(避免 JSON 序列化时的精度问题),添加到路径列表
|
|
|
|
|
paths.append([str(location.lon), str(location.lat)])
|
|
|
|
|
d["path"] = paths
|
|
|
|
|
result.append(d)
|
|
|
|
|
|
|
|
|
|
d["path"] = paths # 关联路径列表到用户字典
|
|
|
|
|
result.append(d) # 添加到最终结果列表
|
|
|
|
|
|
|
|
|
|
# 返回 JSON 响应:safe=False 允许返回非字典类型(此处为列表)
|
|
|
|
|
return JsonResponse(result, safe=False)
|
|
|
|
|
|