From 15bca90ed2ac72c7f7d3a382bf6eefec55cac1a5 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 2 Jul 2026 17:31:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96=E7=A0=81=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E6=93=8D=E4=BD=9C=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增兑换码和邀请码后台操作持久表。 列表接口返回全部操作记录并同步前后端契约。 后台页面展示持久化操作历史并移除最近一条会话记录。 更新 SpacetimeDB 迁移表清单、生成绑定和后端架构文档。 --- apps/admin-web/src/api/adminApiTypes.ts | 11 ++ apps/admin-web/src/app/AdminApp.tsx | 15 -- .../src/pages/AdminDatabaseTablesPage.tsx | 8 + .../src/pages/AdminInviteCodePage.tsx | 100 +++++------ .../src/pages/AdminRedeemCodePage.tsx | 109 ++++++------ ...】server-rs与SpacetimeDB数据契约-2026-05-15.md | 6 + packages/shared/src/contracts/runtime.ts | 11 ++ .../crates/api-server/src/runtime_profile.rs | 43 ++++- .../crates/module-runtime/src/application.rs | 14 ++ server-rs/crates/module-runtime/src/domain.rs | 36 ++++ .../crates/shared-contracts/src/runtime.rs | 13 ++ server-rs/crates/spacetime-client/src/lib.rs | 9 +- .../src/mapper/runtime_profile.rs | 47 ++++- .../spacetime-client/src/module_bindings.rs | 30 ++++ .../profile_code_operation_table.rs | 161 ++++++++++++++++++ .../profile_code_operation_type.rs | 66 +++++++ ...me_profile_code_operation_snapshot_type.rs | 20 +++ ...e_code_admin_list_procedure_result_type.rs | 2 + ...m_code_admin_list_procedure_result_type.rs | 2 + .../crates/spacetime-client/src/runtime.rs | 4 +- .../crates/spacetime-module/src/migration.rs | 1 + .../spacetime-module/src/runtime/profile.rs | 156 ++++++++++++++++- 22 files changed, 708 insertions(+), 156 deletions(-) create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/profile_code_operation_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/profile_code_operation_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_code_operation_snapshot_type.rs diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 6cc8bd9f2..5d408ff65 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -416,8 +416,18 @@ export interface ProfileRedeemCodeAdminResponse { updatedAt: string; } +export interface ProfileCodeOperationAdminResponse { + operationId: string; + codeKind: 'redeem' | 'invite' | string; + code: string; + action: 'create' | 'update' | 'disable' | string; + operatorUserId: string; + createdAt: string; +} + export interface ProfileRedeemCodeAdminListResponse { entries: ProfileRedeemCodeAdminResponse[]; + operations: ProfileCodeOperationAdminResponse[]; } export interface ProfileInviteCodeAdminResponse { @@ -433,6 +443,7 @@ export interface ProfileInviteCodeAdminResponse { export interface ProfileInviteCodeAdminListResponse { entries: ProfileInviteCodeAdminResponse[]; + operations: ProfileCodeOperationAdminResponse[]; } export interface ProfileTaskConfigAdminResponse { diff --git a/apps/admin-web/src/app/AdminApp.tsx b/apps/admin-web/src/app/AdminApp.tsx index 1386c61b4..6a09d347c 100644 --- a/apps/admin-web/src/app/AdminApp.tsx +++ b/apps/admin-web/src/app/AdminApp.tsx @@ -8,9 +8,7 @@ import { } from '../api/adminApiClient'; import type { AdminSessionPayload, - ProfileInviteCodeAdminResponse, ProfileRechargeProductConfigAdminResponse, - ProfileRedeemCodeAdminResponse, ProfileTaskConfigAdminResponse, ProfileWalletConfigAdminResponse, } from '../api/adminApiTypes'; @@ -47,11 +45,6 @@ export function AdminApp() { resolveAdminRoute(window.location.hash), ); const [loginNotice, setLoginNotice] = useState(''); - // 兑换码页会随页签切换卸载,最近操作记录需要放在会话层保留。 - const [redeemResult, setRedeemResult] = - useState(null); - const [inviteResult, setInviteResult] = - useState(null); const [taskConfigResult, setTaskConfigResult] = useState(null); const [profileWalletConfigResult, setProfileWalletConfigResult] = @@ -63,8 +56,6 @@ export function AdminApp() { clearStoredAdminToken(); setToken(''); setAdmin(null); - setRedeemResult(null); - setInviteResult(null); setTaskConfigResult(null); setProfileWalletConfigResult(null); setRechargeProductResult(null); @@ -134,8 +125,6 @@ export function AdminApp() { setStoredAdminToken(response.token); setToken(response.token); setAdmin(response.admin); - setRedeemResult(null); - setInviteResult(null); setTaskConfigResult(null); setProfileWalletConfigResult(null); setRechargeProductResult(null); @@ -197,18 +186,14 @@ export function AdminApp() { ) : null} {routeId === 'redeem' ? ( ) : null} {routeId === 'invite' ? ( ) : null} {routeId === 'creation-announcement' ? ( diff --git a/apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx b/apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx index f61a42bde..cd9d0ea80 100644 --- a/apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx +++ b/apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx @@ -703,6 +703,8 @@ const databaseTableColumnLabelMap: Record = { event_id: '事件ID', event_key: '事件键', event_title: '事件名称', + operation_id: '操作ID', + code_kind: '码类型', scope_kind: '范围类型', scope_id: '范围ID', day_key: '日期键', @@ -811,6 +813,7 @@ const databaseTableColumnLabelMap: Record = { record_id: '记录ID', created_by: '创建人', updated_by: '更新人', + operator_user_id: '操作人ID', total_count: '总数', max_uses: '最大使用次数', global_used_count: '全局使用次数', @@ -837,6 +840,8 @@ const databaseTableColumnDescriptionMap: Record = { event_id: '当前埋点事件的唯一标识', event_key: '埋点事件键', event_title: '埋点事件展示名称', + operation_id: '后台操作记录的唯一标识', + code_kind: '码类型,redeem 表示兑换码,invite 表示邀请码', scope_kind: '埋点统计范围类型', scope_id: '埋点统计范围标识', day_key: '按天聚合时使用的日期键', @@ -943,6 +948,7 @@ const databaseTableColumnDescriptionMap: Record = { record_id: '记录标识', created_by: '创建该记录的主体', updated_by: '最后更新该记录的主体', + operator_user_id: '执行后台操作的用户标识', total_count: '累计总数', max_uses: '允许的最大使用次数', global_used_count: '当前已使用次数', @@ -1188,6 +1194,7 @@ const databaseTableLabelMap: Record = { profile_task_reward_claim: '个人任务领奖', profile_redeem_code: '兑换码', profile_redeem_code_usage: '兑换码使用记录', + profile_code_operation: '码操作记录', profile_invite_code: '邀请码', profile_referral_relation: '邀请关系', profile_played_world: '已玩世界', @@ -1269,6 +1276,7 @@ const databaseTableDescriptionMap: Record = { profile_task_reward_claim: '个人任务领奖记录表', profile_redeem_code: '运营兑换码表', profile_redeem_code_usage: '兑换码使用记录表', + profile_code_operation: '兑换码/邀请码后台操作记录表', profile_invite_code: '用户邀请中心邀请码表', profile_referral_relation: '邀请关系记录表', profile_played_world: '用户已玩世界记录表', diff --git a/apps/admin-web/src/pages/AdminInviteCodePage.tsx b/apps/admin-web/src/pages/AdminInviteCodePage.tsx index 5952d0425..ebb687715 100644 --- a/apps/admin-web/src/pages/AdminInviteCodePage.tsx +++ b/apps/admin-web/src/pages/AdminInviteCodePage.tsx @@ -7,6 +7,7 @@ import { } from '../api/adminApiClient'; import type { AdminUpsertProfileInviteCodeRequest, + ProfileCodeOperationAdminResponse, ProfileInviteCodeAdminResponse, } from '../api/adminApiTypes'; import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm'; @@ -14,16 +15,12 @@ import {handlePageError} from './pageUtils'; interface AdminInviteCodePageProps { token: string; - result: ProfileInviteCodeAdminResponse | null; onUnauthorized: (message?: string) => void; - onResultChange: (result: ProfileInviteCodeAdminResponse) => void; } export function AdminInviteCodePage({ token, - result, onUnauthorized, - onResultChange, }: AdminInviteCodePageProps) { const [inviteCode, setInviteCode] = useState(''); const [startsAt, setStartsAt] = useState(''); @@ -33,6 +30,7 @@ export function AdminInviteCodePage({ const [errorMessage, setErrorMessage] = useState(''); const [listErrorMessage, setListErrorMessage] = useState(''); const [entries, setEntries] = useState([]); + const [operations, setOperations] = useState([]); const [isSaving, setIsSaving] = useState(false); const [isLoading, setIsLoading] = useState(false); const {confirmWrite, confirmDialog} = useAdminWriteConfirm(); @@ -48,6 +46,7 @@ export function AdminInviteCodePage({ try { const response = await listProfileInviteCodes(token); setEntries(response.entries); + setOperations(response.operations ?? []); } catch (error: unknown) { handlePageError(error, onUnauthorized, setListErrorMessage); } finally { @@ -89,9 +88,8 @@ export function AdminInviteCodePage({ expiresAt: expiresAt ? toIsoDateTime(expiresAt) : null, }; const response = await upsertProfileInviteCode(token, payload); - onResultChange(response); - upsertEntry(response); fillForm(response); + await refreshInviteCodes(); } catch (error: unknown) { handlePageError(error, onUnauthorized, setErrorMessage); } finally { @@ -99,23 +97,6 @@ export function AdminInviteCodePage({ } } - function upsertEntry(next: ProfileInviteCodeAdminResponse) { - setEntries((current) => { - const rest = current.filter((entry) => entry.inviteCode !== next.inviteCode); - return [...rest, next].sort((left, right) => { - const leftUpdatedAt = Date.parse(left.updatedAt); - const rightUpdatedAt = Date.parse(right.updatedAt); - if (Number.isFinite(leftUpdatedAt) && Number.isFinite(rightUpdatedAt)) { - const updatedCompare = rightUpdatedAt - leftUpdatedAt; - if (updatedCompare !== 0) { - return updatedCompare; - } - } - return left.inviteCode.localeCompare(right.inviteCode); - }); - }); - } - function fillForm(entry: ProfileInviteCodeAdminResponse) { setInviteCode(entry.inviteCode); setStartsAt(toDateTimeLocalValue(entry.startsAt)); @@ -278,42 +259,32 @@ export function AdminInviteCodePage({
-

记录

- {result?.inviteCode ?? '-'} +

操作记录

+ {operations.length}
- {result ? ( -
-
-
邀请码
-
{result.inviteCode}
-
-
-
有效期
-
{formatValidityWindow(result)}
-
-
-
标签
-
- -
-
-
-
创建
-
{result.createdAt}
-
-
-
更新
-
{result.updatedAt}
-
-
-
Metadata
-
-
-                      {JSON.stringify(result.metadata, null, 2)}
-                    
-
-
-
+ {operations.length ? ( +
+ + + + + + + + + + + {operations.map((operation) => ( + + + + + + + ))} + +
操作邀请码操作人时间
{operationActionLabel(operation.action)}{operation.code}{operation.operatorUserId}{formatDateTime(operation.createdAt)}
+
) : (
暂无记录
)} @@ -475,6 +446,19 @@ function formatDateTime(value: string) { return date.toLocaleString('zh-CN', {hour12: false}); } +function operationActionLabel(action: string) { + if (action === 'create') { + return '新增'; + } + if (action === 'update') { + return '更新'; + } + if (action === 'disable') { + return '停用'; + } + return action; +} + function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } diff --git a/apps/admin-web/src/pages/AdminRedeemCodePage.tsx b/apps/admin-web/src/pages/AdminRedeemCodePage.tsx index 1163119e7..f1df9c315 100644 --- a/apps/admin-web/src/pages/AdminRedeemCodePage.tsx +++ b/apps/admin-web/src/pages/AdminRedeemCodePage.tsx @@ -7,6 +7,7 @@ import { upsertProfileRedeemCode, } from '../api/adminApiClient'; import type { + ProfileCodeOperationAdminResponse, ProfileRedeemCodeAdminResponse, ProfileRedeemCodeMode, } from '../api/adminApiTypes'; @@ -15,9 +16,7 @@ import {handlePageError, splitLines} from './pageUtils'; interface AdminRedeemCodePageProps { token: string; - result: ProfileRedeemCodeAdminResponse | null; onUnauthorized: (message?: string) => void; - onResultChange: (result: ProfileRedeemCodeAdminResponse) => void; } const redeemModes: Array<{value: ProfileRedeemCodeMode; label: string}> = [ @@ -28,9 +27,7 @@ const redeemModes: Array<{value: ProfileRedeemCodeMode; label: string}> = [ export function AdminRedeemCodePage({ token, - result, onUnauthorized, - onResultChange, }: AdminRedeemCodePageProps) { const [code, setCode] = useState(''); const [mode, setMode] = useState('public'); @@ -44,6 +41,7 @@ export function AdminRedeemCodePage({ const [disableErrorMessage, setDisableErrorMessage] = useState(''); const [listErrorMessage, setListErrorMessage] = useState(''); const [entries, setEntries] = useState([]); + const [operations, setOperations] = useState([]); const [isSaving, setIsSaving] = useState(false); const [isDisabling, setIsDisabling] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -60,6 +58,7 @@ export function AdminRedeemCodePage({ try { const response = await listProfileRedeemCodes(token); setEntries(response.entries); + setOperations(response.operations ?? []); } catch (error: unknown) { handlePageError(error, onUnauthorized, setListErrorMessage); } finally { @@ -94,9 +93,8 @@ export function AdminRedeemCodePage({ allowedPublicUserCodes: mode === 'private' ? splitLines(allowedPublicUserCodes) : [], }); - onResultChange(response); - upsertEntry(response); fillForm(response); + await refreshRedeemCodes(); } catch (error: unknown) { handlePageError(error, onUnauthorized, setErrorMessage); } finally { @@ -124,9 +122,8 @@ export function AdminRedeemCodePage({ const response = await disableProfileRedeemCode(token, { code: disableCode.trim(), }); - onResultChange(response); - upsertEntry(response); fillForm(response); + await refreshRedeemCodes(); } catch (error: unknown) { handlePageError(error, onUnauthorized, setDisableErrorMessage); } finally { @@ -134,23 +131,6 @@ export function AdminRedeemCodePage({ } } - function upsertEntry(next: ProfileRedeemCodeAdminResponse) { - setEntries((current) => { - const rest = current.filter((entry) => entry.code !== next.code); - return [...rest, next].sort((left, right) => { - const leftUpdatedAt = Date.parse(left.updatedAt); - const rightUpdatedAt = Date.parse(right.updatedAt); - if (Number.isFinite(leftUpdatedAt) && Number.isFinite(rightUpdatedAt)) { - const updatedCompare = rightUpdatedAt - leftUpdatedAt; - if (updatedCompare !== 0) { - return updatedCompare; - } - } - return left.code.localeCompare(right.code); - }); - }); - } - function fillForm(entry: ProfileRedeemCodeAdminResponse) { setCode(entry.code); setMode(entry.mode); @@ -354,40 +334,32 @@ export function AdminRedeemCodePage({
-

记录

- {result?.mode ?? '-'} +

操作记录

+ {operations.length}
- {result ? ( -
-
-
Code
-
{result.code}
-
-
-
奖励
-
{result.rewardPoints}
-
-
-
最大次数
-
{result.maxUses}
-
-
-
全局已用
-
{result.globalUsedCount}
-
-
-
状态
-
{result.enabled ? '启用' : '停用'}
-
-
-
创建人
-
{result.createdBy}
-
-
-
更新
-
{result.updatedAt}
-
-
+ {operations.length ? ( +
+ + + + + + + + + + + {operations.map((operation) => ( + + + + + + + ))} + +
操作Code操作人时间
{operationActionLabel(operation.action)}{operation.code}{operation.operatorUserId}{formatDateTime(operation.createdAt)}
+
) : (
暂无记录
)} @@ -407,3 +379,24 @@ function parsePositiveInteger(value: string) { function redeemModeLabel(value: ProfileRedeemCodeMode) { return redeemModes.find((item) => item.value === value)?.label ?? value; } + +function operationActionLabel(action: string) { + if (action === 'create') { + return '新增'; + } + if (action === 'update') { + return '更新'; + } + if (action === 'disable') { + return '停用'; + } + return action; +} + +function formatDateTime(value: string) { + const date = new Date(value); + if (!Number.isFinite(date.getTime())) { + return value; + } + return date.toLocaleString('zh-CN', {hour12: false}); +} diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 2533a212a..352fe6e8b 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -620,6 +620,12 @@ npm run check:server-rs-ddd - Rust 结构体:`ProfileInviteCode` - 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs` +### `profile_code_operation` + +- Rust 结构体:`ProfileCodeOperation` +- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs` +- 作用:后台兑换码 / 邀请码的持久操作记录,记录新增、更新、停用的码值、操作人和操作时间。 + ### `profile_membership` - Rust 结构体:`ProfileMembership` diff --git a/packages/shared/src/contracts/runtime.ts b/packages/shared/src/contracts/runtime.ts index 3e76215b4..6ce790428 100644 --- a/packages/shared/src/contracts/runtime.ts +++ b/packages/shared/src/contracts/runtime.ts @@ -409,8 +409,18 @@ export type ProfileRedeemCodeAdminResponse = { updatedAt: string; }; +export type ProfileCodeOperationAdminResponse = { + operationId: string; + codeKind: 'redeem' | 'invite' | string; + code: string; + action: 'create' | 'update' | 'disable' | string; + operatorUserId: string; + createdAt: string; +}; + export type ProfileRedeemCodeAdminListResponse = { entries: ProfileRedeemCodeAdminResponse[]; + operations: ProfileCodeOperationAdminResponse[]; }; export type AdminUpsertProfileRedeemCodeRequest = { @@ -447,6 +457,7 @@ export type ProfileInviteCodeAdminResponse = { export type ProfileInviteCodeAdminListResponse = { entries: ProfileInviteCodeAdminResponse[]; + operations: ProfileCodeOperationAdminResponse[]; }; export type ProfilePlayedWorkSummary = { diff --git a/server-rs/crates/api-server/src/runtime_profile.rs b/server-rs/crates/api-server/src/runtime_profile.rs index 6f5ae4d9a..b4589c634 100644 --- a/server-rs/crates/api-server/src/runtime_profile.rs +++ b/server-rs/crates/api-server/src/runtime_profile.rs @@ -14,8 +14,9 @@ use module_runtime::{ PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE, RuntimeProfileFeedbackEvidenceRecord, - RuntimeProfileFeedbackEvidenceSnapshot, RuntimeProfileFeedbackSubmissionRecord, - RuntimeProfileInviteCodeRecord, RuntimeProfileMembershipBenefitRecord, + RuntimeProfileCodeOperationRecord, RuntimeProfileFeedbackEvidenceSnapshot, + RuntimeProfileFeedbackSubmissionRecord, RuntimeProfileInviteCodeRecord, + RuntimeProfileMembershipBenefitRecord, RuntimeProfileMembershipTier, RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeOrderStatus, RuntimeProfileRechargeProductConfigRecord, RuntimeProfileRechargeProductKind, @@ -53,8 +54,9 @@ use shared_contracts::runtime::{ PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM, PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD, PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC, ProfileDashboardSummaryResponse, - ProfileFeedbackEvidenceItemResponse, ProfileFeedbackSubmissionResponse, - ProfileInviteCodeAdminListResponse, ProfileInviteCodeAdminResponse, + ProfileCodeOperationAdminResponse, ProfileFeedbackEvidenceItemResponse, + ProfileFeedbackSubmissionResponse, ProfileInviteCodeAdminListResponse, + ProfileInviteCodeAdminResponse, ProfileMembershipBenefitResponse, ProfileMembershipResponse, ProfilePlayStatsResponse, ProfilePlayedWorkSummaryResponse, ProfileRechargeCenterResponse, ProfileRechargeOrderResponse, ProfileRechargeProductConfigAdminListResponse, ProfileRechargeProductConfigAdminResponse, @@ -959,7 +961,7 @@ pub async fn admin_list_profile_redeem_codes( Extension(request_context): Extension, Extension(admin): Extension, ) -> Result, Response> { - let entries = state + let record = state .spacetime_client() .admin_list_profile_redeem_codes(admin.session().subject.clone()) .await @@ -973,10 +975,16 @@ pub async fn admin_list_profile_redeem_codes( Ok(json_success_body( Some(&request_context), ProfileRedeemCodeAdminListResponse { - entries: entries + entries: record + .entries .into_iter() .map(build_profile_redeem_code_admin_response) .collect(), + operations: record + .operations + .into_iter() + .map(build_profile_code_operation_admin_response) + .collect(), }, )) } @@ -1054,7 +1062,7 @@ pub async fn admin_list_profile_invite_codes( Extension(request_context): Extension, Extension(admin): Extension, ) -> Result, Response> { - let entries = state + let record = state .spacetime_client() .admin_list_profile_invite_codes(admin.session().subject.clone()) .await @@ -1068,10 +1076,16 @@ pub async fn admin_list_profile_invite_codes( Ok(json_success_body( Some(&request_context), ProfileInviteCodeAdminListResponse { - entries: entries + entries: record + .entries .into_iter() .map(build_profile_invite_code_admin_response) .collect(), + operations: record + .operations + .into_iter() + .map(build_profile_code_operation_admin_response) + .collect(), }, )) } @@ -2018,6 +2032,19 @@ fn build_profile_redeem_code_admin_response( } } +fn build_profile_code_operation_admin_response( + record: RuntimeProfileCodeOperationRecord, +) -> ProfileCodeOperationAdminResponse { + ProfileCodeOperationAdminResponse { + operation_id: record.operation_id, + code_kind: record.code_kind, + code: record.code, + action: record.action, + operator_user_id: record.operator_user_id, + created_at: record.created_at, + } +} + #[cfg(test)] mod tests { use module_auth::{ResolveWechatLoginInput, WechatIdentityProfile}; diff --git a/server-rs/crates/module-runtime/src/application.rs b/server-rs/crates/module-runtime/src/application.rs index 52fa45072..887e1fc9b 100644 --- a/server-rs/crates/module-runtime/src/application.rs +++ b/server-rs/crates/module-runtime/src/application.rs @@ -1525,6 +1525,20 @@ pub fn build_runtime_profile_redeem_code_record( } } +pub fn build_runtime_profile_code_operation_record( + snapshot: RuntimeProfileCodeOperationSnapshot, +) -> RuntimeProfileCodeOperationRecord { + RuntimeProfileCodeOperationRecord { + operation_id: snapshot.operation_id, + code_kind: snapshot.code_kind, + code: snapshot.code, + action: snapshot.action, + operator_user_id: snapshot.operator_user_id, + created_at: format_utc_micros(snapshot.created_at_micros), + created_at_micros: snapshot.created_at_micros, + } +} + pub fn build_runtime_profile_invite_code_record( snapshot: RuntimeProfileInviteCodeSnapshot, ) -> RuntimeProfileInviteCodeRecord { diff --git a/server-rs/crates/module-runtime/src/domain.rs b/server-rs/crates/module-runtime/src/domain.rs index 7f6d08ae5..745d11a93 100644 --- a/server-rs/crates/module-runtime/src/domain.rs +++ b/server-rs/crates/module-runtime/src/domain.rs @@ -1425,6 +1425,17 @@ pub struct RuntimeProfileRedeemCodeSnapshot { pub updated_at_micros: i64, } +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RuntimeProfileCodeOperationSnapshot { + pub operation_id: String, + pub code_kind: String, + pub code: String, + pub action: String, + pub operator_user_id: String, + pub created_at_micros: i64, +} + #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct RuntimeProfileRedeemCodeAdminProcedureResult { @@ -1438,6 +1449,7 @@ pub struct RuntimeProfileRedeemCodeAdminProcedureResult { pub struct RuntimeProfileRedeemCodeAdminListProcedureResult { pub ok: bool, pub entries: Vec, + pub operations: Vec, pub error_message: Option, } @@ -1483,6 +1495,7 @@ pub struct RuntimeProfileInviteCodeAdminProcedureResult { pub struct RuntimeProfileInviteCodeAdminListProcedureResult { pub ok: bool, pub entries: Vec, + pub operations: Vec, pub error_message: Option, } @@ -1867,6 +1880,29 @@ pub struct RuntimeProfileRedeemCodeRecord { pub updated_at_micros: i64, } +#[derive(Clone, Debug, PartialEq)] +pub struct RuntimeProfileCodeOperationRecord { + pub operation_id: String, + pub code_kind: String, + pub code: String, + pub action: String, + pub operator_user_id: String, + pub created_at: String, + pub created_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct RuntimeProfileRedeemCodeAdminListRecord { + pub entries: Vec, + pub operations: Vec, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct RuntimeProfileInviteCodeAdminListRecord { + pub entries: Vec, + pub operations: Vec, +} + #[derive(Clone, Debug, PartialEq)] pub struct RuntimeProfileInviteCodeRecord { pub user_id: String, diff --git a/server-rs/crates/shared-contracts/src/runtime.rs b/server-rs/crates/shared-contracts/src/runtime.rs index 6294df060..c8b99dee6 100644 --- a/server-rs/crates/shared-contracts/src/runtime.rs +++ b/server-rs/crates/shared-contracts/src/runtime.rs @@ -648,10 +648,22 @@ pub struct ProfileRedeemCodeAdminResponse { pub updated_at: String, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ProfileCodeOperationAdminResponse { + pub operation_id: String, + pub code_kind: String, + pub code: String, + pub action: String, + pub operator_user_id: String, + pub created_at: String, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileRedeemCodeAdminListResponse { pub entries: Vec, + pub operations: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -671,6 +683,7 @@ pub struct ProfileInviteCodeAdminResponse { #[serde(rename_all = "camelCase")] pub struct ProfileInviteCodeAdminListResponse { pub entries: Vec, + pub operations: Vec, } fn default_true() -> bool { diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index 2f1353442..1ad8c2924 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -199,9 +199,11 @@ use module_npc::{ use module_runtime::{ AnalyticsMetricQueryResponse as DomainAnalyticsMetricQueryResponse, RuntimeBrowseHistoryRecord, RuntimePlatformTheme as DomainRuntimePlatformTheme, RuntimeProfileDashboardRecord, - RuntimeProfileFeedbackSubmissionRecord, RuntimeProfileInviteCodeRecord, - RuntimeProfilePlayStatsRecord, RuntimeProfileRechargeCenterRecord, - RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeProductConfigRecord, + RuntimeProfileFeedbackSubmissionRecord, RuntimeProfileInviteCodeAdminListRecord, + RuntimeProfileInviteCodeRecord, RuntimeProfilePlayStatsRecord, + RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord, + RuntimeProfileRechargeProductConfigRecord, + RuntimeProfileRedeemCodeAdminListRecord, RuntimeProfileRedeemCodeMode as DomainRuntimeProfileRedeemCodeMode, RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord, RuntimeProfileSaveArchiveRecord, RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord, @@ -213,6 +215,7 @@ use module_runtime::{ build_runtime_browse_history_clear_input, build_runtime_browse_history_list_input, build_runtime_browse_history_record, build_runtime_browse_history_sync_input, build_runtime_profile_dashboard_get_input, build_runtime_profile_dashboard_record, + build_runtime_profile_code_operation_record, build_runtime_profile_feedback_submission_input, build_runtime_profile_feedback_submission_record, build_runtime_profile_invite_code_admin_list_input, diff --git a/server-rs/crates/spacetime-client/src/mapper/runtime_profile.rs b/server-rs/crates/spacetime-client/src/mapper/runtime_profile.rs index eda44488b..825b463d2 100644 --- a/server-rs/crates/spacetime-client/src/mapper/runtime_profile.rs +++ b/server-rs/crates/spacetime-client/src/mapper/runtime_profile.rs @@ -668,12 +668,13 @@ pub(crate) fn map_runtime_profile_redeem_code_admin_procedure_result( pub(crate) fn map_runtime_profile_redeem_code_admin_list_procedure_result( result: RuntimeProfileRedeemCodeAdminListProcedureResult, -) -> Result, SpacetimeClientError> { +) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - Ok(result + Ok(RuntimeProfileRedeemCodeAdminListRecord { + entries: result .entries .into_iter() .map(|snapshot| { @@ -681,7 +682,17 @@ pub(crate) fn map_runtime_profile_redeem_code_admin_list_procedure_result( snapshot, )) }) - .collect()) + .collect(), + operations: result + .operations + .into_iter() + .map(|snapshot| { + build_runtime_profile_code_operation_record( + map_runtime_profile_code_operation_snapshot(snapshot), + ) + }) + .collect(), + }) } pub(crate) fn map_runtime_profile_invite_code_admin_procedure_result( @@ -706,12 +717,13 @@ pub(crate) fn map_runtime_profile_invite_code_admin_procedure_result( pub(crate) fn map_runtime_profile_invite_code_admin_list_procedure_result( result: RuntimeProfileInviteCodeAdminListProcedureResult, -) -> Result, SpacetimeClientError> { +) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - Ok(result + Ok(RuntimeProfileInviteCodeAdminListRecord { + entries: result .entries .into_iter() .map(|snapshot| { @@ -719,7 +731,17 @@ pub(crate) fn map_runtime_profile_invite_code_admin_list_procedure_result( snapshot, )) }) - .collect()) + .collect(), + operations: result + .operations + .into_iter() + .map(|snapshot| { + build_runtime_profile_code_operation_record( + map_runtime_profile_code_operation_snapshot(snapshot), + ) + }) + .collect(), + }) } pub(crate) fn map_runtime_profile_play_stats_procedure_result( @@ -1112,6 +1134,19 @@ pub(crate) fn map_runtime_profile_invite_code_snapshot( } } +pub(crate) fn map_runtime_profile_code_operation_snapshot( + snapshot: RuntimeProfileCodeOperationSnapshot, +) -> module_runtime::RuntimeProfileCodeOperationSnapshot { + module_runtime::RuntimeProfileCodeOperationSnapshot { + operation_id: snapshot.operation_id, + code_kind: snapshot.code_kind, + code: snapshot.code, + action: snapshot.action, + operator_user_id: snapshot.operator_user_id, + created_at_micros: snapshot.created_at_micros, + } +} + pub(crate) fn map_runtime_profile_played_world_snapshot( snapshot: RuntimeProfilePlayedWorldSnapshot, ) -> module_runtime::RuntimeProfilePlayedWorldSnapshot { diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index ba4ae7c9a..a19453595 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.rs @@ -653,6 +653,8 @@ pub mod player_progression_procedure_result_type; pub mod player_progression_snapshot_type; pub mod player_progression_table; pub mod player_progression_type; +pub mod profile_code_operation_table; +pub mod profile_code_operation_type; pub mod profile_dashboard_state_table; pub mod profile_dashboard_state_type; pub mod profile_feedback_submission_table; @@ -921,6 +923,7 @@ pub mod runtime_item_equipment_slot_type; pub mod runtime_item_reward_item_rarity_type; pub mod runtime_item_reward_item_snapshot_type; pub mod runtime_platform_theme_type; +pub mod runtime_profile_code_operation_snapshot_type; pub mod runtime_profile_dashboard_get_input_type; pub mod runtime_profile_dashboard_procedure_result_type; pub mod runtime_profile_dashboard_snapshot_type; @@ -1881,6 +1884,8 @@ pub use player_progression_procedure_result_type::PlayerProgressionProcedureResu pub use player_progression_snapshot_type::PlayerProgressionSnapshot; pub use player_progression_table::*; pub use player_progression_type::PlayerProgression; +pub use profile_code_operation_table::*; +pub use profile_code_operation_type::ProfileCodeOperation; pub use profile_dashboard_state_table::*; pub use profile_dashboard_state_type::ProfileDashboardState; pub use profile_feedback_submission_table::*; @@ -2149,6 +2154,7 @@ pub use runtime_item_equipment_slot_type::RuntimeItemEquipmentSlot; pub use runtime_item_reward_item_rarity_type::RuntimeItemRewardItemRarity; pub use runtime_item_reward_item_snapshot_type::RuntimeItemRewardItemSnapshot; pub use runtime_platform_theme_type::RuntimePlatformTheme; +pub use runtime_profile_code_operation_snapshot_type::RuntimeProfileCodeOperationSnapshot; pub use runtime_profile_dashboard_get_input_type::RuntimeProfileDashboardGetInput; pub use runtime_profile_dashboard_procedure_result_type::RuntimeProfileDashboardProcedureResult; pub use runtime_profile_dashboard_snapshot_type::RuntimeProfileDashboardSnapshot; @@ -2795,6 +2801,7 @@ pub struct DbUpdate { match_3_d_work_profile: __sdk::TableUpdate, npc_state: __sdk::TableUpdate, player_progression: __sdk::TableUpdate, + profile_code_operation: __sdk::TableUpdate, profile_dashboard_state: __sdk::TableUpdate, profile_feedback_submission: __sdk::TableUpdate, profile_invite_code: __sdk::TableUpdate, @@ -3059,6 +3066,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "player_progression" => db_update .player_progression .append(player_progression_table::parse_table_update(table_update)?), + "profile_code_operation" => db_update.profile_code_operation.append( + profile_code_operation_table::parse_table_update(table_update)?, + ), "profile_dashboard_state" => db_update.profile_dashboard_state.append( profile_dashboard_state_table::parse_table_update(table_update)?, ), @@ -3570,6 +3580,12 @@ impl __sdk::DbUpdate for DbUpdate { &self.player_progression, ) .with_updates_by_pk(|row| &row.user_id); + diff.profile_code_operation = cache + .apply_diff_to_table::( + "profile_code_operation", + &self.profile_code_operation, + ) + .with_updates_by_pk(|row| &row.operation_id); diff.profile_dashboard_state = cache .apply_diff_to_table::( "profile_dashboard_state", @@ -4106,6 +4122,9 @@ impl __sdk::DbUpdate for DbUpdate { "player_progression" => db_update .player_progression .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "profile_code_operation" => db_update + .profile_code_operation + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "profile_dashboard_state" => db_update .profile_dashboard_state .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -4497,6 +4516,9 @@ impl __sdk::DbUpdate for DbUpdate { "player_progression" => db_update .player_progression .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "profile_code_operation" => db_update + .profile_code_operation + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "profile_dashboard_state" => db_update .profile_dashboard_state .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -4770,6 +4792,7 @@ pub struct AppliedDiff<'r> { match_3_d_work_profile: __sdk::TableAppliedDiff<'r, Match3DWorkProfileRow>, npc_state: __sdk::TableAppliedDiff<'r, NpcState>, player_progression: __sdk::TableAppliedDiff<'r, PlayerProgression>, + profile_code_operation: __sdk::TableAppliedDiff<'r, ProfileCodeOperation>, profile_dashboard_state: __sdk::TableAppliedDiff<'r, ProfileDashboardState>, profile_feedback_submission: __sdk::TableAppliedDiff<'r, ProfileFeedbackSubmission>, profile_invite_code: __sdk::TableAppliedDiff<'r, ProfileInviteCode>, @@ -5142,6 +5165,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.player_progression, event, ); + callbacks.invoke_table_row_callbacks::( + "profile_code_operation", + &self.profile_code_operation, + event, + ); callbacks.invoke_table_row_callbacks::( "profile_dashboard_state", &self.profile_dashboard_state, @@ -6180,6 +6208,7 @@ impl __sdk::SpacetimeModule for RemoteModule { match_3_d_work_profile_table::register_table(client_cache); npc_state_table::register_table(client_cache); player_progression_table::register_table(client_cache); + profile_code_operation_table::register_table(client_cache); profile_dashboard_state_table::register_table(client_cache); profile_feedback_submission_table::register_table(client_cache); profile_invite_code_table::register_table(client_cache); @@ -6308,6 +6337,7 @@ impl __sdk::SpacetimeModule for RemoteModule { "match_3_d_work_profile", "npc_state", "player_progression", + "profile_code_operation", "profile_dashboard_state", "profile_feedback_submission", "profile_invite_code", diff --git a/server-rs/crates/spacetime-client/src/module_bindings/profile_code_operation_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/profile_code_operation_table.rs new file mode 100644 index 000000000..ef679c70c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/profile_code_operation_table.rs @@ -0,0 +1,161 @@ +// 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_code_operation_type::ProfileCodeOperation; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `profile_code_operation`. +/// +/// Obtain a handle from the [`ProfileCodeOperationTableAccess::profile_code_operation`] method on [`super::RemoteTables`], +/// like `ctx.db.profile_code_operation()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.profile_code_operation().on_insert(...)`. +pub struct ProfileCodeOperationTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `profile_code_operation`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait ProfileCodeOperationTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`ProfileCodeOperationTableHandle`], which mediates access to the table `profile_code_operation`. + fn profile_code_operation(&self) -> ProfileCodeOperationTableHandle<'_>; +} + +impl ProfileCodeOperationTableAccess for super::RemoteTables { + fn profile_code_operation(&self) -> ProfileCodeOperationTableHandle<'_> { + ProfileCodeOperationTableHandle { + imp: self + .imp + .get_table::("profile_code_operation"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct ProfileCodeOperationInsertCallbackId(__sdk::CallbackId); +pub struct ProfileCodeOperationDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for ProfileCodeOperationTableHandle<'ctx> { + type Row = ProfileCodeOperation; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = ProfileCodeOperationInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProfileCodeOperationInsertCallbackId { + ProfileCodeOperationInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: ProfileCodeOperationInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = ProfileCodeOperationDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProfileCodeOperationDeleteCallbackId { + ProfileCodeOperationDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: ProfileCodeOperationDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct ProfileCodeOperationUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for ProfileCodeOperationTableHandle<'ctx> { + type UpdateCallbackId = ProfileCodeOperationUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> ProfileCodeOperationUpdateCallbackId { + ProfileCodeOperationUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: ProfileCodeOperationUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `operation_id` unique index on the table `profile_code_operation`, +/// which allows point queries on the field of the same name +/// via the [`ProfileCodeOperationOperationIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.profile_code_operation().operation_id().find(...)`. +pub struct ProfileCodeOperationOperationIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> ProfileCodeOperationTableHandle<'ctx> { + /// Get a handle on the `operation_id` unique index on the table `profile_code_operation`. + pub fn operation_id(&self) -> ProfileCodeOperationOperationIdUnique<'ctx> { + ProfileCodeOperationOperationIdUnique { + imp: self.imp.get_unique_constraint::("operation_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> ProfileCodeOperationOperationIdUnique<'ctx> { + /// Find the subscribed row whose `operation_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 { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("profile_code_operation"); + _table.add_unique_constraint::("operation_id", |row| &row.operation_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `ProfileCodeOperation`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait profile_code_operationQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ProfileCodeOperation`. + fn profile_code_operation(&self) -> __sdk::__query_builder::Table; +} + +impl profile_code_operationQueryTableAccess for __sdk::QueryTableAccessor { + fn profile_code_operation(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("profile_code_operation") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/profile_code_operation_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/profile_code_operation_type.rs new file mode 100644 index 000000000..03e7573ca --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/profile_code_operation_type.rs @@ -0,0 +1,66 @@ +// 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 ProfileCodeOperation { + pub operation_id: String, + pub code_kind: String, + pub code: String, + pub action: String, + pub operator_user_id: String, + pub created_at: __sdk::Timestamp, +} + +impl __sdk::InModule for ProfileCodeOperation { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `ProfileCodeOperation`. +/// +/// Provides typed access to columns for query building. +pub struct ProfileCodeOperationCols { + pub operation_id: __sdk::__query_builder::Col, + pub code_kind: __sdk::__query_builder::Col, + pub code: __sdk::__query_builder::Col, + pub action: __sdk::__query_builder::Col, + pub operator_user_id: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for ProfileCodeOperation { + type Cols = ProfileCodeOperationCols; + fn cols(table_name: &'static str) -> Self::Cols { + ProfileCodeOperationCols { + operation_id: __sdk::__query_builder::Col::new(table_name, "operation_id"), + code_kind: __sdk::__query_builder::Col::new(table_name, "code_kind"), + code: __sdk::__query_builder::Col::new(table_name, "code"), + action: __sdk::__query_builder::Col::new(table_name, "action"), + operator_user_id: __sdk::__query_builder::Col::new(table_name, "operator_user_id"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + } + } +} + +/// Indexed column accessor struct for the table `ProfileCodeOperation`. +/// +/// Provides typed access to indexed columns for query building. +pub struct ProfileCodeOperationIxCols { + pub code_kind: __sdk::__query_builder::IxCol, + pub operation_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for ProfileCodeOperation { + type IxCols = ProfileCodeOperationIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + ProfileCodeOperationIxCols { + code_kind: __sdk::__query_builder::IxCol::new(table_name, "code_kind"), + operation_id: __sdk::__query_builder::IxCol::new(table_name, "operation_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for ProfileCodeOperation {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_code_operation_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_code_operation_snapshot_type.rs new file mode 100644 index 000000000..bbc43d053 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_code_operation_snapshot_type.rs @@ -0,0 +1,20 @@ +// 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 RuntimeProfileCodeOperationSnapshot { + pub operation_id: String, + pub code_kind: String, + pub code: String, + pub action: String, + pub operator_user_id: String, + pub created_at_micros: i64, +} + +impl __sdk::InModule for RuntimeProfileCodeOperationSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_admin_list_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_admin_list_procedure_result_type.rs index 182db55b3..db56e3c0f 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_admin_list_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_invite_code_admin_list_procedure_result_type.rs @@ -4,6 +4,7 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::runtime_profile_code_operation_snapshot_type::RuntimeProfileCodeOperationSnapshot; use super::runtime_profile_invite_code_snapshot_type::RuntimeProfileInviteCodeSnapshot; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] @@ -11,6 +12,7 @@ use super::runtime_profile_invite_code_snapshot_type::RuntimeProfileInviteCodeSn pub struct RuntimeProfileInviteCodeAdminListProcedureResult { pub ok: bool, pub entries: Vec, + pub operations: Vec, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_redeem_code_admin_list_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_redeem_code_admin_list_procedure_result_type.rs index 9f5340f98..86f26def6 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_redeem_code_admin_list_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_redeem_code_admin_list_procedure_result_type.rs @@ -4,6 +4,7 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::runtime_profile_code_operation_snapshot_type::RuntimeProfileCodeOperationSnapshot; use super::runtime_profile_redeem_code_snapshot_type::RuntimeProfileRedeemCodeSnapshot; #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] @@ -11,6 +12,7 @@ use super::runtime_profile_redeem_code_snapshot_type::RuntimeProfileRedeemCodeSn pub struct RuntimeProfileRedeemCodeAdminListProcedureResult { pub ok: bool, pub entries: Vec, + pub operations: Vec, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/runtime.rs b/server-rs/crates/spacetime-client/src/runtime.rs index 2c71c3a9a..0ff3ced85 100644 --- a/server-rs/crates/spacetime-client/src/runtime.rs +++ b/server-rs/crates/spacetime-client/src/runtime.rs @@ -1138,7 +1138,7 @@ impl SpacetimeClient { pub async fn admin_list_profile_redeem_codes( &self, admin_user_id: String, - ) -> Result, SpacetimeClientError> { + ) -> Result { let procedure_input = build_runtime_profile_redeem_code_admin_list_input(admin_user_id) .map_err(SpacetimeClientError::validation_failed)? .into(); @@ -1228,7 +1228,7 @@ impl SpacetimeClient { pub async fn admin_list_profile_invite_codes( &self, admin_user_id: String, - ) -> Result, SpacetimeClientError> { + ) -> Result { let procedure_input = build_runtime_profile_invite_code_admin_list_input(admin_user_id) .map_err(SpacetimeClientError::validation_failed)? .into(); diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index 51de7fa9c..ff23fe2db 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -200,6 +200,7 @@ macro_rules! migration_tables { profile_task_reward_claim, profile_redeem_code, profile_redeem_code_usage, + profile_code_operation, profile_invite_code, profile_referral_relation, profile_played_world, diff --git a/server-rs/crates/spacetime-module/src/runtime/profile.rs b/server-rs/crates/spacetime-module/src/runtime/profile.rs index 462cf0d78..e085c38ae 100644 --- a/server-rs/crates/spacetime-module/src/runtime/profile.rs +++ b/server-rs/crates/spacetime-module/src/runtime/profile.rs @@ -194,6 +194,27 @@ pub struct ProfileRedeemCodeUsage { pub(crate) created_at: Timestamp, } +#[spacetimedb::table( + accessor = profile_code_operation, + index( + accessor = by_profile_code_operation_code_kind, + btree(columns = [code_kind]) + ), + index( + accessor = by_profile_code_operation_kind_code, + btree(columns = [code_kind, code]) + ) +)] +pub struct ProfileCodeOperation { + #[primary_key] + pub(crate) operation_id: String, + pub(crate) code_kind: String, + pub(crate) code: String, + pub(crate) action: String, + pub(crate) operator_user_id: String, + pub(crate) created_at: Timestamp, +} + #[spacetimedb::table(accessor = profile_invite_code)] pub struct ProfileInviteCode { #[primary_key] @@ -1097,14 +1118,16 @@ pub fn admin_list_profile_redeem_codes( input: RuntimeProfileRedeemCodeAdminListInput, ) -> RuntimeProfileRedeemCodeAdminListProcedureResult { match ctx.try_with_tx(|tx| admin_list_profile_redeem_code_records(tx, input.clone())) { - Ok(entries) => RuntimeProfileRedeemCodeAdminListProcedureResult { + Ok((entries, operations)) => RuntimeProfileRedeemCodeAdminListProcedureResult { ok: true, entries, + operations, error_message: None, }, Err(message) => RuntimeProfileRedeemCodeAdminListProcedureResult { ok: false, entries: Vec::new(), + operations: Vec::new(), error_message: Some(message), }, } @@ -1135,14 +1158,16 @@ pub fn admin_list_profile_invite_codes( input: RuntimeProfileInviteCodeAdminListInput, ) -> RuntimeProfileInviteCodeAdminListProcedureResult { match ctx.try_with_tx(|tx| admin_list_profile_invite_code_records(tx, input.clone())) { - Ok(entries) => RuntimeProfileInviteCodeAdminListProcedureResult { + Ok((entries, operations)) => RuntimeProfileInviteCodeAdminListProcedureResult { ok: true, entries, + operations, error_message: None, }, Err(message) => RuntimeProfileInviteCodeAdminListProcedureResult { ok: false, entries: Vec::new(), + operations: Vec::new(), error_message: Some(message), }, } @@ -2537,6 +2562,13 @@ fn admin_upsert_profile_redeem_code_record( .profile_redeem_code() .code() .find(&validated_input.code); + let action = if existing.is_some() { + "update" + } else { + "create" + }; + let operation_code = validated_input.code.clone(); + let operator_user_id = validated_input.admin_user_id.clone(); let created_at = existing .as_ref() .map(|row| row.created_at) @@ -2563,6 +2595,14 @@ fn admin_upsert_profile_redeem_code_record( updated_at, }; let inserted = ctx.db.profile_redeem_code().insert(row); + insert_profile_code_operation( + ctx, + "redeem", + &operation_code, + action, + &operator_user_id, + updated_at, + ); Ok(build_profile_redeem_code_snapshot_from_row(&inserted)) } @@ -2590,6 +2630,14 @@ fn admin_disable_profile_redeem_code_record( updated_at, ..existing }); + insert_profile_code_operation( + ctx, + "redeem", + &validated_input.code, + "disable", + &validated_input.admin_user_id, + updated_at, + ); Ok(build_profile_redeem_code_snapshot_from_row(&inserted)) } @@ -2611,6 +2659,8 @@ fn admin_upsert_profile_invite_code_record( &validated_input.admin_user_id, &validated_input.invite_code, ); + let operation_code = validated_input.invite_code.clone(); + let operator_user_id = validated_input.admin_user_id.clone(); if let Some(existing) = ctx .db @@ -2638,6 +2688,14 @@ fn admin_upsert_profile_invite_code_record( .expires_at_micros .map(Timestamp::from_micros_since_unix_epoch), }); + insert_profile_code_operation( + ctx, + "invite", + &operation_code, + "update", + &operator_user_id, + updated_at, + ); return Ok(build_profile_invite_code_snapshot_from_row(&inserted)); } @@ -2654,6 +2712,14 @@ fn admin_upsert_profile_invite_code_record( .expires_at_micros .map(Timestamp::from_micros_since_unix_epoch), }); + insert_profile_code_operation( + ctx, + "invite", + &operation_code, + "create", + &operator_user_id, + updated_at, + ); Ok(build_profile_invite_code_snapshot_from_row(&inserted)) } @@ -3171,7 +3237,13 @@ fn list_profile_recharge_product_config_snapshots( fn admin_list_profile_redeem_code_records( ctx: &ReducerContext, input: RuntimeProfileRedeemCodeAdminListInput, -) -> Result, String> { +) -> Result< + ( + Vec, + Vec, + ), + String, +> { let _validated_input = build_runtime_profile_redeem_code_admin_list_input(input.admin_user_id) .map_err(|error| error.to_string())?; @@ -3187,13 +3259,19 @@ fn admin_list_profile_redeem_code_records( .cmp(&left.updated_at_micros) .then_with(|| left.code.cmp(&right.code)) }); - Ok(entries) + Ok((entries, profile_code_operation_snapshots(ctx, "redeem"))) } fn admin_list_profile_invite_code_records( ctx: &ReducerContext, input: RuntimeProfileInviteCodeAdminListInput, -) -> Result, String> { +) -> Result< + ( + Vec, + Vec, + ), + String, +> { let _validated_input = build_runtime_profile_invite_code_admin_list_input(input.admin_user_id) .map_err(|error| error.to_string())?; @@ -3210,7 +3288,7 @@ fn admin_list_profile_invite_code_records( .cmp(&left.updated_at_micros) .then_with(|| left.invite_code.cmp(&right.invite_code)) }); - Ok(entries) + Ok((entries, profile_code_operation_snapshots(ctx, "invite"))) } fn upsert_profile_recharge_product_config_record( @@ -3531,6 +3609,59 @@ fn profile_task_tracking_scope_id( Ok(user_id.to_string()) } +fn insert_profile_code_operation( + ctx: &ReducerContext, + code_kind: &str, + code: &str, + action: &str, + operator_user_id: &str, + created_at: Timestamp, +) { + let created_at_micros = created_at.to_micros_since_unix_epoch(); + let operation_index = ctx + .db + .profile_code_operation() + .by_profile_code_operation_kind_code() + .filter((code_kind, code)) + .count(); + ctx.db.profile_code_operation().insert(ProfileCodeOperation { + operation_id: format!( + "{}:{}:{}:{}:{}", + code_kind.trim(), + code.trim(), + action.trim(), + created_at_micros, + operation_index + ), + code_kind: code_kind.to_string(), + code: code.to_string(), + action: action.to_string(), + operator_user_id: operator_user_id.to_string(), + created_at, + }); +} + +fn profile_code_operation_snapshots( + ctx: &ReducerContext, + code_kind: &str, +) -> Vec { + let mut entries = ctx + .db + .profile_code_operation() + .by_profile_code_operation_code_kind() + .filter(code_kind) + .map(|row| build_profile_code_operation_snapshot_from_row(&row)) + .collect::>(); + entries.sort_by(|left, right| { + right + .created_at_micros + .cmp(&left.created_at_micros) + .then_with(|| left.code.cmp(&right.code)) + .then_with(|| left.operation_id.cmp(&right.operation_id)) + }); + entries +} + fn validate_profile_task_user_scope(config: &ProfileTaskConfig) -> Result<(), String> { if config.scope_kind == RuntimeTrackingScopeKind::User { Ok(()) @@ -4165,6 +4296,19 @@ fn build_profile_invite_code_snapshot_from_row( } } +fn build_profile_code_operation_snapshot_from_row( + row: &ProfileCodeOperation, +) -> RuntimeProfileCodeOperationSnapshot { + RuntimeProfileCodeOperationSnapshot { + operation_id: row.operation_id.clone(), + code_kind: row.code_kind.clone(), + code: row.code.clone(), + action: row.action.clone(), + operator_user_id: row.operator_user_id.clone(), + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + } +} + fn build_profile_wallet_ledger_snapshot_from_row( row: &ProfileWalletLedger, ) -> RuntimeProfileWalletLedgerEntrySnapshot {