haha
This commit is contained in:
@@ -432,7 +432,9 @@ def get_product_with_prices(request, product_id: int):
|
||||
@paginate(PageNumberPagination, page_size=20)
|
||||
@cache_page(settings.CACHE_TTL_SECONDS)
|
||||
def search_products(request, q: str, filters: ProductSearchFilter = Query(...)):
|
||||
"""Search approved products by name or description."""
|
||||
"""Search approved products by name, description, or by user_id (submitter)."""
|
||||
from apps.users.models import User
|
||||
|
||||
prices_prefetch = Prefetch(
|
||||
"prices",
|
||||
queryset=ProductPrice.objects.select_related("website"),
|
||||
@@ -444,6 +446,12 @@ def search_products(request, q: str, filters: ProductSearchFilter = Query(...)):
|
||||
)
|
||||
if filters.category_id:
|
||||
products = products.filter(category_id=filters.category_id)
|
||||
if filters.user_id:
|
||||
try:
|
||||
submitter = User.objects.get(user_id=filters.user_id)
|
||||
products = products.filter(submitted_by=submitter)
|
||||
except User.DoesNotExist:
|
||||
products = products.none()
|
||||
|
||||
needs_price_stats = (
|
||||
filters.min_price is not None
|
||||
|
||||
14
backend/apps/products/migrations/0005_merge_20260130_1626.py
Normal file
14
backend/apps/products/migrations/0005_merge_20260130_1626.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# Generated by Django 4.2.27 on 2026-01-30 08:26
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0002_product_images'),
|
||||
('products', '0004_product_reject_reason_product_reviewed_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
@@ -157,6 +157,7 @@ class ProductFilter(FilterSchema):
|
||||
class ProductSearchFilter(FilterSchema):
|
||||
"""Product search filter schema."""
|
||||
category_id: Optional[int] = None
|
||||
user_id: Optional[str] = None # 按用户ID筛选该用户提交的商品
|
||||
min_price: Optional[Decimal] = None
|
||||
max_price: Optional[Decimal] = None
|
||||
sort_by: Optional[str] = None
|
||||
|
||||
@@ -5,9 +5,9 @@ from .models import User
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
list_display = ['id', 'open_id', 'name', 'email', 'role', 'is_active', 'created_at']
|
||||
list_display = ['id', 'user_id', 'open_id', 'name', 'email', 'role', 'is_active', 'created_at']
|
||||
list_filter = ['role', 'is_active', 'is_staff']
|
||||
search_fields = ['open_id', 'name', 'email']
|
||||
search_fields = ['user_id', 'open_id', 'name', 'email']
|
||||
ordering = ['-created_at']
|
||||
|
||||
fieldsets = (
|
||||
|
||||
@@ -20,6 +20,7 @@ def serialize_user_brief(user: User) -> UserBrief:
|
||||
return UserBrief(
|
||||
id=user.id,
|
||||
open_id=user.open_id,
|
||||
user_id=user.user_id,
|
||||
name=user.name,
|
||||
email=user.email,
|
||||
avatar=user.avatar,
|
||||
@@ -58,6 +59,15 @@ def list_friends(request):
|
||||
return friends
|
||||
|
||||
|
||||
@router.get("/requests/incoming/count", auth=JWTAuth())
|
||||
def incoming_count(request):
|
||||
"""Lightweight: only return count of pending incoming requests (for badge)."""
|
||||
count = FriendRequest.objects.filter(
|
||||
receiver=request.auth, status=FriendRequest.Status.PENDING
|
||||
).count()
|
||||
return {"count": count}
|
||||
|
||||
|
||||
@router.get("/requests/incoming", response=List[FriendRequestOut], auth=JWTAuth())
|
||||
def list_incoming_requests(request):
|
||||
"""List incoming friend requests."""
|
||||
@@ -171,12 +181,18 @@ def cancel_request(request, request_id: int):
|
||||
|
||||
@router.get("/search", response=List[UserBrief], auth=JWTAuth())
|
||||
def search_users(request, q: Optional[str] = None, limit: int = 10):
|
||||
"""Search users by open_id or name."""
|
||||
"""Search users by user_id (exact), open_id or name."""
|
||||
if not q:
|
||||
return []
|
||||
|
||||
q_clean = (q or "").strip()
|
||||
queryset = (
|
||||
User.objects.filter(Q(open_id__icontains=q) | Q(name__icontains=q))
|
||||
User.objects.filter(
|
||||
Q(user_id__iexact=q_clean)
|
||||
| Q(user_id__icontains=q_clean)
|
||||
| Q(open_id__icontains=q_clean)
|
||||
| Q(name__icontains=q_clean)
|
||||
)
|
||||
.exclude(id=request.auth.id)
|
||||
.order_by("id")[: max(1, min(limit, 50))]
|
||||
)
|
||||
|
||||
48
backend/apps/users/migrations/0005_add_user_id.py
Normal file
48
backend/apps/users/migrations/0005_add_user_id.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# Generated manually for user_id (shareable ID for friend/product search)
|
||||
|
||||
import secrets
|
||||
import string
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def generate_user_id_for_user(User):
|
||||
alphabet = string.ascii_lowercase + string.digits
|
||||
for _ in range(10):
|
||||
candidate = "U" + "".join(secrets.choice(alphabet) for _ in range(8))
|
||||
if not User.objects.filter(user_id=candidate).exists():
|
||||
return candidate
|
||||
return "U" + secrets.token_hex(4)
|
||||
|
||||
|
||||
def populate_user_id(apps, schema_editor):
|
||||
User = apps.get_model("users", "User")
|
||||
for user in User.objects.filter(user_id__isnull=True).iterator():
|
||||
user.user_id = generate_user_id_for_user(User)
|
||||
user.save(update_fields=["user_id"])
|
||||
|
||||
|
||||
def noop(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("users", "0004_rename_friendreq_receiver_status_idx_friend_requ_receive_383c2c_idx_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="user_id",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="可分享的用户ID,用于好友搜索、商品搜索等",
|
||||
max_length=16,
|
||||
null=True,
|
||||
unique=True,
|
||||
verbose_name="用户ID",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(populate_user_id, noop),
|
||||
]
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
User models for authentication and profile management.
|
||||
"""
|
||||
import secrets
|
||||
import string
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
|
||||
|
||||
@@ -37,6 +39,14 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
open_id = models.CharField('OpenID', max_length=64, unique=True)
|
||||
user_id = models.CharField(
|
||||
'用户ID',
|
||||
max_length=16,
|
||||
unique=True,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='可分享的用户ID,用于好友搜索、商品搜索等',
|
||||
)
|
||||
name = models.CharField('用户名', max_length=255, blank=True, null=True)
|
||||
email = models.EmailField('邮箱', max_length=320, blank=True, null=True)
|
||||
avatar = models.TextField('头像', blank=True, null=True)
|
||||
@@ -70,11 +80,26 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
indexes = [
|
||||
models.Index(fields=["role", "is_active"]),
|
||||
models.Index(fields=["created_at"]),
|
||||
models.Index(fields=["user_id"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name or self.open_id
|
||||
|
||||
def _generate_user_id(self):
|
||||
"""Generate a unique shareable user_id (e.g. U8x3k2m1)."""
|
||||
alphabet = string.ascii_lowercase + string.digits
|
||||
for _ in range(10):
|
||||
candidate = "U" + "".join(secrets.choice(alphabet) for _ in range(8))
|
||||
if not User.objects.filter(user_id=candidate).exists():
|
||||
return candidate
|
||||
return "U" + secrets.token_hex(4)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.user_id:
|
||||
self.user_id = self._generate_user_id()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def is_admin(self):
|
||||
return self.role == self.Role.ADMIN
|
||||
|
||||
@@ -10,6 +10,7 @@ class UserOut(Schema):
|
||||
"""Public user output schema."""
|
||||
id: int
|
||||
open_id: str
|
||||
user_id: Optional[str] = None # 可分享的用户ID,用于好友/商品搜索
|
||||
name: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
avatar: Optional[str] = None
|
||||
@@ -28,6 +29,7 @@ class UserBrief(Schema):
|
||||
"""Minimal user info for social features."""
|
||||
id: int
|
||||
open_id: str
|
||||
user_id: Optional[str] = None # 可分享的用户ID
|
||||
name: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
avatar: Optional[str] = None
|
||||
|
||||
Reference in New Issue
Block a user