Skip to content

Commit feb03b5

Browse files
authored
Add staged (阶段性) reward tiers to task module (#26)
1 parent 2ee0a1d commit feb03b5

13 files changed

Lines changed: 15564 additions & 4 deletions

File tree

apps/admin/src/components/task/DefinitionForm.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ import {
1313
SelectTrigger,
1414
SelectValue,
1515
} from "#/components/ui/select"
16-
import type { CreateDefinitionInput, TaskCategory } from "#/lib/types/task"
16+
import type {
17+
CreateDefinitionInput,
18+
TaskCategory,
19+
TaskRewardTier,
20+
} from "#/lib/types/task"
1721

1822
interface DefinitionFormProps {
1923
defaultValues?: Partial<CreateDefinitionInput>
@@ -48,12 +52,34 @@ export function DefinitionForm({
4852
isHidden: defaultValues?.isHidden ?? false,
4953
sortOrder: defaultValues?.sortOrder ?? 0,
5054
activityId: defaultValues?.activityId ?? (null as string | null),
55+
rewardTiersJson: JSON.stringify(
56+
defaultValues?.rewardTiers ?? [],
57+
null,
58+
2,
59+
),
5160
},
5261
onSubmit: async ({ value }) => {
5362
const isEventBased =
5463
value.countingMethod === "event_count" ||
5564
value.countingMethod === "event_value"
5665
const trimmedFilter = value.filter.trim()
66+
67+
let rewardTiers: TaskRewardTier[] = []
68+
const rewardTiersRaw = value.rewardTiersJson.trim()
69+
if (rewardTiersRaw.length > 0) {
70+
try {
71+
const parsed = JSON.parse(rewardTiersRaw) as unknown
72+
if (!Array.isArray(parsed)) {
73+
throw new Error("rewardTiers must be an array")
74+
}
75+
rewardTiers = parsed as TaskRewardTier[]
76+
} catch (err) {
77+
throw new Error(
78+
`阶段性奖励 JSON 解析失败: ${err instanceof Error ? err.message : String(err)}`,
79+
)
80+
}
81+
}
82+
5783
const input: CreateDefinitionInput = {
5884
name: value.name,
5985
alias: value.alias || null,
@@ -72,6 +98,7 @@ export function DefinitionForm({
7298
sortOrder: value.sortOrder,
7399
activityId: value.activityId,
74100
rewards: defaultValues?.rewards ?? [{ type: "item", id: "", count: 1 }],
101+
rewardTiers,
75102
}
76103
await onSubmit(input)
77104
},
@@ -384,6 +411,27 @@ export function DefinitionForm({
384411
)}
385412
</form.Field>
386413

414+
<form.Field name="rewardTiersJson">
415+
{(field) => (
416+
<div className="space-y-2">
417+
<Label htmlFor={field.name}>阶段性奖励 (JSON, 可留空)</Label>
418+
<Textarea
419+
id={field.name}
420+
value={field.state.value}
421+
onBlur={field.handleBlur}
422+
onChange={(e) => field.handleChange(e.target.value)}
423+
rows={5}
424+
className="font-mono text-xs"
425+
placeholder='[{"alias":"tier-1","threshold":3,"rewards":[{"type":"item","id":"gold-uuid","count":100}]}]'
426+
/>
427+
<p className="text-xs text-muted-foreground">
428+
到达 threshold 后,该阶段奖励独立结算(autoClaim 任务走邮件自动发放,否则走手动领取)。
429+
alias 必须唯一,threshold 必须严格递增且不超过 targetValue。
430+
</p>
431+
</div>
432+
)}
433+
</form.Field>
434+
387435
<Button type="submit" disabled={isPending}>
388436
{isPending ? m.common_saving() : submitLabel}
389437
</Button>

apps/admin/src/lib/types/task.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ export interface TaskNavigation {
1111
label?: string
1212
}
1313

14+
export interface TaskRewardTier {
15+
alias: string
16+
threshold: number
17+
rewards: ItemEntry[]
18+
}
19+
1420
export interface TaskCategory {
1521
id: string
1622
organizationId: string
@@ -46,6 +52,7 @@ export interface TaskDefinition {
4652
parentProgressValue: number
4753
prerequisiteTaskIds: string[]
4854
rewards: ItemEntry[]
55+
rewardTiers: TaskRewardTier[]
4956
autoClaim: boolean
5057
navigation: TaskNavigation | null
5158
isActive: boolean
@@ -89,6 +96,7 @@ export interface CreateDefinitionInput {
8996
parentProgressValue?: number
9097
prerequisiteTaskIds?: string[]
9198
rewards: ItemEntry[]
99+
rewardTiers?: TaskRewardTier[]
92100
autoClaim?: boolean
93101
navigation?: TaskNavigation | null
94102
isActive?: boolean
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
CREATE TABLE "task_user_milestone_claims" (
2+
"task_id" uuid NOT NULL,
3+
"end_user_id" text NOT NULL,
4+
"organization_id" text NOT NULL,
5+
"period_key" text NOT NULL,
6+
"tier_alias" text NOT NULL,
7+
"claimed_at" timestamp DEFAULT now() NOT NULL,
8+
CONSTRAINT "task_user_milestone_claims_pk" PRIMARY KEY("task_id","end_user_id","period_key","tier_alias")
9+
);
10+
--> statement-breakpoint
11+
ALTER TABLE "task_definitions" ADD COLUMN "reward_tiers" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
12+
ALTER TABLE "task_user_milestone_claims" ADD CONSTRAINT "task_user_milestone_claims_task_id_task_definitions_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."task_definitions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
13+
CREATE INDEX "task_user_milestone_claims_org_user_idx" ON "task_user_milestone_claims" USING btree ("organization_id","end_user_id");--> statement-breakpoint
14+
CREATE INDEX "task_user_milestone_claims_user_task_period_idx" ON "task_user_milestone_claims" USING btree ("end_user_id","task_id","period_key");

0 commit comments

Comments
 (0)