csv
All checks were successful
Deploy to Server / deploy (push) Successful in 24s

This commit is contained in:
jeremygan2021
2026-02-28 11:28:35 +08:00
parent 4f4cfcd6f4
commit dba9d4f724
4 changed files with 177 additions and 13 deletions

View File

@@ -5,6 +5,7 @@ from django.shortcuts import redirect
from unfold.admin import ModelAdmin, TabularInline from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import display from unfold.decorators import display
from .models import Activity, ActivitySignup, Topic, Reply, TopicMedia, Announcement from .models import Activity, ActivitySignup, Topic, Reply, TopicMedia, Announcement
from .admin_actions import export_signups_csv, export_signups_excel
class ActivitySignupInline(TabularInline): class ActivitySignupInline(TabularInline):
model = ActivitySignup model = ActivitySignup
@@ -156,6 +157,7 @@ class ActivitySignupAdmin(ModelAdmin):
list_filter = ('status', 'signup_time', 'activity') list_filter = ('status', 'signup_time', 'activity')
search_fields = ('user__nickname', 'activity__title') search_fields = ('user__nickname', 'activity__title')
autocomplete_fields = ['activity', 'user'] autocomplete_fields = ['activity', 'user']
actions = [export_signups_csv, export_signups_excel]
fieldsets = ( fieldsets = (
('报名详情', { ('报名详情', {

View File

@@ -0,0 +1,149 @@
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 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)
# Base fields to export
base_headers = ['ID', '活动标题', '用户昵称', '用户ID', '报名时间', '状态', '关联订单ID']
# Get dynamic JSON keys
json_keys = get_signup_info_keys(queryset)
# Write header
writer.writerow(base_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 ''
]
# 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
# Base fields to export
base_headers = ['ID', '活动标题', '用户昵称', '用户ID', '报名时间', '状态', '关联订单ID']
# Get dynamic JSON keys
json_keys = get_signup_info_keys(queryset)
# Write header
ws.append(base_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 ''
]
# 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)) # Ensure string for simplicity, or handle types
ws.append(row)
wb.save(response)
return response
export_signups_excel.short_description = "导出选中报名记录为 Excel (含详细信息)"

View File

@@ -1136,16 +1136,17 @@ def wechat_login(request):
else: else:
# 【新建场景】: 都不存在 -> 创建新用户 # 【新建场景】: 都不存在 -> 创建新用户
user = WeChatUser.objects.create(openid=openid)
if phone_number: if phone_number:
user = WeChatUser.objects.create(openid=openid)
user.phone_number = phone_number user.phone_number = phone_number
user.save() user.save()
else:
# 如果没有手机号(静默登录),不自动创建新用户
print(f"未注册用户尝试静默登录: OpenID={openid}")
pass
# 统一更新会话信息 (确保 user 对象是最新的) # 统一更新会话信息 (确保 user 对象存在)
# 重新获取对象以防状态不一致 (可选,但推荐) if user and user.openid == openid:
# user.refresh_from_db()
if user.openid == openid:
user.session_key = session_key user.session_key = session_key
user.unionid = unionid user.unionid = unionid
@@ -1189,7 +1190,8 @@ def wechat_login(request):
# 生成 Token # 生成 Token
if not user: if not user:
return Response({'error': 'Login failed: User not created'}, status=500) # 用户未注册且未提供手机号
return Response({'error': 'User not registered', 'code': 'USER_NOT_FOUND'}, status=404)
signer = TimestampSigner() signer = TimestampSigner()
token = signer.sign(user.openid) token = signer.sign(user.openid)

View File

@@ -26,12 +26,23 @@ function App({ children }: PropsWithChildren<any>) {
} }
} }
// Auto login // Auto login only if user info with phone number exists
const userInfo = Taro.getStorageSync('userInfo')
if (userInfo && userInfo.phone_number) {
console.log('User has phone number, attempting auto login...')
login().then(res => { login().then(res => {
console.log('Logged in as:', res?.nickname) console.log('Auto login success, user:', res?.nickname)
}).catch(err => { }).catch(err => {
console.log('Auto login failed', err) console.log('Auto login failed', err)
// If login fails (e.g. user deleted on backend), clear storage
if (err.statusCode === 404 || err.statusCode === 401) {
Taro.removeStorageSync('userInfo')
Taro.removeStorageSync('token')
}
}) })
} else {
console.log('No phone number found, skipping auto login')
}
}) })
return children return children