feat: 支持充值商品配置和档位首充
This commit is contained in:
@@ -16,6 +16,14 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-05-15 充值商品配置入库且首充按档位独立计算
|
||||||
|
|
||||||
|
- 背景:泥点充值原来依赖代码中的固定商品目录,首充双倍也按账号是否买过任一泥点统一隐藏,导致用户买过 `points_60` 后其它未购买档位也失去首充展示。
|
||||||
|
- 决策:新增 `profile_recharge_product_config` 作为泥点和会员商品配置真相源,默认商品只在空库时播种;后台通过“充值商品”页维护配置。泥点首充资格按 `user_id + product_id` 的历史 `paid` 订单独立判断,`hasPointsRecharged` 仅保留为账号是否发生过任一泥点充值的兼容字段,不再驱动所有商品展示或结算。
|
||||||
|
- 影响范围:`module-runtime` 充值领域输入、`spacetime-module` 充值表与 procedure、`spacetime-client` bindings/facade、`api-server` `/admin/api/profile/recharge-products`、`apps/admin-web` 充值商品页、主站充值弹窗。
|
||||||
|
- 验证方式:执行 `cargo test -p module-runtime recharge --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、`npm run admin-web:typecheck`、`npm run check:spacetime-schema`。
|
||||||
|
- 关联文档:`docs/technical/MY_TAB_ACCOUNT_RECHARGE_IMPLEMENTATION_2026-04-25.md`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`。
|
||||||
|
|
||||||
## 2026-05-14 创作页图像输入统一封装为图像组件
|
## 2026-05-14 创作页图像输入统一封装为图像组件
|
||||||
|
|
||||||
- 背景:拼图创作页已经具备“画面描述生图 / 多参考图生图 / 上传主图后 AI 重绘 / 上传主图后不重绘”四条路径,抓大鹅封面和后续创作页也会复用同一套交互;继续在页面内复制会导致参考图、预览、删除确认和重绘开关漂移。
|
- 背景:拼图创作页已经具备“画面描述生图 / 多参考图生图 / 上传主图后 AI 重绘 / 上传主图后不重绘”四条路径,抓大鹅封面和后续创作页也会复用同一套交互;继续在页面内复制会导致参考图、预览、删除确认和重绘开关漂移。
|
||||||
|
|||||||
@@ -286,6 +286,14 @@
|
|||||||
- 验证:发布前运行 `npm run check:spacetime-schema`,完成 schema 检查、bindings 生成、表目录更新和相关 smoke。
|
- 验证:发布前运行 `npm run check:spacetime-schema`,完成 schema 检查、bindings 生成、表目录更新和相关 smoke。
|
||||||
- 关联:`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`。
|
- 关联:`docs/technical/SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`。
|
||||||
|
|
||||||
|
## SpacetimeDB schema guard 新增表应校验 sidecar,不应把新增本身当失败
|
||||||
|
|
||||||
|
- 现象:新增 SpacetimeDB 表后,schema guard 只要看到新 table accessor 就直接报错,哪怕 `migration.rs`、表目录和生成绑定都已同步。
|
||||||
|
- 原因:守卫脚本把“发现新增表”本身当成失败,而不是把它当成需要校验 sidecar 的信号。
|
||||||
|
- 处理:新增表只应触发 `migration.rs`、`SPACETIMEDB_TABLE_CATALOG.md` 和 bindings 的一致性检查;当 sidecar 都已同步时,新增表应允许通过。不要把“合法新增表”本身判成失败。
|
||||||
|
- 验证:`npm run check:spacetime-schema` 在新增表、迁移、目录和绑定都同步后应通过。
|
||||||
|
- 关联:`scripts/check-spacetime-schema-guard.mjs`、`server-rs/crates/spacetime-module/src/migration.rs`、`docs/technical/SPACETIMEDB_TABLE_CATALOG.md`。
|
||||||
|
|
||||||
## SpacetimeDB publish 报 wasm-bindgen 时先查 shared-contracts feature
|
## SpacetimeDB publish 报 wasm-bindgen 时先查 shared-contracts feature
|
||||||
|
|
||||||
- 现象:发布 `spacetime-module` 时报 `wasm-bindgen detected`,提示 `wasm-bindgen is only for webassembly modules that target the web platform`。
|
- 现象:发布 `spacetime-module` 时报 `wasm-bindgen detected`,提示 `wasm-bindgen is only for webassembly modules that target the web platform`。
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
AdminTrackingEventListQuery,
|
AdminTrackingEventListQuery,
|
||||||
AdminTrackingEventListResponse,
|
AdminTrackingEventListResponse,
|
||||||
AdminUpsertProfileInviteCodeRequest,
|
AdminUpsertProfileInviteCodeRequest,
|
||||||
|
AdminUpsertProfileRechargeProductRequest,
|
||||||
AdminUpsertProfileRedeemCodeRequest,
|
AdminUpsertProfileRedeemCodeRequest,
|
||||||
AdminUpsertProfileTaskConfigRequest,
|
AdminUpsertProfileTaskConfigRequest,
|
||||||
ApiErrorEnvelope,
|
ApiErrorEnvelope,
|
||||||
@@ -21,6 +22,8 @@ import type {
|
|||||||
ApiSuccessEnvelope,
|
ApiSuccessEnvelope,
|
||||||
ProfileInviteCodeAdminListResponse,
|
ProfileInviteCodeAdminListResponse,
|
||||||
ProfileInviteCodeAdminResponse,
|
ProfileInviteCodeAdminResponse,
|
||||||
|
ProfileRechargeProductConfigAdminListResponse,
|
||||||
|
ProfileRechargeProductConfigAdminResponse,
|
||||||
ProfileRedeemCodeAdminListResponse,
|
ProfileRedeemCodeAdminListResponse,
|
||||||
ProfileRedeemCodeAdminResponse,
|
ProfileRedeemCodeAdminResponse,
|
||||||
ProfileTaskConfigAdminListResponse,
|
ProfileTaskConfigAdminListResponse,
|
||||||
@@ -279,6 +282,27 @@ export function disableProfileTaskConfig(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listProfileRechargeProducts(token: string) {
|
||||||
|
return request<ProfileRechargeProductConfigAdminListResponse>(
|
||||||
|
'/admin/api/profile/recharge-products',
|
||||||
|
{token},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function upsertProfileRechargeProduct(
|
||||||
|
token: string,
|
||||||
|
payload: AdminUpsertProfileRechargeProductRequest,
|
||||||
|
) {
|
||||||
|
return request<ProfileRechargeProductConfigAdminResponse>(
|
||||||
|
'/admin/api/profile/recharge-products',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: payload,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeBaseUrl(value: string) {
|
function normalizeBaseUrl(value: string) {
|
||||||
return value.trim().replace(/\/+$/, '');
|
return value.trim().replace(/\/+$/, '');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ export interface AdminDebugHttpResponse {
|
|||||||
export type ProfileRedeemCodeMode = 'public' | 'unique' | 'private';
|
export type ProfileRedeemCodeMode = 'public' | 'unique' | 'private';
|
||||||
export type ProfileTaskCycle = 'daily';
|
export type ProfileTaskCycle = 'daily';
|
||||||
export type TrackingScopeKind = 'site' | 'work' | 'module' | 'user';
|
export type TrackingScopeKind = 'site' | 'work' | 'module' | 'user';
|
||||||
|
export type ProfileRechargeProductKind = 'points' | 'membership';
|
||||||
|
export type ProfileMembershipTier = 'normal' | 'month' | 'season' | 'year';
|
||||||
|
|
||||||
export interface AdminTrackingEventListQuery {
|
export interface AdminTrackingEventListQuery {
|
||||||
eventKey?: string;
|
eventKey?: string;
|
||||||
@@ -207,6 +209,21 @@ export interface AdminDisableProfileTaskConfigRequest {
|
|||||||
taskId: string;
|
taskId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminUpsertProfileRechargeProductRequest {
|
||||||
|
productId: string;
|
||||||
|
title: string;
|
||||||
|
priceCents: number;
|
||||||
|
kind: ProfileRechargeProductKind;
|
||||||
|
pointsAmount: number;
|
||||||
|
bonusPoints: number;
|
||||||
|
durationDays: number;
|
||||||
|
badgeLabel?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
tier: ProfileMembershipTier;
|
||||||
|
enabled: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProfileRedeemCodeAdminResponse {
|
export interface ProfileRedeemCodeAdminResponse {
|
||||||
code: string;
|
code: string;
|
||||||
mode: ProfileRedeemCodeMode;
|
mode: ProfileRedeemCodeMode;
|
||||||
@@ -260,6 +277,29 @@ export interface ProfileTaskConfigAdminListResponse {
|
|||||||
entries: ProfileTaskConfigAdminResponse[];
|
entries: ProfileTaskConfigAdminResponse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProfileRechargeProductConfigAdminResponse {
|
||||||
|
productId: string;
|
||||||
|
title: string;
|
||||||
|
priceCents: number;
|
||||||
|
kind: ProfileRechargeProductKind;
|
||||||
|
pointsAmount: number;
|
||||||
|
bonusPoints: number;
|
||||||
|
durationDays: number;
|
||||||
|
badgeLabel: string;
|
||||||
|
description: string;
|
||||||
|
tier: ProfileMembershipTier;
|
||||||
|
enabled: boolean;
|
||||||
|
sortOrder: number;
|
||||||
|
createdBy: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedBy: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProfileRechargeProductConfigAdminListResponse {
|
||||||
|
entries: ProfileRechargeProductConfigAdminResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminTrackingEventEntryPayload {
|
export interface AdminTrackingEventEntryPayload {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
eventKey: string;
|
eventKey: string;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
AdminSessionPayload,
|
AdminSessionPayload,
|
||||||
ProfileInviteCodeAdminResponse,
|
ProfileInviteCodeAdminResponse,
|
||||||
|
ProfileRechargeProductConfigAdminResponse,
|
||||||
ProfileRedeemCodeAdminResponse,
|
ProfileRedeemCodeAdminResponse,
|
||||||
ProfileTaskConfigAdminResponse,
|
ProfileTaskConfigAdminResponse,
|
||||||
} from '../api/adminApiTypes';
|
} from '../api/adminApiTypes';
|
||||||
@@ -23,6 +24,7 @@ import {AdminDatabaseTablesPage} from '../pages/AdminDatabaseTablesPage';
|
|||||||
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
|
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
|
||||||
import {AdminLoginPage} from '../pages/AdminLoginPage';
|
import {AdminLoginPage} from '../pages/AdminLoginPage';
|
||||||
import {AdminOverviewPage} from '../pages/AdminOverviewPage';
|
import {AdminOverviewPage} from '../pages/AdminOverviewPage';
|
||||||
|
import {AdminRechargeProductPage} from '../pages/AdminRechargeProductPage';
|
||||||
import {AdminRedeemCodePage} from '../pages/AdminRedeemCodePage';
|
import {AdminRedeemCodePage} from '../pages/AdminRedeemCodePage';
|
||||||
import {AdminTaskConfigPage} from '../pages/AdminTaskConfigPage';
|
import {AdminTaskConfigPage} from '../pages/AdminTaskConfigPage';
|
||||||
import {AdminTrackingEventsPage} from '../pages/AdminTrackingEventsPage';
|
import {AdminTrackingEventsPage} from '../pages/AdminTrackingEventsPage';
|
||||||
@@ -47,6 +49,8 @@ export function AdminApp() {
|
|||||||
useState<ProfileInviteCodeAdminResponse | null>(null);
|
useState<ProfileInviteCodeAdminResponse | null>(null);
|
||||||
const [taskConfigResult, setTaskConfigResult] =
|
const [taskConfigResult, setTaskConfigResult] =
|
||||||
useState<ProfileTaskConfigAdminResponse | null>(null);
|
useState<ProfileTaskConfigAdminResponse | null>(null);
|
||||||
|
const [rechargeProductResult, setRechargeProductResult] =
|
||||||
|
useState<ProfileRechargeProductConfigAdminResponse | null>(null);
|
||||||
|
|
||||||
const clearSession = useCallback((message = '') => {
|
const clearSession = useCallback((message = '') => {
|
||||||
clearStoredAdminToken();
|
clearStoredAdminToken();
|
||||||
@@ -55,6 +59,7 @@ export function AdminApp() {
|
|||||||
setRedeemResult(null);
|
setRedeemResult(null);
|
||||||
setInviteResult(null);
|
setInviteResult(null);
|
||||||
setTaskConfigResult(null);
|
setTaskConfigResult(null);
|
||||||
|
setRechargeProductResult(null);
|
||||||
setStatus('guest');
|
setStatus('guest');
|
||||||
setLoginNotice(message);
|
setLoginNotice(message);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -124,6 +129,7 @@ export function AdminApp() {
|
|||||||
setRedeemResult(null);
|
setRedeemResult(null);
|
||||||
setInviteResult(null);
|
setInviteResult(null);
|
||||||
setTaskConfigResult(null);
|
setTaskConfigResult(null);
|
||||||
|
setRechargeProductResult(null);
|
||||||
setLoginNotice('');
|
setLoginNotice('');
|
||||||
setStatus('authenticated');
|
setStatus('authenticated');
|
||||||
}, []);
|
}, []);
|
||||||
@@ -207,6 +213,14 @@ export function AdminApp() {
|
|||||||
onResultChange={setTaskConfigResult}
|
onResultChange={setTaskConfigResult}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{routeId === 'recharge-products' ? (
|
||||||
|
<AdminRechargeProductPage
|
||||||
|
result={rechargeProductResult}
|
||||||
|
token={token}
|
||||||
|
onUnauthorized={handleUnauthorized}
|
||||||
|
onResultChange={setRechargeProductResult}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</AdminShell>
|
</AdminShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Bug,
|
Bug,
|
||||||
|
BadgeDollarSign,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
LogOut,
|
LogOut,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
@@ -32,6 +33,7 @@ const routeIcons = {
|
|||||||
redeem: TicketPercent,
|
redeem: TicketPercent,
|
||||||
invite: TicketCheck,
|
invite: TicketCheck,
|
||||||
tasks: ListChecks,
|
tasks: ListChecks,
|
||||||
|
'recharge-products': BadgeDollarSign,
|
||||||
'creation-entry': SlidersHorizontal,
|
'creation-entry': SlidersHorizontal,
|
||||||
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
|
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export type AdminRouteId =
|
|||||||
| 'redeem'
|
| 'redeem'
|
||||||
| 'invite'
|
| 'invite'
|
||||||
| 'tasks'
|
| 'tasks'
|
||||||
|
| 'recharge-products'
|
||||||
| 'creation-entry';
|
| 'creation-entry';
|
||||||
|
|
||||||
export interface AdminRouteDefinition {
|
export interface AdminRouteDefinition {
|
||||||
@@ -22,6 +23,7 @@ export const adminRoutes: AdminRouteDefinition[] = [
|
|||||||
{id: 'redeem', label: '兑换码', hash: '#redeem'},
|
{id: 'redeem', label: '兑换码', hash: '#redeem'},
|
||||||
{id: 'invite', label: '邀请码', hash: '#invite'},
|
{id: 'invite', label: '邀请码', hash: '#invite'},
|
||||||
{id: 'tasks', label: '任务配置', hash: '#tasks'},
|
{id: 'tasks', label: '任务配置', hash: '#tasks'},
|
||||||
|
{id: 'recharge-products', label: '充值商品', hash: '#recharge-products'},
|
||||||
{id: 'creation-entry', label: '入口开关', hash: '#creation-entry'},
|
{id: 'creation-entry', label: '入口开关', hash: '#creation-entry'},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
451
apps/admin-web/src/pages/AdminRechargeProductPage.tsx
Normal file
451
apps/admin-web/src/pages/AdminRechargeProductPage.tsx
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
import {RefreshCcw, Save} from 'lucide-react';
|
||||||
|
import {FormEvent, useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
listProfileRechargeProducts,
|
||||||
|
upsertProfileRechargeProduct,
|
||||||
|
} from '../api/adminApiClient';
|
||||||
|
import type {
|
||||||
|
ProfileMembershipTier,
|
||||||
|
ProfileRechargeProductConfigAdminResponse,
|
||||||
|
ProfileRechargeProductKind,
|
||||||
|
} from '../api/adminApiTypes';
|
||||||
|
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||||
|
import {handlePageError} from './pageUtils';
|
||||||
|
|
||||||
|
interface AdminRechargeProductPageProps {
|
||||||
|
token: string;
|
||||||
|
result: ProfileRechargeProductConfigAdminResponse | null;
|
||||||
|
onUnauthorized: (message?: string) => void;
|
||||||
|
onResultChange: (result: ProfileRechargeProductConfigAdminResponse) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const productKinds: Array<{value: ProfileRechargeProductKind; label: string}> = [
|
||||||
|
{value: 'points', label: '泥点'},
|
||||||
|
{value: 'membership', label: '会员'},
|
||||||
|
];
|
||||||
|
|
||||||
|
const membershipTiers: Array<{value: ProfileMembershipTier; label: string}> = [
|
||||||
|
{value: 'month', label: '月卡'},
|
||||||
|
{value: 'season', label: '季卡'},
|
||||||
|
{value: 'year', label: '年卡'},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function AdminRechargeProductPage({
|
||||||
|
token,
|
||||||
|
result,
|
||||||
|
onUnauthorized,
|
||||||
|
onResultChange,
|
||||||
|
}: AdminRechargeProductPageProps) {
|
||||||
|
const [entries, setEntries] = useState<
|
||||||
|
ProfileRechargeProductConfigAdminResponse[]
|
||||||
|
>([]);
|
||||||
|
const [productId, setProductId] = useState('points_60');
|
||||||
|
const [title, setTitle] = useState('60泥点');
|
||||||
|
const [priceCents, setPriceCents] = useState('600');
|
||||||
|
const [kind, setKind] = useState<ProfileRechargeProductKind>('points');
|
||||||
|
const [pointsAmount, setPointsAmount] = useState('60');
|
||||||
|
const [bonusPoints, setBonusPoints] = useState('60');
|
||||||
|
const [durationDays, setDurationDays] = useState('0');
|
||||||
|
const [badgeLabel, setBadgeLabel] = useState('首充双倍');
|
||||||
|
const [description, setDescription] = useState('首充送60泥点');
|
||||||
|
const [tier, setTier] = useState<ProfileMembershipTier>('normal');
|
||||||
|
const [enabled, setEnabled] = useState(true);
|
||||||
|
const [sortOrder, setSortOrder] = useState('0');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [listErrorMessage, setListErrorMessage] = useState('');
|
||||||
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refreshProducts();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
async function refreshProducts() {
|
||||||
|
setIsLoading(true);
|
||||||
|
setListErrorMessage('');
|
||||||
|
try {
|
||||||
|
const response = await listProfileRechargeProducts(token);
|
||||||
|
const sortedEntries = sortProducts(response.entries);
|
||||||
|
setEntries(sortedEntries);
|
||||||
|
const firstEntry = sortedEntries[0];
|
||||||
|
if (firstEntry) {
|
||||||
|
fillForm(firstEntry);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
handlePageError(error, onUnauthorized, setListErrorMessage);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave(event: FormEvent<HTMLFormElement>) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (isSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorMessage('');
|
||||||
|
const confirmed = await confirmWrite({
|
||||||
|
action: enabled ? '保存充值商品' : '停用充值商品',
|
||||||
|
target: productId.trim(),
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
const response = await upsertProfileRechargeProduct(token, {
|
||||||
|
productId: productId.trim(),
|
||||||
|
title: title.trim(),
|
||||||
|
priceCents: parsePositiveInteger(priceCents),
|
||||||
|
kind,
|
||||||
|
pointsAmount: kind === 'points' ? parsePositiveInteger(pointsAmount) : 0,
|
||||||
|
bonusPoints: kind === 'points' ? parseNonNegativeInteger(bonusPoints) : 0,
|
||||||
|
durationDays:
|
||||||
|
kind === 'membership' ? parsePositiveInteger(durationDays) : 0,
|
||||||
|
badgeLabel: kind === 'points' ? badgeLabel.trim() : '',
|
||||||
|
description: description.trim(),
|
||||||
|
tier: kind === 'membership' ? tier : 'normal',
|
||||||
|
enabled,
|
||||||
|
sortOrder: parseInteger(sortOrder),
|
||||||
|
});
|
||||||
|
onResultChange(response);
|
||||||
|
upsertEntry(response);
|
||||||
|
fillForm(response);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
handlePageError(error, onUnauthorized, setErrorMessage);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertEntry(next: ProfileRechargeProductConfigAdminResponse) {
|
||||||
|
setEntries((current) => {
|
||||||
|
const rest = current.filter((entry) => entry.productId !== next.productId);
|
||||||
|
return sortProducts([...rest, next]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillForm(entry: ProfileRechargeProductConfigAdminResponse) {
|
||||||
|
setProductId(entry.productId);
|
||||||
|
setTitle(entry.title);
|
||||||
|
setPriceCents(String(entry.priceCents));
|
||||||
|
setKind(entry.kind);
|
||||||
|
setPointsAmount(String(entry.pointsAmount));
|
||||||
|
setBonusPoints(String(entry.bonusPoints));
|
||||||
|
setDurationDays(String(entry.durationDays));
|
||||||
|
setBadgeLabel(entry.badgeLabel);
|
||||||
|
setDescription(entry.description);
|
||||||
|
setTier(entry.tier);
|
||||||
|
setEnabled(entry.enabled);
|
||||||
|
setSortOrder(String(entry.sortOrder));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="admin-page admin-page-wide">
|
||||||
|
<div className="admin-page-heading">
|
||||||
|
<div>
|
||||||
|
<h2>充值商品</h2>
|
||||||
|
<p>泥点与会员档位</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="admin-secondary-button"
|
||||||
|
disabled={isLoading}
|
||||||
|
type="button"
|
||||||
|
onClick={refreshProducts}
|
||||||
|
>
|
||||||
|
<RefreshCcw size={17} aria-hidden="true" />
|
||||||
|
<span>{isLoading ? '刷新中' : '刷新'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{listErrorMessage ? (
|
||||||
|
<div className="admin-alert" role="status">
|
||||||
|
{listErrorMessage}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="admin-two-column admin-two-column-wide">
|
||||||
|
<form className="admin-panel admin-form" onSubmit={handleSave}>
|
||||||
|
<div className="admin-form-row">
|
||||||
|
<label className="admin-field admin-field-fill">
|
||||||
|
<span>Product ID</span>
|
||||||
|
<input
|
||||||
|
value={productId}
|
||||||
|
onChange={(event) => setProductId(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="admin-switch-field">
|
||||||
|
<input
|
||||||
|
checked={enabled}
|
||||||
|
type="checkbox"
|
||||||
|
onChange={(event) => setEnabled(event.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>启用</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="admin-segmented-control" role="tablist">
|
||||||
|
{productKinds.map((item) => (
|
||||||
|
<button
|
||||||
|
data-active={kind === item.value}
|
||||||
|
key={item.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setKind(item.value);
|
||||||
|
if (item.value === 'points') {
|
||||||
|
setTier('normal');
|
||||||
|
setDurationDays('0');
|
||||||
|
} else {
|
||||||
|
setBonusPoints('0');
|
||||||
|
setPointsAmount('0');
|
||||||
|
setTier(tier === 'normal' ? 'month' : tier);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="admin-form-row">
|
||||||
|
<label className="admin-field">
|
||||||
|
<span>标题</span>
|
||||||
|
<input
|
||||||
|
value={title}
|
||||||
|
onChange={(event) => setTitle(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="admin-field">
|
||||||
|
<span>价格分</span>
|
||||||
|
<input
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
type="number"
|
||||||
|
value={priceCents}
|
||||||
|
onChange={(event) => setPriceCents(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{kind === 'points' ? (
|
||||||
|
<div className="admin-form-row">
|
||||||
|
<label className="admin-field">
|
||||||
|
<span>基础泥点</span>
|
||||||
|
<input
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
type="number"
|
||||||
|
value={pointsAmount}
|
||||||
|
onChange={(event) => setPointsAmount(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="admin-field">
|
||||||
|
<span>首充赠送</span>
|
||||||
|
<input
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
type="number"
|
||||||
|
value={bonusPoints}
|
||||||
|
onChange={(event) => setBonusPoints(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="admin-form-row">
|
||||||
|
<label className="admin-field">
|
||||||
|
<span>会员档位</span>
|
||||||
|
<select
|
||||||
|
value={tier}
|
||||||
|
onChange={(event) =>
|
||||||
|
setTier(event.target.value as ProfileMembershipTier)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{membershipTiers.map((item) => (
|
||||||
|
<option key={item.value} value={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="admin-field">
|
||||||
|
<span>有效天数</span>
|
||||||
|
<input
|
||||||
|
min={1}
|
||||||
|
step={1}
|
||||||
|
type="number"
|
||||||
|
value={durationDays}
|
||||||
|
onChange={(event) => setDurationDays(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="admin-form-row">
|
||||||
|
<label className="admin-field">
|
||||||
|
<span>角标</span>
|
||||||
|
<input
|
||||||
|
disabled={kind !== 'points'}
|
||||||
|
value={badgeLabel}
|
||||||
|
onChange={(event) => setBadgeLabel(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="admin-field">
|
||||||
|
<span>排序</span>
|
||||||
|
<input
|
||||||
|
inputMode="numeric"
|
||||||
|
value={sortOrder}
|
||||||
|
onChange={(event) => setSortOrder(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="admin-field">
|
||||||
|
<span>描述</span>
|
||||||
|
<input
|
||||||
|
value={description}
|
||||||
|
onChange={(event) => setDescription(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{errorMessage ? (
|
||||||
|
<div className="admin-alert" role="status">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="admin-primary-button"
|
||||||
|
disabled={isSaving || !productId.trim() || !title.trim()}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<Save size={17} aria-hidden="true" />
|
||||||
|
<span>{isSaving ? '保存中' : '保存'}</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="admin-stack">
|
||||||
|
<section className="admin-panel">
|
||||||
|
<div className="admin-panel-heading">
|
||||||
|
<h3>商品列表</h3>
|
||||||
|
<span>{entries.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="admin-table-wrap">
|
||||||
|
<table className="admin-table admin-table-compact">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>商品</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>价格</th>
|
||||||
|
<th>内容</th>
|
||||||
|
<th>状态</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{entries.map((entry) => (
|
||||||
|
<tr key={entry.productId}>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
className="admin-text-button"
|
||||||
|
type="button"
|
||||||
|
onClick={() => fillForm(entry)}
|
||||||
|
>
|
||||||
|
{entry.title || entry.productId}
|
||||||
|
</button>
|
||||||
|
<small>{entry.productId}</small>
|
||||||
|
</td>
|
||||||
|
<td>{formatProductKind(entry.kind)}</td>
|
||||||
|
<td>{formatPrice(entry.priceCents)}</td>
|
||||||
|
<td>{formatProductContent(entry)}</td>
|
||||||
|
<td>{entry.enabled ? '启用' : '停用'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{result ? (
|
||||||
|
<section className="admin-panel">
|
||||||
|
<div className="admin-panel-heading">
|
||||||
|
<h3>最近保存</h3>
|
||||||
|
<span>{result.updatedAt}</span>
|
||||||
|
</div>
|
||||||
|
<dl className="admin-info-list">
|
||||||
|
<div>
|
||||||
|
<dt>商品</dt>
|
||||||
|
<dd>{result.productId}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>状态</dt>
|
||||||
|
<dd>{result.enabled ? '启用' : '停用'}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{confirmDialog}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortProducts(entries: ProfileRechargeProductConfigAdminResponse[]) {
|
||||||
|
return [...entries].sort((left, right) => {
|
||||||
|
if (left.sortOrder !== right.sortOrder) {
|
||||||
|
return left.sortOrder - right.sortOrder;
|
||||||
|
}
|
||||||
|
return left.productId.localeCompare(right.productId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatProductKind(kind: ProfileRechargeProductKind) {
|
||||||
|
return kind === 'points' ? '泥点' : '会员';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTier(tier: ProfileMembershipTier) {
|
||||||
|
if (tier === 'month') {
|
||||||
|
return '月卡';
|
||||||
|
}
|
||||||
|
if (tier === 'season') {
|
||||||
|
return '季卡';
|
||||||
|
}
|
||||||
|
if (tier === 'year') {
|
||||||
|
return '年卡';
|
||||||
|
}
|
||||||
|
return '普通';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatProductContent(entry: ProfileRechargeProductConfigAdminResponse) {
|
||||||
|
if (entry.kind === 'points') {
|
||||||
|
return `${entry.pointsAmount}+${entry.bonusPoints}`;
|
||||||
|
}
|
||||||
|
return `${formatTier(entry.tier)} ${entry.durationDays}天`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPrice(priceCents: number) {
|
||||||
|
return `¥${(priceCents / 100).toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePositiveInteger(value: string) {
|
||||||
|
const parsed = parseInteger(value);
|
||||||
|
return parsed > 0 ? parsed : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNonNegativeInteger(value: string) {
|
||||||
|
const parsed = parseInteger(value);
|
||||||
|
return parsed > 0 ? parsed : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseInteger(value: string) {
|
||||||
|
const parsed = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isFinite(parsed)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
@@ -24,9 +24,13 @@
|
|||||||
| `points_1280` | 1280 | 12800 | 首充双倍 | 首充送1280泥点 |
|
| `points_1280` | 1280 | 12800 | 首充双倍 | 首充送1280泥点 |
|
||||||
| `points_3280` | 3280 | 32800 | 首充双倍 | 首充送3280泥点 |
|
| `points_3280` | 3280 | 32800 | 首充双倍 | 首充送3280泥点 |
|
||||||
|
|
||||||
泥点充值固定为 `¥6 / ¥18 / ¥30 / ¥68 / ¥128 / ¥328` 六个档位。全部档位参与首充双倍:用户历史上没有 `points_recharge` 流水时,本次购买到账泥点为基础泥点与等额赠送泥点之和;已有充值流水后只到账基础泥点。实际到账泥点写入交易流水,余额以 SpacetimeDB projection 为准。
|
泥点充值默认初始化 `¥6 / ¥18 / ¥30 / ¥68 / ¥128 / ¥328` 六个档位。默认档位只作为空库种子写入 `profile_recharge_product_config`,运行时充值中心展示、下单校验和支付确认结算都以 SpacetimeDB 配置表为准,不再把代码中的商品目录作为业务真相源。
|
||||||
|
|
||||||
充值中心返回的 `hasPointsRecharged` 是首充资格的展示与结算共同依据:当它为 `true` 时,后端下发的泥点套餐应只保留基础泥点、清空首充徽标与赠送文案;前端即使收到旧版本快照中残留的 `bonusPoints` / `badgeLabel`,也必须按 `hasPointsRecharged` 隐藏“首充双倍”和 `基础+赠送` 展示。这样可以避免第二次充值只到账基础泥点时,弹窗仍显示 `60+60` 等已失效权益。
|
全部泥点档位参与档位首充双倍:首充资格按 `productId` 独立判断。用户购买过 `points_60` 后,再次购买 `points_60` 只到账基础泥点;但 `points_180`、`points_300` 等未购买过的档位仍保留各自的首充赠送。实际到账泥点写入 `profile_recharge_order.points_delta` 与钱包流水,余额以 SpacetimeDB projection 为准。
|
||||||
|
|
||||||
|
充值中心返回的 `pointProducts` 已由后端按当前账号和每个 `productId` 计算有效展示状态:已完成首充的档位清空 `bonusPoints`、`badgeLabel` 与首充说明,未完成首充的档位继续显示 `首充双倍` 和 `基础+赠送`。`hasPointsRecharged` 仅保留为兼容字段,表示账号是否发生过任一泥点充值,不再作为隐藏所有档位首充或计算结算金额的依据。前端不得再用 `hasPointsRecharged` 对所有泥点商品做统一屏蔽。
|
||||||
|
|
||||||
|
后台通过“充值商品”页维护 `profile_recharge_product_config`,字段包括 `productId`、标题、商品类型、金额分、基础泥点、首充赠送泥点、会员天数、徽标、说明、会员层级、启用状态和排序。保存后新的充值中心快照、下单与支付确认立即读取配置表;历史订单继续保留下单当时写入的商品标题、金额和状态。
|
||||||
|
|
||||||
### 2.2 会员卡套餐
|
### 2.2 会员卡套餐
|
||||||
|
|
||||||
@@ -157,7 +161,7 @@
|
|||||||
|
|
||||||
1. 普通用户打开弹窗能看到泥点与会员套餐。
|
1. 普通用户打开弹窗能看到泥点与会员套餐。
|
||||||
2. 泥点购买后余额增加,流水来源为 `points_recharge`。
|
2. 泥点购买后余额增加,流水来源为 `points_recharge`。
|
||||||
3. 首充赠送只在首次泥点充值时生效。
|
3. 首充赠送按泥点档位独立生效。
|
||||||
4. 已产生 `points_recharge` 流水后,再打开充值弹窗不应展示“首充双倍”徽标或 `60+60` 等赠送泥点组合。
|
4. 某个 `productId` 已成功完成泥点充值后,再打开充值弹窗时仅该档位不再展示“首充双倍”徽标或 `60+60` 等赠送泥点组合,其他未购买过的泥点档位仍展示各自首充权益。
|
||||||
5. 会员购买后会员状态与到期时间立即更新。
|
5. 会员购买后会员状态与到期时间立即更新。
|
||||||
6. 移动端弹窗单列可滚动,桌面端接近参考图卡片网格。
|
6. 移动端弹窗单列可滚动,桌面端接近参考图卡片网格。
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ spacetime sql <db> "SELECT * FROM custom_world_gallery_entry"
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| 运维迁移 | `database_migration_operator`, `database_migration_import_chunk` |
|
| 运维迁移 | `database_migration_operator`, `database_migration_import_chunk` |
|
||||||
| 认证 | `auth_store_snapshot`, `auth_store_projection_meta`, `user_account`, `auth_identity`, `refresh_session` |
|
| 认证 | `auth_store_snapshot`, `auth_store_projection_meta`, `user_account`, `auth_identity`, `refresh_session` |
|
||||||
| 运行时档案 | `runtime_setting`, `runtime_snapshot`, `user_browse_history`, `profile_dashboard_state`, `profile_wallet_ledger`, `analytics_date_dimension`, `tracking_event`, `tracking_daily_stat`, `profile_task_config`, `profile_task_progress`, `profile_task_reward_claim`, `profile_redeem_code`, `profile_redeem_code_usage`, `profile_invite_code`, `profile_referral_relation`, `profile_played_world`, `profile_membership`, `profile_recharge_order`, `profile_feedback_submission`, `profile_save_archive` |
|
| 运行时档案 | `runtime_setting`, `runtime_snapshot`, `user_browse_history`, `profile_dashboard_state`, `profile_wallet_ledger`, `analytics_date_dimension`, `tracking_event`, `tracking_daily_stat`, `profile_task_config`, `profile_task_progress`, `profile_task_reward_claim`, `profile_redeem_code`, `profile_redeem_code_usage`, `profile_invite_code`, `profile_referral_relation`, `profile_played_world`, `profile_membership`, `profile_recharge_product_config`, `profile_recharge_order`, `profile_feedback_submission`, `profile_save_archive` |
|
||||||
| RPG 运行时 | `story_session`, `story_event`, `npc_state`, `inventory_slot`, `battle_state`, `treasure_record`, `quest_record`, `quest_log`, `player_progression`, `chapter_progression` |
|
| RPG 运行时 | `story_session`, `story_event`, `npc_state`, `inventory_slot`, `battle_state`, `treasure_record`, `quest_record`, `quest_log`, `player_progression`, `chapter_progression` |
|
||||||
| 世界创作 | `custom_world_profile`, `custom_world_session`, `custom_world_agent_session`, `custom_world_agent_message`, `custom_world_agent_operation`, `custom_world_draft_card`, `custom_world_gallery_entry` |
|
| 世界创作 | `custom_world_profile`, `custom_world_session`, `custom_world_agent_session`, `custom_world_agent_message`, `custom_world_agent_operation`, `custom_world_draft_card`, `custom_world_gallery_entry` |
|
||||||
| 拼图 | `puzzle_agent_session`, `puzzle_agent_message`, `puzzle_work_profile`, `puzzle_event`, `puzzle_runtime_run`, `puzzle_leaderboard_entry` |
|
| 拼图 | `puzzle_agent_session`, `puzzle_agent_message`, `puzzle_work_profile`, `puzzle_event`, `puzzle_runtime_run`, `puzzle_leaderboard_entry` |
|
||||||
@@ -336,11 +336,23 @@ SELECT * FROM public_work_like WHERE user_id = '<user_id>';
|
|||||||
SELECT * FROM profile_membership WHERE user_id = '<user_id>';
|
SELECT * FROM profile_membership WHERE user_id = '<user_id>';
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `profile_recharge_product_config`
|
||||||
|
|
||||||
|
- 作用:充值商品配置表,是泥点和会员充值商品的运行时真相源;默认商品只在空库时作为种子写入,后台“充值商品”页后续维护该表。
|
||||||
|
- 结构:`product_id PK: String`, `title: String`, `price_cents: u64`, `kind: RuntimeProfileRechargeProductKind`, `points_amount: u64`, `bonus_points: u64`, `duration_days: u32`, `badge_label: String`, `description: String`, `tier: RuntimeProfileMembershipTier`, `enabled: bool`, `sort_order: i32`, `created_by: String`, `created_at: Timestamp`, `updated_by: String`, `updated_at: Timestamp`。
|
||||||
|
- 索引:主键 `product_id`。
|
||||||
|
- 充值口径:下单只允许读取 `enabled = true` 的商品;支付确认允许按订单 `product_id` 读取已存在商品完成历史 pending 订单。泥点首充按 `user_id + product_id` 独立判断,只清空已购买档位的 `bonus_points`、`badge_label` 和首充说明。
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT * FROM profile_recharge_product_config ORDER BY sort_order;
|
||||||
|
SELECT * FROM profile_recharge_product_config WHERE product_id = '<product_id>';
|
||||||
|
```
|
||||||
|
|
||||||
### `profile_recharge_order`
|
### `profile_recharge_order`
|
||||||
|
|
||||||
- 作用:充值订单表,记录用户购买泥点或会员的订单、支付渠道、支付时间、积分变更和会员到期时间。
|
- 作用:充值订单表,记录用户购买泥点或会员的订单、支付渠道、支付时间、积分变更和会员到期时间。
|
||||||
- 结构:`order_id PK: String`, `user_id: String`, `product_id: String`, `product_title: String`, `kind: RuntimeProfileRechargeProductKind`, `amount_cents: u64`, `status: RuntimeProfileRechargeOrderStatus`, `payment_channel: String`, `paid_at: Option<Timestamp>`, `provider_transaction_id: Option<String>`, `created_at: Timestamp`, `points_delta: i64`, `membership_expires_at: Option<Timestamp>`。
|
- 结构:`order_id PK: String`, `user_id: String`, `product_id: String`, `product_title: String`, `kind: RuntimeProfileRechargeProductKind`, `amount_cents: u64`, `status: RuntimeProfileRechargeOrderStatus`, `payment_channel: String`, `paid_at: Option<Timestamp>`, `provider_transaction_id: Option<String>`, `created_at: Timestamp`, `points_delta: i64`, `membership_expires_at: Option<Timestamp>`。
|
||||||
- 支付口径:`mock` 渠道创建后立即 `paid` 并入账;微信小程序 `wechat_mp` 渠道创建时为 `pending`,微信支付通知确认后改为 `paid`,`provider_transaction_id` 保存微信支付平台订单号。
|
- 支付口径:`mock` 渠道创建后立即 `paid` 并入账;微信小程序 `wechat_mp` 渠道创建时为 `pending`,微信支付通知确认后改为 `paid`,`provider_transaction_id` 保存微信支付平台订单号。订单商品信息来自 `profile_recharge_product_config`,泥点首充赠送按同一用户的同一 `product_id` 历史 `paid` 订单独立计算。
|
||||||
- 索引:`user_id`, `(user_id, created_at)`。
|
- 索引:`user_id`, `(user_id, created_at)`。
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
|
|||||||
@@ -572,9 +572,6 @@ function compareTables(baseTables, currentTables) {
|
|||||||
for (const [accessor, table] of currentTables) {
|
for (const [accessor, table] of currentTables) {
|
||||||
if (!baseTables.has(accessor)) {
|
if (!baseTables.has(accessor)) {
|
||||||
schemaChanged = true;
|
schemaChanged = true;
|
||||||
failures.push(
|
|
||||||
`${table.path}:${table.line}: 新增 SpacetimeDB 表 ${accessor}。请同步 migration.rs、表目录和生成绑定。`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ use crate::{
|
|||||||
},
|
},
|
||||||
runtime_profile::{
|
runtime_profile::{
|
||||||
admin_disable_profile_redeem_code, admin_disable_profile_task_config,
|
admin_disable_profile_redeem_code, admin_disable_profile_task_config,
|
||||||
admin_list_profile_invite_codes, admin_list_profile_redeem_codes,
|
admin_list_profile_invite_codes, admin_list_profile_recharge_products,
|
||||||
admin_list_profile_task_configs, admin_upsert_profile_invite_code,
|
admin_list_profile_redeem_codes, admin_list_profile_task_configs,
|
||||||
|
admin_upsert_profile_invite_code, admin_upsert_profile_recharge_product,
|
||||||
admin_upsert_profile_redeem_code, admin_upsert_profile_task_config,
|
admin_upsert_profile_redeem_code, admin_upsert_profile_task_config,
|
||||||
},
|
},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
@@ -104,7 +105,14 @@ pub fn router(state: AppState) -> Router<AppState> {
|
|||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/admin/api/profile/tasks/disable",
|
"/admin/api/profile/tasks/disable",
|
||||||
axum::routing::post(admin_disable_profile_task_config)
|
axum::routing::post(admin_disable_profile_task_config).route_layer(
|
||||||
|
middleware::from_fn_with_state(state.clone(), require_admin_auth),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/admin/api/profile/recharge-products",
|
||||||
|
get(admin_list_profile_recharge_products)
|
||||||
|
.post(admin_upsert_profile_recharge_product)
|
||||||
.route_layer(middleware::from_fn_with_state(state, require_admin_auth)),
|
.route_layer(middleware::from_fn_with_state(state, require_admin_auth)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ use module_runtime::{
|
|||||||
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM, RuntimeProfileFeedbackEvidenceRecord,
|
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM, RuntimeProfileFeedbackEvidenceRecord,
|
||||||
RuntimeProfileFeedbackEvidenceSnapshot, RuntimeProfileFeedbackSubmissionRecord,
|
RuntimeProfileFeedbackEvidenceSnapshot, RuntimeProfileFeedbackSubmissionRecord,
|
||||||
RuntimeProfileInviteCodeRecord, RuntimeProfileMembershipBenefitRecord,
|
RuntimeProfileInviteCodeRecord, RuntimeProfileMembershipBenefitRecord,
|
||||||
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
|
RuntimeProfileMembershipTier, RuntimeProfileRechargeCenterRecord,
|
||||||
RuntimeProfileRechargeOrderStatus, RuntimeProfileRechargeProductRecord,
|
RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeOrderStatus,
|
||||||
RuntimeProfileRedeemCodeMode, RuntimeProfileRedeemCodeRecord,
|
RuntimeProfileRechargeProductConfigRecord, RuntimeProfileRechargeProductKind,
|
||||||
RuntimeProfileRewardCodeRedeemRecord, RuntimeProfileTaskCenterRecord,
|
RuntimeProfileRechargeProductRecord, RuntimeProfileRedeemCodeMode,
|
||||||
RuntimeProfileTaskClaimRecord, RuntimeProfileTaskConfigRecord, RuntimeProfileTaskCycle,
|
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
|
||||||
RuntimeProfileTaskItemRecord, RuntimeProfileTaskStatus, RuntimeProfileWalletLedgerSourceType,
|
RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord, RuntimeProfileTaskConfigRecord,
|
||||||
RuntimeReferralInviteCenterRecord, RuntimeTrackingScopeKind,
|
RuntimeProfileTaskCycle, RuntimeProfileTaskItemRecord, RuntimeProfileTaskStatus,
|
||||||
|
RuntimeProfileWalletLedgerSourceType, RuntimeReferralInviteCenterRecord,
|
||||||
|
RuntimeTrackingScopeKind,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
@@ -23,12 +25,16 @@ use shared_contracts::runtime::{
|
|||||||
ANALYTICS_GRANULARITY_DAY, ANALYTICS_GRANULARITY_MONTH, ANALYTICS_GRANULARITY_QUARTER,
|
ANALYTICS_GRANULARITY_DAY, ANALYTICS_GRANULARITY_MONTH, ANALYTICS_GRANULARITY_QUARTER,
|
||||||
ANALYTICS_GRANULARITY_WEEK, ANALYTICS_GRANULARITY_YEAR, AdminDisableProfileRedeemCodeRequest,
|
ANALYTICS_GRANULARITY_WEEK, ANALYTICS_GRANULARITY_YEAR, AdminDisableProfileRedeemCodeRequest,
|
||||||
AdminDisableProfileTaskConfigRequest, AdminUpsertProfileInviteCodeRequest,
|
AdminDisableProfileTaskConfigRequest, AdminUpsertProfileInviteCodeRequest,
|
||||||
AdminUpsertProfileRedeemCodeRequest, AdminUpsertProfileTaskConfigRequest,
|
AdminUpsertProfileRechargeProductRequest, AdminUpsertProfileRedeemCodeRequest,
|
||||||
AnalyticsBucketMetricResponse, AnalyticsMetricQueryResponse, ClaimProfileTaskRewardResponse,
|
AdminUpsertProfileTaskConfigRequest, AnalyticsBucketMetricResponse,
|
||||||
|
AnalyticsMetricQueryResponse, ClaimProfileTaskRewardResponse,
|
||||||
ConfirmWechatProfileRechargeOrderResponse, CreateProfileRechargeOrderRequest,
|
ConfirmWechatProfileRechargeOrderResponse, CreateProfileRechargeOrderRequest,
|
||||||
CreateProfileRechargeOrderResponse, PROFILE_FEEDBACK_STATUS_OPEN, PROFILE_TASK_CYCLE_DAILY,
|
CreateProfileRechargeOrderResponse, PROFILE_FEEDBACK_STATUS_OPEN,
|
||||||
PROFILE_TASK_STATUS_CLAIMABLE, PROFILE_TASK_STATUS_CLAIMED, PROFILE_TASK_STATUS_DISABLED,
|
PROFILE_MEMBERSHIP_TIER_MONTH, PROFILE_MEMBERSHIP_TIER_NORMAL, PROFILE_MEMBERSHIP_TIER_SEASON,
|
||||||
PROFILE_TASK_STATUS_INCOMPLETE, PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME,
|
PROFILE_MEMBERSHIP_TIER_YEAR, PROFILE_RECHARGE_PRODUCT_KIND_MEMBERSHIP,
|
||||||
|
PROFILE_RECHARGE_PRODUCT_KIND_POINTS, PROFILE_TASK_CYCLE_DAILY, PROFILE_TASK_STATUS_CLAIMABLE,
|
||||||
|
PROFILE_TASK_STATUS_CLAIMED, PROFILE_TASK_STATUS_DISABLED, PROFILE_TASK_STATUS_INCOMPLETE,
|
||||||
|
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME,
|
||||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND,
|
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND,
|
||||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD,
|
PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD,
|
||||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD,
|
PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD,
|
||||||
@@ -42,6 +48,7 @@ use shared_contracts::runtime::{
|
|||||||
ProfileInviteCodeAdminListResponse, ProfileInviteCodeAdminResponse,
|
ProfileInviteCodeAdminListResponse, ProfileInviteCodeAdminResponse,
|
||||||
ProfileMembershipBenefitResponse, ProfileMembershipResponse, ProfilePlayStatsResponse,
|
ProfileMembershipBenefitResponse, ProfileMembershipResponse, ProfilePlayStatsResponse,
|
||||||
ProfilePlayedWorkSummaryResponse, ProfileRechargeCenterResponse, ProfileRechargeOrderResponse,
|
ProfilePlayedWorkSummaryResponse, ProfileRechargeCenterResponse, ProfileRechargeOrderResponse,
|
||||||
|
ProfileRechargeProductConfigAdminListResponse, ProfileRechargeProductConfigAdminResponse,
|
||||||
ProfileRechargeProductResponse, ProfileRedeemCodeAdminListResponse,
|
ProfileRechargeProductResponse, ProfileRedeemCodeAdminListResponse,
|
||||||
ProfileRedeemCodeAdminResponse, ProfileReferralInviteCenterResponse,
|
ProfileRedeemCodeAdminResponse, ProfileReferralInviteCenterResponse,
|
||||||
ProfileReferralInvitedUserResponse, ProfileTaskCenterResponse,
|
ProfileReferralInvitedUserResponse, ProfileTaskCenterResponse,
|
||||||
@@ -669,6 +676,84 @@ pub async fn admin_disable_profile_task_config(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn admin_list_profile_recharge_products(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
Extension(admin): Extension<AuthenticatedAdmin>,
|
||||||
|
) -> Result<Json<Value>, Response> {
|
||||||
|
let entries = state
|
||||||
|
.spacetime_client()
|
||||||
|
.admin_list_profile_recharge_products(admin.session().subject.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
runtime_profile_error_response(
|
||||||
|
&request_context,
|
||||||
|
map_runtime_profile_client_error(error),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
ProfileRechargeProductConfigAdminListResponse {
|
||||||
|
entries: entries
|
||||||
|
.into_iter()
|
||||||
|
.map(build_profile_recharge_product_config_admin_response)
|
||||||
|
.collect(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn admin_upsert_profile_recharge_product(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
Extension(admin): Extension<AuthenticatedAdmin>,
|
||||||
|
Json(payload): Json<AdminUpsertProfileRechargeProductRequest>,
|
||||||
|
) -> Result<Json<Value>, Response> {
|
||||||
|
let kind = parse_profile_recharge_product_kind(&payload.kind).map_err(|error| {
|
||||||
|
runtime_profile_error_response(
|
||||||
|
&request_context,
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let tier = parse_profile_membership_tier(&payload.tier).map_err(|error| {
|
||||||
|
runtime_profile_error_response(
|
||||||
|
&request_context,
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let updated_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000;
|
||||||
|
let record = state
|
||||||
|
.spacetime_client()
|
||||||
|
.admin_upsert_profile_recharge_product(
|
||||||
|
admin.session().subject.clone(),
|
||||||
|
payload.product_id,
|
||||||
|
payload.title,
|
||||||
|
payload.price_cents,
|
||||||
|
kind,
|
||||||
|
payload.points_amount,
|
||||||
|
payload.bonus_points,
|
||||||
|
payload.duration_days,
|
||||||
|
payload.badge_label.unwrap_or_default(),
|
||||||
|
payload.description.unwrap_or_default(),
|
||||||
|
tier,
|
||||||
|
payload.enabled,
|
||||||
|
payload.sort_order.unwrap_or(10),
|
||||||
|
updated_at_micros as i64,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
runtime_profile_error_response(
|
||||||
|
&request_context,
|
||||||
|
map_runtime_profile_client_error(error),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
build_profile_recharge_product_config_admin_response(record),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn admin_list_profile_redeem_codes(
|
pub async fn admin_list_profile_redeem_codes(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Extension(request_context): Extension<RequestContext>,
|
Extension(request_context): Extension<RequestContext>,
|
||||||
@@ -1172,6 +1257,29 @@ fn build_profile_task_config_admin_response(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_profile_recharge_product_config_admin_response(
|
||||||
|
record: RuntimeProfileRechargeProductConfigRecord,
|
||||||
|
) -> ProfileRechargeProductConfigAdminResponse {
|
||||||
|
ProfileRechargeProductConfigAdminResponse {
|
||||||
|
product_id: record.product_id,
|
||||||
|
title: record.title,
|
||||||
|
price_cents: record.price_cents,
|
||||||
|
kind: format_profile_recharge_product_kind(record.kind).to_string(),
|
||||||
|
points_amount: record.points_amount,
|
||||||
|
bonus_points: record.bonus_points,
|
||||||
|
duration_days: record.duration_days,
|
||||||
|
badge_label: record.badge_label,
|
||||||
|
description: record.description,
|
||||||
|
tier: format_profile_membership_tier(record.tier).to_string(),
|
||||||
|
enabled: record.enabled,
|
||||||
|
sort_order: record.sort_order,
|
||||||
|
created_by: record.created_by,
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_by: record.updated_by,
|
||||||
|
updated_at: record.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_admin_invite_code_metadata(metadata: Option<Value>) -> Result<String, AppError> {
|
fn normalize_admin_invite_code_metadata(metadata: Option<Value>) -> Result<String, AppError> {
|
||||||
let metadata = match metadata {
|
let metadata = match metadata {
|
||||||
Some(Value::Null) | None => json!({}),
|
Some(Value::Null) | None => json!({}),
|
||||||
@@ -1233,6 +1341,28 @@ fn parse_profile_task_cycle(raw: &str) -> Result<RuntimeProfileTaskCycle, String
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_profile_recharge_product_kind(
|
||||||
|
raw: &str,
|
||||||
|
) -> Result<RuntimeProfileRechargeProductKind, String> {
|
||||||
|
match raw.trim().to_ascii_lowercase().as_str() {
|
||||||
|
PROFILE_RECHARGE_PRODUCT_KIND_POINTS => Ok(RuntimeProfileRechargeProductKind::Points),
|
||||||
|
PROFILE_RECHARGE_PRODUCT_KIND_MEMBERSHIP => {
|
||||||
|
Ok(RuntimeProfileRechargeProductKind::Membership)
|
||||||
|
}
|
||||||
|
_ => Err("充值商品类型无效".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_profile_membership_tier(raw: &str) -> Result<RuntimeProfileMembershipTier, String> {
|
||||||
|
match raw.trim().to_ascii_lowercase().as_str() {
|
||||||
|
PROFILE_MEMBERSHIP_TIER_NORMAL => Ok(RuntimeProfileMembershipTier::Normal),
|
||||||
|
PROFILE_MEMBERSHIP_TIER_MONTH => Ok(RuntimeProfileMembershipTier::Month),
|
||||||
|
PROFILE_MEMBERSHIP_TIER_SEASON => Ok(RuntimeProfileMembershipTier::Season),
|
||||||
|
PROFILE_MEMBERSHIP_TIER_YEAR => Ok(RuntimeProfileMembershipTier::Year),
|
||||||
|
_ => Err("会员档位无效".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_tracking_scope_kind(raw: &str) -> Result<RuntimeTrackingScopeKind, String> {
|
fn parse_tracking_scope_kind(raw: &str) -> Result<RuntimeTrackingScopeKind, String> {
|
||||||
match raw.trim().to_ascii_lowercase().as_str() {
|
match raw.trim().to_ascii_lowercase().as_str() {
|
||||||
TRACKING_SCOPE_KIND_SITE => Ok(RuntimeTrackingScopeKind::Site),
|
TRACKING_SCOPE_KIND_SITE => Ok(RuntimeTrackingScopeKind::Site),
|
||||||
@@ -1269,6 +1399,22 @@ fn format_profile_task_status(status: RuntimeProfileTaskStatus) -> &'static str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_profile_recharge_product_kind(kind: RuntimeProfileRechargeProductKind) -> &'static str {
|
||||||
|
match kind {
|
||||||
|
RuntimeProfileRechargeProductKind::Points => PROFILE_RECHARGE_PRODUCT_KIND_POINTS,
|
||||||
|
RuntimeProfileRechargeProductKind::Membership => PROFILE_RECHARGE_PRODUCT_KIND_MEMBERSHIP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_profile_membership_tier(tier: RuntimeProfileMembershipTier) -> &'static str {
|
||||||
|
match tier {
|
||||||
|
RuntimeProfileMembershipTier::Normal => PROFILE_MEMBERSHIP_TIER_NORMAL,
|
||||||
|
RuntimeProfileMembershipTier::Month => PROFILE_MEMBERSHIP_TIER_MONTH,
|
||||||
|
RuntimeProfileMembershipTier::Season => PROFILE_MEMBERSHIP_TIER_SEASON,
|
||||||
|
RuntimeProfileMembershipTier::Year => PROFILE_MEMBERSHIP_TIER_YEAR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn format_tracking_scope_kind(scope_kind: RuntimeTrackingScopeKind) -> &'static str {
|
fn format_tracking_scope_kind(scope_kind: RuntimeTrackingScopeKind) -> &'static str {
|
||||||
match scope_kind {
|
match scope_kind {
|
||||||
RuntimeTrackingScopeKind::Site => TRACKING_SCOPE_KIND_SITE,
|
RuntimeTrackingScopeKind::Site => TRACKING_SCOPE_KIND_SITE,
|
||||||
@@ -1702,6 +1848,7 @@ mod tests {
|
|||||||
for uri in [
|
for uri in [
|
||||||
"/admin/api/profile/redeem-codes",
|
"/admin/api/profile/redeem-codes",
|
||||||
"/admin/api/profile/invite-codes",
|
"/admin/api/profile/invite-codes",
|
||||||
|
"/admin/api/profile/recharge-products",
|
||||||
] {
|
] {
|
||||||
let response = app
|
let response = app
|
||||||
.clone()
|
.clone()
|
||||||
|
|||||||
@@ -292,6 +292,31 @@ pub fn build_runtime_profile_recharge_product_record(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_runtime_profile_recharge_product_config_record(
|
||||||
|
snapshot: RuntimeProfileRechargeProductConfigSnapshot,
|
||||||
|
) -> RuntimeProfileRechargeProductConfigRecord {
|
||||||
|
RuntimeProfileRechargeProductConfigRecord {
|
||||||
|
product_id: snapshot.product_id,
|
||||||
|
title: snapshot.title,
|
||||||
|
price_cents: snapshot.price_cents,
|
||||||
|
kind: snapshot.kind,
|
||||||
|
points_amount: snapshot.points_amount,
|
||||||
|
bonus_points: snapshot.bonus_points,
|
||||||
|
duration_days: snapshot.duration_days,
|
||||||
|
badge_label: snapshot.badge_label,
|
||||||
|
description: snapshot.description,
|
||||||
|
tier: snapshot.tier,
|
||||||
|
enabled: snapshot.enabled,
|
||||||
|
sort_order: snapshot.sort_order,
|
||||||
|
created_by: snapshot.created_by,
|
||||||
|
created_at: format_utc_micros(snapshot.created_at_micros),
|
||||||
|
created_at_micros: snapshot.created_at_micros,
|
||||||
|
updated_by: snapshot.updated_by,
|
||||||
|
updated_at: format_utc_micros(snapshot.updated_at_micros),
|
||||||
|
updated_at_micros: snapshot.updated_at_micros,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_runtime_profile_membership_benefit_record(
|
pub fn build_runtime_profile_membership_benefit_record(
|
||||||
snapshot: RuntimeProfileMembershipBenefitSnapshot,
|
snapshot: RuntimeProfileMembershipBenefitSnapshot,
|
||||||
) -> RuntimeProfileMembershipBenefitRecord {
|
) -> RuntimeProfileMembershipBenefitRecord {
|
||||||
@@ -1114,9 +1139,9 @@ fn hash_runtime_profile_recharge_order_key(
|
|||||||
|
|
||||||
pub fn resolve_runtime_profile_points_recharge_delta(
|
pub fn resolve_runtime_profile_points_recharge_delta(
|
||||||
product: &RuntimeProfileRechargeProductSnapshot,
|
product: &RuntimeProfileRechargeProductSnapshot,
|
||||||
has_points_recharged: bool,
|
has_product_recharged: bool,
|
||||||
) -> u64 {
|
) -> u64 {
|
||||||
let bonus_points = if has_points_recharged {
|
let bonus_points = if has_product_recharged {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
product.bonus_points
|
product.bonus_points
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use shared_kernel::{
|
|||||||
|
|
||||||
use crate::domain::*;
|
use crate::domain::*;
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use crate::{format_utc_micros, runtime_profile_recharge_product_by_id};
|
use crate::format_utc_micros;
|
||||||
|
|
||||||
pub const PROFILE_USER_TAG_MAX_COUNT: usize = 8;
|
pub const PROFILE_USER_TAG_MAX_COUNT: usize = 8;
|
||||||
pub const PROFILE_USER_TAG_MAX_CHARS: usize = 16;
|
pub const PROFILE_USER_TAG_MAX_CHARS: usize = 16;
|
||||||
@@ -259,9 +259,6 @@ pub fn build_runtime_profile_recharge_order_create_input(
|
|||||||
let user_id = normalize_runtime_profile_user_id(user_id)?;
|
let user_id = normalize_runtime_profile_user_id(user_id)?;
|
||||||
let product_id =
|
let product_id =
|
||||||
normalize_required_string(product_id).ok_or(RuntimeProfileFieldError::MissingProductId)?;
|
normalize_required_string(product_id).ok_or(RuntimeProfileFieldError::MissingProductId)?;
|
||||||
if runtime_profile_recharge_product_by_id(&product_id).is_none() {
|
|
||||||
return Err(RuntimeProfileFieldError::UnknownRechargeProduct);
|
|
||||||
}
|
|
||||||
let payment_channel = normalize_required_string(payment_channel)
|
let payment_channel = normalize_required_string(payment_channel)
|
||||||
.unwrap_or_else(|| PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK.to_string());
|
.unwrap_or_else(|| PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK.to_string());
|
||||||
|
|
||||||
@@ -273,6 +270,78 @@ pub fn build_runtime_profile_recharge_order_create_input(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_runtime_profile_recharge_product_admin_list_input(
|
||||||
|
admin_user_id: String,
|
||||||
|
) -> Result<RuntimeProfileRechargeProductAdminListInput, RuntimeProfileFieldError> {
|
||||||
|
let admin_user_id = normalize_runtime_profile_user_id(admin_user_id)?;
|
||||||
|
Ok(RuntimeProfileRechargeProductAdminListInput { admin_user_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn build_runtime_profile_recharge_product_admin_upsert_input(
|
||||||
|
admin_user_id: String,
|
||||||
|
product_id: String,
|
||||||
|
title: String,
|
||||||
|
price_cents: u64,
|
||||||
|
kind: RuntimeProfileRechargeProductKind,
|
||||||
|
points_amount: u64,
|
||||||
|
bonus_points: u64,
|
||||||
|
duration_days: u32,
|
||||||
|
badge_label: String,
|
||||||
|
description: String,
|
||||||
|
tier: RuntimeProfileMembershipTier,
|
||||||
|
enabled: bool,
|
||||||
|
sort_order: i32,
|
||||||
|
updated_at_micros: i64,
|
||||||
|
) -> Result<RuntimeProfileRechargeProductAdminUpsertInput, RuntimeProfileFieldError> {
|
||||||
|
let admin_user_id = normalize_runtime_profile_user_id(admin_user_id)?;
|
||||||
|
let product_id =
|
||||||
|
normalize_required_string(product_id).ok_or(RuntimeProfileFieldError::MissingProductId)?;
|
||||||
|
let title =
|
||||||
|
normalize_required_string(title).ok_or(RuntimeProfileFieldError::MissingProductTitle)?;
|
||||||
|
if price_cents == 0 {
|
||||||
|
return Err(RuntimeProfileFieldError::InvalidRechargeProductPrice);
|
||||||
|
}
|
||||||
|
match kind {
|
||||||
|
RuntimeProfileRechargeProductKind::Points => {
|
||||||
|
if points_amount == 0 {
|
||||||
|
return Err(RuntimeProfileFieldError::InvalidRechargeProductPoints);
|
||||||
|
}
|
||||||
|
if duration_days != 0 || tier != RuntimeProfileMembershipTier::Normal {
|
||||||
|
return Err(RuntimeProfileFieldError::InvalidRechargeProductTier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RuntimeProfileRechargeProductKind::Membership => {
|
||||||
|
if duration_days == 0 {
|
||||||
|
return Err(RuntimeProfileFieldError::InvalidRechargeProductDuration);
|
||||||
|
}
|
||||||
|
if points_amount != 0
|
||||||
|
|| bonus_points != 0
|
||||||
|
|| tier == RuntimeProfileMembershipTier::Normal
|
||||||
|
{
|
||||||
|
return Err(RuntimeProfileFieldError::InvalidRechargeProductTier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RuntimeProfileRechargeProductAdminUpsertInput {
|
||||||
|
admin_user_id,
|
||||||
|
product_id,
|
||||||
|
title,
|
||||||
|
price_cents,
|
||||||
|
kind,
|
||||||
|
points_amount,
|
||||||
|
bonus_points,
|
||||||
|
duration_days,
|
||||||
|
badge_label: normalize_optional_string(Some(badge_label)).unwrap_or_default(),
|
||||||
|
description: normalize_optional_string(Some(description)).unwrap_or_default(),
|
||||||
|
tier,
|
||||||
|
enabled,
|
||||||
|
sort_order,
|
||||||
|
updated_at_micros,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_runtime_profile_recharge_order_paid_input(
|
pub fn build_runtime_profile_recharge_order_paid_input(
|
||||||
order_id: String,
|
order_id: String,
|
||||||
paid_at_micros: i64,
|
paid_at_micros: i64,
|
||||||
|
|||||||
@@ -986,6 +986,27 @@ pub struct RuntimeProfileRechargeProductSnapshot {
|
|||||||
pub tier: RuntimeProfileMembershipTier,
|
pub tier: RuntimeProfileMembershipTier,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct RuntimeProfileRechargeProductConfigSnapshot {
|
||||||
|
pub product_id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub price_cents: u64,
|
||||||
|
pub kind: RuntimeProfileRechargeProductKind,
|
||||||
|
pub points_amount: u64,
|
||||||
|
pub bonus_points: u64,
|
||||||
|
pub duration_days: u32,
|
||||||
|
pub badge_label: String,
|
||||||
|
pub description: String,
|
||||||
|
pub tier: RuntimeProfileMembershipTier,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub sort_order: i32,
|
||||||
|
pub created_by: String,
|
||||||
|
pub created_at_micros: i64,
|
||||||
|
pub updated_by: String,
|
||||||
|
pub updated_at_micros: i64,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct RuntimeProfileMembershipBenefitSnapshot {
|
pub struct RuntimeProfileMembershipBenefitSnapshot {
|
||||||
@@ -1054,6 +1075,47 @@ pub struct RuntimeProfileRechargeCenterProcedureResult {
|
|||||||
pub error_message: Option<String>,
|
pub error_message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct RuntimeProfileRechargeProductAdminListInput {
|
||||||
|
pub admin_user_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct RuntimeProfileRechargeProductAdminUpsertInput {
|
||||||
|
pub admin_user_id: String,
|
||||||
|
pub product_id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub price_cents: u64,
|
||||||
|
pub kind: RuntimeProfileRechargeProductKind,
|
||||||
|
pub points_amount: u64,
|
||||||
|
pub bonus_points: u64,
|
||||||
|
pub duration_days: u32,
|
||||||
|
pub badge_label: String,
|
||||||
|
pub description: String,
|
||||||
|
pub tier: RuntimeProfileMembershipTier,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub sort_order: i32,
|
||||||
|
pub updated_at_micros: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct RuntimeProfileRechargeProductAdminListProcedureResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub entries: Vec<RuntimeProfileRechargeProductConfigSnapshot>,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct RuntimeProfileRechargeProductAdminProcedureResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub record: Option<RuntimeProfileRechargeProductConfigSnapshot>,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct RuntimeProfileRechargeCenterGetInput {
|
pub struct RuntimeProfileRechargeCenterGetInput {
|
||||||
@@ -1463,6 +1525,28 @@ pub struct RuntimeProfileRechargeProductRecord {
|
|||||||
pub tier: RuntimeProfileMembershipTier,
|
pub tier: RuntimeProfileMembershipTier,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct RuntimeProfileRechargeProductConfigRecord {
|
||||||
|
pub product_id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub price_cents: u64,
|
||||||
|
pub kind: RuntimeProfileRechargeProductKind,
|
||||||
|
pub points_amount: u64,
|
||||||
|
pub bonus_points: u64,
|
||||||
|
pub duration_days: u32,
|
||||||
|
pub badge_label: String,
|
||||||
|
pub description: String,
|
||||||
|
pub tier: RuntimeProfileMembershipTier,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub sort_order: i32,
|
||||||
|
pub created_by: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub created_at_micros: i64,
|
||||||
|
pub updated_by: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub updated_at_micros: i64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct RuntimeProfileMembershipBenefitRecord {
|
pub struct RuntimeProfileMembershipBenefitRecord {
|
||||||
pub benefit_name: String,
|
pub benefit_name: String,
|
||||||
|
|||||||
@@ -74,6 +74,12 @@ pub enum RuntimeProfileFieldError {
|
|||||||
TaskAlreadyClaimed,
|
TaskAlreadyClaimed,
|
||||||
MissingOrderId,
|
MissingOrderId,
|
||||||
MissingProductId,
|
MissingProductId,
|
||||||
|
MissingProductTitle,
|
||||||
|
InvalidRechargeProductPrice,
|
||||||
|
InvalidRechargeProductPoints,
|
||||||
|
InvalidRechargeProductDuration,
|
||||||
|
InvalidRechargeProductKind,
|
||||||
|
InvalidRechargeProductTier,
|
||||||
MissingWorldKey,
|
MissingWorldKey,
|
||||||
MissingBottomTab,
|
MissingBottomTab,
|
||||||
MissingCheckpointSessionId,
|
MissingCheckpointSessionId,
|
||||||
@@ -136,6 +142,14 @@ impl std::fmt::Display for RuntimeProfileFieldError {
|
|||||||
Self::TaskAlreadyClaimed => f.write_str("任务奖励已领取"),
|
Self::TaskAlreadyClaimed => f.write_str("任务奖励已领取"),
|
||||||
Self::MissingOrderId => f.write_str("recharge.order_id 不能为空"),
|
Self::MissingOrderId => f.write_str("recharge.order_id 不能为空"),
|
||||||
Self::MissingProductId => f.write_str("recharge.product_id 不能为空"),
|
Self::MissingProductId => f.write_str("recharge.product_id 不能为空"),
|
||||||
|
Self::MissingProductTitle => f.write_str("recharge.product_title 不能为空"),
|
||||||
|
Self::InvalidRechargeProductPrice => f.write_str("recharge.price_cents 必须大于 0"),
|
||||||
|
Self::InvalidRechargeProductPoints => f.write_str("泥点商品 points_amount 必须大于 0"),
|
||||||
|
Self::InvalidRechargeProductDuration => {
|
||||||
|
f.write_str("会员商品 duration_days 必须大于 0")
|
||||||
|
}
|
||||||
|
Self::InvalidRechargeProductKind => f.write_str("充值商品类型无效"),
|
||||||
|
Self::InvalidRechargeProductTier => f.write_str("会员商品 tier 无效"),
|
||||||
Self::MissingWorldKey => f.write_str("profile.world_key 不能为空"),
|
Self::MissingWorldKey => f.write_str("profile.world_key 不能为空"),
|
||||||
Self::MissingBottomTab => f.write_str("runtime_snapshot.bottom_tab 不能为空"),
|
Self::MissingBottomTab => f.write_str("runtime_snapshot.bottom_tab 不能为空"),
|
||||||
Self::MissingCheckpointSessionId => f.write_str("checkpoint.session_id 不能为空"),
|
Self::MissingCheckpointSessionId => f.write_str("checkpoint.session_id 不能为空"),
|
||||||
|
|||||||
@@ -77,19 +77,11 @@ pub fn runtime_profile_recharge_point_products() -> Vec<RuntimeProfileRechargePr
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 中文注释:充值中心展示当前账号本次实际可生效的首充赠送状态。
|
/// 中文注释:保留旧展示 helper 的兼容入口;首充资格已改为按商品档位在配置表侧计算。
|
||||||
pub fn resolve_runtime_profile_recharge_point_products(
|
pub fn resolve_runtime_profile_recharge_point_products(
|
||||||
has_points_recharged: bool,
|
_has_points_recharged: bool,
|
||||||
) -> Vec<RuntimeProfileRechargeProductSnapshot> {
|
) -> Vec<RuntimeProfileRechargeProductSnapshot> {
|
||||||
let mut products = runtime_profile_recharge_point_products();
|
runtime_profile_recharge_point_products()
|
||||||
if has_points_recharged {
|
|
||||||
for product in &mut products {
|
|
||||||
product.bonus_points = 0;
|
|
||||||
product.badge_label.clear();
|
|
||||||
product.description = product.title.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
products
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runtime_profile_recharge_membership_products() -> Vec<RuntimeProfileRechargeProductSnapshot>
|
pub fn runtime_profile_recharge_membership_products() -> Vec<RuntimeProfileRechargeProductSnapshot>
|
||||||
@@ -722,32 +714,33 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn recharge_point_products_resolve_effective_first_bonus_display() {
|
fn recharge_point_products_do_not_hide_all_first_bonus_by_account_flag() {
|
||||||
let first_recharge_products = resolve_runtime_profile_recharge_point_products(false);
|
let first_recharge_products = resolve_runtime_profile_recharge_point_products(false);
|
||||||
assert_eq!(first_recharge_products[0].bonus_points, 60);
|
assert_eq!(first_recharge_products[0].bonus_points, 60);
|
||||||
assert_eq!(first_recharge_products[0].badge_label, "首充双倍");
|
assert_eq!(first_recharge_products[0].badge_label, "首充双倍");
|
||||||
assert_eq!(first_recharge_products[0].description, "首充送60泥点");
|
assert_eq!(first_recharge_products[0].description, "首充送60泥点");
|
||||||
|
|
||||||
let repeated_recharge_products = resolve_runtime_profile_recharge_point_products(true);
|
let repeated_recharge_products = resolve_runtime_profile_recharge_point_products(true);
|
||||||
assert_eq!(repeated_recharge_products[0].bonus_points, 0);
|
assert_eq!(repeated_recharge_products[0].bonus_points, 60);
|
||||||
assert_eq!(repeated_recharge_products[0].badge_label, "");
|
assert_eq!(repeated_recharge_products[0].badge_label, "首充双倍");
|
||||||
assert_eq!(repeated_recharge_products[0].description, "60泥点");
|
assert_eq!(repeated_recharge_products[0].description, "首充送60泥点");
|
||||||
assert_eq!(repeated_recharge_products[5].bonus_points, 0);
|
assert_eq!(repeated_recharge_products[5].bonus_points, 3280);
|
||||||
assert_eq!(repeated_recharge_products[5].badge_label, "");
|
assert_eq!(repeated_recharge_products[5].badge_label, "首充双倍");
|
||||||
assert_eq!(repeated_recharge_products[5].description, "3280泥点");
|
assert_eq!(repeated_recharge_products[5].description, "首充送3280泥点");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_recharge_order_input_rejects_unknown_product() {
|
fn build_recharge_order_input_accepts_configured_product_id_later() {
|
||||||
let error = build_runtime_profile_recharge_order_create_input(
|
let input = build_runtime_profile_recharge_order_create_input(
|
||||||
"user-1".to_string(),
|
"user-1".to_string(),
|
||||||
"bad-product".to_string(),
|
"custom-points-600".to_string(),
|
||||||
"mock".to_string(),
|
"mock".to_string(),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
.expect_err("unknown product should fail");
|
.expect("product existence is validated against database config later");
|
||||||
|
|
||||||
assert_eq!(error, RuntimeProfileFieldError::UnknownRechargeProduct);
|
assert_eq!(input.product_id, "custom-points-600");
|
||||||
|
assert_eq!(input.payment_channel, "mock");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ pub const PROFILE_TASK_STATUS_INCOMPLETE: &str = "incomplete";
|
|||||||
pub const PROFILE_TASK_STATUS_CLAIMABLE: &str = "claimable";
|
pub const PROFILE_TASK_STATUS_CLAIMABLE: &str = "claimable";
|
||||||
pub const PROFILE_TASK_STATUS_CLAIMED: &str = "claimed";
|
pub const PROFILE_TASK_STATUS_CLAIMED: &str = "claimed";
|
||||||
pub const PROFILE_TASK_STATUS_DISABLED: &str = "disabled";
|
pub const PROFILE_TASK_STATUS_DISABLED: &str = "disabled";
|
||||||
|
pub const PROFILE_RECHARGE_PRODUCT_KIND_POINTS: &str = "points";
|
||||||
|
pub const PROFILE_RECHARGE_PRODUCT_KIND_MEMBERSHIP: &str = "membership";
|
||||||
|
pub const PROFILE_MEMBERSHIP_TIER_NORMAL: &str = "normal";
|
||||||
|
pub const PROFILE_MEMBERSHIP_TIER_MONTH: &str = "month";
|
||||||
|
pub const PROFILE_MEMBERSHIP_TIER_SEASON: &str = "season";
|
||||||
|
pub const PROFILE_MEMBERSHIP_TIER_YEAR: &str = "year";
|
||||||
pub const PROFILE_FEEDBACK_STATUS_OPEN: &str = "open";
|
pub const PROFILE_FEEDBACK_STATUS_OPEN: &str = "open";
|
||||||
pub const TRACKING_SCOPE_KIND_SITE: &str = "site";
|
pub const TRACKING_SCOPE_KIND_SITE: &str = "site";
|
||||||
pub const TRACKING_SCOPE_KIND_WORK: &str = "work";
|
pub const TRACKING_SCOPE_KIND_WORK: &str = "work";
|
||||||
@@ -436,6 +442,33 @@ pub struct ProfileTaskConfigAdminListResponse {
|
|||||||
pub entries: Vec<ProfileTaskConfigAdminResponse>,
|
pub entries: Vec<ProfileTaskConfigAdminResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ProfileRechargeProductConfigAdminResponse {
|
||||||
|
pub product_id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub price_cents: u64,
|
||||||
|
pub kind: String,
|
||||||
|
pub points_amount: u64,
|
||||||
|
pub bonus_points: u64,
|
||||||
|
pub duration_days: u32,
|
||||||
|
pub badge_label: String,
|
||||||
|
pub description: String,
|
||||||
|
pub tier: String,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub sort_order: i32,
|
||||||
|
pub created_by: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_by: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ProfileRechargeProductConfigAdminListResponse {
|
||||||
|
pub entries: Vec<ProfileRechargeProductConfigAdminResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AnalyticsMetricQueryRequest {
|
pub struct AnalyticsMetricQueryRequest {
|
||||||
@@ -478,6 +511,27 @@ pub struct AdminUpsertProfileTaskConfigRequest {
|
|||||||
pub sort_order: Option<i32>,
|
pub sort_order: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AdminUpsertProfileRechargeProductRequest {
|
||||||
|
pub product_id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub price_cents: u64,
|
||||||
|
pub kind: String,
|
||||||
|
pub points_amount: u64,
|
||||||
|
pub bonus_points: u64,
|
||||||
|
pub duration_days: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub badge_label: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub tier: String,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub enabled: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub sort_order: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AdminDisableProfileTaskConfigRequest {
|
pub struct AdminDisableProfileTaskConfigRequest {
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ use module_runtime::{
|
|||||||
RuntimePlatformTheme as DomainRuntimePlatformTheme, RuntimeProfileDashboardRecord,
|
RuntimePlatformTheme as DomainRuntimePlatformTheme, RuntimeProfileDashboardRecord,
|
||||||
RuntimeProfileFeedbackSubmissionRecord, RuntimeProfileInviteCodeRecord,
|
RuntimeProfileFeedbackSubmissionRecord, RuntimeProfileInviteCodeRecord,
|
||||||
RuntimeProfilePlayStatsRecord, RuntimeProfileRechargeCenterRecord,
|
RuntimeProfilePlayStatsRecord, RuntimeProfileRechargeCenterRecord,
|
||||||
RuntimeProfileRechargeOrderRecord,
|
RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeProductConfigRecord,
|
||||||
RuntimeProfileRedeemCodeMode as DomainRuntimeProfileRedeemCodeMode,
|
RuntimeProfileRedeemCodeMode as DomainRuntimeProfileRedeemCodeMode,
|
||||||
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
|
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
|
||||||
RuntimeProfileSaveArchiveRecord, RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord,
|
RuntimeProfileSaveArchiveRecord, RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord,
|
||||||
@@ -185,6 +185,9 @@ use module_runtime::{
|
|||||||
build_runtime_profile_recharge_center_get_input, build_runtime_profile_recharge_center_record,
|
build_runtime_profile_recharge_center_get_input, build_runtime_profile_recharge_center_record,
|
||||||
build_runtime_profile_recharge_order_create_input,
|
build_runtime_profile_recharge_order_create_input,
|
||||||
build_runtime_profile_recharge_order_get_input,
|
build_runtime_profile_recharge_order_get_input,
|
||||||
|
build_runtime_profile_recharge_product_admin_list_input,
|
||||||
|
build_runtime_profile_recharge_product_admin_upsert_input,
|
||||||
|
build_runtime_profile_recharge_product_config_record,
|
||||||
build_runtime_profile_redeem_code_admin_disable_input,
|
build_runtime_profile_redeem_code_admin_disable_input,
|
||||||
build_runtime_profile_redeem_code_admin_list_input,
|
build_runtime_profile_redeem_code_admin_list_input,
|
||||||
build_runtime_profile_redeem_code_admin_upsert_input, build_runtime_profile_redeem_code_record,
|
build_runtime_profile_redeem_code_admin_upsert_input, build_runtime_profile_redeem_code_record,
|
||||||
|
|||||||
@@ -309,6 +309,39 @@ impl From<module_runtime::RuntimeProfileTaskConfigAdminDisableInput>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<module_runtime::RuntimeProfileRechargeProductAdminListInput>
|
||||||
|
for RuntimeProfileRechargeProductAdminListInput
|
||||||
|
{
|
||||||
|
fn from(input: module_runtime::RuntimeProfileRechargeProductAdminListInput) -> Self {
|
||||||
|
Self {
|
||||||
|
admin_user_id: input.admin_user_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<module_runtime::RuntimeProfileRechargeProductAdminUpsertInput>
|
||||||
|
for RuntimeProfileRechargeProductAdminUpsertInput
|
||||||
|
{
|
||||||
|
fn from(input: module_runtime::RuntimeProfileRechargeProductAdminUpsertInput) -> Self {
|
||||||
|
Self {
|
||||||
|
admin_user_id: input.admin_user_id,
|
||||||
|
product_id: input.product_id,
|
||||||
|
title: input.title,
|
||||||
|
price_cents: input.price_cents,
|
||||||
|
kind: map_runtime_profile_recharge_product_kind(input.kind),
|
||||||
|
points_amount: input.points_amount,
|
||||||
|
bonus_points: input.bonus_points,
|
||||||
|
duration_days: input.duration_days,
|
||||||
|
badge_label: input.badge_label,
|
||||||
|
description: input.description,
|
||||||
|
tier: map_runtime_profile_membership_tier(input.tier),
|
||||||
|
enabled: input.enabled,
|
||||||
|
sort_order: input.sort_order,
|
||||||
|
updated_at_micros: input.updated_at_micros,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<module_runtime::RuntimeProfileRedeemCodeAdminUpsertInput>
|
impl From<module_runtime::RuntimeProfileRedeemCodeAdminUpsertInput>
|
||||||
for RuntimeProfileRedeemCodeAdminUpsertInput
|
for RuntimeProfileRedeemCodeAdminUpsertInput
|
||||||
{
|
{
|
||||||
@@ -1157,6 +1190,40 @@ pub(crate) fn map_runtime_profile_task_config_admin_procedure_result(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map_runtime_profile_recharge_product_admin_list_procedure_result(
|
||||||
|
result: RuntimeProfileRechargeProductAdminListProcedureResult,
|
||||||
|
) -> Result<Vec<RuntimeProfileRechargeProductConfigRecord>, SpacetimeClientError> {
|
||||||
|
if !result.ok {
|
||||||
|
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result
|
||||||
|
.entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|snapshot| {
|
||||||
|
build_runtime_profile_recharge_product_config_record(
|
||||||
|
map_runtime_profile_recharge_product_config_snapshot(snapshot),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map_runtime_profile_recharge_product_admin_procedure_result(
|
||||||
|
result: RuntimeProfileRechargeProductAdminProcedureResult,
|
||||||
|
) -> Result<RuntimeProfileRechargeProductConfigRecord, SpacetimeClientError> {
|
||||||
|
if !result.ok {
|
||||||
|
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot = result
|
||||||
|
.record
|
||||||
|
.ok_or_else(|| SpacetimeClientError::missing_snapshot("recharge product config 快照"))?;
|
||||||
|
|
||||||
|
Ok(build_runtime_profile_recharge_product_config_record(
|
||||||
|
map_runtime_profile_recharge_product_config_snapshot(snapshot),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn map_runtime_profile_redeem_code_admin_procedure_result(
|
pub(crate) fn map_runtime_profile_redeem_code_admin_procedure_result(
|
||||||
result: RuntimeProfileRedeemCodeAdminProcedureResult,
|
result: RuntimeProfileRedeemCodeAdminProcedureResult,
|
||||||
) -> Result<RuntimeProfileRedeemCodeRecord, SpacetimeClientError> {
|
) -> Result<RuntimeProfileRedeemCodeRecord, SpacetimeClientError> {
|
||||||
@@ -2237,6 +2304,29 @@ pub(crate) fn map_runtime_profile_recharge_product_snapshot(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map_runtime_profile_recharge_product_config_snapshot(
|
||||||
|
snapshot: RuntimeProfileRechargeProductConfigSnapshot,
|
||||||
|
) -> module_runtime::RuntimeProfileRechargeProductConfigSnapshot {
|
||||||
|
module_runtime::RuntimeProfileRechargeProductConfigSnapshot {
|
||||||
|
product_id: snapshot.product_id,
|
||||||
|
title: snapshot.title,
|
||||||
|
price_cents: snapshot.price_cents,
|
||||||
|
kind: map_runtime_profile_recharge_product_kind_back(snapshot.kind),
|
||||||
|
points_amount: snapshot.points_amount,
|
||||||
|
bonus_points: snapshot.bonus_points,
|
||||||
|
duration_days: snapshot.duration_days,
|
||||||
|
badge_label: snapshot.badge_label,
|
||||||
|
description: snapshot.description,
|
||||||
|
tier: map_runtime_profile_membership_tier_back(snapshot.tier),
|
||||||
|
enabled: snapshot.enabled,
|
||||||
|
sort_order: snapshot.sort_order,
|
||||||
|
created_by: snapshot.created_by,
|
||||||
|
created_at_micros: snapshot.created_at_micros,
|
||||||
|
updated_by: snapshot.updated_by,
|
||||||
|
updated_at_micros: snapshot.updated_at_micros,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn map_runtime_profile_membership_benefit_snapshot(
|
pub(crate) fn map_runtime_profile_membership_benefit_snapshot(
|
||||||
snapshot: RuntimeProfileMembershipBenefitSnapshot,
|
snapshot: RuntimeProfileMembershipBenefitSnapshot,
|
||||||
) -> module_runtime::RuntimeProfileMembershipBenefitSnapshot {
|
) -> module_runtime::RuntimeProfileMembershipBenefitSnapshot {
|
||||||
@@ -5037,6 +5127,19 @@ pub(crate) fn map_runtime_profile_redeem_code_mode_back(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map_runtime_profile_recharge_product_kind(
|
||||||
|
value: module_runtime::RuntimeProfileRechargeProductKind,
|
||||||
|
) -> crate::module_bindings::RuntimeProfileRechargeProductKind {
|
||||||
|
match value {
|
||||||
|
module_runtime::RuntimeProfileRechargeProductKind::Points => {
|
||||||
|
crate::module_bindings::RuntimeProfileRechargeProductKind::Points
|
||||||
|
}
|
||||||
|
module_runtime::RuntimeProfileRechargeProductKind::Membership => {
|
||||||
|
crate::module_bindings::RuntimeProfileRechargeProductKind::Membership
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn map_runtime_profile_recharge_product_kind_back(
|
pub(crate) fn map_runtime_profile_recharge_product_kind_back(
|
||||||
value: crate::module_bindings::RuntimeProfileRechargeProductKind,
|
value: crate::module_bindings::RuntimeProfileRechargeProductKind,
|
||||||
) -> module_runtime::RuntimeProfileRechargeProductKind {
|
) -> module_runtime::RuntimeProfileRechargeProductKind {
|
||||||
@@ -5050,6 +5153,25 @@ pub(crate) fn map_runtime_profile_recharge_product_kind_back(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map_runtime_profile_membership_tier(
|
||||||
|
value: module_runtime::RuntimeProfileMembershipTier,
|
||||||
|
) -> crate::module_bindings::RuntimeProfileMembershipTier {
|
||||||
|
match value {
|
||||||
|
module_runtime::RuntimeProfileMembershipTier::Normal => {
|
||||||
|
crate::module_bindings::RuntimeProfileMembershipTier::Normal
|
||||||
|
}
|
||||||
|
module_runtime::RuntimeProfileMembershipTier::Month => {
|
||||||
|
crate::module_bindings::RuntimeProfileMembershipTier::Month
|
||||||
|
}
|
||||||
|
module_runtime::RuntimeProfileMembershipTier::Season => {
|
||||||
|
crate::module_bindings::RuntimeProfileMembershipTier::Season
|
||||||
|
}
|
||||||
|
module_runtime::RuntimeProfileMembershipTier::Year => {
|
||||||
|
crate::module_bindings::RuntimeProfileMembershipTier::Year
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn map_runtime_profile_membership_status_back(
|
pub(crate) fn map_runtime_profile_membership_status_back(
|
||||||
value: crate::module_bindings::RuntimeProfileMembershipStatus,
|
value: crate::module_bindings::RuntimeProfileMembershipStatus,
|
||||||
) -> module_runtime::RuntimeProfileMembershipStatus {
|
) -> module_runtime::RuntimeProfileMembershipStatus {
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::runtime_profile_recharge_product_admin_list_input_type::RuntimeProfileRechargeProductAdminListInput;
|
||||||
|
use super::runtime_profile_recharge_product_admin_list_procedure_result_type::RuntimeProfileRechargeProductAdminListProcedureResult;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
struct AdminListProfileRechargeProductsArgs {
|
||||||
|
pub input: RuntimeProfileRechargeProductAdminListInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for AdminListProfileRechargeProductsArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `admin_list_profile_recharge_products`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
|
pub trait admin_list_profile_recharge_products {
|
||||||
|
fn admin_list_profile_recharge_products(
|
||||||
|
&self,
|
||||||
|
input: RuntimeProfileRechargeProductAdminListInput,
|
||||||
|
) {
|
||||||
|
self.admin_list_profile_recharge_products_then(input, |_, _| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_list_profile_recharge_products_then(
|
||||||
|
&self,
|
||||||
|
input: RuntimeProfileRechargeProductAdminListInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(
|
||||||
|
&super::ProcedureEventContext,
|
||||||
|
Result<RuntimeProfileRechargeProductAdminListProcedureResult, __sdk::InternalError>,
|
||||||
|
) + Send
|
||||||
|
+ 'static,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl admin_list_profile_recharge_products for super::RemoteProcedures {
|
||||||
|
fn admin_list_profile_recharge_products_then(
|
||||||
|
&self,
|
||||||
|
input: RuntimeProfileRechargeProductAdminListInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(
|
||||||
|
&super::ProcedureEventContext,
|
||||||
|
Result<RuntimeProfileRechargeProductAdminListProcedureResult, __sdk::InternalError>,
|
||||||
|
) + Send
|
||||||
|
+ 'static,
|
||||||
|
) {
|
||||||
|
self.imp.invoke_procedure_with_callback::<_, RuntimeProfileRechargeProductAdminListProcedureResult>(
|
||||||
|
"admin_list_profile_recharge_products",
|
||||||
|
AdminListProfileRechargeProductsArgs { input, },
|
||||||
|
__callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::runtime_profile_recharge_product_admin_procedure_result_type::RuntimeProfileRechargeProductAdminProcedureResult;
|
||||||
|
use super::runtime_profile_recharge_product_admin_upsert_input_type::RuntimeProfileRechargeProductAdminUpsertInput;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
struct AdminUpsertProfileRechargeProductArgs {
|
||||||
|
pub input: RuntimeProfileRechargeProductAdminUpsertInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for AdminUpsertProfileRechargeProductArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `admin_upsert_profile_recharge_product`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
|
pub trait admin_upsert_profile_recharge_product {
|
||||||
|
fn admin_upsert_profile_recharge_product(
|
||||||
|
&self,
|
||||||
|
input: RuntimeProfileRechargeProductAdminUpsertInput,
|
||||||
|
) {
|
||||||
|
self.admin_upsert_profile_recharge_product_then(input, |_, _| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_upsert_profile_recharge_product_then(
|
||||||
|
&self,
|
||||||
|
input: RuntimeProfileRechargeProductAdminUpsertInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(
|
||||||
|
&super::ProcedureEventContext,
|
||||||
|
Result<RuntimeProfileRechargeProductAdminProcedureResult, __sdk::InternalError>,
|
||||||
|
) + Send
|
||||||
|
+ 'static,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl admin_upsert_profile_recharge_product for super::RemoteProcedures {
|
||||||
|
fn admin_upsert_profile_recharge_product_then(
|
||||||
|
&self,
|
||||||
|
input: RuntimeProfileRechargeProductAdminUpsertInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(
|
||||||
|
&super::ProcedureEventContext,
|
||||||
|
Result<RuntimeProfileRechargeProductAdminProcedureResult, __sdk::InternalError>,
|
||||||
|
) + Send
|
||||||
|
+ 'static,
|
||||||
|
) {
|
||||||
|
self.imp
|
||||||
|
.invoke_procedure_with_callback::<_, RuntimeProfileRechargeProductAdminProcedureResult>(
|
||||||
|
"admin_upsert_profile_recharge_product",
|
||||||
|
AdminUpsertProfileRechargeProductArgs { input },
|
||||||
|
__callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,9 +11,11 @@ pub mod acknowledge_quest_completion_reducer;
|
|||||||
pub mod admin_disable_profile_redeem_code_procedure;
|
pub mod admin_disable_profile_redeem_code_procedure;
|
||||||
pub mod admin_disable_profile_task_config_procedure;
|
pub mod admin_disable_profile_task_config_procedure;
|
||||||
pub mod admin_list_profile_invite_codes_procedure;
|
pub mod admin_list_profile_invite_codes_procedure;
|
||||||
|
pub mod admin_list_profile_recharge_products_procedure;
|
||||||
pub mod admin_list_profile_redeem_codes_procedure;
|
pub mod admin_list_profile_redeem_codes_procedure;
|
||||||
pub mod admin_list_profile_task_configs_procedure;
|
pub mod admin_list_profile_task_configs_procedure;
|
||||||
pub mod admin_upsert_profile_invite_code_procedure;
|
pub mod admin_upsert_profile_invite_code_procedure;
|
||||||
|
pub mod admin_upsert_profile_recharge_product_procedure;
|
||||||
pub mod admin_upsert_profile_redeem_code_procedure;
|
pub mod admin_upsert_profile_redeem_code_procedure;
|
||||||
pub mod admin_upsert_profile_task_config_procedure;
|
pub mod admin_upsert_profile_task_config_procedure;
|
||||||
pub mod advance_puzzle_next_level_procedure;
|
pub mod advance_puzzle_next_level_procedure;
|
||||||
@@ -461,6 +463,8 @@ pub mod profile_played_world_table;
|
|||||||
pub mod profile_played_world_type;
|
pub mod profile_played_world_type;
|
||||||
pub mod profile_recharge_order_table;
|
pub mod profile_recharge_order_table;
|
||||||
pub mod profile_recharge_order_type;
|
pub mod profile_recharge_order_type;
|
||||||
|
pub mod profile_recharge_product_config_table;
|
||||||
|
pub mod profile_recharge_product_config_type;
|
||||||
pub mod profile_redeem_code_table;
|
pub mod profile_redeem_code_table;
|
||||||
pub mod profile_redeem_code_type;
|
pub mod profile_redeem_code_type;
|
||||||
pub mod profile_redeem_code_usage_table;
|
pub mod profile_redeem_code_usage_table;
|
||||||
@@ -652,6 +656,11 @@ pub mod runtime_profile_recharge_order_get_input_type;
|
|||||||
pub mod runtime_profile_recharge_order_paid_input_type;
|
pub mod runtime_profile_recharge_order_paid_input_type;
|
||||||
pub mod runtime_profile_recharge_order_snapshot_type;
|
pub mod runtime_profile_recharge_order_snapshot_type;
|
||||||
pub mod runtime_profile_recharge_order_status_type;
|
pub mod runtime_profile_recharge_order_status_type;
|
||||||
|
pub mod runtime_profile_recharge_product_admin_list_input_type;
|
||||||
|
pub mod runtime_profile_recharge_product_admin_list_procedure_result_type;
|
||||||
|
pub mod runtime_profile_recharge_product_admin_procedure_result_type;
|
||||||
|
pub mod runtime_profile_recharge_product_admin_upsert_input_type;
|
||||||
|
pub mod runtime_profile_recharge_product_config_snapshot_type;
|
||||||
pub mod runtime_profile_recharge_product_kind_type;
|
pub mod runtime_profile_recharge_product_kind_type;
|
||||||
pub mod runtime_profile_recharge_product_snapshot_type;
|
pub mod runtime_profile_recharge_product_snapshot_type;
|
||||||
pub mod runtime_profile_redeem_code_admin_disable_input_type;
|
pub mod runtime_profile_redeem_code_admin_disable_input_type;
|
||||||
@@ -857,9 +866,11 @@ pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion;
|
|||||||
pub use admin_disable_profile_redeem_code_procedure::admin_disable_profile_redeem_code;
|
pub use admin_disable_profile_redeem_code_procedure::admin_disable_profile_redeem_code;
|
||||||
pub use admin_disable_profile_task_config_procedure::admin_disable_profile_task_config;
|
pub use admin_disable_profile_task_config_procedure::admin_disable_profile_task_config;
|
||||||
pub use admin_list_profile_invite_codes_procedure::admin_list_profile_invite_codes;
|
pub use admin_list_profile_invite_codes_procedure::admin_list_profile_invite_codes;
|
||||||
|
pub use admin_list_profile_recharge_products_procedure::admin_list_profile_recharge_products;
|
||||||
pub use admin_list_profile_redeem_codes_procedure::admin_list_profile_redeem_codes;
|
pub use admin_list_profile_redeem_codes_procedure::admin_list_profile_redeem_codes;
|
||||||
pub use admin_list_profile_task_configs_procedure::admin_list_profile_task_configs;
|
pub use admin_list_profile_task_configs_procedure::admin_list_profile_task_configs;
|
||||||
pub use admin_upsert_profile_invite_code_procedure::admin_upsert_profile_invite_code;
|
pub use admin_upsert_profile_invite_code_procedure::admin_upsert_profile_invite_code;
|
||||||
|
pub use admin_upsert_profile_recharge_product_procedure::admin_upsert_profile_recharge_product;
|
||||||
pub use admin_upsert_profile_redeem_code_procedure::admin_upsert_profile_redeem_code;
|
pub use admin_upsert_profile_redeem_code_procedure::admin_upsert_profile_redeem_code;
|
||||||
pub use admin_upsert_profile_task_config_procedure::admin_upsert_profile_task_config;
|
pub use admin_upsert_profile_task_config_procedure::admin_upsert_profile_task_config;
|
||||||
pub use advance_puzzle_next_level_procedure::advance_puzzle_next_level;
|
pub use advance_puzzle_next_level_procedure::advance_puzzle_next_level;
|
||||||
@@ -1307,6 +1318,8 @@ pub use profile_played_world_table::*;
|
|||||||
pub use profile_played_world_type::ProfilePlayedWorld;
|
pub use profile_played_world_type::ProfilePlayedWorld;
|
||||||
pub use profile_recharge_order_table::*;
|
pub use profile_recharge_order_table::*;
|
||||||
pub use profile_recharge_order_type::ProfileRechargeOrder;
|
pub use profile_recharge_order_type::ProfileRechargeOrder;
|
||||||
|
pub use profile_recharge_product_config_table::*;
|
||||||
|
pub use profile_recharge_product_config_type::ProfileRechargeProductConfig;
|
||||||
pub use profile_redeem_code_table::*;
|
pub use profile_redeem_code_table::*;
|
||||||
pub use profile_redeem_code_type::ProfileRedeemCode;
|
pub use profile_redeem_code_type::ProfileRedeemCode;
|
||||||
pub use profile_redeem_code_usage_table::*;
|
pub use profile_redeem_code_usage_table::*;
|
||||||
@@ -1498,6 +1511,11 @@ pub use runtime_profile_recharge_order_get_input_type::RuntimeProfileRechargeOrd
|
|||||||
pub use runtime_profile_recharge_order_paid_input_type::RuntimeProfileRechargeOrderPaidInput;
|
pub use runtime_profile_recharge_order_paid_input_type::RuntimeProfileRechargeOrderPaidInput;
|
||||||
pub use runtime_profile_recharge_order_snapshot_type::RuntimeProfileRechargeOrderSnapshot;
|
pub use runtime_profile_recharge_order_snapshot_type::RuntimeProfileRechargeOrderSnapshot;
|
||||||
pub use runtime_profile_recharge_order_status_type::RuntimeProfileRechargeOrderStatus;
|
pub use runtime_profile_recharge_order_status_type::RuntimeProfileRechargeOrderStatus;
|
||||||
|
pub use runtime_profile_recharge_product_admin_list_input_type::RuntimeProfileRechargeProductAdminListInput;
|
||||||
|
pub use runtime_profile_recharge_product_admin_list_procedure_result_type::RuntimeProfileRechargeProductAdminListProcedureResult;
|
||||||
|
pub use runtime_profile_recharge_product_admin_procedure_result_type::RuntimeProfileRechargeProductAdminProcedureResult;
|
||||||
|
pub use runtime_profile_recharge_product_admin_upsert_input_type::RuntimeProfileRechargeProductAdminUpsertInput;
|
||||||
|
pub use runtime_profile_recharge_product_config_snapshot_type::RuntimeProfileRechargeProductConfigSnapshot;
|
||||||
pub use runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
|
pub use runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
|
||||||
pub use runtime_profile_recharge_product_snapshot_type::RuntimeProfileRechargeProductSnapshot;
|
pub use runtime_profile_recharge_product_snapshot_type::RuntimeProfileRechargeProductSnapshot;
|
||||||
pub use runtime_profile_redeem_code_admin_disable_input_type::RuntimeProfileRedeemCodeAdminDisableInput;
|
pub use runtime_profile_redeem_code_admin_disable_input_type::RuntimeProfileRedeemCodeAdminDisableInput;
|
||||||
@@ -2020,6 +2038,7 @@ pub struct DbUpdate {
|
|||||||
profile_membership: __sdk::TableUpdate<ProfileMembership>,
|
profile_membership: __sdk::TableUpdate<ProfileMembership>,
|
||||||
profile_played_world: __sdk::TableUpdate<ProfilePlayedWorld>,
|
profile_played_world: __sdk::TableUpdate<ProfilePlayedWorld>,
|
||||||
profile_recharge_order: __sdk::TableUpdate<ProfileRechargeOrder>,
|
profile_recharge_order: __sdk::TableUpdate<ProfileRechargeOrder>,
|
||||||
|
profile_recharge_product_config: __sdk::TableUpdate<ProfileRechargeProductConfig>,
|
||||||
profile_redeem_code: __sdk::TableUpdate<ProfileRedeemCode>,
|
profile_redeem_code: __sdk::TableUpdate<ProfileRedeemCode>,
|
||||||
profile_redeem_code_usage: __sdk::TableUpdate<ProfileRedeemCodeUsage>,
|
profile_redeem_code_usage: __sdk::TableUpdate<ProfileRedeemCodeUsage>,
|
||||||
profile_referral_relation: __sdk::TableUpdate<ProfileReferralRelation>,
|
profile_referral_relation: __sdk::TableUpdate<ProfileReferralRelation>,
|
||||||
@@ -2224,6 +2243,11 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
|
|||||||
"profile_recharge_order" => db_update.profile_recharge_order.append(
|
"profile_recharge_order" => db_update.profile_recharge_order.append(
|
||||||
profile_recharge_order_table::parse_table_update(table_update)?,
|
profile_recharge_order_table::parse_table_update(table_update)?,
|
||||||
),
|
),
|
||||||
|
"profile_recharge_product_config" => {
|
||||||
|
db_update.profile_recharge_product_config.append(
|
||||||
|
profile_recharge_product_config_table::parse_table_update(table_update)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
"profile_redeem_code" => db_update
|
"profile_redeem_code" => db_update
|
||||||
.profile_redeem_code
|
.profile_redeem_code
|
||||||
.append(profile_redeem_code_table::parse_table_update(table_update)?),
|
.append(profile_redeem_code_table::parse_table_update(table_update)?),
|
||||||
@@ -2627,6 +2651,12 @@ impl __sdk::DbUpdate for DbUpdate {
|
|||||||
&self.profile_recharge_order,
|
&self.profile_recharge_order,
|
||||||
)
|
)
|
||||||
.with_updates_by_pk(|row| &row.order_id);
|
.with_updates_by_pk(|row| &row.order_id);
|
||||||
|
diff.profile_recharge_product_config = cache
|
||||||
|
.apply_diff_to_table::<ProfileRechargeProductConfig>(
|
||||||
|
"profile_recharge_product_config",
|
||||||
|
&self.profile_recharge_product_config,
|
||||||
|
)
|
||||||
|
.with_updates_by_pk(|row| &row.product_id);
|
||||||
diff.profile_redeem_code = cache
|
diff.profile_redeem_code = cache
|
||||||
.apply_diff_to_table::<ProfileRedeemCode>(
|
.apply_diff_to_table::<ProfileRedeemCode>(
|
||||||
"profile_redeem_code",
|
"profile_redeem_code",
|
||||||
@@ -2969,6 +2999,9 @@ impl __sdk::DbUpdate for DbUpdate {
|
|||||||
"profile_recharge_order" => db_update
|
"profile_recharge_order" => db_update
|
||||||
.profile_recharge_order
|
.profile_recharge_order
|
||||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
|
"profile_recharge_product_config" => db_update
|
||||||
|
.profile_recharge_product_config
|
||||||
|
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
"profile_redeem_code" => db_update
|
"profile_redeem_code" => db_update
|
||||||
.profile_redeem_code
|
.profile_redeem_code
|
||||||
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
|
||||||
@@ -3246,6 +3279,9 @@ impl __sdk::DbUpdate for DbUpdate {
|
|||||||
"profile_recharge_order" => db_update
|
"profile_recharge_order" => db_update
|
||||||
.profile_recharge_order
|
.profile_recharge_order
|
||||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
|
"profile_recharge_product_config" => db_update
|
||||||
|
.profile_recharge_product_config
|
||||||
|
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
"profile_redeem_code" => db_update
|
"profile_redeem_code" => db_update
|
||||||
.profile_redeem_code
|
.profile_redeem_code
|
||||||
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
|
||||||
@@ -3427,6 +3463,7 @@ pub struct AppliedDiff<'r> {
|
|||||||
profile_membership: __sdk::TableAppliedDiff<'r, ProfileMembership>,
|
profile_membership: __sdk::TableAppliedDiff<'r, ProfileMembership>,
|
||||||
profile_played_world: __sdk::TableAppliedDiff<'r, ProfilePlayedWorld>,
|
profile_played_world: __sdk::TableAppliedDiff<'r, ProfilePlayedWorld>,
|
||||||
profile_recharge_order: __sdk::TableAppliedDiff<'r, ProfileRechargeOrder>,
|
profile_recharge_order: __sdk::TableAppliedDiff<'r, ProfileRechargeOrder>,
|
||||||
|
profile_recharge_product_config: __sdk::TableAppliedDiff<'r, ProfileRechargeProductConfig>,
|
||||||
profile_redeem_code: __sdk::TableAppliedDiff<'r, ProfileRedeemCode>,
|
profile_redeem_code: __sdk::TableAppliedDiff<'r, ProfileRedeemCode>,
|
||||||
profile_redeem_code_usage: __sdk::TableAppliedDiff<'r, ProfileRedeemCodeUsage>,
|
profile_redeem_code_usage: __sdk::TableAppliedDiff<'r, ProfileRedeemCodeUsage>,
|
||||||
profile_referral_relation: __sdk::TableAppliedDiff<'r, ProfileReferralRelation>,
|
profile_referral_relation: __sdk::TableAppliedDiff<'r, ProfileReferralRelation>,
|
||||||
@@ -3717,6 +3754,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
|
|||||||
&self.profile_recharge_order,
|
&self.profile_recharge_order,
|
||||||
event,
|
event,
|
||||||
);
|
);
|
||||||
|
callbacks.invoke_table_row_callbacks::<ProfileRechargeProductConfig>(
|
||||||
|
"profile_recharge_product_config",
|
||||||
|
&self.profile_recharge_product_config,
|
||||||
|
event,
|
||||||
|
);
|
||||||
callbacks.invoke_table_row_callbacks::<ProfileRedeemCode>(
|
callbacks.invoke_table_row_callbacks::<ProfileRedeemCode>(
|
||||||
"profile_redeem_code",
|
"profile_redeem_code",
|
||||||
&self.profile_redeem_code,
|
&self.profile_redeem_code,
|
||||||
@@ -4609,6 +4651,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
|||||||
profile_membership_table::register_table(client_cache);
|
profile_membership_table::register_table(client_cache);
|
||||||
profile_played_world_table::register_table(client_cache);
|
profile_played_world_table::register_table(client_cache);
|
||||||
profile_recharge_order_table::register_table(client_cache);
|
profile_recharge_order_table::register_table(client_cache);
|
||||||
|
profile_recharge_product_config_table::register_table(client_cache);
|
||||||
profile_redeem_code_table::register_table(client_cache);
|
profile_redeem_code_table::register_table(client_cache);
|
||||||
profile_redeem_code_usage_table::register_table(client_cache);
|
profile_redeem_code_usage_table::register_table(client_cache);
|
||||||
profile_referral_relation_table::register_table(client_cache);
|
profile_referral_relation_table::register_table(client_cache);
|
||||||
@@ -4699,6 +4742,7 @@ impl __sdk::SpacetimeModule for RemoteModule {
|
|||||||
"profile_membership",
|
"profile_membership",
|
||||||
"profile_played_world",
|
"profile_played_world",
|
||||||
"profile_recharge_order",
|
"profile_recharge_order",
|
||||||
|
"profile_recharge_product_config",
|
||||||
"profile_redeem_code",
|
"profile_redeem_code",
|
||||||
"profile_redeem_code_usage",
|
"profile_redeem_code_usage",
|
||||||
"profile_referral_relation",
|
"profile_referral_relation",
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use super::profile_recharge_product_config_type::ProfileRechargeProductConfig;
|
||||||
|
use super::runtime_profile_membership_tier_type::RuntimeProfileMembershipTier;
|
||||||
|
use super::runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
/// Table handle for the table `profile_recharge_product_config`.
|
||||||
|
///
|
||||||
|
/// Obtain a handle from the [`ProfileRechargeProductConfigTableAccess::profile_recharge_product_config`] method on [`super::RemoteTables`],
|
||||||
|
/// like `ctx.db.profile_recharge_product_config()`.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.profile_recharge_product_config().on_insert(...)`.
|
||||||
|
pub struct ProfileRechargeProductConfigTableHandle<'ctx> {
|
||||||
|
imp: __sdk::TableHandle<ProfileRechargeProductConfig>,
|
||||||
|
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the table `profile_recharge_product_config`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteTables`].
|
||||||
|
pub trait ProfileRechargeProductConfigTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Obtain a [`ProfileRechargeProductConfigTableHandle`], which mediates access to the table `profile_recharge_product_config`.
|
||||||
|
fn profile_recharge_product_config(&self) -> ProfileRechargeProductConfigTableHandle<'_>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileRechargeProductConfigTableAccess for super::RemoteTables {
|
||||||
|
fn profile_recharge_product_config(&self) -> ProfileRechargeProductConfigTableHandle<'_> {
|
||||||
|
ProfileRechargeProductConfigTableHandle {
|
||||||
|
imp: self
|
||||||
|
.imp
|
||||||
|
.get_table::<ProfileRechargeProductConfig>("profile_recharge_product_config"),
|
||||||
|
ctx: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProfileRechargeProductConfigInsertCallbackId(__sdk::CallbackId);
|
||||||
|
pub struct ProfileRechargeProductConfigDeleteCallbackId(__sdk::CallbackId);
|
||||||
|
|
||||||
|
impl<'ctx> __sdk::Table for ProfileRechargeProductConfigTableHandle<'ctx> {
|
||||||
|
type Row = ProfileRechargeProductConfig;
|
||||||
|
type EventContext = super::EventContext;
|
||||||
|
|
||||||
|
fn count(&self) -> u64 {
|
||||||
|
self.imp.count()
|
||||||
|
}
|
||||||
|
fn iter(&self) -> impl Iterator<Item = ProfileRechargeProductConfig> + '_ {
|
||||||
|
self.imp.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
type InsertCallbackId = ProfileRechargeProductConfigInsertCallbackId;
|
||||||
|
|
||||||
|
fn on_insert(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> ProfileRechargeProductConfigInsertCallbackId {
|
||||||
|
ProfileRechargeProductConfigInsertCallbackId(self.imp.on_insert(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_insert(&self, callback: ProfileRechargeProductConfigInsertCallbackId) {
|
||||||
|
self.imp.remove_on_insert(callback.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteCallbackId = ProfileRechargeProductConfigDeleteCallbackId;
|
||||||
|
|
||||||
|
fn on_delete(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
|
||||||
|
) -> ProfileRechargeProductConfigDeleteCallbackId {
|
||||||
|
ProfileRechargeProductConfigDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_delete(&self, callback: ProfileRechargeProductConfigDeleteCallbackId) {
|
||||||
|
self.imp.remove_on_delete(callback.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProfileRechargeProductConfigUpdateCallbackId(__sdk::CallbackId);
|
||||||
|
|
||||||
|
impl<'ctx> __sdk::TableWithPrimaryKey for ProfileRechargeProductConfigTableHandle<'ctx> {
|
||||||
|
type UpdateCallbackId = ProfileRechargeProductConfigUpdateCallbackId;
|
||||||
|
|
||||||
|
fn on_update(
|
||||||
|
&self,
|
||||||
|
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
|
||||||
|
) -> ProfileRechargeProductConfigUpdateCallbackId {
|
||||||
|
ProfileRechargeProductConfigUpdateCallbackId(self.imp.on_update(Box::new(callback)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_on_update(&self, callback: ProfileRechargeProductConfigUpdateCallbackId) {
|
||||||
|
self.imp.remove_on_update(callback.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access to the `product_id` unique index on the table `profile_recharge_product_config`,
|
||||||
|
/// which allows point queries on the field of the same name
|
||||||
|
/// via the [`ProfileRechargeProductConfigProductIdUnique::find`] method.
|
||||||
|
///
|
||||||
|
/// Users are encouraged not to explicitly reference this type,
|
||||||
|
/// but to directly chain method calls,
|
||||||
|
/// like `ctx.db.profile_recharge_product_config().product_id().find(...)`.
|
||||||
|
pub struct ProfileRechargeProductConfigProductIdUnique<'ctx> {
|
||||||
|
imp: __sdk::UniqueConstraintHandle<ProfileRechargeProductConfig, String>,
|
||||||
|
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> ProfileRechargeProductConfigTableHandle<'ctx> {
|
||||||
|
/// Get a handle on the `product_id` unique index on the table `profile_recharge_product_config`.
|
||||||
|
pub fn product_id(&self) -> ProfileRechargeProductConfigProductIdUnique<'ctx> {
|
||||||
|
ProfileRechargeProductConfigProductIdUnique {
|
||||||
|
imp: self.imp.get_unique_constraint::<String>("product_id"),
|
||||||
|
phantom: std::marker::PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx> ProfileRechargeProductConfigProductIdUnique<'ctx> {
|
||||||
|
/// Find the subscribed row whose `product_id` column value is equal to `col_val`,
|
||||||
|
/// if such a row is present in the client cache.
|
||||||
|
pub fn find(&self, col_val: &String) -> Option<ProfileRechargeProductConfig> {
|
||||||
|
self.imp.find(col_val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
|
||||||
|
let _table = client_cache
|
||||||
|
.get_or_make_table::<ProfileRechargeProductConfig>("profile_recharge_product_config");
|
||||||
|
_table.add_unique_constraint::<String>("product_id", |row| &row.product_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub(super) fn parse_table_update(
|
||||||
|
raw_updates: __ws::v2::TableUpdate,
|
||||||
|
) -> __sdk::Result<__sdk::TableUpdate<ProfileRechargeProductConfig>> {
|
||||||
|
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
|
||||||
|
__sdk::InternalError::failed_parse(
|
||||||
|
"TableUpdate<ProfileRechargeProductConfig>",
|
||||||
|
"TableUpdate",
|
||||||
|
)
|
||||||
|
.with_cause(e)
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for query builder access to the table `ProfileRechargeProductConfig`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`__sdk::QueryTableAccessor`].
|
||||||
|
pub trait profile_recharge_product_configQueryTableAccess {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
/// Get a query builder for the table `ProfileRechargeProductConfig`.
|
||||||
|
fn profile_recharge_product_config(
|
||||||
|
&self,
|
||||||
|
) -> __sdk::__query_builder::Table<ProfileRechargeProductConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl profile_recharge_product_configQueryTableAccess for __sdk::QueryTableAccessor {
|
||||||
|
fn profile_recharge_product_config(
|
||||||
|
&self,
|
||||||
|
) -> __sdk::__query_builder::Table<ProfileRechargeProductConfig> {
|
||||||
|
__sdk::__query_builder::Table::new("profile_recharge_product_config")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::runtime_profile_membership_tier_type::RuntimeProfileMembershipTier;
|
||||||
|
use super::runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct ProfileRechargeProductConfig {
|
||||||
|
pub product_id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub price_cents: u64,
|
||||||
|
pub kind: RuntimeProfileRechargeProductKind,
|
||||||
|
pub points_amount: u64,
|
||||||
|
pub bonus_points: u64,
|
||||||
|
pub duration_days: u32,
|
||||||
|
pub badge_label: String,
|
||||||
|
pub description: String,
|
||||||
|
pub tier: RuntimeProfileMembershipTier,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub sort_order: i32,
|
||||||
|
pub created_by: String,
|
||||||
|
pub created_at: __sdk::Timestamp,
|
||||||
|
pub updated_by: String,
|
||||||
|
pub updated_at: __sdk::Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for ProfileRechargeProductConfig {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Column accessor struct for the table `ProfileRechargeProductConfig`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to columns for query building.
|
||||||
|
pub struct ProfileRechargeProductConfigCols {
|
||||||
|
pub product_id: __sdk::__query_builder::Col<ProfileRechargeProductConfig, String>,
|
||||||
|
pub title: __sdk::__query_builder::Col<ProfileRechargeProductConfig, String>,
|
||||||
|
pub price_cents: __sdk::__query_builder::Col<ProfileRechargeProductConfig, u64>,
|
||||||
|
pub kind: __sdk::__query_builder::Col<
|
||||||
|
ProfileRechargeProductConfig,
|
||||||
|
RuntimeProfileRechargeProductKind,
|
||||||
|
>,
|
||||||
|
pub points_amount: __sdk::__query_builder::Col<ProfileRechargeProductConfig, u64>,
|
||||||
|
pub bonus_points: __sdk::__query_builder::Col<ProfileRechargeProductConfig, u64>,
|
||||||
|
pub duration_days: __sdk::__query_builder::Col<ProfileRechargeProductConfig, u32>,
|
||||||
|
pub badge_label: __sdk::__query_builder::Col<ProfileRechargeProductConfig, String>,
|
||||||
|
pub description: __sdk::__query_builder::Col<ProfileRechargeProductConfig, String>,
|
||||||
|
pub tier:
|
||||||
|
__sdk::__query_builder::Col<ProfileRechargeProductConfig, RuntimeProfileMembershipTier>,
|
||||||
|
pub enabled: __sdk::__query_builder::Col<ProfileRechargeProductConfig, bool>,
|
||||||
|
pub sort_order: __sdk::__query_builder::Col<ProfileRechargeProductConfig, i32>,
|
||||||
|
pub created_by: __sdk::__query_builder::Col<ProfileRechargeProductConfig, String>,
|
||||||
|
pub created_at: __sdk::__query_builder::Col<ProfileRechargeProductConfig, __sdk::Timestamp>,
|
||||||
|
pub updated_by: __sdk::__query_builder::Col<ProfileRechargeProductConfig, String>,
|
||||||
|
pub updated_at: __sdk::__query_builder::Col<ProfileRechargeProductConfig, __sdk::Timestamp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasCols for ProfileRechargeProductConfig {
|
||||||
|
type Cols = ProfileRechargeProductConfigCols;
|
||||||
|
fn cols(table_name: &'static str) -> Self::Cols {
|
||||||
|
ProfileRechargeProductConfigCols {
|
||||||
|
product_id: __sdk::__query_builder::Col::new(table_name, "product_id"),
|
||||||
|
title: __sdk::__query_builder::Col::new(table_name, "title"),
|
||||||
|
price_cents: __sdk::__query_builder::Col::new(table_name, "price_cents"),
|
||||||
|
kind: __sdk::__query_builder::Col::new(table_name, "kind"),
|
||||||
|
points_amount: __sdk::__query_builder::Col::new(table_name, "points_amount"),
|
||||||
|
bonus_points: __sdk::__query_builder::Col::new(table_name, "bonus_points"),
|
||||||
|
duration_days: __sdk::__query_builder::Col::new(table_name, "duration_days"),
|
||||||
|
badge_label: __sdk::__query_builder::Col::new(table_name, "badge_label"),
|
||||||
|
description: __sdk::__query_builder::Col::new(table_name, "description"),
|
||||||
|
tier: __sdk::__query_builder::Col::new(table_name, "tier"),
|
||||||
|
enabled: __sdk::__query_builder::Col::new(table_name, "enabled"),
|
||||||
|
sort_order: __sdk::__query_builder::Col::new(table_name, "sort_order"),
|
||||||
|
created_by: __sdk::__query_builder::Col::new(table_name, "created_by"),
|
||||||
|
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||||
|
updated_by: __sdk::__query_builder::Col::new(table_name, "updated_by"),
|
||||||
|
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indexed column accessor struct for the table `ProfileRechargeProductConfig`.
|
||||||
|
///
|
||||||
|
/// Provides typed access to indexed columns for query building.
|
||||||
|
pub struct ProfileRechargeProductConfigIxCols {
|
||||||
|
pub product_id: __sdk::__query_builder::IxCol<ProfileRechargeProductConfig, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::HasIxCols for ProfileRechargeProductConfig {
|
||||||
|
type IxCols = ProfileRechargeProductConfigIxCols;
|
||||||
|
fn ix_cols(table_name: &'static str) -> Self::IxCols {
|
||||||
|
ProfileRechargeProductConfigIxCols {
|
||||||
|
product_id: __sdk::__query_builder::IxCol::new(table_name, "product_id"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::__query_builder::CanBeLookupTable for ProfileRechargeProductConfig {}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct RuntimeProfileRechargeProductAdminListInput {
|
||||||
|
pub admin_user_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for RuntimeProfileRechargeProductAdminListInput {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::runtime_profile_recharge_product_config_snapshot_type::RuntimeProfileRechargeProductConfigSnapshot;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct RuntimeProfileRechargeProductAdminListProcedureResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub entries: Vec<RuntimeProfileRechargeProductConfigSnapshot>,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for RuntimeProfileRechargeProductAdminListProcedureResult {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::runtime_profile_recharge_product_config_snapshot_type::RuntimeProfileRechargeProductConfigSnapshot;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct RuntimeProfileRechargeProductAdminProcedureResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub record: Option<RuntimeProfileRechargeProductConfigSnapshot>,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for RuntimeProfileRechargeProductAdminProcedureResult {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::runtime_profile_membership_tier_type::RuntimeProfileMembershipTier;
|
||||||
|
use super::runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct RuntimeProfileRechargeProductAdminUpsertInput {
|
||||||
|
pub admin_user_id: String,
|
||||||
|
pub product_id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub price_cents: u64,
|
||||||
|
pub kind: RuntimeProfileRechargeProductKind,
|
||||||
|
pub points_amount: u64,
|
||||||
|
pub bonus_points: u64,
|
||||||
|
pub duration_days: u32,
|
||||||
|
pub badge_label: String,
|
||||||
|
pub description: String,
|
||||||
|
pub tier: RuntimeProfileMembershipTier,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub sort_order: i32,
|
||||||
|
pub updated_at_micros: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for RuntimeProfileRechargeProductAdminUpsertInput {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||||
|
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||||
|
|
||||||
|
#![allow(unused, clippy::all)]
|
||||||
|
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||||
|
|
||||||
|
use super::runtime_profile_membership_tier_type::RuntimeProfileMembershipTier;
|
||||||
|
use super::runtime_profile_recharge_product_kind_type::RuntimeProfileRechargeProductKind;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct RuntimeProfileRechargeProductConfigSnapshot {
|
||||||
|
pub product_id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub price_cents: u64,
|
||||||
|
pub kind: RuntimeProfileRechargeProductKind,
|
||||||
|
pub points_amount: u64,
|
||||||
|
pub bonus_points: u64,
|
||||||
|
pub duration_days: u32,
|
||||||
|
pub badge_label: String,
|
||||||
|
pub description: String,
|
||||||
|
pub tier: RuntimeProfileMembershipTier,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub sort_order: i32,
|
||||||
|
pub created_by: String,
|
||||||
|
pub created_at_micros: i64,
|
||||||
|
pub updated_by: String,
|
||||||
|
pub updated_at_micros: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for RuntimeProfileRechargeProductConfigSnapshot {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
@@ -657,6 +657,78 @@ impl SpacetimeClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn admin_list_profile_recharge_products(
|
||||||
|
&self,
|
||||||
|
admin_user_id: String,
|
||||||
|
) -> Result<Vec<RuntimeProfileRechargeProductConfigRecord>, SpacetimeClientError> {
|
||||||
|
let procedure_input =
|
||||||
|
build_runtime_profile_recharge_product_admin_list_input(admin_user_id)
|
||||||
|
.map_err(SpacetimeClientError::validation_failed)?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.admin_list_profile_recharge_products_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(SpacetimeClientError::from_sdk_error)
|
||||||
|
.and_then(map_runtime_profile_recharge_product_admin_list_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub async fn admin_upsert_profile_recharge_product(
|
||||||
|
&self,
|
||||||
|
admin_user_id: String,
|
||||||
|
product_id: String,
|
||||||
|
title: String,
|
||||||
|
price_cents: u64,
|
||||||
|
kind: module_runtime::RuntimeProfileRechargeProductKind,
|
||||||
|
points_amount: u64,
|
||||||
|
bonus_points: u64,
|
||||||
|
duration_days: u32,
|
||||||
|
badge_label: String,
|
||||||
|
description: String,
|
||||||
|
tier: module_runtime::RuntimeProfileMembershipTier,
|
||||||
|
enabled: bool,
|
||||||
|
sort_order: i32,
|
||||||
|
updated_at_micros: i64,
|
||||||
|
) -> Result<RuntimeProfileRechargeProductConfigRecord, SpacetimeClientError> {
|
||||||
|
let procedure_input = build_runtime_profile_recharge_product_admin_upsert_input(
|
||||||
|
admin_user_id,
|
||||||
|
product_id,
|
||||||
|
title,
|
||||||
|
price_cents,
|
||||||
|
kind,
|
||||||
|
points_amount,
|
||||||
|
bonus_points,
|
||||||
|
duration_days,
|
||||||
|
badge_label,
|
||||||
|
description,
|
||||||
|
tier,
|
||||||
|
enabled,
|
||||||
|
sort_order,
|
||||||
|
updated_at_micros,
|
||||||
|
)
|
||||||
|
.map_err(SpacetimeClientError::validation_failed)?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
self.call_after_connect(move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.admin_upsert_profile_recharge_product_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(SpacetimeClientError::from_sdk_error)
|
||||||
|
.and_then(map_runtime_profile_recharge_product_admin_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn admin_upsert_profile_redeem_code(
|
pub async fn admin_upsert_profile_redeem_code(
|
||||||
&self,
|
&self,
|
||||||
admin_user_id: String,
|
admin_user_id: String,
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ macro_rules! migration_tables {
|
|||||||
public_work_play_daily_stat,
|
public_work_play_daily_stat,
|
||||||
public_work_like,
|
public_work_like,
|
||||||
profile_membership,
|
profile_membership,
|
||||||
|
profile_recharge_product_config,
|
||||||
profile_recharge_order,
|
profile_recharge_order,
|
||||||
profile_feedback_submission,
|
profile_feedback_submission,
|
||||||
profile_save_archive,
|
profile_save_archive,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS: i64 = 7;
|
|||||||
const PROFILE_REFERRAL_INVITED_USERS_LIMIT: usize = 20;
|
const PROFILE_REFERRAL_INVITED_USERS_LIMIT: usize = 20;
|
||||||
const PROFILE_NEW_USER_REGISTRATION_LEDGER_PREFIX: &str = "new-user-registration";
|
const PROFILE_NEW_USER_REGISTRATION_LEDGER_PREFIX: &str = "new-user-registration";
|
||||||
const PROFILE_TASK_SYSTEM_USER_ID: &str = "system:profile-task";
|
const PROFILE_TASK_SYSTEM_USER_ID: &str = "system:profile-task";
|
||||||
|
const PROFILE_RECHARGE_PRODUCT_SYSTEM_USER_ID: &str = "system:recharge-product";
|
||||||
const PROFILE_TASK_LOGIN_EVENT_ID_PREFIX: &str = "daily-login";
|
const PROFILE_TASK_LOGIN_EVENT_ID_PREFIX: &str = "daily-login";
|
||||||
const PROFILE_TRACKING_PROFILE_MODULE_KEY: &str = "profile";
|
const PROFILE_TRACKING_PROFILE_MODULE_KEY: &str = "profile";
|
||||||
|
|
||||||
@@ -328,6 +329,28 @@ pub struct ProfileMembership {
|
|||||||
pub(crate) updated_at: Timestamp,
|
pub(crate) updated_at: Timestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[spacetimedb::table(accessor = profile_recharge_product_config)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ProfileRechargeProductConfig {
|
||||||
|
#[primary_key]
|
||||||
|
pub(crate) product_id: String,
|
||||||
|
pub(crate) title: String,
|
||||||
|
pub(crate) price_cents: u64,
|
||||||
|
pub(crate) kind: RuntimeProfileRechargeProductKind,
|
||||||
|
pub(crate) points_amount: u64,
|
||||||
|
pub(crate) bonus_points: u64,
|
||||||
|
pub(crate) duration_days: u32,
|
||||||
|
pub(crate) badge_label: String,
|
||||||
|
pub(crate) description: String,
|
||||||
|
pub(crate) tier: RuntimeProfileMembershipTier,
|
||||||
|
pub(crate) enabled: bool,
|
||||||
|
pub(crate) sort_order: i32,
|
||||||
|
pub(crate) created_by: String,
|
||||||
|
pub(crate) created_at: Timestamp,
|
||||||
|
pub(crate) updated_by: String,
|
||||||
|
pub(crate) updated_at: Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
accessor = profile_recharge_order,
|
accessor = profile_recharge_order,
|
||||||
index(accessor = by_profile_recharge_order_user_id, btree(columns = [user_id])),
|
index(accessor = by_profile_recharge_order_user_id, btree(columns = [user_id])),
|
||||||
@@ -655,6 +678,44 @@ pub fn admin_disable_profile_task_config(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[spacetimedb::procedure]
|
||||||
|
pub fn admin_list_profile_recharge_products(
|
||||||
|
ctx: &mut ProcedureContext,
|
||||||
|
input: RuntimeProfileRechargeProductAdminListInput,
|
||||||
|
) -> RuntimeProfileRechargeProductAdminListProcedureResult {
|
||||||
|
match ctx.try_with_tx(|tx| list_profile_recharge_product_config_snapshots(tx, input.clone())) {
|
||||||
|
Ok(entries) => RuntimeProfileRechargeProductAdminListProcedureResult {
|
||||||
|
ok: true,
|
||||||
|
entries,
|
||||||
|
error_message: None,
|
||||||
|
},
|
||||||
|
Err(message) => RuntimeProfileRechargeProductAdminListProcedureResult {
|
||||||
|
ok: false,
|
||||||
|
entries: Vec::new(),
|
||||||
|
error_message: Some(message),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[spacetimedb::procedure]
|
||||||
|
pub fn admin_upsert_profile_recharge_product(
|
||||||
|
ctx: &mut ProcedureContext,
|
||||||
|
input: RuntimeProfileRechargeProductAdminUpsertInput,
|
||||||
|
) -> RuntimeProfileRechargeProductAdminProcedureResult {
|
||||||
|
match ctx.try_with_tx(|tx| upsert_profile_recharge_product_config_record(tx, input.clone())) {
|
||||||
|
Ok(record) => RuntimeProfileRechargeProductAdminProcedureResult {
|
||||||
|
ok: true,
|
||||||
|
record: Some(record),
|
||||||
|
error_message: None,
|
||||||
|
},
|
||||||
|
Err(message) => RuntimeProfileRechargeProductAdminProcedureResult {
|
||||||
|
ok: false,
|
||||||
|
record: None,
|
||||||
|
error_message: Some(message),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 新用户注册赠送由后端注册链路调用;流水 ID 固定,保证重试不重复发放。
|
// 新用户注册赠送由后端注册链路调用;流水 ID 固定,保证重试不重复发放。
|
||||||
#[spacetimedb::procedure]
|
#[spacetimedb::procedure]
|
||||||
pub fn grant_new_user_registration_wallet_reward(
|
pub fn grant_new_user_registration_wallet_reward(
|
||||||
@@ -1454,6 +1515,22 @@ fn build_public_work_like_id(source_type: &str, profile_id: &str, user_id: &str)
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn point_recharge_display_is_resolved_per_product() {
|
||||||
|
let products = runtime_profile_recharge_point_products();
|
||||||
|
let repeated = resolve_profile_recharge_product_display(products[0].clone(), true);
|
||||||
|
let untouched = resolve_profile_recharge_product_display(products[1].clone(), false);
|
||||||
|
|
||||||
|
assert_eq!(repeated.product_id, "points_60");
|
||||||
|
assert_eq!(repeated.bonus_points, 0);
|
||||||
|
assert_eq!(repeated.badge_label, "");
|
||||||
|
assert_eq!(repeated.description, "60泥点");
|
||||||
|
assert_eq!(untouched.product_id, "points_180");
|
||||||
|
assert_eq!(untouched.bonus_points, 180);
|
||||||
|
assert_eq!(untouched.badge_label, "首充双倍");
|
||||||
|
assert_eq!(untouched.description, "首充送180泥点");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn duplicate_tracking_event_ids_are_treated_as_idempotent_replays() {
|
fn duplicate_tracking_event_ids_are_treated_as_idempotent_replays() {
|
||||||
assert!(should_skip_existing_tracking_event_id(true));
|
assert!(should_skip_existing_tracking_event_id(true));
|
||||||
@@ -2091,8 +2168,8 @@ fn create_profile_recharge_order_record(
|
|||||||
input.created_at_micros,
|
input.created_at_micros,
|
||||||
)
|
)
|
||||||
.map_err(|error| error.to_string())?;
|
.map_err(|error| error.to_string())?;
|
||||||
let product = runtime_profile_recharge_product_by_id(&validated_input.product_id)
|
let product = enabled_profile_recharge_product_by_id(ctx, &validated_input.product_id)
|
||||||
.ok_or_else(|| "recharge.product_id 不存在".to_string())?;
|
.ok_or_else(|| "recharge.product_id 不存在或已下架".to_string())?;
|
||||||
let created_at = Timestamp::from_micros_since_unix_epoch(validated_input.created_at_micros);
|
let created_at = Timestamp::from_micros_since_unix_epoch(validated_input.created_at_micros);
|
||||||
let should_settle_immediately =
|
let should_settle_immediately =
|
||||||
validated_input.payment_channel == PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK;
|
validated_input.payment_channel == PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK;
|
||||||
@@ -2201,7 +2278,7 @@ fn mark_profile_recharge_order_paid_record(
|
|||||||
return Err("profile_recharge_order 当前状态不能确认支付".to_string());
|
return Err("profile_recharge_order 当前状态不能确认支付".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let product = runtime_profile_recharge_product_by_id(&order.product_id)
|
let product = profile_recharge_product_by_id(ctx, &order.product_id)
|
||||||
.ok_or_else(|| "recharge.product_id 不存在".to_string())?;
|
.ok_or_else(|| "recharge.product_id 不存在".to_string())?;
|
||||||
let paid_at = Timestamp::from_micros_since_unix_epoch(validated_input.paid_at_micros);
|
let paid_at = Timestamp::from_micros_since_unix_epoch(validated_input.paid_at_micros);
|
||||||
let (points_delta, membership_expires_at) = apply_profile_recharge_purchase(
|
let (points_delta, membership_expires_at) = apply_profile_recharge_purchase(
|
||||||
@@ -2238,7 +2315,7 @@ fn apply_profile_recharge_purchase(
|
|||||||
) -> Result<(i64, Option<Timestamp>), String> {
|
) -> Result<(i64, Option<Timestamp>), String> {
|
||||||
match product.kind {
|
match product.kind {
|
||||||
RuntimeProfileRechargeProductKind::Points => {
|
RuntimeProfileRechargeProductKind::Points => {
|
||||||
let has_recharged = has_profile_points_recharged(ctx, user_id);
|
let has_recharged = has_profile_product_recharged(ctx, user_id, &product.product_id);
|
||||||
let points_delta =
|
let points_delta =
|
||||||
resolve_runtime_profile_points_recharge_delta(product, has_recharged);
|
resolve_runtime_profile_points_recharge_delta(product, has_recharged);
|
||||||
apply_profile_wallet_delta(
|
apply_profile_wallet_delta(
|
||||||
@@ -2907,6 +2984,7 @@ fn build_profile_recharge_center_snapshot(
|
|||||||
ctx: &ReducerContext,
|
ctx: &ReducerContext,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
) -> RuntimeProfileRechargeCenterSnapshot {
|
) -> RuntimeProfileRechargeCenterSnapshot {
|
||||||
|
ensure_default_profile_recharge_product_config(ctx);
|
||||||
let wallet_balance = ctx
|
let wallet_balance = ctx
|
||||||
.db
|
.db
|
||||||
.profile_dashboard_state()
|
.profile_dashboard_state()
|
||||||
@@ -2916,13 +2994,31 @@ fn build_profile_recharge_center_snapshot(
|
|||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
let has_points_recharged = has_profile_points_recharged(ctx, user_id);
|
let has_points_recharged = has_profile_points_recharged(ctx, user_id);
|
||||||
|
let mut point_products = Vec::new();
|
||||||
|
let mut membership_products = Vec::new();
|
||||||
|
for row in profile_recharge_product_config_rows(ctx, false) {
|
||||||
|
let product = build_profile_recharge_product_snapshot_from_config_row(&row);
|
||||||
|
match product.kind {
|
||||||
|
RuntimeProfileRechargeProductKind::Points => {
|
||||||
|
let has_product_recharged =
|
||||||
|
has_profile_product_recharged(ctx, user_id, &product.product_id);
|
||||||
|
point_products.push(resolve_profile_recharge_product_display(
|
||||||
|
product,
|
||||||
|
has_product_recharged,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
RuntimeProfileRechargeProductKind::Membership => {
|
||||||
|
membership_products.push(product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RuntimeProfileRechargeCenterSnapshot {
|
RuntimeProfileRechargeCenterSnapshot {
|
||||||
user_id: user_id.to_string(),
|
user_id: user_id.to_string(),
|
||||||
wallet_balance,
|
wallet_balance,
|
||||||
membership: build_profile_membership_snapshot(ctx, user_id),
|
membership: build_profile_membership_snapshot(ctx, user_id),
|
||||||
point_products: resolve_runtime_profile_recharge_point_products(has_points_recharged),
|
point_products,
|
||||||
membership_products: runtime_profile_recharge_membership_products(),
|
membership_products,
|
||||||
benefits: runtime_profile_membership_benefits(),
|
benefits: runtime_profile_membership_benefits(),
|
||||||
latest_order: latest_profile_recharge_order(ctx, user_id)
|
latest_order: latest_profile_recharge_order(ctx, user_id)
|
||||||
.map(|row| build_profile_recharge_order_snapshot_from_row(&row)),
|
.map(|row| build_profile_recharge_order_snapshot_from_row(&row)),
|
||||||
@@ -3052,6 +3148,21 @@ fn list_profile_task_config_snapshots(
|
|||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn list_profile_recharge_product_config_snapshots(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
input: RuntimeProfileRechargeProductAdminListInput,
|
||||||
|
) -> Result<Vec<RuntimeProfileRechargeProductConfigSnapshot>, String> {
|
||||||
|
let _validated_input =
|
||||||
|
build_runtime_profile_recharge_product_admin_list_input(input.admin_user_id)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
ensure_default_profile_recharge_product_config(ctx);
|
||||||
|
|
||||||
|
Ok(profile_recharge_product_config_rows(ctx, true)
|
||||||
|
.iter()
|
||||||
|
.map(build_profile_recharge_product_config_snapshot_from_row)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
fn admin_list_profile_redeem_code_records(
|
fn admin_list_profile_redeem_code_records(
|
||||||
ctx: &ReducerContext,
|
ctx: &ReducerContext,
|
||||||
input: RuntimeProfileRedeemCodeAdminListInput,
|
input: RuntimeProfileRedeemCodeAdminListInput,
|
||||||
@@ -3097,6 +3208,74 @@ fn admin_list_profile_invite_code_records(
|
|||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn upsert_profile_recharge_product_config_record(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
input: RuntimeProfileRechargeProductAdminUpsertInput,
|
||||||
|
) -> Result<RuntimeProfileRechargeProductConfigSnapshot, String> {
|
||||||
|
let validated_input = build_runtime_profile_recharge_product_admin_upsert_input(
|
||||||
|
input.admin_user_id,
|
||||||
|
input.product_id,
|
||||||
|
input.title,
|
||||||
|
input.price_cents,
|
||||||
|
input.kind,
|
||||||
|
input.points_amount,
|
||||||
|
input.bonus_points,
|
||||||
|
input.duration_days,
|
||||||
|
input.badge_label,
|
||||||
|
input.description,
|
||||||
|
input.tier,
|
||||||
|
input.enabled,
|
||||||
|
input.sort_order,
|
||||||
|
input.updated_at_micros,
|
||||||
|
)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
ensure_default_profile_recharge_product_config(ctx);
|
||||||
|
|
||||||
|
let updated_at = Timestamp::from_micros_since_unix_epoch(validated_input.updated_at_micros);
|
||||||
|
let existing = ctx
|
||||||
|
.db
|
||||||
|
.profile_recharge_product_config()
|
||||||
|
.product_id()
|
||||||
|
.find(&validated_input.product_id);
|
||||||
|
if let Some(row) = existing.as_ref() {
|
||||||
|
ctx.db
|
||||||
|
.profile_recharge_product_config()
|
||||||
|
.product_id()
|
||||||
|
.delete(&row.product_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inserted = ctx
|
||||||
|
.db
|
||||||
|
.profile_recharge_product_config()
|
||||||
|
.insert(ProfileRechargeProductConfig {
|
||||||
|
product_id: validated_input.product_id,
|
||||||
|
title: validated_input.title,
|
||||||
|
price_cents: validated_input.price_cents,
|
||||||
|
kind: validated_input.kind,
|
||||||
|
points_amount: validated_input.points_amount,
|
||||||
|
bonus_points: validated_input.bonus_points,
|
||||||
|
duration_days: validated_input.duration_days,
|
||||||
|
badge_label: validated_input.badge_label,
|
||||||
|
description: validated_input.description,
|
||||||
|
tier: validated_input.tier,
|
||||||
|
enabled: validated_input.enabled,
|
||||||
|
sort_order: validated_input.sort_order,
|
||||||
|
created_by: existing
|
||||||
|
.as_ref()
|
||||||
|
.map(|row| row.created_by.clone())
|
||||||
|
.unwrap_or_else(|| validated_input.admin_user_id.clone()),
|
||||||
|
created_at: existing
|
||||||
|
.as_ref()
|
||||||
|
.map(|row| row.created_at)
|
||||||
|
.unwrap_or(updated_at),
|
||||||
|
updated_by: validated_input.admin_user_id,
|
||||||
|
updated_at,
|
||||||
|
});
|
||||||
|
Ok(build_profile_recharge_product_config_snapshot_from_row(
|
||||||
|
&inserted,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn upsert_profile_task_config_record(
|
fn upsert_profile_task_config_record(
|
||||||
ctx: &ReducerContext,
|
ctx: &ReducerContext,
|
||||||
input: RuntimeProfileTaskConfigAdminUpsertInput,
|
input: RuntimeProfileTaskConfigAdminUpsertInput,
|
||||||
@@ -3518,6 +3697,96 @@ fn ensure_default_profile_task_config(ctx: &ReducerContext) -> ProfileTaskConfig
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_default_profile_recharge_product_config(ctx: &ReducerContext) {
|
||||||
|
if ctx.db.profile_recharge_product_config().count() > 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = ctx.timestamp;
|
||||||
|
for (sort_order, product) in runtime_profile_recharge_point_products()
|
||||||
|
.into_iter()
|
||||||
|
.chain(runtime_profile_recharge_membership_products())
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
ctx.db
|
||||||
|
.profile_recharge_product_config()
|
||||||
|
.insert(ProfileRechargeProductConfig {
|
||||||
|
product_id: product.product_id,
|
||||||
|
title: product.title,
|
||||||
|
price_cents: product.price_cents,
|
||||||
|
kind: product.kind,
|
||||||
|
points_amount: product.points_amount,
|
||||||
|
bonus_points: product.bonus_points,
|
||||||
|
duration_days: product.duration_days,
|
||||||
|
badge_label: product.badge_label,
|
||||||
|
description: product.description,
|
||||||
|
tier: product.tier,
|
||||||
|
enabled: true,
|
||||||
|
sort_order: sort_order as i32,
|
||||||
|
created_by: PROFILE_RECHARGE_PRODUCT_SYSTEM_USER_ID.to_string(),
|
||||||
|
created_at: now,
|
||||||
|
updated_by: PROFILE_RECHARGE_PRODUCT_SYSTEM_USER_ID.to_string(),
|
||||||
|
updated_at: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn profile_recharge_product_config_rows(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
include_disabled: bool,
|
||||||
|
) -> Vec<ProfileRechargeProductConfig> {
|
||||||
|
ensure_default_profile_recharge_product_config(ctx);
|
||||||
|
let mut rows = ctx
|
||||||
|
.db
|
||||||
|
.profile_recharge_product_config()
|
||||||
|
.iter()
|
||||||
|
.filter(|row| include_disabled || row.enabled)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
rows.sort_by(|left, right| {
|
||||||
|
left.sort_order
|
||||||
|
.cmp(&right.sort_order)
|
||||||
|
.then_with(|| left.product_id.cmp(&right.product_id))
|
||||||
|
});
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
fn profile_recharge_product_by_id(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
product_id: &str,
|
||||||
|
) -> Option<RuntimeProfileRechargeProductSnapshot> {
|
||||||
|
ensure_default_profile_recharge_product_config(ctx);
|
||||||
|
ctx.db
|
||||||
|
.profile_recharge_product_config()
|
||||||
|
.product_id()
|
||||||
|
.find(&product_id.to_string())
|
||||||
|
.map(|row| build_profile_recharge_product_snapshot_from_config_row(&row))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enabled_profile_recharge_product_by_id(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
product_id: &str,
|
||||||
|
) -> Option<RuntimeProfileRechargeProductSnapshot> {
|
||||||
|
ensure_default_profile_recharge_product_config(ctx);
|
||||||
|
ctx.db
|
||||||
|
.profile_recharge_product_config()
|
||||||
|
.product_id()
|
||||||
|
.find(&product_id.to_string())
|
||||||
|
.filter(|row| row.enabled)
|
||||||
|
.map(|row| build_profile_recharge_product_snapshot_from_config_row(&row))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_profile_recharge_product_display(
|
||||||
|
mut product: RuntimeProfileRechargeProductSnapshot,
|
||||||
|
has_product_recharged: bool,
|
||||||
|
) -> RuntimeProfileRechargeProductSnapshot {
|
||||||
|
if product.kind == RuntimeProfileRechargeProductKind::Points && has_product_recharged {
|
||||||
|
product.bonus_points = 0;
|
||||||
|
product.badge_label.clear();
|
||||||
|
product.description = product.title.clone();
|
||||||
|
}
|
||||||
|
product
|
||||||
|
}
|
||||||
|
|
||||||
fn build_profile_membership_snapshot(
|
fn build_profile_membership_snapshot(
|
||||||
ctx: &ReducerContext,
|
ctx: &ReducerContext,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
@@ -3754,9 +4023,19 @@ fn apply_profile_wallet_signed_delta(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn has_profile_points_recharged(ctx: &ReducerContext, user_id: &str) -> bool {
|
fn has_profile_points_recharged(ctx: &ReducerContext, user_id: &str) -> bool {
|
||||||
ctx.db.profile_wallet_ledger().iter().any(|row| {
|
ctx.db.profile_recharge_order().iter().any(|row| {
|
||||||
row.user_id == user_id
|
row.user_id == user_id
|
||||||
&& row.source_type == RuntimeProfileWalletLedgerSourceType::PointsRecharge
|
&& row.kind == RuntimeProfileRechargeProductKind::Points
|
||||||
|
&& row.status == RuntimeProfileRechargeOrderStatus::Paid
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_profile_product_recharged(ctx: &ReducerContext, user_id: &str, product_id: &str) -> bool {
|
||||||
|
ctx.db.profile_recharge_order().iter().any(|row| {
|
||||||
|
row.user_id == user_id
|
||||||
|
&& row.product_id == product_id
|
||||||
|
&& row.kind == RuntimeProfileRechargeProductKind::Points
|
||||||
|
&& row.status == RuntimeProfileRechargeOrderStatus::Paid
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3894,6 +4173,46 @@ fn build_profile_task_config_snapshot_from_row(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_profile_recharge_product_config_snapshot_from_row(
|
||||||
|
row: &ProfileRechargeProductConfig,
|
||||||
|
) -> RuntimeProfileRechargeProductConfigSnapshot {
|
||||||
|
RuntimeProfileRechargeProductConfigSnapshot {
|
||||||
|
product_id: row.product_id.clone(),
|
||||||
|
title: row.title.clone(),
|
||||||
|
price_cents: row.price_cents,
|
||||||
|
kind: row.kind,
|
||||||
|
points_amount: row.points_amount,
|
||||||
|
bonus_points: row.bonus_points,
|
||||||
|
duration_days: row.duration_days,
|
||||||
|
badge_label: row.badge_label.clone(),
|
||||||
|
description: row.description.clone(),
|
||||||
|
tier: row.tier,
|
||||||
|
enabled: row.enabled,
|
||||||
|
sort_order: row.sort_order,
|
||||||
|
created_by: row.created_by.clone(),
|
||||||
|
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
|
||||||
|
updated_by: row.updated_by.clone(),
|
||||||
|
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_profile_recharge_product_snapshot_from_config_row(
|
||||||
|
row: &ProfileRechargeProductConfig,
|
||||||
|
) -> RuntimeProfileRechargeProductSnapshot {
|
||||||
|
RuntimeProfileRechargeProductSnapshot {
|
||||||
|
product_id: row.product_id.clone(),
|
||||||
|
title: row.title.clone(),
|
||||||
|
price_cents: row.price_cents,
|
||||||
|
kind: row.kind,
|
||||||
|
points_amount: row.points_amount,
|
||||||
|
bonus_points: row.bonus_points,
|
||||||
|
duration_days: row.duration_days,
|
||||||
|
badge_label: row.badge_label.clone(),
|
||||||
|
description: row.description.clone(),
|
||||||
|
tier: row.tier,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn build_profile_recharge_order_snapshot_from_row(
|
fn build_profile_recharge_order_snapshot_from_row(
|
||||||
row: &ProfileRechargeOrder,
|
row: &ProfileRechargeOrder,
|
||||||
) -> RuntimeProfileRechargeOrderSnapshot {
|
) -> RuntimeProfileRechargeOrderSnapshot {
|
||||||
|
|||||||
@@ -1039,7 +1039,7 @@ test('profile recharge modal buys points through mock channel outside mini progr
|
|||||||
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('profile recharge modal hides first bonus display after points recharge', async () => {
|
test('profile recharge modal trusts per-product first bonus display after points recharge', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
mockGetRpgProfileRechargeCenter.mockResolvedValueOnce({
|
mockGetRpgProfileRechargeCenter.mockResolvedValueOnce({
|
||||||
walletBalance: 60,
|
walletBalance: 60,
|
||||||
@@ -1076,8 +1076,8 @@ test('profile recharge modal hides first bonus display after points recharge', a
|
|||||||
const rechargeDialog = await screen.findByText('账户充值');
|
const rechargeDialog = await screen.findByText('账户充值');
|
||||||
expect(rechargeDialog).toBeTruthy();
|
expect(rechargeDialog).toBeTruthy();
|
||||||
expect(screen.getByRole('button', { name: /60泥点/u })).toBeTruthy();
|
expect(screen.getByRole('button', { name: /60泥点/u })).toBeTruthy();
|
||||||
expect(screen.queryByText('首充双倍')).toBeNull();
|
expect(screen.getByText('首充双倍')).toBeTruthy();
|
||||||
expect(screen.queryByText('60+60泥点')).toBeNull();
|
expect(screen.getByText('60+60泥点')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('profile recharge modal posts requestPayment params in mini program web-view', async () => {
|
test('profile recharge modal posts requestPayment params in mini program web-view', async () => {
|
||||||
|
|||||||
@@ -2498,20 +2498,16 @@ async function confirmWechatRechargeOrderUntilSettled(
|
|||||||
|
|
||||||
function RechargeProductCard({
|
function RechargeProductCard({
|
||||||
product,
|
product,
|
||||||
hasPointsRecharged,
|
|
||||||
submittingProductId,
|
submittingProductId,
|
||||||
onBuy,
|
onBuy,
|
||||||
}: {
|
}: {
|
||||||
product: ProfileRechargeProduct;
|
product: ProfileRechargeProduct;
|
||||||
hasPointsRecharged: boolean;
|
|
||||||
submittingProductId: string | null;
|
submittingProductId: string | null;
|
||||||
onBuy: (product: ProfileRechargeProduct) => void;
|
onBuy: (product: ProfileRechargeProduct) => void;
|
||||||
}) {
|
}) {
|
||||||
const submitting = submittingProductId === product.productId;
|
const submitting = submittingProductId === product.productId;
|
||||||
const effectiveBonusPoints =
|
const effectiveBonusPoints = product.bonusPoints;
|
||||||
product.kind === 'points' && hasPointsRecharged ? 0 : product.bonusPoints;
|
const badgeLabel = product.badgeLabel;
|
||||||
const badgeLabel =
|
|
||||||
product.kind === 'points' && hasPointsRecharged ? '' : product.badgeLabel;
|
|
||||||
const value =
|
const value =
|
||||||
product.kind === 'points'
|
product.kind === 'points'
|
||||||
? `${product.pointsAmount}${effectiveBonusPoints > 0 ? `+${effectiveBonusPoints}` : ''}泥点`
|
? `${product.pointsAmount}${effectiveBonusPoints > 0 ? `+${effectiveBonusPoints}` : ''}泥点`
|
||||||
@@ -2646,7 +2642,6 @@ function ProfileRechargeModal({
|
|||||||
<RechargeProductCard
|
<RechargeProductCard
|
||||||
key={product.productId}
|
key={product.productId}
|
||||||
product={product}
|
product={product}
|
||||||
hasPointsRecharged={center?.hasPointsRecharged === true}
|
|
||||||
submittingProductId={submittingProductId}
|
submittingProductId={submittingProductId}
|
||||||
onBuy={onBuy}
|
onBuy={onBuy}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user