v0.2 Preview: Construction Plan.
CI / test (push) Failing after 1m31s

This commit is contained in:
2026-08-08 21:25:27 +08:00
parent b88d15698a
commit 1f98f7152d
822 changed files with 96959 additions and 8 deletions
View File
+10
View File
@@ -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)
+6
View File
@@ -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'),
),
]
+68
View File
@@ -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"
)
]
+24
View File
@@ -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"},
)
+32
View File
@@ -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}
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+8
View File
@@ -0,0 +1,8 @@
from django.urls import path
from .views import ProgressionProfileView
urlpatterns = [
path("me/", ProgressionProfileView.as_view(), name="progression-profile"),
]
+44
View File
@@ -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")
],
}
)