持久化码后台操作记录

新增兑换码和邀请码后台操作持久表。

列表接口返回全部操作记录并同步前后端契约。

后台页面展示持久化操作历史并移除最近一条会话记录。

更新 SpacetimeDB 迁移表清单、生成绑定和后端架构文档。
This commit is contained in:
2026-07-02 17:31:33 +08:00
parent 4e5ed0dd88
commit 15bca90ed2
22 changed files with 708 additions and 156 deletions
+11
View File
@@ -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 {
-15
View File
@@ -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<ProfileRedeemCodeAdminResponse | null>(null);
const [inviteResult, setInviteResult] =
useState<ProfileInviteCodeAdminResponse | null>(null);
const [taskConfigResult, setTaskConfigResult] =
useState<ProfileTaskConfigAdminResponse | null>(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' ? (
<AdminRedeemCodePage
result={redeemResult}
token={token}
onUnauthorized={handleUnauthorized}
onResultChange={setRedeemResult}
/>
) : null}
{routeId === 'invite' ? (
<AdminInviteCodePage
result={inviteResult}
token={token}
onUnauthorized={handleUnauthorized}
onResultChange={setInviteResult}
/>
) : null}
{routeId === 'creation-announcement' ? (
@@ -703,6 +703,8 @@ const databaseTableColumnLabelMap: Record<string, string> = {
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<string, string> = {
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<string, string> = {
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<string, string> = {
record_id: '记录标识',
created_by: '创建该记录的主体',
updated_by: '最后更新该记录的主体',
operator_user_id: '执行后台操作的用户标识',
total_count: '累计总数',
max_uses: '允许的最大使用次数',
global_used_count: '当前已使用次数',
@@ -1188,6 +1194,7 @@ const databaseTableLabelMap: Record<string, string> = {
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<string, string> = {
profile_task_reward_claim: '个人任务领奖记录表',
profile_redeem_code: '运营兑换码表',
profile_redeem_code_usage: '兑换码使用记录表',
profile_code_operation: '兑换码/邀请码后台操作记录表',
profile_invite_code: '用户邀请中心邀请码表',
profile_referral_relation: '邀请关系记录表',
profile_played_world: '用户已玩世界记录表',
@@ -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<ProfileInviteCodeAdminResponse[]>([]);
const [operations, setOperations] = useState<ProfileCodeOperationAdminResponse[]>([]);
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({
<section className="admin-panel admin-result-panel">
<div className="admin-panel-heading">
<h3></h3>
<span>{result?.inviteCode ?? '-'}</span>
<h3></h3>
<span>{operations.length}</span>
</div>
{result ? (
<dl className="admin-info-list">
<div>
<dt></dt>
<dd>{result.inviteCode}</dd>
</div>
<div>
<dt></dt>
<dd>{formatValidityWindow(result)}</dd>
</div>
<div>
<dt></dt>
<dd>
<TagList tags={metadataUserTags(result.metadata)} />
</dd>
</div>
<div>
<dt></dt>
<dd>{result.createdAt}</dd>
</div>
<div>
<dt></dt>
<dd>{result.updatedAt}</dd>
</div>
<div>
<dt>Metadata</dt>
<dd>
<pre className="admin-code-block">
{JSON.stringify(result.metadata, null, 2)}
</pre>
</dd>
</div>
</dl>
{operations.length ? (
<div className="admin-table-wrap">
<table className="admin-table admin-table-compact">
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{operations.map((operation) => (
<tr key={operation.operationId}>
<td>{operationActionLabel(operation.action)}</td>
<td>{operation.code}</td>
<td>{operation.operatorUserId}</td>
<td>{formatDateTime(operation.createdAt)}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="admin-empty-state"></div>
)}
@@ -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<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
@@ -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<ProfileRedeemCodeMode>('public');
@@ -44,6 +41,7 @@ export function AdminRedeemCodePage({
const [disableErrorMessage, setDisableErrorMessage] = useState('');
const [listErrorMessage, setListErrorMessage] = useState('');
const [entries, setEntries] = useState<ProfileRedeemCodeAdminResponse[]>([]);
const [operations, setOperations] = useState<ProfileCodeOperationAdminResponse[]>([]);
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({
<section className="admin-panel admin-result-panel">
<div className="admin-panel-heading">
<h3></h3>
<span>{result?.mode ?? '-'}</span>
<h3></h3>
<span>{operations.length}</span>
</div>
{result ? (
<dl className="admin-info-list">
<div>
<dt>Code</dt>
<dd>{result.code}</dd>
</div>
<div>
<dt></dt>
<dd>{result.rewardPoints}</dd>
</div>
<div>
<dt></dt>
<dd>{result.maxUses}</dd>
</div>
<div>
<dt></dt>
<dd>{result.globalUsedCount}</dd>
</div>
<div>
<dt></dt>
<dd>{result.enabled ? '启用' : '停用'}</dd>
</div>
<div>
<dt></dt>
<dd>{result.createdBy}</dd>
</div>
<div>
<dt></dt>
<dd>{result.updatedAt}</dd>
</div>
</dl>
{operations.length ? (
<div className="admin-table-wrap">
<table className="admin-table admin-table-compact">
<thead>
<tr>
<th></th>
<th>Code</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{operations.map((operation) => (
<tr key={operation.operationId}>
<td>{operationActionLabel(operation.action)}</td>
<td>{operation.code}</td>
<td>{operation.operatorUserId}</td>
<td>{formatDateTime(operation.createdAt)}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="admin-empty-state"></div>
)}
@@ -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});
}
@@ -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`
+11
View File
@@ -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 = {
@@ -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<RequestContext>,
Extension(admin): Extension<AuthenticatedAdmin>,
) -> Result<Json<Value>, 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<RequestContext>,
Extension(admin): Extension<AuthenticatedAdmin>,
) -> Result<Json<Value>, 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};
@@ -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 {
@@ -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<RuntimeProfileRedeemCodeSnapshot>,
pub operations: Vec<RuntimeProfileCodeOperationSnapshot>,
pub error_message: Option<String>,
}
@@ -1483,6 +1495,7 @@ pub struct RuntimeProfileInviteCodeAdminProcedureResult {
pub struct RuntimeProfileInviteCodeAdminListProcedureResult {
pub ok: bool,
pub entries: Vec<RuntimeProfileInviteCodeSnapshot>,
pub operations: Vec<RuntimeProfileCodeOperationSnapshot>,
pub error_message: Option<String>,
}
@@ -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<RuntimeProfileRedeemCodeRecord>,
pub operations: Vec<RuntimeProfileCodeOperationRecord>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RuntimeProfileInviteCodeAdminListRecord {
pub entries: Vec<RuntimeProfileInviteCodeRecord>,
pub operations: Vec<RuntimeProfileCodeOperationRecord>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RuntimeProfileInviteCodeRecord {
pub user_id: String,
@@ -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<ProfileRedeemCodeAdminResponse>,
pub operations: Vec<ProfileCodeOperationAdminResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -671,6 +683,7 @@ pub struct ProfileInviteCodeAdminResponse {
#[serde(rename_all = "camelCase")]
pub struct ProfileInviteCodeAdminListResponse {
pub entries: Vec<ProfileInviteCodeAdminResponse>,
pub operations: Vec<ProfileCodeOperationAdminResponse>,
}
fn default_true() -> bool {
+6 -3
View File
@@ -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,
@@ -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<Vec<RuntimeProfileRedeemCodeRecord>, SpacetimeClientError> {
) -> Result<RuntimeProfileRedeemCodeAdminListRecord, SpacetimeClientError> {
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<Vec<RuntimeProfileInviteCodeRecord>, SpacetimeClientError> {
) -> Result<RuntimeProfileInviteCodeAdminListRecord, SpacetimeClientError> {
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 {
@@ -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<Match3DWorkProfileRow>,
npc_state: __sdk::TableUpdate<NpcState>,
player_progression: __sdk::TableUpdate<PlayerProgression>,
profile_code_operation: __sdk::TableUpdate<ProfileCodeOperation>,
profile_dashboard_state: __sdk::TableUpdate<ProfileDashboardState>,
profile_feedback_submission: __sdk::TableUpdate<ProfileFeedbackSubmission>,
profile_invite_code: __sdk::TableUpdate<ProfileInviteCode>,
@@ -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::<ProfileCodeOperation>(
"profile_code_operation",
&self.profile_code_operation,
)
.with_updates_by_pk(|row| &row.operation_id);
diff.profile_dashboard_state = cache
.apply_diff_to_table::<ProfileDashboardState>(
"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::<ProfileCodeOperation>(
"profile_code_operation",
&self.profile_code_operation,
event,
);
callbacks.invoke_table_row_callbacks::<ProfileDashboardState>(
"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",
@@ -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<ProfileCodeOperation>,
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::<ProfileCodeOperation>("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<Item = ProfileCodeOperation> + '_ {
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<ProfileCodeOperation, String>,
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::<String>("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<ProfileCodeOperation> {
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::<ProfileCodeOperation>("profile_code_operation");
_table.add_unique_constraint::<String>("operation_id", |row| &row.operation_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<ProfileCodeOperation>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<ProfileCodeOperation>", "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<ProfileCodeOperation>;
}
impl profile_code_operationQueryTableAccess for __sdk::QueryTableAccessor {
fn profile_code_operation(&self) -> __sdk::__query_builder::Table<ProfileCodeOperation> {
__sdk::__query_builder::Table::new("profile_code_operation")
}
}
@@ -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<ProfileCodeOperation, String>,
pub code_kind: __sdk::__query_builder::Col<ProfileCodeOperation, String>,
pub code: __sdk::__query_builder::Col<ProfileCodeOperation, String>,
pub action: __sdk::__query_builder::Col<ProfileCodeOperation, String>,
pub operator_user_id: __sdk::__query_builder::Col<ProfileCodeOperation, String>,
pub created_at: __sdk::__query_builder::Col<ProfileCodeOperation, __sdk::Timestamp>,
}
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<ProfileCodeOperation, String>,
pub operation_id: __sdk::__query_builder::IxCol<ProfileCodeOperation, String>,
}
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 {}
@@ -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;
}
@@ -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<RuntimeProfileInviteCodeSnapshot>,
pub operations: Vec<RuntimeProfileCodeOperationSnapshot>,
pub error_message: Option<String>,
}
@@ -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<RuntimeProfileRedeemCodeSnapshot>,
pub operations: Vec<RuntimeProfileCodeOperationSnapshot>,
pub error_message: Option<String>,
}
@@ -1138,7 +1138,7 @@ impl SpacetimeClient {
pub async fn admin_list_profile_redeem_codes(
&self,
admin_user_id: String,
) -> Result<Vec<RuntimeProfileRedeemCodeRecord>, SpacetimeClientError> {
) -> Result<RuntimeProfileRedeemCodeAdminListRecord, SpacetimeClientError> {
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<Vec<RuntimeProfileInviteCodeRecord>, SpacetimeClientError> {
) -> Result<RuntimeProfileInviteCodeAdminListRecord, SpacetimeClientError> {
let procedure_input = build_runtime_profile_invite_code_admin_list_input(admin_user_id)
.map_err(SpacetimeClientError::validation_failed)?
.into();
@@ -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,
@@ -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<Vec<RuntimeProfileRedeemCodeSnapshot>, String> {
) -> Result<
(
Vec<RuntimeProfileRedeemCodeSnapshot>,
Vec<RuntimeProfileCodeOperationSnapshot>,
),
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<Vec<RuntimeProfileInviteCodeSnapshot>, String> {
) -> Result<
(
Vec<RuntimeProfileInviteCodeSnapshot>,
Vec<RuntimeProfileCodeOperationSnapshot>,
),
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<RuntimeProfileCodeOperationSnapshot> {
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::<Vec<_>>();
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 {