In managing water resources, accurately tracking both usage and recharge statistics is crucial. This document outlines the implementation of a Django API endpoint designed to retrieve recharge water volume statistics, mirroring the existing api_device_water_usage_statistics
function. The implementation emphasizes performance optimization, scalability, and maintainability.
The DeviceInfoModel
represents information about each device, including identifiers, owner details, location, pricing, and various attributes related to water usage and monitoring.
device_id
: Unique identifier for the device.owner_name
, owner_phone
: Owner's contact information.location
: Foreign key linking to the Location
model.pre_price
, aft_price
, and allowable_water_volume
.The DeviceRechargeModel
records recharge transactions for devices. It captures both monetary and water volume aspects of each recharge.
device_id
, license_no
: Identifiers linking the recharge to a specific device.create_time
: Timestamp of the recharge transaction.current_recharge_money
, total_recharge_money
: Monetary values for the current and cumulative recharges.current_recharge_water
, total_recharge_water
: Water volume for the current and cumulative recharges.operate_user
, note
: Metadata about the operation.The DeviceDailyUsageModel
tracks daily water and electricity usage for each device.
device_id
: Identifier linking to the device.statistics_date
: Date of the usage record.water_use
, electricity_use
: Daily usage metrics.water_accumulation
, electric_accumulation
: Cumulative usage metrics.The new API, api_device_recharge_statistics
, aims to provide recharge water volume statistics with the following requirements:
search_time
parameter to filter data by year or month.location_l1_id
, location_l2_id
, location_l3_id
).statistics_type
parameter.Create a new API function named api_device_recharge_statistics
within your Django views. This function will handle GET requests to retrieve recharge statistics.
Extract and validate input parameters from the request:
search_time
: Determines the time range for the query (e.g., "2024" for yearly, "2024-03" for monthly).statistics_type
: Specifies the aggregation type ("日" for daily, "月" for monthly).location_l1_id
, location_l2_id
, location_l3_id
: Filter devices based on location hierarchy.Leverage Django's ORM capabilities to efficiently query and aggregate recharge data:
annotate
with aggregation functions like Sum
to compute daily or monthly recharge volumes.select_related
and prefetch_related
to reduce the number of database hits.bulk_create
and bulk_update
for batch operations when necessary.license_no
and device_id
.Structure the response to include comprehensive statistics for each device:
device_id
, owner_name
, license_no
: Device metadata.recharge_usage
: List of recharge volumes per day or month.total
: Total recharge water volume.Incorporate strategies to enhance the API's performance:
values()
and values_list()
to retrieve only necessary fields.The following Python code implements the api_device_recharge_statistics
function, incorporating best practices for performance and maintainability:
@custom_require_http_methods(['GET'])
@json_request_handler
@token_validation_handler
def api_device_recharge_statistics(request):
"""
Recharge Water Volume Statistics API
"""
from django.db.models import Sum
from django.db.models.functions import TruncDay, TruncMonth
from django.core.cache import cache
from django.core.paginator import Paginator
import datetime
from collections import defaultdict
# Parse input parameters
search_time = request.json_data.get('search_time') # e.g., "2024" or "2024-03"
statistics_type = request.json_data.get('statistics_type') or '日' # Default to daily
location_l1_id = request.json_data.get('location_l1_id')
location_l2_id = request.json_data.get('location_l2_id')
location_l3_id = request.json_data.get('location_l3_id')
# Validate search_time
if not search_time:
return error_response('请指定查询时间')
# Determine the date range based on search_time and statistics_type
try:
if statistics_type == '日':
search_date = datetime.datetime.strptime(search_time, "%Y-%m")
first_day = datetime.date(search_date.year, search_date.month, 1)
if search_date.month < 12:
last_day = datetime.date(search_date.year, search_date.month + 1, 1) - datetime.timedelta(days=1)
else:
last_day = datetime.date(search_date.year + 1, 1, 1) - datetime.timedelta(days=1)
else:
search_date = datetime.datetime.strptime(search_time, "%Y")
first_day = datetime.date(search_date.year, 1, 1)
last_day = datetime.date(search_date.year, 12, 31)
except ValueError as e:
return error_response(f'日期格式错误:{e}')
# Filter devices based on location
device_query = DeviceInfoModel.objects.all()
if location_l3_id:
device_query = device_query.filter(location_id=location_l3_id)
elif location_l2_id:
location = Location.objects.filter(id=location_l2_id).first()
if location:
device_query = device_query.filter(location__in=location.get_leafnodes())
elif location_l1_id:
location = Location.objects.filter(id=location_l1_id).first()
if location:
device_query = device_query.filter(location__in=location.get_leafnodes())
if not device_query.exists():
return error_response('未查询到设备数据')
# Implement caching
cache_key = f"recharge_stats_{statistics_type}_{search_time}_{location_l1_id}_{location_l2_id}_{location_l3_id}"
cached_data = cache.get(cache_key)
if cached_data:
return success_response(data=cached_data)
# Get license numbers for the devices
license_numbers = list(device_query.values_list('license_no', flat=True))
# Query recharge data
if statistics_type == '日':
recharge_data = DeviceRechargeModel.objects.filter(
license_no__in=license_numbers,
create_time__range=(first_day, last_day)
).annotate(
day=TruncDay('create_time')
).values(
'license_no', 'day'
).annotate(
daily_recharge_water=Sum('current_recharge_water')
)
else:
recharge_data = DeviceRechargeModel.objects.filter(
license_no__in=license_numbers,
create_time__range=(first_day, last_day)
).annotate(
month=TruncMonth('create_time')
).values(
'license_no', 'month'
).annotate(
monthly_recharge_water=Sum('current_recharge_water')
)
# Group data by license_no
data_by_license_no = defaultdict(dict)
for entry in recharge_data:
license_no = entry['license_no']
if statistics_type == '日':
date_key = entry['day'].date()
recharge_water = entry['daily_recharge_water']
data_by_license_no[license_no][date_key] = recharge_water
else:
date_key = entry['month'].date()
recharge_water = entry['monthly_recharge_water']
data_by_license_no[license_no][date_key] = recharge_water
# Fetch device info in bulk to minimize queries
device_info = DeviceInfoModel.objects.filter(license_no__in=license_numbers).values(
'device_id', 'owner_name', 'license_no'
)
device_info_dict = {d['license_no']: d for d in device_info}
# Prepare the response data
specific = []
for device in device_query:
license_no = device.license_no
recharge_usage = data_by_license_no.get(license_no, {})
recharge_water_usage = {}
total_recharge_water = 0
if statistics_type == '日':
date_range = (first_day + datetime.timedelta(days=i) for i in range((last_day - first_day).days + 1))
else:
date_range = (datetime.date(search_date.year, month, 1) for month in range(1, 13))
for current_date in date_range:
water_recharge = recharge_usage.get(current_date, 0)
recharge_water_usage[current_date] = water_recharge
total_recharge_water += water_recharge
device_statistics_data = {
"device_id": device.device_id,
"owner_name": device.owner_name,
"license_no": device.license_no,
"recharge_usage": list(recharge_water_usage.values()),
"total": total_recharge_water,
}
specific.append(device_statistics_data)
# Optional: Implement pagination if the dataset is large
paginator = Paginator(specific, 100) # Show 100 devices per page
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
response_data = {
"page": page_number,
"total_pages": paginator.num_pages,
"total_devices": paginator.count,
"data": page_obj.object_list
}
# Cache the response data for 1 hour
cache.set(cache_key, response_data, 60 * 60)
return success_response(data=response_data)
The API accepts the following parameters:
search_time
: Specifies the time frame for the query. It can be a year (e.g., "2024") or a year-month (e.g., "2024-03").statistics_type
: Determines the aggregation level. Defaults to daily ("日") if not specified.location_l1_id
, location_l2_id
, location_l3_id
: Used to filter devices based on their location hierarchy.Based on the search_time
and statistics_type
, the API calculates the appropriate date range:
The API filters devices based on the provided location hierarchy. It uses the get_leafnodes()
method to retrieve all leaf nodes under a specified location, ensuring comprehensive coverage.
Using Django ORM's aggregation functions:
TruncDay
and TruncMonth
: Truncate datetime fields to the desired granularity.Sum
: Aggregate the total recharge water volume per day or month.To enhance performance, the API caches the response data based on the input parameters. This reduces database load for frequently requested data.
For scenarios with a large number of devices, pagination is implemented to limit the number of records returned per request, ensuring efficient data handling and reduced response times.
The API response includes:
page
: Current page number.total_pages
: Total number of pages available.total_devices
: Total number of devices matching the query.data
: List of recharge statistics for each device.Each device's recharge statistics contain:
device_id
, owner_name
, license_no
: Device metadata.recharge_usage
: Recharge volumes per period.total
: Total recharge water volume.Ensure that critical fields such as license_no
and device_id
are indexed in the database. This significantly improves query performance, especially for large datasets.
class DeviceInfoModel(models.Model):
device_id = models.CharField(max_length=10, null=False, primary_key=True, db_index=True)
license_no = models.CharField('许可证编号', max_length=255, null=True, db_index=True)
# ... other fields ...
class Meta:
indexes = [
models.Index(fields=['license_no']),
]
When handling multiple records, use Django's bulk_create
and bulk_update
to minimize database queries.
Utilize select_related
and prefetch_related
to optimize related object retrieval, thereby reducing the number of database hits.
# Example of using select_related for foreign keys
device_info = DeviceInfoModel.objects.select_related('location').get(device_id='device1')
Retrieve only necessary fields using values()
or values_list()
to reduce the amount of data transferred.
# Fetch only specific fields
recharge_data = DeviceRechargeModel.objects.filter(...)
.values('license_no', 'create_time')
.annotate(total_recharge=Sum('current_recharge_water'))
Implement caching for frequently accessed data to reduce database load and improve response times.
Handle large datasets efficiently by breaking them into manageable pages, enhancing both performance and user experience.
Ensure that the API is secure by implementing the following measures:
Rigorous testing ensures the reliability and accuracy of the API:
The api_device_recharge_statistics
API provides a robust solution for retrieving recharge water volume statistics. By adhering to Django's best practices and implementing performance optimizations, the API ensures efficient data retrieval and scalability. Proper structuring and thorough testing further enhance its reliability, making it a valuable tool for managing water resource usage and recharge metrics.