Commit 061dc393c5c24eb8dc941d841a329afbb82a9398
1 parent
92ffd6ee
Add T31: surface characters' totalSegments/remainingSegments/status
The schema field existed since T1 but nothing read or wrote it — admin-characters.html now shows a 片段配额 column (remainingSegments/ totalSegments, "-" for admin_seed/unlimited characters) so an operator can see mode 3's (T35) purchased-segment quota once it exists. No server.js change needed: PATCH already allows arbitrary field updates (mountCrud's isPatch branch), this is purely making the existing plumbing visible plus a round-trip test proving it actually holds the values mode 3 will write. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
Showing
2 changed files
with
40 additions
and
0 deletions
admin-characters.html
| ... | ... | @@ -69,6 +69,7 @@ |
| 69 | 69 | <th>默认</th> |
| 70 | 70 | <th>来源</th> |
| 71 | 71 | <th>状态</th> |
| 72 | + <th>片段配额</th> | |
| 72 | 73 | <th>创建时间</th> |
| 73 | 74 | <th>操作</th> |
| 74 | 75 | </tr> |
| ... | ... | @@ -97,6 +98,14 @@ function escapeHtml(s) { |
| 97 | 98 | return div.innerHTML; |
| 98 | 99 | } |
| 99 | 100 | |
| 101 | +// T31: totalSegments/remainingSegments are only ever non-null for | |
| 102 | +// source:"user_paid" characters (mode 3, T35) — admin_seed characters like | |
| 103 | +// the default one stay null forever (unlimited plays, not a purchased quota). | |
| 104 | +function segmentsLabel(c) { | |
| 105 | + if (c.totalSegments == null) return "-"; | |
| 106 | + return `${c.remainingSegments ?? 0}/${c.totalSegments}`; | |
| 107 | +} | |
| 108 | + | |
| 100 | 109 | async function uploadImage(file) { |
| 101 | 110 | const fd = new FormData(); |
| 102 | 111 | fd.append("file", file); |
| ... | ... | @@ -134,6 +143,7 @@ function render() { |
| 134 | 143 | <td><input type="checkbox" data-field="isDefault" ${c.isDefault ? "checked" : ""} /></td> |
| 135 | 144 | <td>${c.source}</td> |
| 136 | 145 | <td>${c.status}</td> |
| 146 | + <td>${segmentsLabel(c)}</td> | |
| 137 | 147 | <td>${new Date(c.createdAt).toLocaleDateString()}</td> |
| 138 | 148 | <td class="row-actions"> |
| 139 | 149 | <button data-save="${c.id}">保存</button> |
| ... | ... | @@ -150,6 +160,7 @@ function render() { |
| 150 | 160 | <td>${c.isDefault ? '<span class="default-badge">默认</span>' : ""}</td> |
| 151 | 161 | <td>${c.source}</td> |
| 152 | 162 | <td>${c.status}</td> |
| 163 | + <td>${segmentsLabel(c)}</td> | |
| 153 | 164 | <td>${new Date(c.createdAt).toLocaleDateString()}</td> |
| 154 | 165 | <td class="row-actions"> |
| 155 | 166 | <button class="secondary" data-edit="${c.id}">编辑</button> | ... | ... |
dressup-crud.test.js
| ... | ... | @@ -122,6 +122,35 @@ test("characters: setting isDefault on a second character clears it on the first |
| 122 | 122 | ); |
| 123 | 123 | }); |
| 124 | 124 | |
| 125 | +// T31 (dev-plan-5.md): totalSegments/remainingSegments/status were already | |
| 126 | +// part of the schema (T1) but never actually read/written until mode 3 | |
| 127 | +// (T35) exists. This just proves the plumbing (PATCH, admin.html's | |
| 128 | +// segmentsLabel()) has something real to render — admin_seed characters | |
| 129 | +// stay null/active by default (unaffected). | |
| 130 | +test("characters: totalSegments/remainingSegments/status round-trip via PATCH (mode 3's shape, before T35 exists)", async () => { | |
| 131 | + const created = await fetch( | |
| 132 | + `${BASE}/api/admin/characters`, | |
| 133 | + authed({ method: "POST", body: JSON.stringify({ name: "付费人物占位", imageUrl: "/x.png", appearanceDescriptor: "d" }) }) | |
| 134 | + ).then((r) => r.json()); | |
| 135 | + assert.equal(created.totalSegments, null); | |
| 136 | + assert.equal(created.remainingSegments, null); | |
| 137 | + assert.equal(created.status, "active"); | |
| 138 | + | |
| 139 | + const patched = await fetch( | |
| 140 | + `${BASE}/api/admin/characters/${created.id}`, | |
| 141 | + authed({ method: "PATCH", body: JSON.stringify({ totalSegments: 3, remainingSegments: 3, status: "active" }) }) | |
| 142 | + ).then((r) => r.json()); | |
| 143 | + assert.equal(patched.totalSegments, 3); | |
| 144 | + assert.equal(patched.remainingSegments, 3); | |
| 145 | + | |
| 146 | + const expired = await fetch( | |
| 147 | + `${BASE}/api/admin/characters/${created.id}`, | |
| 148 | + authed({ method: "PATCH", body: JSON.stringify({ remainingSegments: 0, status: "expired" }) }) | |
| 149 | + ).then((r) => r.json()); | |
| 150 | + assert.equal(expired.status, "expired"); | |
| 151 | + assert.equal(expired.totalSegments, 3, "totalSegments is history, not reset by going expired"); | |
| 152 | +}); | |
| 153 | + | |
| 125 | 154 | test("garments: create/list round-trip with defaults", async () => { |
| 126 | 155 | const res = await fetch( |
| 127 | 156 | `${BASE}/api/admin/garments`, | ... | ... |