diff --git a/src/DjangoBlog-master/owntracks/views.py b/src/DjangoBlog-master/owntracks/views.py index 4c72bdd..11289cd 100644 --- a/src/DjangoBlog-master/owntracks/views.py +++ b/src/DjangoBlog-master/owntracks/views.py @@ -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)