This commit is contained in:
@@ -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 = (
|
||||||
('报名详情', {
|
('报名详情', {
|
||||||
|
|||||||
149
backend/community/admin_actions.py
Normal file
149
backend/community/admin_actions.py
Normal 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 (含详细信息)"
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user