import csv import json import datetime from django.http import HttpResponse from django.utils.encoding import escape_uri_path def flatten_json(y): """ Flatten a nested json object """ out = {} def flatten(x, name=''): if type(x) is dict: for a in x: flatten(x[a], name + a + '_') elif type(x) is list: i = 0 for a in x: flatten(a, name + str(i) + '_') i += 1 else: out[name[:-1]] = x flatten(y) return out def get_signup_info_keys(queryset): """ Collect all unique keys from the signup_info JSON across the queryset """ keys = set() for obj in queryset: if obj.signup_info and isinstance(obj.signup_info, dict): # Flatten the dictionary first to get all nested keys flat_info = flatten_json(obj.signup_info) keys.update(flat_info.keys()) return sorted(list(keys)) def get_row_data(obj): """ Build a dictionary of all exportable data for a single ActivitySignup object """ data = {} # 1. 基础信息 data['ID'] = str(obj.id) data['活动标题'] = obj.activity.title data['报名时间'] = obj.signup_time data['状态'] = obj.get_status_display() # 2. 用户信息 if obj.user: data['用户昵称'] = obj.user.nickname data['用户ID'] = str(obj.user.id) data['用户OpenID'] = obj.user.openid data['用户绑定手机'] = obj.user.phone_number or '' data['用户地区'] = f"{obj.user.country} {obj.user.province} {obj.user.city}".strip() data['用户注册时间'] = obj.user.created_at else: data['用户昵称'] = 'Unknown' data['用户ID'] = '' data['用户OpenID'] = '' data['用户绑定手机'] = '' data['用户地区'] = '' data['用户注册时间'] = '' # 3. 订单/发货信息 if obj.order: data['关联订单ID'] = str(obj.order.id) data['订单状态'] = obj.order.get_status_display() data['收货人姓名'] = obj.order.customer_name data['收货电话'] = obj.order.phone_number data['收货地址'] = obj.order.shipping_address data['快递公司'] = obj.order.courier_name or '' data['快递单号'] = obj.order.tracking_number or '' data['订单总价'] = str(obj.order.total_price) data['商户订单号'] = obj.order.out_trade_no or '' else: data['关联订单ID'] = '' data['订单状态'] = '' data['收货人姓名'] = '' data['收货电话'] = '' data['收货地址'] = '' data['快递公司'] = '' data['快递单号'] = '' data['订单总价'] = '' data['商户订单号'] = '' return data def export_signups_csv(modeladmin, request, queryset): """ Export selected signups to CSV, including flattened JSON fields """ opts = modeladmin.model._meta filename = f"{opts.verbose_name}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" response = HttpResponse(content_type='text/csv; charset=utf-8-sig') response['Content-Disposition'] = f'attachment; filename={escape_uri_path(filename)}' writer = csv.writer(response) # Fixed headers fixed_headers = [ 'ID', '活动标题', '状态', '报名时间', '用户ID', '用户昵称', '用户OpenID', '用户绑定手机', '用户地区', '用户注册时间', '关联订单ID', '订单状态', '收货人姓名', '收货电话', '收货地址', '快递公司', '快递单号', '订单总价', '商户订单号' ] # Get dynamic JSON keys json_keys = get_signup_info_keys(queryset) # Write header writer.writerow(fixed_headers + json_keys) # Write data for obj in queryset: row_data = get_row_data(obj) # Build the row based on fixed_headers order row = [] for header in fixed_headers: val = row_data.get(header, '') if isinstance(val, (datetime.datetime, datetime.date)): val = val.strftime('%Y-%m-%d %H:%M:%S') row.append(str(val)) # Add JSON data flat_info = {} if obj.signup_info and isinstance(obj.signup_info, dict): flat_info = flatten_json(obj.signup_info) for key in json_keys: val = flat_info.get(key, '') if val is None: val = '' row.append(str(val)) writer.writerow(row) return response export_signups_csv.short_description = "导出选中报名记录为 CSV (含发货/详细信息)" def export_signups_excel(modeladmin, request, queryset): """ Export selected signups to Excel, including flattened JSON fields """ try: from openpyxl import Workbook except ImportError: modeladmin.message_user(request, "请先安装 openpyxl 库以使用 Excel 导出功能", level='error') return opts = modeladmin.model._meta filename = f"{opts.verbose_name}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" response = HttpResponse( content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ) response['Content-Disposition'] = f'attachment; filename={escape_uri_path(filename)}' wb = Workbook() ws = wb.active ws.title = str(opts.verbose_name)[:31] # Sheet name limit is 31 chars # Fixed headers fixed_headers = [ 'ID', '活动标题', '状态', '报名时间', '用户ID', '用户昵称', '用户OpenID', '用户绑定手机', '用户地区', '用户注册时间', '关联订单ID', '订单状态', '收货人姓名', '收货电话', '收货地址', '快递公司', '快递单号', '订单总价', '商户订单号' ] # Get dynamic JSON keys json_keys = get_signup_info_keys(queryset) # Write header ws.append(fixed_headers + json_keys) # Write data for obj in queryset: row_data = get_row_data(obj) row = [] for header in fixed_headers: val = row_data.get(header, '') # Excel handles datetime natively, but we need to remove timezone info if present to avoid Excel errors or warnings depending on version # Here we convert to naive datetime for simplicity or keep as is if library handles it. # openpyxl supports datetime, but usually it's safer to remove tzinfo for compatibility. if isinstance(val, (datetime.datetime, datetime.date)): if hasattr(val, 'replace'): val = val.replace(tzinfo=None) # Ensure None becomes empty string if val is None: val = "" row.append(val) # Add JSON data flat_info = {} if obj.signup_info and isinstance(obj.signup_info, dict): flat_info = flatten_json(obj.signup_info) for key in json_keys: val = flat_info.get(key, '') if val is None: val = '' row.append(str(val)) # JSON values as strings ws.append(row) wb.save(response) return response export_signups_excel.short_description = "导出选中报名记录为 Excel (含发货/详细信息)"