@@ -0,0 +1,10 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Card, RewardTransaction, UserAbility, UserCard, UserPet
|
||||
|
||||
|
||||
admin.site.register(UserPet)
|
||||
admin.site.register(UserAbility)
|
||||
admin.site.register(Card)
|
||||
admin.site.register(UserCard)
|
||||
admin.site.register(RewardTransaction)
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ProgressionConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'progression'
|
||||
@@ -0,0 +1,83 @@
|
||||
# Generated by Django 4.2.23 on 2026-08-08 11:15
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Card',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('name', models.CharField(max_length=80)),
|
||||
('mathematician', models.CharField(max_length=80)),
|
||||
('description', models.TextField()),
|
||||
('image', models.CharField(blank=True, max_length=200)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserPet',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(default='小葫芦', max_length=40)),
|
||||
('level', models.PositiveIntegerField(default=1)),
|
||||
('experience', models.PositiveIntegerField(default=0)),
|
||||
('appearance', models.JSONField(blank=True, default=dict)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='pet', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserCard',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('source', models.CharField(max_length=80)),
|
||||
('acquired_at', models.DateTimeField(auto_now_add=True)),
|
||||
('card', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='progression.card')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cards', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserAbility',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('dimension', models.CharField(choices=[('vision', '眼光'), ('humanities', '人文'), ('detection', '侦探'), ('modeling', '建模'), ('connection', '联结')], max_length=20)),
|
||||
('level', models.PositiveIntegerField(default=0)),
|
||||
('fragments', models.PositiveIntegerField(default=0)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='abilities', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RewardTransaction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('idempotency_key', models.CharField(max_length=100)),
|
||||
('source', models.CharField(max_length=80)),
|
||||
('rewards', models.JSONField(default=dict)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rewards', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='usercard',
|
||||
constraint=models.UniqueConstraint(fields=('user', 'card'), name='unique_user_card'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='userability',
|
||||
constraint=models.UniqueConstraint(fields=('user', 'dimension'), name='unique_user_ability'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='rewardtransaction',
|
||||
constraint=models.UniqueConstraint(fields=('user', 'idempotency_key'), name='unique_user_reward_transaction'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,68 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
||||
class UserPet(models.Model):
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="pet")
|
||||
name = models.CharField(max_length=40, default="小葫芦")
|
||||
level = models.PositiveIntegerField(default=1)
|
||||
experience = models.PositiveIntegerField(default=0)
|
||||
appearance = models.JSONField(default=dict, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
class UserAbility(models.Model):
|
||||
class Dimension(models.TextChoices):
|
||||
VISION = "vision", "眼光"
|
||||
HUMANITIES = "humanities", "人文"
|
||||
DETECTION = "detection", "侦探"
|
||||
MODELING = "modeling", "建模"
|
||||
CONNECTION = "connection", "联结"
|
||||
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="abilities")
|
||||
dimension = models.CharField(max_length=20, choices=Dimension.choices)
|
||||
level = models.PositiveIntegerField(default=0)
|
||||
fragments = models.PositiveIntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=("user", "dimension"), name="unique_user_ability")
|
||||
]
|
||||
|
||||
|
||||
class Card(models.Model):
|
||||
slug = models.SlugField(unique=True)
|
||||
name = models.CharField(max_length=80)
|
||||
mathematician = models.CharField(max_length=80)
|
||||
description = models.TextField()
|
||||
image = models.CharField(max_length=200, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class UserCard(models.Model):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="cards")
|
||||
card = models.ForeignKey(Card, on_delete=models.PROTECT)
|
||||
source = models.CharField(max_length=80)
|
||||
acquired_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=("user", "card"), name="unique_user_card")
|
||||
]
|
||||
|
||||
|
||||
class RewardTransaction(models.Model):
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="rewards")
|
||||
idempotency_key = models.CharField(max_length=100)
|
||||
source = models.CharField(max_length=80)
|
||||
rewards = models.JSONField(default=dict)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("user", "idempotency_key"), name="unique_user_reward_transaction"
|
||||
)
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
from django.db import transaction
|
||||
|
||||
from .models import Card, UserAbility, UserCard, UserPet
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def initialize_math_identity(user, identity):
|
||||
UserPet.objects.get_or_create(user=user)
|
||||
for dimension, _ in UserAbility.Dimension.choices:
|
||||
UserAbility.objects.get_or_create(user=user, dimension=dimension)
|
||||
card, _ = Card.objects.get_or_create(
|
||||
slug=f"mathbti-{identity.code}",
|
||||
defaults={
|
||||
"name": f"{identity.mathematician}人物卡",
|
||||
"mathematician": identity.mathematician,
|
||||
"description": identity.description,
|
||||
"image": identity.portrait,
|
||||
},
|
||||
)
|
||||
UserCard.objects.get_or_create(
|
||||
user=user,
|
||||
card=card,
|
||||
defaults={"source": "mathbti"},
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
import pytest
|
||||
|
||||
from accounts.models import User
|
||||
from math_life.models import MathIdentity
|
||||
from progression.models import Card, UserAbility, UserCard, UserPet
|
||||
from progression.services import initialize_math_identity
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_initialize_math_identity_重复调用保持精灵能力和人物卡唯一():
|
||||
user = User.objects.create_user(
|
||||
username="identity_user",
|
||||
password="StrongPass_2026",
|
||||
nickname="身份用户",
|
||||
)
|
||||
identity = MathIdentity.objects.create(
|
||||
code="1000",
|
||||
name="定理工匠",
|
||||
clan="信仰者",
|
||||
mathematician="高斯",
|
||||
description="严谨推导与纯粹热爱。",
|
||||
portrait="/static/img/mathematicians/1000.png",
|
||||
)
|
||||
|
||||
initialize_math_identity(user, identity)
|
||||
initialize_math_identity(user, identity)
|
||||
|
||||
assert UserPet.objects.filter(user=user).count() == 1
|
||||
assert UserAbility.objects.filter(user=user).count() == len(UserAbility.Dimension.choices)
|
||||
assert Card.objects.filter(slug="mathbti-1000").count() == 1
|
||||
assert UserCard.objects.filter(user=user, card__slug="mathbti-1000").count() == 1
|
||||
assert set(UserAbility.objects.filter(user=user).values_list("level", flat=True)) == {0}
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,8 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import ProgressionProfileView
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("me/", ProgressionProfileView.as_view(), name="progression-profile"),
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .models import UserAbility, UserPet
|
||||
|
||||
|
||||
class ProgressionProfileView(APIView):
|
||||
def get(self, request):
|
||||
pet, _ = UserPet.objects.get_or_create(user=request.user)
|
||||
existing = {ability.dimension: ability for ability in request.user.abilities.all()}
|
||||
abilities = []
|
||||
for dimension, label in UserAbility.Dimension.choices:
|
||||
ability = existing.get(dimension)
|
||||
if ability is None:
|
||||
ability = UserAbility.objects.create(user=request.user, dimension=dimension)
|
||||
abilities.append(
|
||||
{
|
||||
"dimension": dimension,
|
||||
"label": label,
|
||||
"level": ability.level,
|
||||
"fragments": ability.fragments,
|
||||
}
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"pet": {
|
||||
"name": pet.name,
|
||||
"level": pet.level,
|
||||
"experience": pet.experience,
|
||||
"appearance": pet.appearance,
|
||||
},
|
||||
"abilities": abilities,
|
||||
"cards": [
|
||||
{
|
||||
"slug": item.card.slug,
|
||||
"name": item.card.name,
|
||||
"mathematician": item.card.mathematician,
|
||||
"image": item.card.image,
|
||||
"acquired_at": item.acquired_at,
|
||||
}
|
||||
for item in request.user.cards.select_related("card")
|
||||
],
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user