feat: add invite code validity controls
- Add invite code starts/expires fields across contracts, API, Spacetime bindings, and admin UI - Enforce pending/expired invite code redemption behavior and expose admin status - Add admin write-operation confirmation guard and documentation - Add invite code contract/runtime tests
This commit is contained in:
@@ -11,6 +11,7 @@ import type {
|
||||
ProfileTaskCycle,
|
||||
TrackingScopeKind,
|
||||
} from '../api/adminApiTypes';
|
||||
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||
import {
|
||||
filterAdminTrackingEventDefinitions,
|
||||
findAdminTrackingEventDefinition,
|
||||
@@ -28,12 +29,7 @@ const taskCycles: Array<{value: ProfileTaskCycle; label: string}> = [
|
||||
{value: 'daily', label: '每日'},
|
||||
];
|
||||
|
||||
const scopeKinds: Array<{value: TrackingScopeKind; label: string}> = [
|
||||
{value: 'user', label: '用户'},
|
||||
{value: 'site', label: '整站'},
|
||||
{value: 'work', label: '作品'},
|
||||
{value: 'module', label: '模块'},
|
||||
];
|
||||
const profileTaskScopeKind = 'user' satisfies TrackingScopeKind;
|
||||
|
||||
export function AdminTaskConfigPage({
|
||||
token,
|
||||
@@ -49,7 +45,6 @@ export function AdminTaskConfigPage({
|
||||
const [eventKeySearch, setEventKeySearch] = useState('每日登录');
|
||||
const [isEventKeyPickerOpen, setIsEventKeyPickerOpen] = useState(false);
|
||||
const [cycle, setCycle] = useState<ProfileTaskCycle>('daily');
|
||||
const [scopeKind, setScopeKind] = useState<TrackingScopeKind>('user');
|
||||
const [threshold, setThreshold] = useState('1');
|
||||
const [rewardPoints, setRewardPoints] = useState('10');
|
||||
const [sortOrder, setSortOrder] = useState('10');
|
||||
@@ -61,6 +56,7 @@ export function AdminTaskConfigPage({
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDisabling, setIsDisabling] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
void refreshTaskConfigs();
|
||||
@@ -102,6 +98,14 @@ export function AdminTaskConfigPage({
|
||||
}
|
||||
|
||||
setErrorMessage('');
|
||||
const confirmed = await confirmWrite({
|
||||
action: '保存任务配置',
|
||||
target: taskId.trim(),
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const response = await upsertProfileTaskConfig(token, {
|
||||
@@ -110,7 +114,7 @@ export function AdminTaskConfigPage({
|
||||
description,
|
||||
eventKey: eventKey.trim(),
|
||||
cycle,
|
||||
scopeKind,
|
||||
scopeKind: profileTaskScopeKind,
|
||||
threshold: parsePositiveInteger(threshold),
|
||||
rewardPoints: parsePositiveInteger(rewardPoints),
|
||||
enabled,
|
||||
@@ -132,6 +136,14 @@ export function AdminTaskConfigPage({
|
||||
}
|
||||
|
||||
setDisableErrorMessage('');
|
||||
const confirmed = await confirmWrite({
|
||||
action: '停用任务配置',
|
||||
target: disableTaskId.trim(),
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDisabling(true);
|
||||
try {
|
||||
const response = await disableProfileTaskConfig(token, {
|
||||
@@ -165,7 +177,6 @@ export function AdminTaskConfigPage({
|
||||
setDescription(entry.description);
|
||||
setEventKey(entry.eventKey);
|
||||
setCycle(entry.cycle);
|
||||
setScopeKind(entry.scopeKind);
|
||||
setThreshold(String(entry.threshold));
|
||||
setRewardPoints(String(entry.rewardPoints));
|
||||
setSortOrder(String(entry.sortOrder));
|
||||
@@ -181,7 +192,6 @@ export function AdminTaskConfigPage({
|
||||
setEventKey(nextEventKey);
|
||||
if (nextDefinition) {
|
||||
setEventKeySearch(nextDefinition.title);
|
||||
setScopeKind(nextDefinition.scopeKind);
|
||||
} else {
|
||||
setEventKeySearch(nextEventKey);
|
||||
}
|
||||
@@ -349,21 +359,6 @@ export function AdminTaskConfigPage({
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="admin-field">
|
||||
<span>埋点范围</span>
|
||||
<select
|
||||
value={scopeKind}
|
||||
onChange={(event) =>
|
||||
setScopeKind(event.target.value as TrackingScopeKind)
|
||||
}
|
||||
>
|
||||
{scopeKinds.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="admin-form-row">
|
||||
@@ -530,6 +525,7 @@ export function AdminTaskConfigPage({
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{confirmDialog}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user