From da2e3c13247c36d5cd2b6eccc70e6f52d25455ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A4=E5=BA=94=E9=9B=AA?= <2439192341@qq.com> Date: Fri, 7 Nov 2025 20:52:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0owntracks-tyx=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9=EF=BC=9A=E5=8C=85=E5=90=AB=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- owntracks-tyx/__init__.py | 7 + owntracks-tyx/admin.py | 26 +++ owntracks-tyx/apps.py | 22 ++ owntracks-tyx/migrations/0001_initial.py | 38 ++++ ...0002_alter_owntracklog_options_and_more.py | 29 +++ owntracks-tyx/migrations/__init__.py | 7 + owntracks-tyx/models.py | 43 ++++ owntracks-tyx/tests.py | 101 +++++++++ owntracks-tyx/urls.py | 31 +++ owntracks-tyx/views.py | 209 ++++++++++++++++++ 10 files changed, 513 insertions(+) create mode 100644 owntracks-tyx/__init__.py create mode 100644 owntracks-tyx/admin.py create mode 100644 owntracks-tyx/apps.py create mode 100644 owntracks-tyx/migrations/0001_initial.py create mode 100644 owntracks-tyx/migrations/0002_alter_owntracklog_options_and_more.py create mode 100644 owntracks-tyx/migrations/__init__.py create mode 100644 owntracks-tyx/models.py create mode 100644 owntracks-tyx/tests.py create mode 100644 owntracks-tyx/urls.py create mode 100644 owntracks-tyx/views.py diff --git a/owntracks-tyx/__init__.py b/owntracks-tyx/__init__.py new file mode 100644 index 0000000..4ac65ce --- /dev/null +++ b/owntracks-tyx/__init__.py @@ -0,0 +1,7 @@ +""" +owntracks 应用的数据库迁移包 +此文件标识 migrations 目录为 Python 包,便于 Django 迁移系统识别和管理。 + +注意:除非有特殊需求,否则不要修改此文件内容。 +Django 会自动管理迁移文件的生成和执行。 +""" \ No newline at end of file diff --git a/owntracks-tyx/admin.py b/owntracks-tyx/admin.py new file mode 100644 index 0000000..978592a --- /dev/null +++ b/owntracks-tyx/admin.py @@ -0,0 +1,26 @@ +#tyx +""" +owntracks 应用的管理后台配置 +注册模型到Django管理界面 +""" + +from django.contrib import admin + +# 导入模型(当前未实际导入,需要添加) +# from .models import OwnTrackLog + +class OwnTrackLogsAdmin(admin.ModelAdmin): + """ + OwnTrackLog 模型的管理界面配置类 + 可以在此自定义管理界面的显示和行为 + + 当前使用默认配置,可以添加以下自定义: + - list_display: 列表页显示的字段 + - list_filter: 筛选器字段 + - search_fields: 搜索字段 + - ordering: 排序字段 + """ + pass + +# 注册模型到管理后台(当前未注册,需要取消注释) +# admin.site.register(OwnTrackLog, OwnTrackLogsAdmin) \ No newline at end of file diff --git a/owntracks-tyx/apps.py b/owntracks-tyx/apps.py new file mode 100644 index 0000000..982f691 --- /dev/null +++ b/owntracks-tyx/apps.py @@ -0,0 +1,22 @@ +#tyx +""" +owntracks 应用的配置类 +用于配置应用的特定设置 +""" + +from django.apps import AppConfig + + +class OwntracksConfig(AppConfig): + """ + owntracks 应用的配置类 + 继承自Django的AppConfig,用于应用级别的配置 + """ + + # 应用的名称,Django使用这个名称来识别应用 + name = 'owntracks' + + # 可以在此添加其他应用配置,例如: + # verbose_name = "位置追踪" # 在admin中显示的中文名称 + # default_auto_field = 'django.db.models.BigAutoField' # 默认主键类型 + diff --git a/owntracks-tyx/migrations/0001_initial.py b/owntracks-tyx/migrations/0001_initial.py new file mode 100644 index 0000000..40264cc --- /dev/null +++ b/owntracks-tyx/migrations/0001_initial.py @@ -0,0 +1,38 @@ +#tyx +# Generated by Django 4.1.7 on 2023-03-02 07:14 +""" +数据库迁移文件 +由Django自动生成,用于创建和修改数据库表结构 + +注意:迁移文件通常不需要手动修改或添加注释 +因为它们是由Django根据models.py的变化自动生成的 +""" +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='OwnTrackLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tid', models.CharField(max_length=100, verbose_name='用户')), + ('lat', models.FloatField(verbose_name='纬度')), + ('lon', models.FloatField(verbose_name='经度')), + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), + ], + options={ + 'verbose_name': 'OwnTrackLogs', + 'verbose_name_plural': 'OwnTrackLogs', + 'ordering': ['created_time'], + 'get_latest_by': 'created_time', + }, + ), + ] diff --git a/owntracks-tyx/migrations/0002_alter_owntracklog_options_and_more.py b/owntracks-tyx/migrations/0002_alter_owntracklog_options_and_more.py new file mode 100644 index 0000000..24bc313 --- /dev/null +++ b/owntracks-tyx/migrations/0002_alter_owntracklog_options_and_more.py @@ -0,0 +1,29 @@ +#tyx +# Generated by Django 4.2.5 on 2023-09-06 13:19 +""" +数据库迁移文件 +由Django自动生成,用于创建和修改数据库表结构 + +注意:迁移文件通常不需要手动修改或添加注释 +因为它们是由Django根据models.py的变化自动生成的 +""" +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('owntracks', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='owntracklog', + options={'get_latest_by': 'creation_time', 'ordering': ['creation_time'], 'verbose_name': 'OwnTrackLogs', 'verbose_name_plural': 'OwnTrackLogs'}, + ), + migrations.RenameField( + model_name='owntracklog', + old_name='created_time', + new_name='creation_time', + ), + ] diff --git a/owntracks-tyx/migrations/__init__.py b/owntracks-tyx/migrations/__init__.py new file mode 100644 index 0000000..4ac65ce --- /dev/null +++ b/owntracks-tyx/migrations/__init__.py @@ -0,0 +1,7 @@ +""" +owntracks 应用的数据库迁移包 +此文件标识 migrations 目录为 Python 包,便于 Django 迁移系统识别和管理。 + +注意:除非有特殊需求,否则不要修改此文件内容。 +Django 会自动管理迁移文件的生成和执行。 +""" \ No newline at end of file diff --git a/owntracks-tyx/models.py b/owntracks-tyx/models.py new file mode 100644 index 0000000..96f3a53 --- /dev/null +++ b/owntracks-tyx/models.py @@ -0,0 +1,43 @@ +#tyx +""" +owntracks 应用的数据模型定义 +用于存储和管理 OwnTracks 位置追踪数据 +""" + +from django.db import models +from django.utils.timezone import now + + +class OwnTrackLog(models.Model): + """ + OwnTracks 位置日志数据模型 + 存储用户的位置追踪信息,包括经纬度和时间戳 + """ + + # 用户标识符,对应 OwnTracks 中的 tid + tid = models.CharField(max_length=100, null=False, verbose_name='用户') + + # 纬度坐标,浮点数类型 + lat = models.FloatField(verbose_name='纬度') + + # 经度坐标,浮点数类型 + lon = models.FloatField(verbose_name='经度') + + # 记录创建时间,默认使用当前时间 + creation_time = models.DateTimeField('创建时间', default=now) + + def __str__(self): + """ + 字符串表示方法 + 返回用户ID用于在admin等界面显示 + """ + return self.tid + + class Meta: + """ + 模型元数据配置 + """ + ordering = ['creation_time'] # 默认按创建时间升序排序 + verbose_name = "OwnTrackLogs" # 在admin中显示的单数名称 + verbose_name_plural = verbose_name # 在admin中显示的复数名称 + get_latest_by = 'creation_time' # 获取最新记录时使用的字段 \ No newline at end of file diff --git a/owntracks-tyx/tests.py b/owntracks-tyx/tests.py new file mode 100644 index 0000000..5399217 --- /dev/null +++ b/owntracks-tyx/tests.py @@ -0,0 +1,101 @@ +#tyx +""" +owntracks 应用的测试用例 +验证模型和视图功能的正确性 +""" + +import json + +from django.test import Client, RequestFactory, TestCase + +from accounts.models import BlogUser +from .models import OwnTrackLog + + +class OwnTrackLogTest(TestCase): + """ + OwnTrackLog 模型和视图的测试类 + """ + + def setUp(self): + """ + 测试初始化方法 + 在每个测试方法执行前运行,准备测试环境 + """ + self.client = Client() # Django测试客户端 + self.factory = RequestFactory() # 请求工厂,用于创建请求对象 + + def test_own_track_log(self): + """ + 测试位置日志功能 + 包括数据接收、存储和权限验证 + """ + # 测试用例1:正常的位置数据提交 + o = { + 'tid': 12, # 用户ID + 'lat': 123.123, # 纬度 + 'lon': 134.341 # 经度 + } + + # 发送POST请求提交位置数据 + self.client.post( + '/owntracks/logtracks', + json.dumps(o), # 将字典转换为JSON字符串 + content_type='application/json') # 设置内容类型为JSON + + # 验证数据是否成功保存 + length = len(OwnTrackLog.objects.all()) + self.assertEqual(length, 1) # 应该有一条记录 + + # 测试用例2:缺少必要字段的数据提交 + o = { + 'tid': 12, + 'lat': 123.123 + # 缺少经度字段 + } + + # 发送不完整的数据 + self.client.post( + '/owntracks/logtracks', + json.dumps(o), + content_type='application/json') + + # 验证数据条数没有增加 + length = len(OwnTrackLog.objects.all()) + self.assertEqual(length, 1) # 应该还是只有一条记录 + + # 测试用例3:未登录用户访问地图页面 + rsp = self.client.get('/owntracks/show_maps') + 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') + + # 创建测试位置记录 + s = OwnTrackLog() + s.tid = 12 + s.lon = 123.234 + s.lat = 34.234 + s.save() + + # 测试用例4:登录用户访问日期列表页面 + rsp = self.client.get('/owntracks/show_dates') + self.assertEqual(rsp.status_code, 200) # 应该返回200成功 + + # 测试用例5:登录用户访问地图页面 + rsp = self.client.get('/owntracks/show_maps') + self.assertEqual(rsp.status_code, 200) # 应该返回200成功 + + # 测试用例6:获取位置数据(默认日期) + rsp = self.client.get('/owntracks/get_datas') + self.assertEqual(rsp.status_code, 200) # 应该返回200成功 + + # 测试用例7:获取指定日期的位置数据 + rsp = self.client.get('/owntracks/get_datas?date=2018-02-26') + self.assertEqual(rsp.status_code, 200) # 应该返回200成功 \ No newline at end of file diff --git a/owntracks-tyx/urls.py b/owntracks-tyx/urls.py new file mode 100644 index 0000000..eab4b9c --- /dev/null +++ b/owntracks-tyx/urls.py @@ -0,0 +1,31 @@ +#tyx +""" +owntracks 应用的URL配置 +定义应用的路由映射关系 +""" + +from django.urls import path + +from . import views + +# 应用命名空间,用于反向解析URL时避免冲突 +app_name = "owntracks" + +# URL模式列表 +urlpatterns = [ + # 接收位置数据接口 + # path: 定义URL路径和对应的视图函数 + # 'owntracks/logtracks': URL路径 + # views.manage_owntrack_log: 对应的视图函数 + # name='logtracks': URL名称,用于反向解析 + path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'), + + # 地图展示页面 + path('owntracks/show_maps', views.show_maps, name='show_maps'), + + # 位置数据API接口 + path('owntracks/get_datas', views.get_datas, name='get_datas'), + + # 日期列表页面 + path('owntracks/show_dates', views.show_log_dates, name='show_dates') +] \ No newline at end of file diff --git a/owntracks-tyx/views.py b/owntracks-tyx/views.py new file mode 100644 index 0000000..2419dba --- /dev/null +++ b/owntracks-tyx/views.py @@ -0,0 +1,209 @@ +#tyx +""" +owntracks 应用的视图函数 +处理位置数据的接收、展示和查询 +""" + +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 + +# 获取当前模块的日志器 +logger = logging.getLogger(__name__) + + +@csrf_exempt # 免除CSRF保护,便于外部应用调用 +def manage_owntrack_log(request): + """ + 接收和处理 OwnTracks 客户端发送的位置数据 + POST请求:接收JSON格式的位置数据并存入数据库 + """ + 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) + + # 处理日期数据:格式化为YYYY-MM-DD,去重并排序 + 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): + """ + 将GPS坐标转换为高德地图坐标系统 + 由于GPS坐标系与高德地图不同,需要进行坐标转换 + + Args: + locations: OwnTrackLog 对象列表,包含经纬度信息 + + Returns: + str: 转换后的坐标字符串,格式为"经度,纬度;经度,纬度;..." + """ + convert_result = [] + it = iter(locations) # 创建迭代器 + + # 每次处理30个坐标(高德API限制) + item = list(itertools.islice(it, 30)) + while item: + # 将坐标格式化为高德API要求的格式 + 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 + } + + # 调用高德坐标转换API + rsp = requests.get(url=api, params=query) + result = json.loads(rsp.text) + + # 检查API响应是否包含转换结果 + 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): + """ + 提供位置数据的JSON API接口 + 返回指定日期的用户轨迹数据,用于在地图上绘制路径 + + Returns: + JsonResponse: 包含用户轨迹数据的JSON响应 + """ + # 获取当前时间并设置为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): + # 按用户ID分组,每个用户的轨迹单独处理 + for tid, item in groupby( + sorted(models, key=lambda k: k.tid), key=lambda k: k.tid): + + d = dict() # 创建用户数据字典 + d["name"] = tid # 设置用户名 + + paths = list() # 初始化路径点列表 + + # 选项1:使用高德转换后的经纬度(当前被注释) + # locations = convert_to_amap( + # sorted(item, key=lambda x: x.creation_time)) + # for i in locations.split(';'): + # paths.append(i.split(',')) + + # 选项2:使用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) # safe=False允许非字典对象被序列化 \ No newline at end of file