diff --git a/backend/community/admin_actions.py b/backend/community/admin_actions.py index 72146c8..4aecd23 100644 --- a/backend/community/admin_actions.py +++ b/backend/community/admin_actions.py @@ -37,6 +37,58 @@ def get_signup_info_keys(queryset): 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 @@ -49,26 +101,30 @@ def export_signups_csv(modeladmin, request, queryset): writer = csv.writer(response) - # Base fields to export - base_headers = ['ID', '活动标题', '用户昵称', '用户ID', '报名时间', '状态', '关联订单ID'] + # Fixed headers + fixed_headers = [ + 'ID', '活动标题', '状态', '报名时间', + '用户ID', '用户昵称', '用户OpenID', '用户绑定手机', '用户地区', '用户注册时间', + '关联订单ID', '订单状态', '收货人姓名', '收货电话', '收货地址', '快递公司', '快递单号', '订单总价', '商户订单号' + ] # Get dynamic JSON keys json_keys = get_signup_info_keys(queryset) # Write header - writer.writerow(base_headers + json_keys) + writer.writerow(fixed_headers + json_keys) # Write data for obj in queryset: - row = [ - str(obj.id), - obj.activity.title, - obj.user.nickname if obj.user else 'Unknown', - str(obj.user.id) if obj.user else '', - obj.signup_time.strftime('%Y-%m-%d %H:%M:%S'), - obj.get_status_display(), - str(obj.order.id) if obj.order else '' - ] + 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 = {} @@ -85,7 +141,7 @@ def export_signups_csv(modeladmin, request, queryset): return response -export_signups_csv.short_description = "导出选中报名记录为 CSV (含详细信息)" +export_signups_csv.short_description = "导出选中报名记录为 CSV (含发货/详细信息)" def export_signups_excel(modeladmin, request, queryset): """ @@ -109,27 +165,38 @@ def export_signups_excel(modeladmin, request, queryset): ws = wb.active ws.title = str(opts.verbose_name)[:31] # Sheet name limit is 31 chars - # Base fields to export - base_headers = ['ID', '活动标题', '用户昵称', '用户ID', '报名时间', '状态', '关联订单ID'] + # Fixed headers + fixed_headers = [ + 'ID', '活动标题', '状态', '报名时间', + '用户ID', '用户昵称', '用户OpenID', '用户绑定手机', '用户地区', '用户注册时间', + '关联订单ID', '订单状态', '收货人姓名', '收货电话', '收货地址', '快递公司', '快递单号', '订单总价', '商户订单号' + ] # Get dynamic JSON keys json_keys = get_signup_info_keys(queryset) # Write header - ws.append(base_headers + json_keys) + ws.append(fixed_headers + json_keys) # Write data for obj in queryset: - row = [ - obj.id, - obj.activity.title, - obj.user.nickname if obj.user else 'Unknown', - obj.user.id if obj.user else '', - obj.signup_time.replace(tzinfo=None) if obj.signup_time else '', # Remove tz for Excel - obj.get_status_display(), - obj.order.id if obj.order else '' - ] + 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): @@ -139,11 +206,11 @@ def export_signups_excel(modeladmin, request, queryset): val = flat_info.get(key, '') if val is None: val = '' - row.append(str(val)) # Ensure string for simplicity, or handle types + row.append(str(val)) # JSON values as strings ws.append(row) wb.save(response) return response -export_signups_excel.short_description = "导出选中报名记录为 Excel (含详细信息)" +export_signups_excel.short_description = "导出选中报名记录为 Excel (含发货/详细信息)" diff --git a/backend/requirements.txt b/backend/requirements.txt index 3515011..debcb3e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -22,4 +22,5 @@ gunicorn==21.2.0 requests django-filter django-admin-sortable2 +openpyxl