diff --git a/backend/community/admin.py b/backend/community/admin.py index 530fd6c..d0b571c 100644 --- a/backend/community/admin.py +++ b/backend/community/admin.py @@ -5,6 +5,7 @@ from django.shortcuts import redirect from unfold.admin import ModelAdmin, TabularInline from unfold.decorators import display from .models import Activity, ActivitySignup, Topic, Reply, TopicMedia, Announcement +from .admin_actions import export_signups_csv, export_signups_excel class ActivitySignupInline(TabularInline): model = ActivitySignup @@ -156,6 +157,7 @@ class ActivitySignupAdmin(ModelAdmin): list_filter = ('status', 'signup_time', 'activity') search_fields = ('user__nickname', 'activity__title') autocomplete_fields = ['activity', 'user'] + actions = [export_signups_csv, export_signups_excel] fieldsets = ( ('报名详情', { diff --git a/backend/community/admin_actions.py b/backend/community/admin_actions.py new file mode 100644 index 0000000..72146c8 --- /dev/null +++ b/backend/community/admin_actions.py @@ -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 (含详细信息)" diff --git a/backend/shop/views.py b/backend/shop/views.py index 90d71cb..9131719 100644 --- a/backend/shop/views.py +++ b/backend/shop/views.py @@ -1136,16 +1136,17 @@ def wechat_login(request): else: # 【新建场景】: 都不存在 -> 创建新用户 - user = WeChatUser.objects.create(openid=openid) if phone_number: + user = WeChatUser.objects.create(openid=openid) user.phone_number = phone_number user.save() + else: + # 如果没有手机号(静默登录),不自动创建新用户 + print(f"未注册用户尝试静默登录: OpenID={openid}") + pass - # 统一更新会话信息 (确保 user 对象是最新的) - # 重新获取对象以防状态不一致 (可选,但推荐) - # user.refresh_from_db() - - if user.openid == openid: + # 统一更新会话信息 (确保 user 对象存在) + if user and user.openid == openid: user.session_key = session_key user.unionid = unionid @@ -1189,7 +1190,8 @@ def wechat_login(request): # 生成 Token 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() token = signer.sign(user.openid) diff --git a/miniprogram/src/app.ts b/miniprogram/src/app.ts index dd997ff..8791ab6 100644 --- a/miniprogram/src/app.ts +++ b/miniprogram/src/app.ts @@ -26,12 +26,23 @@ function App({ children }: PropsWithChildren) { } } - // Auto login - login().then(res => { - console.log('Logged in as:', res?.nickname) - }).catch(err => { - console.log('Auto login failed', err) - }) + // 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 => { + console.log('Auto login success, user:', res?.nickname) + }).catch(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