From 1b5751a065e870a2d263b29424cfcc47a1b6d8d4 Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Sat, 28 Feb 2026 11:36:27 +0800 Subject: [PATCH] csv --- backend/shop/admin.py | 2 + backend/shop/admin_actions.py | 170 ++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 backend/shop/admin_actions.py diff --git a/backend/shop/admin.py b/backend/shop/admin.py index 8d3ecef..15e9aff 100644 --- a/backend/shop/admin.py +++ b/backend/shop/admin.py @@ -7,6 +7,7 @@ from django.shortcuts import redirect from unfold.admin import ModelAdmin, TabularInline from unfold.decorators import display from .models import ESP32Config, Order, Salesperson, WeChatPayConfig, Service, VCCourse, ProductFeature, CommissionLog, WeChatUser, Distributor, Withdrawal, ServiceOrder, CourseEnrollment, AdminPhoneNumber +from .admin_actions import export_orders_excel import qrcode from io import BytesIO import base64 @@ -402,6 +403,7 @@ class OrderAdmin(ModelAdmin): list_filter = ('status', 'salesperson', 'distributor', 'created_at') search_fields = ('id', 'customer_name', 'phone_number', 'wechat_trade_no') readonly_fields = ('total_price', 'created_at', 'wechat_trade_no') + actions = [export_orders_excel] def get_item_name(self, obj): if obj.config: diff --git a/backend/shop/admin_actions.py b/backend/shop/admin_actions.py new file mode 100644 index 0000000..f6bffd0 --- /dev/null +++ b/backend/shop/admin_actions.py @@ -0,0 +1,170 @@ +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_order_signup_keys(queryset): + """ + Collect all unique keys from the signup_info JSON of ActivitySignups related to the Order queryset + """ + keys = set() + for order in queryset: + # Check if order has related activity signups + # Assuming reverse relation name is 'activity_signups' based on ActivitySignup model + signups = order.activity_signups.all() + for signup in signups: + if signup.signup_info and isinstance(signup.signup_info, dict): + flat_info = flatten_json(signup.signup_info) + keys.update(flat_info.keys()) + return sorted(list(keys)) + +def get_order_row_data(order): + """ + Build a dictionary of all exportable data for a single Order object + """ + data = {} + + # 1. 订单基础信息 + data['订单ID'] = str(order.id) + data['商户订单号'] = order.out_trade_no or '' + data['微信支付单号'] = order.wechat_trade_no or '' + data['订单状态'] = order.get_status_display() + data['订单总价'] = str(order.total_price) + data['购买数量'] = str(order.quantity) + data['创建时间'] = order.created_at.strftime('%Y-%m-%d %H:%M:%S') + + # 商品信息 + if order.config: + data['商品类型'] = '硬件' + data['商品名称'] = order.config.name + elif order.course: + data['商品类型'] = '课程' + data['商品名称'] = order.course.title + elif order.activity: + data['商品类型'] = '活动' + data['商品名称'] = order.activity.title + else: + data['商品类型'] = '未知' + data['商品名称'] = '未知' + + # 2. 发货/收货信息 + data['收货人姓名'] = order.customer_name + data['收货电话'] = order.phone_number + data['收货地址'] = order.shipping_address + data['快递公司'] = order.courier_name or '' + data['快递单号'] = order.tracking_number or '' + + # 3. 下单用户信息 + if order.wechat_user: + data['用户昵称'] = order.wechat_user.nickname + data['用户ID'] = str(order.wechat_user.id) + data['用户OpenID'] = order.wechat_user.openid + data['用户绑定手机'] = order.wechat_user.phone_number or '' + else: + data['用户昵称'] = 'Unknown' + data['用户ID'] = '' + data['用户OpenID'] = '' + data['用户绑定手机'] = '' + + # 4. 销售/分销信息 + data['销售员'] = order.salesperson.name if order.salesperson else '' + data['分销员'] = order.distributor.user.nickname if order.distributor else '' + + return data + +def get_order_signup_data(order): + """ + Get flattened signup info for the order. + If multiple signups exist, we only take the first one for simplicity in flat export, + or we could try to merge them but keys might conflict. + Given the context, usually 1 order -> 1 signup for activity. + """ + flat_info = {} + signup = order.activity_signups.first() + if signup and signup.signup_info and isinstance(signup.signup_info, dict): + flat_info = flatten_json(signup.signup_info) + return flat_info + +def export_orders_excel(modeladmin, request, queryset): + """ + Export selected orders to Excel, including related Activity Signup JSON info + """ + try: + from openpyxl import Workbook + except ImportError: + modeladmin.message_user(request, "请先安装 openpyxl 库以使用 Excel 导出功能", level='error') + return + + opts = modeladmin.model._meta + filename = f"Orders_{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 = "订单导出" + + # Fixed headers + fixed_headers = [ + '订单ID', '商户订单号', '微信支付单号', '订单状态', '订单总价', '购买数量', '创建时间', + '商品类型', '商品名称', + '收货人姓名', '收货电话', '收货地址', '快递公司', '快递单号', + '用户ID', '用户昵称', '用户OpenID', '用户绑定手机', + '销售员', '分销员' + ] + + # Dynamic headers from ActivitySignup (only if orders contain activity orders) + json_keys = get_order_signup_keys(queryset) + + # Write header + # Add a prefix to json keys to distinguish them? Or keep as is. + # Let's keep as is but maybe add a note in header if needed. + ws.append(fixed_headers + json_keys) + + # Write data + for order in queryset: + row_data = get_order_row_data(order) + signup_data = get_order_signup_data(order) + + row = [] + for header in fixed_headers: + val = row_data.get(header, '') + row.append(str(val)) # Convert everything to string for safety + + for key in json_keys: + val = signup_data.get(key, '') + if val is None: + val = '' + row.append(str(val)) + + ws.append(row) + + wb.save(response) + return response + +export_orders_excel.short_description = "导出选中订单为 Excel (含报名信息)"