This commit is contained in:
@@ -55,9 +55,9 @@ class Activity(models.Model):
|
|||||||
@property
|
@property
|
||||||
def current_signups(self):
|
def current_signups(self):
|
||||||
"""
|
"""
|
||||||
当前有效报名人数
|
当前有效报名人数(仅统计已确认/已支付的报名)
|
||||||
"""
|
"""
|
||||||
return self.signups.exclude(status='cancelled').count()
|
return self.signups.filter(status='confirmed').count()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class ActivitySerializer(serializers.ModelSerializer):
|
|||||||
current_signups = serializers.IntegerField(read_only=True)
|
current_signups = serializers.IntegerField(read_only=True)
|
||||||
has_signed_up = serializers.SerializerMethodField()
|
has_signed_up = serializers.SerializerMethodField()
|
||||||
is_signed_up = serializers.SerializerMethodField()
|
is_signed_up = serializers.SerializerMethodField()
|
||||||
|
my_signup_status = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Activity
|
model = Activity
|
||||||
@@ -17,14 +18,25 @@ class ActivitySerializer(serializers.ModelSerializer):
|
|||||||
def get_has_signed_up(self, obj):
|
def get_has_signed_up(self, obj):
|
||||||
return self.get_is_signed_up(obj)
|
return self.get_is_signed_up(obj)
|
||||||
|
|
||||||
|
def get_my_signup_status(self, obj):
|
||||||
|
request = self.context.get('request')
|
||||||
|
if not request:
|
||||||
|
return None
|
||||||
|
user = get_current_wechat_user(request)
|
||||||
|
if user:
|
||||||
|
# Return the status of the non-cancelled signup
|
||||||
|
signup = obj.signups.filter(user=user).exclude(status='cancelled').first()
|
||||||
|
return signup.status if signup else None
|
||||||
|
return None
|
||||||
|
|
||||||
def get_is_signed_up(self, obj):
|
def get_is_signed_up(self, obj):
|
||||||
request = self.context.get('request')
|
request = self.context.get('request')
|
||||||
if not request:
|
if not request:
|
||||||
return False
|
return False
|
||||||
user = get_current_wechat_user(request)
|
user = get_current_wechat_user(request)
|
||||||
if user:
|
if user:
|
||||||
# Check if there is a valid signup (not cancelled)
|
# Check if there is a valid signup (only confirmed counts)
|
||||||
return obj.signups.filter(user=user).exclude(status='cancelled').exists()
|
return obj.signups.filter(user=user, status='confirmed').exists()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_signup_form_config(self, obj):
|
def get_signup_form_config(self, obj):
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ class ActivityViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
contact_phone = signup_info.get('phone') or user.phone_number or ''
|
contact_phone = signup_info.get('phone') or user.phone_number or ''
|
||||||
if contact_name: order.customer_name = contact_name
|
if contact_name: order.customer_name = contact_name
|
||||||
if contact_phone: order.phone_number = contact_phone
|
if contact_phone: order.phone_number = contact_phone
|
||||||
|
|
||||||
|
# Ensure activity is linked
|
||||||
|
if not order.activity:
|
||||||
|
order.activity = activity
|
||||||
|
|
||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
if not order:
|
if not order:
|
||||||
@@ -105,6 +110,9 @@ class ActivityViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
|
|
||||||
if pending_order:
|
if pending_order:
|
||||||
order = pending_order
|
order = pending_order
|
||||||
|
# Ensure shipping address is up-to-date
|
||||||
|
order.shipping_address = activity.location or '线下活动'
|
||||||
|
order.save()
|
||||||
else:
|
else:
|
||||||
contact_name = signup_info.get('name') or signup_info.get('participant_name') or user.nickname or 'Activity User'
|
contact_name = signup_info.get('name') or signup_info.get('participant_name') or user.nickname or 'Activity User'
|
||||||
contact_phone = signup_info.get('phone') or user.phone_number or ''
|
contact_phone = signup_info.get('phone') or user.phone_number or ''
|
||||||
@@ -136,7 +144,8 @@ class ActivityViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
'total': int(activity.price * 100),
|
'total': int(activity.price * 100),
|
||||||
'currency': 'CNY'
|
'currency': 'CNY'
|
||||||
},
|
},
|
||||||
notify_url=wxpay._notify_url
|
notify_url=wxpay._notify_url,
|
||||||
|
attach=f'{{"type":"activity","activity_id":{activity.id}}}'
|
||||||
)
|
)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 从环境变量获取数据库配置 (Docker 环境会自动注入这些变量)
|
# 从环境变量获取数据库配置 (Docker 环境会自动注入这些变量)
|
||||||
# DB_HOST = os.environ.get('DB_HOST', '121.43.104.161')
|
#DB_HOST = os.environ.get('DB_HOST', '121.43.104.161')
|
||||||
DB_HOST = os.environ.get('DB_HOST', '6.6.6.66')
|
DB_HOST = os.environ.get('DB_HOST', '6.6.6.66')
|
||||||
if DB_HOST:
|
if DB_HOST:
|
||||||
DATABASES['default'] = {
|
DATABASES['default'] = {
|
||||||
|
|||||||
@@ -373,12 +373,14 @@ class OrderAdmin(ModelAdmin):
|
|||||||
return f"[硬件] {obj.config.name}"
|
return f"[硬件] {obj.config.name}"
|
||||||
if obj.course:
|
if obj.course:
|
||||||
return f"[课程] {obj.course.title}"
|
return f"[课程] {obj.course.title}"
|
||||||
|
if obj.activity:
|
||||||
|
return f"[活动] {obj.activity.title}"
|
||||||
return "未知商品"
|
return "未知商品"
|
||||||
get_item_name.short_description = "购买商品"
|
get_item_name.short_description = "购买商品"
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('订单信息', {
|
('订单信息', {
|
||||||
'fields': ('config', 'course', 'quantity', 'total_price', 'status', 'created_at')
|
'fields': ('config', 'course', 'activity', 'quantity', 'total_price', 'status', 'created_at')
|
||||||
}),
|
}),
|
||||||
('客户信息', {
|
('客户信息', {
|
||||||
'fields': ('customer_name', 'phone_number', 'shipping_address', 'wechat_user')
|
'fields': ('customer_name', 'phone_number', 'shipping_address', 'wechat_user')
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ class OrderSerializer(serializers.ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
config_name = serializers.CharField(source='config.name', read_only=True)
|
config_name = serializers.CharField(source='config.name', read_only=True)
|
||||||
course_title = serializers.CharField(source='course.title', read_only=True)
|
course_title = serializers.CharField(source='course.title', read_only=True)
|
||||||
|
activity_title = serializers.CharField(source='activity.title', read_only=True)
|
||||||
config_image = serializers.SerializerMethodField()
|
config_image = serializers.SerializerMethodField()
|
||||||
salesperson_name = serializers.CharField(source='salesperson.name', read_only=True)
|
salesperson_name = serializers.CharField(source='salesperson.name', read_only=True)
|
||||||
salesperson_code = serializers.CharField(source='salesperson.code', read_only=True)
|
salesperson_code = serializers.CharField(source='salesperson.code', read_only=True)
|
||||||
@@ -215,7 +216,7 @@ class OrderSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Order
|
model = Order
|
||||||
fields = ['id', 'config', 'config_name', 'config_image', 'course', 'course_title', 'quantity', 'total_price', 'status', 'created_at', 'updated_at', 'wechat_trade_no',
|
fields = ['id', 'config', 'config_name', 'config_image', 'course', 'course_title', 'activity', 'activity_title', 'quantity', 'total_price', 'status', 'created_at', 'updated_at', 'wechat_trade_no',
|
||||||
'customer_name', 'phone_number', 'shipping_address', 'ref_code', 'salesperson_name', 'salesperson_code', 'courier_name', 'tracking_number']
|
'customer_name', 'phone_number', 'shipping_address', 'ref_code', 'salesperson_name', 'salesperson_code', 'courier_name', 'tracking_number']
|
||||||
read_only_fields = ['total_price', 'status', 'wechat_trade_no', 'created_at', 'updated_at']
|
read_only_fields = ['total_price', 'status', 'wechat_trade_no', 'created_at', 'updated_at']
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
@@ -230,11 +231,14 @@ class OrderSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
config = data.get('config')
|
config = data.get('config')
|
||||||
course = data.get('course')
|
course = data.get('course')
|
||||||
|
activity = data.get('activity')
|
||||||
|
|
||||||
if not config and not course:
|
if not config and not course and not activity:
|
||||||
raise serializers.ValidationError("必须选择一种商品(硬件配置或课程)")
|
raise serializers.ValidationError("必须选择一种商品(硬件配置、课程或活动)")
|
||||||
|
|
||||||
if config and course:
|
# Count how many types are selected
|
||||||
|
selected_types = sum([bool(config), bool(course), bool(activity)])
|
||||||
|
if selected_types > 1:
|
||||||
raise serializers.ValidationError("一次只能购买一种类型的商品")
|
raise serializers.ValidationError("一次只能购买一种类型的商品")
|
||||||
|
|
||||||
if config and not data.get('shipping_address'):
|
if config and not data.get('shipping_address'):
|
||||||
@@ -255,6 +259,12 @@ class OrderSerializer(serializers.ModelSerializer):
|
|||||||
return obj.course.cover_image_url
|
return obj.course.cover_image_url
|
||||||
if obj.course.cover_image:
|
if obj.course.cover_image:
|
||||||
return obj.course.cover_image.url
|
return obj.course.cover_image.url
|
||||||
|
elif obj.activity:
|
||||||
|
# Use activity.display_banner_url logic
|
||||||
|
if obj.activity.banner:
|
||||||
|
return obj.activity.banner.url
|
||||||
|
if obj.activity.banner_url:
|
||||||
|
return obj.activity.banner_url
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
@@ -263,6 +273,7 @@ class OrderSerializer(serializers.ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
config = validated_data.get('config')
|
config = validated_data.get('config')
|
||||||
course = validated_data.get('course')
|
course = validated_data.get('course')
|
||||||
|
activity = validated_data.get('activity')
|
||||||
quantity = validated_data.get('quantity', 1)
|
quantity = validated_data.get('quantity', 1)
|
||||||
ref_code = validated_data.pop('ref_code', None)
|
ref_code = validated_data.pop('ref_code', None)
|
||||||
|
|
||||||
@@ -270,6 +281,8 @@ class OrderSerializer(serializers.ModelSerializer):
|
|||||||
validated_data['total_price'] = config.price * quantity
|
validated_data['total_price'] = config.price * quantity
|
||||||
elif course:
|
elif course:
|
||||||
validated_data['total_price'] = course.price * quantity
|
validated_data['total_price'] = course.price * quantity
|
||||||
|
elif activity:
|
||||||
|
validated_data['total_price'] = activity.price * quantity
|
||||||
|
|
||||||
# 尝试关联销售员或分销员
|
# 尝试关联销售员或分销员
|
||||||
if ref_code:
|
if ref_code:
|
||||||
|
|||||||
@@ -387,9 +387,11 @@ const ActivityDetail = () => {
|
|||||||
variants={buttonTap}
|
variants={buttonTap}
|
||||||
whileTap="tap"
|
whileTap="tap"
|
||||||
onClick={handleSignUp}
|
onClick={handleSignUp}
|
||||||
disabled={signUpMutation.isPending || activity.is_signed_up}
|
disabled={signUpMutation.isPending || activity.is_signed_up || (activity.my_signup_status === 'pending' && !activity.is_paid)}
|
||||||
>
|
>
|
||||||
{signUpMutation.isPending ? '提交中...' : activity.is_signed_up ? '已报名' : '立即报名'}
|
{signUpMutation.isPending ? '提交中...' :
|
||||||
|
activity.is_signed_up ? '已报名' :
|
||||||
|
(activity.my_signup_status === 'pending' ? (activity.is_paid ? '去支付' : '审核中') : '立即报名')}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export default function OrderDetail() {
|
|||||||
<View className='section-title'>商品信息</View>
|
<View className='section-title'>商品信息</View>
|
||||||
<View className='info-row'>
|
<View className='info-row'>
|
||||||
<Text className='label'>商品名称</Text>
|
<Text className='label'>商品名称</Text>
|
||||||
<Text className='value'>{order.config_name || order.course_title}</Text>
|
<Text className='value'>{order.config_name || order.course_title || (order.activity_title ? `报名活动:${order.activity_title}` : '未知商品')}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className='info-row'>
|
<View className='info-row'>
|
||||||
<Text className='label'>数量</Text>
|
<Text className='label'>数量</Text>
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ export default function OrderList() {
|
|||||||
<View className='body'>
|
<View className='body'>
|
||||||
<Image src={order.config_image || 'https://via.placeholder.com/80'} className='img' mode='aspectFill' />
|
<Image src={order.config_image || 'https://via.placeholder.com/80'} className='img' mode='aspectFill' />
|
||||||
<View className='info'>
|
<View className='info'>
|
||||||
<Text className='name'>{order.config_name}</Text>
|
<Text className='name'>
|
||||||
|
{order.config_name || order.course_title || (order.activity_title ? `报名活动:${order.activity_title}` : '未知商品')}
|
||||||
|
</Text>
|
||||||
<Text className='qty'>x {order.quantity}</Text>
|
<Text className='qty'>x {order.quantity}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View className='price'>
|
<View className='price'>
|
||||||
|
|||||||
@@ -145,7 +145,16 @@ const ActivityDetail = () => {
|
|||||||
|
|
||||||
const isFull = activity.max_participants > 0 && (activity.current_signups || 0) >= activity.max_participants
|
const isFull = activity.max_participants > 0 && (activity.current_signups || 0) >= activity.max_participants
|
||||||
const isEnded = new Date(activity.end_time) < new Date()
|
const isEnded = new Date(activity.end_time) < new Date()
|
||||||
const canSignup = activity.is_active && !isFull && !isEnded && !activity.has_signed_up
|
|
||||||
|
const hasConfirmed = activity.has_signed_up
|
||||||
|
const isPending = activity.my_signup_status === 'pending'
|
||||||
|
const isPaid = activity.is_paid
|
||||||
|
|
||||||
|
const canSignup = activity.is_active && !isEnded &&
|
||||||
|
(
|
||||||
|
(!hasConfirmed && !isPending && !isFull) ||
|
||||||
|
(isPending && isPaid)
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className='activity-detail-page'>
|
<View className='activity-detail-page'>
|
||||||
@@ -223,9 +232,11 @@ const ActivityDetail = () => {
|
|||||||
onClick={handleSignup}
|
onClick={handleSignup}
|
||||||
>
|
>
|
||||||
{submitting ? '提交中...' : (
|
{submitting ? '提交中...' : (
|
||||||
activity.has_signed_up ? '您已报名' : (
|
hasConfirmed ? '您已报名' : (
|
||||||
isEnded ? '活动已结束' : (
|
isPending ? (isPaid ? '去支付' : '审核中') : (
|
||||||
isFull ? '名额已满' : '立即报名'
|
isEnded ? '活动已结束' : (
|
||||||
|
isFull ? '名额已满' : '立即报名'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user