From bfc2f8ca148ec9744c3998ec468f4704e333030d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=85=A7?= <1096877868@qq.com> Date: Sun, 26 Oct 2025 14:17:45 +0800 Subject: [PATCH] =?UTF-8?q?zh=E7=AC=AC=E5=85=AD=E5=91=A8=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/.idea/.gitignore | 5 + src/.idea/.name | 1 + .../inspectionProfiles/profiles_settings.xml | 6 + src/.idea/misc.xml | 7 + src/.idea/modules.xml | 8 + src/.idea/src.iml | 8 + src/.idea/vcs.xml | 6 + src/owntracks/__init__.py | 0 src/owntracks/admin.py | 11 ++ src/owntracks/apps.py | 10 ++ src/owntracks/models.py | 47 +++++ src/owntracks/tests.py | 105 +++++++++++ src/owntracks/urls.py | 28 +++ src/owntracks/views.py | 170 ++++++++++++++++++ 14 files changed, 412 insertions(+) create mode 100644 src/.idea/.gitignore create mode 100644 src/.idea/.name create mode 100644 src/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 src/.idea/misc.xml create mode 100644 src/.idea/modules.xml create mode 100644 src/.idea/src.iml create mode 100644 src/.idea/vcs.xml create mode 100644 src/owntracks/__init__.py create mode 100644 src/owntracks/admin.py create mode 100644 src/owntracks/apps.py create mode 100644 src/owntracks/models.py create mode 100644 src/owntracks/tests.py create mode 100644 src/owntracks/urls.py create mode 100644 src/owntracks/views.py diff --git a/src/.idea/.gitignore b/src/.idea/.gitignore new file mode 100644 index 0000000..10b731c --- /dev/null +++ b/src/.idea/.gitignore @@ -0,0 +1,5 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/src/.idea/.name b/src/.idea/.name new file mode 100644 index 0000000..c8d4ea1 --- /dev/null +++ b/src/.idea/.name @@ -0,0 +1 @@ +views.py \ No newline at end of file diff --git a/src/.idea/inspectionProfiles/profiles_settings.xml b/src/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/src/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/.idea/misc.xml b/src/.idea/misc.xml new file mode 100644 index 0000000..db8786c --- /dev/null +++ b/src/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/src/.idea/modules.xml b/src/.idea/modules.xml new file mode 100644 index 0000000..f669a0e --- /dev/null +++ b/src/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/.idea/src.iml b/src/.idea/src.iml new file mode 100644 index 0000000..f571432 --- /dev/null +++ b/src/.idea/src.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/.idea/vcs.xml b/src/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/src/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/owntracks/__init__.py b/src/owntracks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/owntracks/admin.py b/src/owntracks/admin.py new file mode 100644 index 0000000..31c1dba --- /dev/null +++ b/src/owntracks/admin.py @@ -0,0 +1,11 @@ +# 导入Django的admin模块,用于在管理后台注册和管理数据模型 +from django.contrib import admin + +# Register your models here. # Django自动生成的注释,提示在此处注册需要在管理后台显示的模型 + +# 定义一个管理配置类OwnTrackLogsAdmin,继承自admin.ModelAdmin +# 这个类用于配置OwnTrackLog模型在Django管理后台的显示和操作方式 +class OwnTrackLogsAdmin(admin.ModelAdmin): + # pass表示暂时不添加任何自定义配置,使用默认的管理后台设置 + # 后续可以在这里添加各种属性(如list_display、search_fields等)来自定义管理界面 + pass \ No newline at end of file diff --git a/src/owntracks/apps.py b/src/owntracks/apps.py new file mode 100644 index 0000000..7114d87 --- /dev/null +++ b/src/owntracks/apps.py @@ -0,0 +1,10 @@ +# 导入Django的AppConfig类,用于配置应用的元数据和初始化行为 +from django.apps import AppConfig + + +# 定义一个应用配置类OwntracksConfig,继承自AppConfig +# 该类用于配置名为'owntracks'的Django应用 +class OwntracksConfig(AppConfig): + # name属性指定了应用的名称,必须与应用的目录名一致 + # 这个名称会被Django用于识别和管理该应用 + name = 'owntracks' diff --git a/src/owntracks/models.py b/src/owntracks/models.py new file mode 100644 index 0000000..3326c0d --- /dev/null +++ b/src/owntracks/models.py @@ -0,0 +1,47 @@ +# 导入Django的models模块,用于定义数据模型 +from django.db import models +# 导入Django的时区工具,用于处理时间相关操作 +from django.utils.timezone import now + + +# Create your models here. # Django自动生成的注释,提示在此处创建数据模型 + +# 定义一个名为OwnTrackLog的数据模型类,继承自models.Model +# 这个模型用于存储OwnTracks(一个位置追踪应用)的位置日志信息 +class OwnTrackLog(models.Model): + # 定义tid字段,CharField表示字符串类型 + # max_length=100限制最大长度为100字符 + # null=False表示该字段不允许为空 + # verbose_name='用户'用于在admin后台显示的字段名称 + tid = models.CharField(max_length=100, null=False, verbose_name='用户') + + # 定义lat字段,FloatField表示浮点型,用于存储纬度信息 + # verbose_name='纬度'用于在admin后台显示的字段名称 + lat = models.FloatField(verbose_name='纬度') + + # 定义lon字段,FloatField表示浮点型,用于存储经度信息 + # verbose_name='经度'用于在admin后台显示的字段名称 + lon = models.FloatField(verbose_name='经度') + + # 定义creation_time字段,DateTimeField表示日期时间类型 + # '创建时间'是字段的位置参数,等同于verbose_name='创建时间' + # default=now设置默认值为当前时间(使用Django的时区设置) + creation_time = models.DateTimeField('创建时间', default=now) + + # 定义对象的字符串表示方法 + # 当打印该模型的实例时,会返回tid字段的值 + def __str__(self): + return self.tid + + # Meta类用于定义模型的元数据 + class Meta: + # ordering=['creation_time']指定查询该模型数据时的默认排序方式 + # 按creation_time字段升序排列(加负号表示降序,如['-creation_time']) + ordering = ['creation_time'] + # verbose_name定义模型在admin后台的单数显示名称 + verbose_name = "OwnTrackLogs" + # verbose_name_plural定义模型在admin后台的复数显示名称 + verbose_name_plural = verbose_name + # get_latest_by='creation_time'指定使用creation_time字段来获取最新记录 + # 可以通过模型管理器的latest()方法获取最新记录 + get_latest_by = 'creation_time' \ No newline at end of file diff --git a/src/owntracks/tests.py b/src/owntracks/tests.py new file mode 100644 index 0000000..2c63b28 --- /dev/null +++ b/src/owntracks/tests.py @@ -0,0 +1,105 @@ +# 导入json模块,用于处理JSON数据格式 +import json + +# 从Django测试框架导入必要的测试工具 +# Client用于模拟用户在视图上的请求 +# RequestFactory用于创建请求对象 +# TestCase是Django测试的基础类 +from django.test import Client, RequestFactory, TestCase + +# 导入账户模型BlogUser,用于测试用户相关功能 +from accounts.models import BlogUser +# 从当前应用导入要测试的模型OwnTrackLog +from .models import OwnTrackLog + + +# Create your tests here. # Django自动生成的注释,提示在此处编写测试代码 + +# 定义测试类OwnTrackLogTest,继承自TestCase +# 该类包含对OwnTrackLog模型及相关视图的测试用例 +class OwnTrackLogTest(TestCase): + # setUp方法在每个测试方法执行前运行,用于初始化测试环境 + def setUp(self): + # 创建一个测试客户端,用于模拟用户请求 + self.client = Client() + # 创建一个请求工厂,用于构建复杂的请求对象 + self.factory = RequestFactory() + + # 定义具体的测试方法,方法名以test_开头 + def test_own_track_log(self): + # 定义一个符合要求的测试数据字典,包含tid、lat、lon字段 + o = { + 'tid': 12, + 'lat': 123.123, + 'lon': 134.341 + } + + # 使用测试客户端发送POST请求到指定URL + # 发送JSON格式的数据,指定content_type为application/json + self.client.post( + '/owntracks/logtracks', # 请求的URL + json.dumps(o), # 将字典转换为JSON字符串 + content_type='application/json') # 指定内容类型 + + # 检查数据库中OwnTrackLog记录的数量 + length = len(OwnTrackLog.objects.all()) + # 断言记录数为1,验证第一条数据成功保存 + self.assertEqual(length, 1) + + # 定义一个不完整的测试数据字典,缺少lon字段 + o = { + 'tid': 12, + 'lat': 123.123 + } + + # 再次发送POST请求,使用不完整的数据 + self.client.post( + '/owntracks/logtracks', + json.dumps(o), + content_type='application/json') + + # 再次检查数据库中记录的数量 + length = len(OwnTrackLog.objects.all()) + # 断言记录数仍为1,验证不完整数据没有被保存 + self.assertEqual(length, 1) + + # 测试未登录状态下访问/show_maps页面 + rsp = self.client.get('/owntracks/show_maps') + # 断言返回状态码为302(重定向),验证未登录用户被重定向 + self.assertEqual(rsp.status_code, 302) + + # 创建一个超级用户,用于测试登录状态下的功能 + user = BlogUser.objects.create_superuser( + email="liangliangyy1@gmail.com", + username="liangliangyy1", + password="liangliangyy1") + + # 使用测试客户端登录刚刚创建的用户 + self.client.login(username='liangliangyy1', password='liangliangyy1') + + # 手动创建并保存一条OwnTrackLog记录 + s = OwnTrackLog() + s.tid = 12 + s.lon = 123.234 + s.lat = 34.234 + s.save() + + # 测试登录状态下访问/show_dates页面 + rsp = self.client.get('/owntracks/show_dates') + # 断言返回状态码为200(成功) + self.assertEqual(rsp.status_code, 200) + + # 测试登录状态下访问/show_maps页面 + rsp = self.client.get('/owntracks/show_maps') + # 断言返回状态码为200(成功) + self.assertEqual(rsp.status_code, 200) + + # 测试登录状态下访问/get_datas页面 + rsp = self.client.get('/owntracks/get_datas') + # 断言返回状态码为200(成功) + self.assertEqual(rsp.status_code, 200) + + # 测试登录状态下带日期参数访问/get_datas页面 + rsp = self.client.get('/owntracks/get_datas?date=2018-02-26') + # 断言返回状态码为200(成功) + self.assertEqual(rsp.status_code, 200) \ No newline at end of file diff --git a/src/owntracks/urls.py b/src/owntracks/urls.py new file mode 100644 index 0000000..ed19e92 --- /dev/null +++ b/src/owntracks/urls.py @@ -0,0 +1,28 @@ +# 导入Django的path函数,用于定义URL路径 +from django.urls import path + +# 从当前应用导入views模块,包含视图函数 +from . import views + +# 定义应用的命名空间为"owntracks" +# 用于在模板中引用URL时避免命名冲突,格式为"app_name:url_name" +app_name = "owntracks" + +# 定义URL模式列表,每个path对应一个URL路径与视图函数的映射 +urlpatterns = [ + # 定义路径'owntracks/logtracks',映射到views.manage_owntrack_log视图函数 + # name='logtracks'为该URL指定名称,用于在模板和代码中引用 + path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'), + + # 定义路径'owntracks/show_maps',映射到views.show_maps视图函数 + # name='show_maps'为该URL指定名称 + path('owntracks/show_maps', views.show_maps, name='show_maps'), + + # 定义路径'owntracks/get_datas',映射到views.get_datas视图函数 + # name='get_datas'为该URL指定名称 + path('owntracks/get_datas', views.get_datas, name='get_datas'), + + # 定义路径'owntracks/show_dates',映射到views.show_log_dates视图函数 + # name='show_dates'为该URL指定名称 + path('owntracks/show_dates', views.show_log_dates, name='show_dates') +] \ No newline at end of file diff --git a/src/owntracks/views.py b/src/owntracks/views.py new file mode 100644 index 0000000..1db80e7 --- /dev/null +++ b/src/owntracks/views.py @@ -0,0 +1,170 @@ +# Create your views here. +# 导入必要的模块 +import datetime # 处理日期时间 +import itertools # 提供迭代器相关功能 +import json # 处理JSON数据 +import logging # 日志记录 +from datetime import timezone # 时区处理 +from itertools import groupby # 用于对序列进行分组 + +import django # Django框架核心 +import requests # 发送HTTP请求 +from django.contrib.auth.decorators import login_required # 登录验证装饰器 +from django.http import HttpResponse # HTTP响应 +from django.http import JsonResponse # JSON格式响应 +from django.shortcuts import render # 渲染模板 +from django.views.decorators.csrf import csrf_exempt # 禁用CSRF验证装饰器 + +from .models import OwnTrackLog # 导入当前应用的模型 + +# 配置日志记录器 +logger = logging.getLogger(__name__) + + +# 禁用CSRF验证的视图函数,用于处理OwnTracks的日志记录 +@csrf_exempt +def manage_owntrack_log(request): + try: + # 从请求中读取并解析JSON数据 + 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)) + # 验证字段是否存在 + if tid and lat and lon: + # 创建并保存新的日志记录 + m = OwnTrackLog() + m.tid = tid + m.lat = lat + m.lon = lon + m.save() + return HttpResponse('ok') # 返回成功响应 + else: + return HttpResponse('data error') # 数据不完整错误响应 + except Exception as e: + # 记录异常信息 + logger.error(e) + return HttpResponse('error') # 异常错误响应 + + +# 需要登录才能访问的视图,用于显示地图 +@login_required +def show_maps(request): + # 仅允许超级用户访问 + if request.user.is_superuser: + # 获取当前UTC日期作为默认日期 + defaultdate = str(datetime.datetime.now(timezone.utc).date()) + # 从请求参数中获取日期,默认为当前日期 + date = request.GET.get('date', defaultdate) + # 准备上下文数据 + context = { + 'date': date + } + # 渲染模板并返回 + return render(request, 'owntracks/show_maps.html', context) + else: + # 非超级用户返回403禁止访问 + from django.http import HttpResponseForbidden + return HttpResponseForbidden() + + +# 需要登录才能访问的视图,用于显示日志日期列表 +@login_required +def show_log_dates(request): + # 获取所有记录的创建时间 + dates = OwnTrackLog.objects.values_list('creation_time', flat=True) + # 提取日期部分并去重排序 + 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) + + +# 将GPS坐标转换为高德地图坐标的函数 +def convert_to_amap(locations): + convert_result = [] + # 创建迭代器 + it = iter(locations) + + # 每次处理30个坐标(高德API限制) + item = list(itertools.islice(it, 30)) + while item: + # 格式化坐标为"经度,纬度;经度,纬度"形式 + datas = ';'.join( + set(map(lambda x: str(x.lon) + ',' + str(x.lat), item))) + + # 高德API密钥和接口地址 + key = '8440a376dfc9743d8924bf0ad141f28e' + api = 'http://restapi.amap.com/v3/assistant/coordinate/convert' + # 请求参数 + query = { + 'key': key, + 'locations': datas, + 'coordsys': 'gps' # 源坐标系统为GPS + } + # 发送转换请求 + rsp = requests.get(url=api, params=query) + result = json.loads(rsp.text) + # 处理转换结果 + if "locations" in result: + convert_result.append(result['locations']) + # 处理下一批坐标 + item = list(itertools.islice(it, 30)) + + # 合并所有转换结果并返回 + return ";".join(convert_result) + + +# 需要登录才能访问的视图,用于获取轨迹数据 +@login_required +def get_datas(request): + # 获取当前UTC时间 + now = django.utils.timezone.now().replace(tzinfo=timezone.utc) + # 默认查询日期为今天 + querydate = django.utils.timezone.datetime( + now.year, now.month, now.day, 0, 0, 0) + # 如果请求中指定了日期,则使用指定日期 + if request.GET.get('date', None): + 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) + # 计算查询日期的下一天(用于范围查询) + nextdate = querydate + datetime.timedelta(days=1) + # 查询指定日期范围内的所有记录 + models = OwnTrackLog.objects.filter( + creation_time__range=(querydate, nextdate)) + result = list() + # 如果有查询结果 + if models and len(models): + # 按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() + # 注释掉的代码:使用高德转换后的经纬度 + # locations = convert_to_amap( + # sorted(item, key=lambda x: x.creation_time)) + # for i in locations.split(';'): + # paths.append(i.split(',')) + + # 使用GPS原始经纬度 + # 按创建时间排序并添加到路径列表 + for location in sorted(item, key=lambda x: x.creation_time): + paths.append([str(location.lon), str(location.lat)]) + d["path"] = paths + result.append(d) + # 返回JSON格式的结果 + return JsonResponse(result, safe=False) \ No newline at end of file