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