From f48446ab2ab99db2a75923f3c16addb927dda3e3 Mon Sep 17 00:00:00 2001 From: nch Date: Sun, 2 Nov 2025 18:32:21 +0800 Subject: [PATCH 1/2] =?UTF-8?q?nch=E7=9A=84=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DjangoBlog-master/owntracks/models.py | 27 ++++- .../DjangoBlog-master/owntracks/tests.py | 55 ++++++--- .../DjangoBlog-master/owntracks/urls.py | 21 ++++ .../DjangoBlog-master/owntracks/views.py | 105 +++++++++++++++--- 4 files changed, 175 insertions(+), 33 deletions(-) diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/models.py b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/models.py index 760942c..f883bb5 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/models.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/models.py @@ -5,16 +5,41 @@ from django.utils.timezone import now # Create your models here. class OwnTrackLog(models.Model): + """ + OwnTracks位置数据记录模型 + 用于存储移动设备通过OwnTracks应用上报的地理位置信息 + """ + + # 设备标识符字段,必填,最大长度100字符 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): + """ + 定义模型的字符串表示形式 + 在Admin后台和其他显示场合使用设备ID作为标识 + """ return self.tid class Meta: + """模型元数据配置""" + + # 默认按创建时间升序排列 ordering = ['creation_time'] + + # 在Admin后台中显示的单数名称 verbose_name = "OwnTrackLogs" + + # 在Admin后台中显示的复数名称 verbose_name_plural = verbose_name - get_latest_by = 'creation_time' + + # 指定获取最新记录时使用的字段 + get_latest_by = 'creation_time' \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/tests.py b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/tests.py index 3b4b9d8..d1fceac 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/tests.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/tests.py @@ -9,56 +9,83 @@ from .models import OwnTrackLog # Create your tests here. class OwnTrackLogTest(TestCase): + """测试OwnTrackLog位置追踪功能""" + def setUp(self): - self.client = Client() - self.factory = RequestFactory() + """测试初始化设置""" + self.client = Client() # Django测试客户端 + self.factory = RequestFactory() # 请求工厂,用于创建请求对象 def test_own_track_log(self): + """测试位置数据记录功能""" + # 测试用例1:正常的位置数据提交 o = { - 'tid': 12, - 'lat': 123.123, - 'lon': 134.341 + 'tid': 12, # 设备ID + 'lat': 123.123, # 纬度 + 'lon': 134.341 # 经度 } + # 发送POST请求提交位置数据 self.client.post( '/owntracks/logtracks', - json.dumps(o), - content_type='application/json') + json.dumps(o), # 将数据转换为JSON格式 + content_type='application/json' # 设置内容类型为JSON + ) + + # 验证数据是否成功保存到数据库 length = len(OwnTrackLog.objects.all()) - self.assertEqual(length, 1) + self.assertEqual(length, 1) # 应该有一条记录 + # 测试用例2:缺少经度的不完整数据提交 o = { 'tid': 12, 'lat': 123.123 + # 缺少lon字段 } + # 发送不完整数据的POST请求 self.client.post( '/owntracks/logtracks', json.dumps(o), - content_type='application/json') + content_type='application/json' + ) + + # 验证不完整数据不应该被保存(记录数仍为1) length = len(OwnTrackLog.objects.all()) self.assertEqual(length, 1) + # 测试用例3:未登录用户访问地图页面 rsp = self.client.get('/owntracks/show_maps') - self.assertEqual(rsp.status_code, 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') + + # 创建一条位置记录用于后续测试 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) + self.assertEqual(rsp.status_code, 200) # 应该成功返回 + + # 测试用例5:登录用户访问地图页面 rsp = self.client.get('/owntracks/show_maps') - self.assertEqual(rsp.status_code, 200) + self.assertEqual(rsp.status_code, 200) # 应该成功返回 + + # 测试用例6:登录用户获取位置数据(默认日期) rsp = self.client.get('/owntracks/get_datas') - self.assertEqual(rsp.status_code, 200) + self.assertEqual(rsp.status_code, 200) # 应该成功返回 + + # 测试用例7:登录用户获取指定日期的位置数据 rsp = self.client.get('/owntracks/get_datas?date=2018-02-26') - self.assertEqual(rsp.status_code, 200) + self.assertEqual(rsp.status_code, 200) # 应该成功返回 diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/urls.py b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/urls.py index c19ada8..7852b43 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/urls.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/urls.py @@ -2,11 +2,32 @@ from django.urls import path from . import views +# 定义应用命名空间,用于URL反向解析时区分不同应用的同名URL app_name = "owntracks" +# URL模式配置列表 urlpatterns = [ + # 接收OwnTracks位置数据上报的端点 + # 路径:/owntracks/logtracks + # 视图:manage_owntrack_log - 处理位置数据存储 + # 名称:logtracks - 用于URL反向解析 path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'), + + # 显示轨迹地图页面的端点 + # 路径:/owntracks/show_maps + # 视图:show_maps - 渲染地图展示页面 + # 名称:show_maps - 用于URL反向解析 path('owntracks/show_maps', views.show_maps, name='show_maps'), + + # 获取位置数据API端点 + # 路径:/owntracks/get_datas + # 视图:get_datas - 返回JSON格式的位置轨迹数据 + # 名称:get_datas - 用于URL反向解析 path('owntracks/get_datas', views.get_datas, name='get_datas'), + + # 显示有记录的日期列表端点 + # 路径:/owntracks/show_dates + # 视图:show_log_dates - 显示所有有位置记录的日期 + # 名称:show_dates - 用于URL反向解析 path('owntracks/show_dates', views.show_log_dates, name='show_dates') ] diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/views.py b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/views.py index 4c72bdd..bf29efa 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/views.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/owntracks/views.py @@ -16,112 +16,181 @@ from django.views.decorators.csrf import csrf_exempt from .models import OwnTrackLog +# 获取logger实例,用于记录日志 logger = logging.getLogger(__name__) +# 处理OwnTracks位置数据的POST请求,不需要CSRF验证 @csrf_exempt def manage_owntrack_log(request): + """ + 接收并存储OwnTracks移动端发送的位置数据 + 支持格式:JSON格式的位置信息,包含tid(设备ID)、lat(纬度)、lon(经度) + """ try: + # 解析请求中的JSON数据 s = json.loads(request.read().decode('utf-8')) - tid = s['tid'] - lat = s['lat'] - lon = s['lon'] + 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') + m.save() # 保存到数据库 + return HttpResponse('ok') # 返回成功响应 else: - return HttpResponse('data error') + return HttpResponse('data error') # 数据不完整错误 except Exception as e: + # 记录异常信息 logger.error(e) - return HttpResponse('error') + return HttpResponse('error') # 返回错误响应 +# 显示地图页面,需要用户登录 @login_required def show_maps(request): + """ + 显示位置轨迹地图页面 + 仅超级用户可以访问,支持按日期筛选显示轨迹 + """ if request.user.is_superuser: + # 设置默认日期为当前UTC日期 defaultdate = str(datetime.datetime.now(timezone.utc).date()) + # 从GET参数获取日期,如未提供则使用默认日期 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 + 'results': results # 日期列表 } + # 渲染日期列表页面 return render(request, 'owntracks/show_log_dates.html', context) def convert_to_amap(locations): + """ + 将GPS坐标批量转换为高德地图坐标 + 由于高德API限制,每次最多转换30个坐标点 + + 参数: + locations: OwnTrackLog对象的查询集 + + 返回: + 转换后的坐标字符串,格式为"经度,纬度;经度,纬度;..." + """ convert_result = [] + # 创建迭代器用于分批处理 it = iter(locations) + # 每次处理30个坐标点 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' + 'locations': datas, # 要转换的坐标 + 'coordsys': 'gps' # 源坐标系为GPS } + # 调用高德坐标转换API 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)) + convert_result.append(result['locations']) # 添加转换结果 + item = list(itertools.islice(it, 30)) # 获取下一批坐标 + # 合并所有批次的转换结果 return ";".join(convert_result) +# 获取特定日期的位置数据,需要用户登录 @login_required def get_datas(request): + """ + 根据日期参数获取该日期所有设备的位置轨迹数据 + 返回JSON格式,包含每个设备的轨迹路径 + + 支持两种坐标格式: + 1. 原始GPS坐标(当前使用) + 2. 高德转换坐标(注释状态) + """ + # 设置默认查询时间为当前日期的开始时间(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) + + # 查询日期范围:从查询日期的00:00到次日00:00 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 + d["name"] = tid # 设备ID作为轨迹名称 + paths = list() - # 使用高德转换后的经纬度 + + # 使用高德转换后的经纬度(当前注释掉,使用原始GPS坐标) # locations = convert_to_amap( # sorted(item, key=lambda x: x.creation_time)) # for i in locations.split(';'): # paths.append(i.split(',')) - # 使用GPS原始经纬度 + + # 使用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) - return JsonResponse(result, safe=False) + + d["path"] = paths # 设备的轨迹路径 + result.append(d) # 添加到结果列表 + + # 返回JSON格式的轨迹数据 + return JsonResponse(result, safe=False) \ No newline at end of file From 7004b167364e9c09aae00bb86eaa2ad4418c6d15 Mon Sep 17 00:00:00 2001 From: nch Date: Sat, 8 Nov 2025 12:50:42 +0800 Subject: [PATCH 2/2] =?UTF-8?q?nch=E7=9A=84=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DjangoBlog-master/accounts/forms.py | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/forms.py b/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/forms.py index fce4137..04c3e00 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/forms.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/accounts/forms.py @@ -8,40 +8,73 @@ from . import utils from .models import BlogUser +# 登录表单,继承自Django的AuthenticationForm class LoginForm(AuthenticationForm): def __init__(self, *args, **kwargs): + # 调用父类的初始化方法 super(LoginForm, self).__init__(*args, **kwargs) + # 为用户名字段添加占位符和CSS类 self.fields['username'].widget = widgets.TextInput( attrs={'placeholder': "username", "class": "form-control"}) + # 为密码字段添加占位符和CSS类 self.fields['password'].widget = widgets.PasswordInput( attrs={'placeholder': "password", "class": "form-control"}) +# 注册表单,继承自Django的UserCreationForm class RegisterForm(UserCreationForm): def __init__(self, *args, **kwargs): + # 调用父类的初始化方法 super(RegisterForm, self).__init__(*args, **kwargs) + # 为各个表单字段添加占位符和CSS类 + # 为用户名字段自定义widget self.fields['username'].widget = widgets.TextInput( - attrs={'placeholder': "username", "class": "form-control"}) + attrs={ + 'placeholder': "username", # 输入框内的提示文本,用户点击时会消失 + "class": "form-control" # CSS类名,用于应用Bootstrap等UI框架的样式 + }) + + # 为邮箱字段自定义widget self.fields['email'].widget = widgets.EmailInput( - attrs={'placeholder': "email", "class": "form-control"}) + attrs={ + 'placeholder': "email", # 提示用户输入邮箱地址 + "class": "form-control" # 统一的表单控件样式类 + }) + + # 为密码字段自定义widget(密码输入框1) self.fields['password1'].widget = widgets.PasswordInput( - attrs={'placeholder': "password", "class": "form-control"}) + attrs={ + 'placeholder': "password", # 提示用户输入密码 + "class": "form-control" # 密码输入框会显示为圆点或星号,隐藏实际内容 + }) + + # 为确认密码字段自定义widget(密码输入框2) self.fields['password2'].widget = widgets.PasswordInput( - attrs={'placeholder': "repeat password", "class": "form-control"}) + attrs={ + 'placeholder': "repeat password", # 提示用户再次输入密码进行确认 + "class": "form-control" # 同样使用密码输入框类型 + }) + # 邮箱验证方法 def clean_email(self): email = self.cleaned_data['email'] + # 检查邮箱是否已存在 if get_user_model().objects.filter(email=email).exists(): raise ValidationError(_("email already exists")) return email + # 元类配置 class Meta: + # 指定使用的用户模型 model = get_user_model() + # 表单包含的字段 fields = ("username", "email") +# 忘记密码表单 class ForgetPasswordForm(forms.Form): + # 新密码字段 new_password1 = forms.CharField( label=_("New password"), widget=forms.PasswordInput( @@ -52,6 +85,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # 确认密码字段 new_password2 = forms.CharField( label="确认密码", widget=forms.PasswordInput( @@ -62,6 +96,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # 邮箱字段 email = forms.EmailField( label='邮箱', widget=forms.TextInput( @@ -72,6 +107,7 @@ class ForgetPasswordForm(forms.Form): ), ) + # 验证码字段 code = forms.CharField( label=_('Code'), widget=forms.TextInput( @@ -82,17 +118,21 @@ class ForgetPasswordForm(forms.Form): ), ) + # 确认密码验证方法 def clean_new_password2(self): password1 = self.data.get("new_password1") password2 = self.data.get("new_password2") + # 检查两次输入的密码是否一致 if password1 and password2 and password1 != password2: raise ValidationError(_("passwords do not match")) + # 验证密码强度 password_validation.validate_password(password2) - return password2 + # 邮箱验证方法 def clean_email(self): user_email = self.cleaned_data.get("email") + # 检查邮箱是否存在 if not BlogUser.objects.filter( email=user_email ).exists(): @@ -100,8 +140,10 @@ class ForgetPasswordForm(forms.Form): raise ValidationError(_("email does not exist")) return user_email + # 验证码验证方法 def clean_code(self): code = self.cleaned_data.get("code") + # 验证邮箱和验证码是否匹配 error = utils.verify( email=self.cleaned_data.get("email"), code=code, @@ -111,7 +153,9 @@ class ForgetPasswordForm(forms.Form): return code +# 忘记密码验证码请求表单 class ForgetPasswordCodeForm(forms.Form): + # 邮箱字段,用于发送验证码 email = forms.EmailField( label=_('Email'), )