diff --git a/apps/admin-web/src/pages/AdminDatabaseTablesPage.test.tsx b/apps/admin-web/src/pages/AdminDatabaseTablesPage.test.tsx index 645127aa3..3c2c04cbd 100644 --- a/apps/admin-web/src/pages/AdminDatabaseTablesPage.test.tsx +++ b/apps/admin-web/src/pages/AdminDatabaseTablesPage.test.tsx @@ -1,14 +1,14 @@ /* @vitest-environment jsdom */ -import {render, screen, waitFor} from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import {beforeEach, expect, test, vi} from 'vitest'; +import { beforeEach, expect, test, vi } from 'vitest'; import { getAdminDatabaseTableRows, getAdminDatabaseTables, } from '../api/adminApiClient'; -import {AdminDatabaseTablesPage} from './AdminDatabaseTablesPage'; +import { AdminDatabaseTablesPage } from './AdminDatabaseTablesPage'; vi.mock('../api/adminApiClient', () => ({ formatAdminApiError: vi.fn((error: unknown) => @@ -36,12 +36,7 @@ beforeEach(() => { invite_code: 'INV-1001', inviter_user_id: 'u-a', }, - raw: [ - 'u-b', - 'u-a', - 'INV-1001', - '2026-05-02T00:00:00Z', - ], + raw: ['u-b', 'u-a', 'INV-1001', '2026-05-02T00:00:00Z'], }, { cells: { @@ -69,32 +64,37 @@ beforeEach(() => { test('后台表查询页支持宽表滚动容器和表头排序', async () => { const user = userEvent.setup(); - const {container} = render( + const { container } = render( , ); await screen.findByText('u-b'); + await screen.findByText('2026-05-02 08:00:00'); const tableWrap = container.querySelector('.admin-table-wrap'); expect(tableWrap?.querySelector('.admin-database-table')).not.toBeNull(); - expect(screen.getByRole('option', {name: '邀请关系(profile_referral_relation)'}).getAttribute('title')).toBe( - '原始表名:profile_referral_relation。邀请关系记录表。', - ); - expect(screen.getByText('已选表:邀请关系(profile_referral_relation)')).toBeTruthy(); - expect(screen.getByRole('heading', {name: '邀请关系'}).getAttribute('title')).toBe( - '原始表名:profile_referral_relation。邀请关系记录表。', - ); - expect(screen.getByRole('button', {name: '被邀请人ID'}).getAttribute('title')).toBe( - '原始字段名:invitee_user_id。被邀请人的用户标识。点击可按此列排序。', - ); + expect( + screen + .getByRole('option', { name: '邀请关系(profile_referral_relation)' }) + .getAttribute('title'), + ).toBe('原始表名:profile_referral_relation。邀请关系记录表。'); + expect( + screen.getByText('已选表:邀请关系(profile_referral_relation)'), + ).toBeTruthy(); + expect( + screen.getByRole('heading', { name: '邀请关系' }).getAttribute('title'), + ).toBe('原始表名:profile_referral_relation。邀请关系记录表。'); + expect( + screen.getByRole('button', { name: '被邀请人ID' }).getAttribute('title'), + ).toBe('原始字段名:invitee_user_id。被邀请人的用户标识。点击可按此列排序。'); expect(readFirstColumnValues(container)).toEqual(['u-b', 'u-a', 'u-c']); - await user.click(screen.getByRole('button', {name: '邀请人ID'})); + await user.click(screen.getByRole('button', { name: '邀请人ID' })); await waitFor(() => { expect(readFirstColumnValues(container)).toEqual(['u-b', 'u-c', 'u-a']); }); - await user.click(screen.getByRole('button', {name: '邀请人ID'})); + await user.click(screen.getByRole('button', { name: '邀请人ID' })); await waitFor(() => { expect(readFirstColumnValues(container)).toEqual(['u-a', 'u-b', 'u-c']); }); diff --git a/apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx b/apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx index cd9d0ea80..88192e2b8 100644 --- a/apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx +++ b/apps/admin-web/src/pages/AdminDatabaseTablesPage.tsx @@ -7,7 +7,7 @@ import { Search, X, } from 'lucide-react'; -import {FormEvent, useEffect, useMemo, useState} from 'react'; +import { FormEvent, useEffect, useMemo, useState } from 'react'; import { getAdminDatabaseTableRows, @@ -17,7 +17,7 @@ import type { AdminDatabaseTableRowPayload, AdminDatabaseTableRowsResponse, } from '../api/adminApiTypes'; -import {handlePageError} from './pageUtils'; +import { handlePageError } from './pageUtils'; interface AdminDatabaseTablesPageProps { token: string; @@ -35,8 +35,11 @@ export function AdminDatabaseTablesPage({ const [search, setSearch] = useState(''); const [filters, setFilters] = useState(''); const [limit, setLimit] = useState('100'); - const [result, setResult] = useState(null); - const [detailRow, setDetailRow] = useState(null); + const [result, setResult] = useState( + null, + ); + const [detailRow, setDetailRow] = + useState(null); const [errorMessage, setErrorMessage] = useState(''); const [copyMessage, setCopyMessage] = useState(''); const [sortColumn, setSortColumn] = useState(''); @@ -90,7 +93,9 @@ export function AdminDatabaseTablesPage({ const tableOptions = useMemo(() => { const optionNames = - tableName && !tables.includes(tableName) ? [tableName, ...tables] : tables; + tableName && !tables.includes(tableName) + ? [tableName, ...tables] + : tables; return optionNames.map(getDatabaseTableHeader); }, [tableName, tables]); @@ -119,7 +124,7 @@ export function AdminDatabaseTablesPage({ } return [...rows] - .map((row, index) => ({index, row})) + .map((row, index) => ({ index, row })) .sort((left, right) => { const comparison = compareTableCellValues( left.row.cells[sortColumn], @@ -131,7 +136,7 @@ export function AdminDatabaseTablesPage({ } return left.index - right.index; }) - .map(({row}) => row); + .map(({ row }) => row); }, [result, sortColumn, sortDirection, visibleColumns]); async function loadTables() { @@ -168,11 +173,15 @@ export function AdminDatabaseTablesPage({ setIsLoadingRows(true); setErrorMessage(''); try { - const response = await getAdminDatabaseTableRows(token, normalizedTableName, { - search: querySearch, - filters: queryFilters, - limit: parseLimit(queryLimit), - }); + const response = await getAdminDatabaseTableRows( + token, + normalizedTableName, + { + search: querySearch, + filters: queryFilters, + limit: parseLimit(queryLimit), + }, + ); setResult(response); setCopyMessage(''); } catch (error: unknown) { @@ -199,7 +208,7 @@ export function AdminDatabaseTablesPage({ setSearch(''); setFilters(''); setLimit('100'); - void refreshRows(tableName, {search: '', filters: '', limit: '100'}); + void refreshRows(tableName, { search: '', filters: '', limit: '100' }); } function handleSortColumn(column: string) { @@ -219,7 +228,11 @@ export function AdminDatabaseTablesPage({ return; } - const copiedText = JSON.stringify(detailRow.raw ?? detailRow.cells, null, 2); + const copiedText = JSON.stringify( + detailRow.raw ?? detailRow.cells, + null, + 2, + ); try { await navigator.clipboard.writeText(copiedText); setCopyMessage('已复制 JSON'); @@ -265,7 +278,7 @@ export function AdminDatabaseTablesPage({ value={tableName} onChange={(event) => handleTableChange(event.target.value)} > - {tableOptions.map(({name, optionLabel, description}) => ( + {tableOptions.map(({ name, optionLabel, description }) => ( {optionLabel} @@ -296,7 +309,11 @@ export function AdminDatabaseTablesPage({ onChange={(event) => setLimit(event.target.value)} /> - + {isLoadingRows ? '查询中' : '查询'} @@ -327,14 +344,16 @@ export function AdminDatabaseTablesPage({ - {resultTableHeader.label} + + {resultTableHeader.label} + {result?.totalReturned ?? 0} 条 - {columnHeaders.map(({column, label, description}) => { + {columnHeaders.map(({ column, label, description }) => { const isSorted = sortColumn === column; return ( setDetailRow(row)} > {visibleColumns.map((column) => { - const cellValue = formatCellValue(row.cells[column]); + const cellValue = formatCellValue( + row.cells[column], + column, + ); return ( - 暂无数据 + + 暂无数据 + )} @@ -419,7 +443,11 @@ export function AdminDatabaseTablesPage({ {detailRow ? ( - + 行详情 @@ -440,7 +468,9 @@ export function AdminDatabaseTablesPage({ - {copyMessage ? {copyMessage} : null} + {copyMessage ? ( + {copyMessage} + ) : null} {JSON.stringify(detailRow.raw ?? detailRow.cells, null, 2)} @@ -457,7 +487,9 @@ function readHashTableName() { if (queryIndex < 0) { return ''; } - return new URLSearchParams(hash.slice(queryIndex + 1)).get('table')?.trim() ?? ''; + return ( + new URLSearchParams(hash.slice(queryIndex + 1)).get('table')?.trim() ?? '' + ); } function parseLimit(value: string) { @@ -501,7 +533,10 @@ function getDatabaseTableLabel(tableName: string) { } function getDatabaseTableDescription(tableName: string, label: string) { - return databaseTableDescriptionMap[tableName] ?? `当前 SpacetimeDB 中的 ${label} 表`; + return ( + databaseTableDescriptionMap[tableName] ?? + `当前 SpacetimeDB 中的 ${label} 表` + ); } function getDatabaseTableColumnHeader(tableName: string, column: string) { @@ -512,7 +547,7 @@ function getDatabaseTableColumnHeader(tableName: string, column: string) { normalizedColumn, label, ); - return {column: normalizedColumn, label, description}; + return { column: normalizedColumn, label, description }; } function getDatabaseTableColumnLabel(column: string) { @@ -541,8 +576,7 @@ function getDatabaseTableColumnDescription( ) { const exactDescription = databaseTableColumnDescriptionMap[column]; const description = - exactDescription ?? - `当前表 ${tableName || '未知'} 中的 ${label} 字段`; + exactDescription ?? `当前表 ${tableName || '未知'} 中的 ${label} 字段`; return `原始字段名:${column}。${description}。点击可按此列排序。`; } @@ -566,7 +600,9 @@ function compareTableCellValues( } if (left.kind !== right.kind) { - return direction * (getSortKindOrder(left.kind) - getSortKindOrder(right.kind)); + return ( + direction * (getSortKindOrder(left.kind) - getSortKindOrder(right.kind)) + ); } let comparison = 0; @@ -578,7 +614,10 @@ function compareTableCellValues( comparison = Number(left.value) - Number(getSortableBooleanValue(right)); break; case 'text': - comparison = tableSortCollator.compare(left.value, getSortableTextValue(right)); + comparison = tableSortCollator.compare( + left.value, + getSortableTextValue(right), + ); break; } @@ -587,26 +626,26 @@ function compareTableCellValues( function normalizeTableCellSortValue(value: unknown): SortableTableCellValue { if (value === null || typeof value === 'undefined' || value === '') { - return {kind: 'empty'}; + return { kind: 'empty' }; } if (typeof value === 'number' && Number.isFinite(value)) { - return {kind: 'number', value}; + return { kind: 'number', value }; } if (typeof value === 'boolean') { - return {kind: 'boolean', value}; + return { kind: 'boolean', value }; } if (typeof value === 'string') { const trimmed = value.trim(); if (!trimmed) { - return {kind: 'empty'}; + return { kind: 'empty' }; } - return {kind: 'text', value: trimmed}; + return { kind: 'text', value: trimmed }; } - return {kind: 'text', value: stringifyUnknownValue(value)}; + return { kind: 'text', value: stringifyUnknownValue(value) }; } function buildRowKey(row: AdminDatabaseTableRowPayload, rowIndex: number) { @@ -614,13 +653,24 @@ function buildRowKey(row: AdminDatabaseTableRowPayload, rowIndex: number) { return `${rowIndex}-${String(firstValue ?? '')}`; } -function formatCellValue(value: unknown): FormattedTableCellValue { +function formatCellValue(value: unknown, column = ''): FormattedTableCellValue { if (value === null || typeof value === 'undefined' || value === '') { - return {content: '-', fullText: '-'}; + return { content: '-', fullText: '-' }; } - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { const text = String(value); - return {content: text, fullText: text}; + const readableTimestamp = formatReadableTimestampValue(value, column); + if (readableTimestamp) { + return { + content: readableTimestamp, + fullText: `${readableTimestamp}(原始值:${text})`, + }; + } + return { content: text, fullText: text }; } return { content: stringifyUnknownValue(value), @@ -628,6 +678,107 @@ function formatCellValue(value: unknown): FormattedTableCellValue { }; } +function formatReadableTimestampValue( + value: string | number | boolean, + column: string, +) { + if (typeof value === 'boolean' || !isTimestampColumn(column)) { + return ''; + } + + const timestampMs = parseTimestampMillis(value, column); + if (timestampMs === null) { + return ''; + } + + const date = new Date(timestampMs); + if (Number.isNaN(date.getTime())) { + return ''; + } + + return formatBeijingDateTime(date); +} + +function isTimestampColumn(column: string) { + const normalizedColumn = column.trim().toLowerCase(); + return ( + normalizedColumn.endsWith('_at') || + normalizedColumn.endsWith('_at_ms') || + normalizedColumn.endsWith('_at_micros') || + normalizedColumn.endsWith('_timestamp') || + normalizedColumn.endsWith('_timestamp_ms') || + normalizedColumn.endsWith('_timestamp_micros') + ); +} + +function parseTimestampMillis(value: string | number, column: string) { + if (typeof value === 'number') { + return parseNumericTimestampMillis(value, column); + } + + const trimmed = value.trim(); + if (!trimmed) { + return null; + } + + const numericValue = Number(trimmed); + if (Number.isFinite(numericValue) && /^-?\d+(\.\d+)?$/.test(trimmed)) { + return parseNumericTimestampMillis(numericValue, column); + } + + const parsed = Date.parse(trimmed); + return Number.isNaN(parsed) ? null : parsed; +} + +function parseNumericTimestampMillis(value: number, column: string) { + if (!Number.isFinite(value) || value <= 0) { + return null; + } + + const normalizedColumn = column.trim().toLowerCase(); + if ( + normalizedColumn.endsWith('_at_ms') || + normalizedColumn.endsWith('_timestamp_ms') + ) { + return value; + } + if ( + normalizedColumn.endsWith('_at_micros') || + normalizedColumn.endsWith('_timestamp_micros') + ) { + return Math.floor(value / 1_000); + } + + if (value >= 1_000_000_000_000_000) { + return Math.floor(value / 1_000); + } + if (value >= 1_000_000_000_000) { + return value; + } + if (value >= 1_000_000_000) { + return value * 1_000; + } + + return null; +} + +function formatBeijingDateTime(date: Date) { + const parts = new Intl.DateTimeFormat('zh-CN', { + day: '2-digit', + hour: '2-digit', + hourCycle: 'h23', + minute: '2-digit', + month: '2-digit', + second: '2-digit', + timeZone: 'Asia/Shanghai', + year: 'numeric', + }).formatToParts(date); + const partMap = Object.fromEntries( + parts.map((part) => [part.type, part.value]), + ); + return `${partMap.year}-${partMap.month}-${partMap.day} ${partMap.hour}:${partMap.minute}:${partMap.second}`; +} + function stringifyPrettyUnknownValue(value: unknown) { try { const serialized = JSON.stringify(value, null, 2); @@ -662,10 +813,10 @@ function getSortKindOrder(kind: SortableTableCellValue['kind']): number { } type SortableTableCellValue = - | {kind: 'empty'} - | {kind: 'number'; value: number} - | {kind: 'boolean'; value: boolean} - | {kind: 'text'; value: string}; + | { kind: 'empty' } + | { kind: 'number'; value: number } + | { kind: 'boolean'; value: boolean } + | { kind: 'text'; value: string }; interface DatabaseTableHeader { name: string; @@ -1257,7 +1408,8 @@ const databaseTableLabelMap: Record = { }; const databaseTableDescriptionMap: Record = { - database_migration_operator: '管理数据库迁移导出、导入和增量导入权限的操作员表', + database_migration_operator: + '管理数据库迁移导出、导入和增量导入权限的操作员表', database_migration_import_chunk: '大迁移 JSON 分片导入的临时表', auth_store_snapshot: '旧认证仓储的整份 JSON 快照表', user_account: '用户账号主表', @@ -1347,23 +1499,25 @@ function getSortableBooleanValue(value: SortableTableCellValue) { } function getSortableTextValue(value: SortableTableCellValue) { - return isSortableTextValue(value) ? value.value : stringifyUnknownValue(value); + return isSortableTextValue(value) + ? value.value + : stringifyUnknownValue(value); } function isSortableNumberValue( value: SortableTableCellValue, -): value is Extract { +): value is Extract { return value.kind === 'number'; } function isSortableBooleanValue( value: SortableTableCellValue, -): value is Extract { +): value is Extract { return value.kind === 'boolean'; } function isSortableTextValue( value: SortableTableCellValue, -): value is Extract { +): value is Extract { return value.kind === 'text'; } diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index e4cfbf736..2e84c0168 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -192,9 +192,11 @@ npm run check:server-rs-ddd 3. 充值中心、下单校验和支付确认入账都读取 `profile_recharge_product_config`。历史订单保留下单时写入的商品标题、金额、渠道、状态和 provider transaction id,不随配置改动回写。 4. 泥点首充资格按 `user_id + product_id` 的历史 `paid` 订单独立判断。某个档位已支付后,只隐藏该档位的首充赠送;其它未购买档位仍展示和结算首充赠送。 5. `hasPointsRecharged` 只保留为账号是否发生过任一泥点充值的兼容字段,不得驱动所有商品展示隐藏或结算金额计算。前端只渲染后端返回的商品快照。 -6. `paymentChannel` 缺失、未知或和设备不匹配时必须拒绝;真实微信渠道只允许 `wechat_mp`、`wechat_h5`、`wechat_native`,生产配置不得把真实支付静默降级为 `mock`。 -7. access JWT 只携带最小设备快照 `device.client_type`、`device.client_runtime`、`device.client_platform`。充值下单按该快照拦截渠道:小程序只允许 `wechat_mp`,手机微信内网页只允许 `wechat_h5`,桌面微信内网页只允许 `wechat_native`。 +6. `paymentChannel` 缺失、未知或冒用小程序支付设备时必须拒绝;真实微信渠道只允许 `wechat_mp`、`wechat_mp_virtual`、`wechat_jsapi`、`wechat_h5`、`wechat_native`,生产配置不得把真实支付静默降级为 `mock`。 +7. access JWT 只携带最小设备快照 `device.client_type`、`device.client_runtime`、`device.client_platform`。充值下单按该快照拦截小程序渠道:小程序只允许 `wechat_mp` / `wechat_mp_virtual`;微信内浏览器使用 `wechat_jsapi`;普通 Web 使用 `wechat_native`,历史普通 Web 登录态若缺少设备快照也允许继续进入 JSAPI / H5 / Native 渠道的后续支付配置校验,但不放宽小程序虚拟支付。 8. 所有微信真实渠道都以微信支付通知或服务端查单确认 `SUCCESS` 为到账事实;小程序、H5 跳转和 Native 二维码返回都不能直接发放泥点或会员。 +9. 微信 Native 下单显式传 `time_expire`,当前有效期为 5 分钟,并通过 `wechatNativePayment.expiresAt` 下发给前端二维码弹窗展示。 +10. 普通微信支付渠道的新建 pending 充值订单会写入 `profile_recharge_order_expiration_schedule`。到期处理由 `api-server` 后台 worker claim 调度行后调用微信查单;只有微信返回 `SUCCESS` 才补确认入账,返回 `NOTPAY` / `CLOSED` / `REVOKED` / `PAYERROR` 才关闭本地订单,查询失败或 `USERPAYING` 保留租约等待重试。SpacetimeDB module 不直接发起微信 HTTP 请求。 ## 创作入口泥点扣费契约 @@ -695,6 +697,12 @@ npm run check:server-rs-ddd - Rust 结构体:`ProfileRechargeOrder` - 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs` +### `profile_recharge_order_expiration_schedule` + +- Rust 结构体:`ProfileRechargeOrderExpirationSchedule` +- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs` +- 作用:普通微信充值订单的到期查单调度表。表内只保存待检查订单、计划检查时间和 worker 短租约;支付成功或本地关闭后删除对应行。 + ### `profile_redeem_code` - Rust 结构体:`ProfileRedeemCode` diff --git a/docs/【技术方案】微信虚拟支付接入-2026-05-26.md b/docs/【技术方案】微信虚拟支付接入-2026-05-26.md index 61057acd3..1cd3fcf81 100644 --- a/docs/【技术方案】微信虚拟支付接入-2026-05-26.md +++ b/docs/【技术方案】微信虚拟支付接入-2026-05-26.md @@ -6,13 +6,14 @@ - 泥点充值在微信小程序 WebView 内走 `wechat_mp_virtual`,由小程序页调用 `wx.requestVirtualPayment` 的 `short_series_coin` 模式。 - 会员商品在微信小程序 WebView 内同样走 `wechat_mp_virtual`,由小程序页调用 `wx.requestVirtualPayment` 的 `short_series_goods` 模式,并在 `signData` 内带 `productId` 与 `goodsPrice`。 -- H5 与桌面微信环境仍分别走 `wechat_h5` / `wechat_native`,不进入虚拟支付链路。 +- 微信内浏览器走 `wechat_jsapi`,复用微信支付 V3 JSAPI 下单返回的预支付参数并通过 `WeixinJSBridge.invoke('getBrandWCPayRequest')` 调起支付;普通 Web 统一走 `wechat_native` 二维码支付,不进入虚拟支付链路,也不依赖 H5 产品权限。`wechat_h5` 仅作为未来 H5 产品权限明确开通后的保留渠道。 - `session_key` 只保存在后端认证仓储内,用于计算虚拟支付用户态签名,不下发给前端。 - 客户端支付成功回调只代表已拉起支付并返回成功;最终到账仍以后端虚拟支付消息推送写入订单为准,普通微信支付订单则继续走微信支付 V3 notify / query。虚拟支付订单的确认接口只读取本地订单真相,不再用普通微信支付 V3 查单。 - 小程序 WebView 普通进入不预登录;H5 触发受保护入口或支付前必须保留 `clientRuntime=wechat_mini_program` 等宿主上下文,并用 `MicroMessenger + miniProgram` User-Agent 兜底识别首点 bridge 未就绪场景,再跳转小程序原生授权态,确保后端拿到带 `session_key` 的微信登录态。 ## 关键文件 +- JSAPI 支付缺少当前用户 openid 时,前端调用 `GET /api/auth/wechat/bind-start` 发起 OAuth;后端把当前 `user_id` 写入 OAuth state,微信回调仍走 `/api/auth/wechat/callback`,但只把获得的微信身份绑定到当前账号,不走普通 `/api/auth/wechat/start` 的登录/切号流程。 - 前端渠道选择:`src/services/payment/paymentPlatform.ts` - 充值入口:`src/components/rpg-entry/RpgEntryHomeView.tsx` - 小程序支付承接页:`miniprogram/pages/wechat-pay/index.shared.js` diff --git a/docs/【项目基线】当前产品与工程约束-2026-05-15.md b/docs/【项目基线】当前产品与工程约束-2026-05-15.md index 13628c52b..8f6ad684c 100644 --- a/docs/【项目基线】当前产品与工程约束-2026-05-15.md +++ b/docs/【项目基线】当前产品与工程约束-2026-05-15.md @@ -60,8 +60,8 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台。当 2. 泥点默认档位为 `60 / 180 / 300 / 680 / 1280 / 3280`,会员默认档位为月卡、季卡、年卡;实际展示、下单校验和支付确认都以后端返回的充值商品配置为准。 3. 首充双倍按泥点商品档位独立计算。用户买过 `points_60` 后,只影响 `points_60` 的首充展示和结算,其它未购买档位仍保留各自首充权益。 4. 前端不得用 `hasPointsRecharged` 统一隐藏所有泥点档位首充权益;该字段只表示账号是否发生过任一泥点充值。 -5. 充值支付渠道只允许由设备平台隔离层解析为 `wechat_mp`、`wechat_h5` 或 `wechat_native`;生产真实支付不得默认落到 `mock`,缺失或未知 `paymentChannel` 必须拒绝。 -6. 小程序 WebView 充值使用 `wechat_mp` 渠道时,H5 只跳转 native 支付页并在返回后请求服务端查单确认;手机微信内网页使用 `wechat_h5` 跳转微信 H5 支付;桌面微信内网页使用 `wechat_native` 二维码。只有微信通知或查单确认 `SUCCESS` 后才刷新余额或会员状态。 +5. 充值支付渠道只允许由设备平台隔离层解析为 `wechat_mp`、`wechat_mp_virtual`、`wechat_jsapi`、`wechat_h5` 或 `wechat_native`;生产真实支付不得默认落到 `mock`,缺失或未知 `paymentChannel` 必须拒绝。 +6. 小程序 WebView 充值使用 `wechat_mp_virtual` 调起小程序虚拟支付;微信内浏览器使用 `wechat_jsapi` 调起微信支付 JSAPI;普通 Web 使用 `wechat_native` 二维码支付,避免因移动 UA、触控能力或窄屏误入 `wechat_h5`。只有微信通知或查单确认 `SUCCESS` 后才刷新余额或会员状态。 7. 后端必须按 access JWT 中的最小设备快照拦截真实微信充值路径,不能只依赖前端隐藏入口或请求体传入的 `paymentChannel`。 8. 后台“充值商品”页维护泥点和会员商品配置,保存后影响新的充值中心快照、下单和支付确认;历史订单保留下单时快照。 diff --git a/packages/shared/src/contracts/runtime.ts b/packages/shared/src/contracts/runtime.ts index 6ce790428..94a4bb030 100644 --- a/packages/shared/src/contracts/runtime.ts +++ b/packages/shared/src/contracts/runtime.ts @@ -164,6 +164,7 @@ export type ProfileRechargeCenterResponse = { }; export type WechatMiniProgramPayParams = { + appId?: string; timeStamp: string; nonceStr: string; package: string; @@ -184,6 +185,7 @@ export type WechatH5Payment = { export type WechatNativePayment = { codeUrl: string; + expiresAt: string; }; export type CreateProfileRechargeOrderRequest = { diff --git a/scripts/dev.mjs b/scripts/dev.mjs index 169ec23f6..befbe0be6 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -1,5 +1,5 @@ -import {spawn, spawnSync} from 'node:child_process'; -import {randomBytes} from 'node:crypto'; +import { spawn, spawnSync } from 'node:child_process'; +import { randomBytes } from 'node:crypto'; import { createWriteStream, existsSync, @@ -11,9 +11,9 @@ import { watch, writeFileSync, } from 'node:fs'; -import {basename, join, relative, resolve} from 'node:path'; -import {createInterface} from 'node:readline'; -import {fileURLToPath} from 'node:url'; +import { basename, join, relative, resolve } from 'node:path'; +import { createInterface } from 'node:readline'; +import { fileURLToPath } from 'node:url'; import { formatPortDecision, @@ -39,7 +39,10 @@ const modulePath = resolve(serverRsDir, 'crates/spacetime-module'); const viteCliPath = resolve(repoRoot, 'scripts/vite-cli.mjs'); const adminWebDir = resolve(repoRoot, 'apps/admin-web'); -function resolveLocalGenarrativeFfmpeg(env = process.env, platform = process.platform) { +function resolveLocalGenarrativeFfmpeg( + env = process.env, + platform = process.platform, +) { if (platform !== 'win32') { return null; } @@ -60,7 +63,7 @@ function resolveLocalGenarrativeFfmpeg(env = process.env, platform = process.pla return null; } - return {binDir, ffmpegPath, ffprobePath}; + return { binDir, ffmpegPath, ffprobePath }; } function resolvePathEnvKey(env, platform = process.platform) { @@ -206,7 +209,9 @@ function parseArgs(argv, baseEnv) { spacetimeHost: env.SPACETIME_HOST || '127.0.0.1', spacetimePort: normalizePort(env.SPACETIME_PORT, 3101), spacetimeDataDir: resolve(serverRsDir, '.spacetimedb/local/data'), - spacetimeServerUrl: String(env.GENARRATIVE_SPACETIME_SERVER_URL ?? '').trim(), + spacetimeServerUrl: String( + env.GENARRATIVE_SPACETIME_SERVER_URL ?? '', + ).trim(), portRangeSpec: String(env.GENARRATIVE_DEV_PORT_RANGE ?? '').trim(), database: readLocalSpacetimeDatabase() || @@ -275,7 +280,10 @@ function parseArgs(argv, baseEnv) { explicitOptions.add('spacetimeHost'); break; case '--spacetime-port': - options.spacetimePort = normalizePort(readValue(), options.spacetimePort); + options.spacetimePort = normalizePort( + readValue(), + options.spacetimePort, + ); options.spacetimeServerUrl = ''; explicitOptions.add('spacetimePort'); break; @@ -339,7 +347,7 @@ function parseArgs(argv, baseEnv) { throw new Error('--api-timeout-seconds 必须是数字'); } - return {command, explicitOptions, options}; + return { command, explicitOptions, options }; } function normalizeServiceName(rawName) { @@ -400,7 +408,8 @@ function buildDevStackServiceSnapshot(runner, serviceName, updatedAt) { host: runtime.host ?? endpoint.host, port: runtime.port ?? endpoint.port, url: runtime.url ?? endpoint.url, - command: runtime.command ?? resolveDevStackServiceCommand(runner, serviceName), + command: + runtime.command ?? resolveDevStackServiceCommand(runner, serviceName), startedAt: runtime.startedAt ?? null, updatedAt: runtime.updatedAt ?? updatedAt, exitCode: runtime.exitCode ?? null, @@ -409,7 +418,7 @@ function buildDevStackServiceSnapshot(runner, serviceName, updatedAt) { } function resolveDevStackServiceEndpoint(runner, serviceName) { - const {options, state} = runner; + const { options, state } = runner; switch (serviceName) { case 'spacetime': return { @@ -445,7 +454,7 @@ function resolveDevStackServiceEndpoint(runner, serviceName) { } function resolveDevStackServiceCommand(runner, serviceName) { - const {options, state} = runner; + const { options, state } = runner; switch (serviceName) { case 'spacetime': return state.spacetimeReused @@ -505,12 +514,14 @@ function isSccacheRustcWrapper(value) { } function buildLocalRustProcessEnv(env, options = {}) { - const mergedEnv = {...env}; + const mergedEnv = { ...env }; const wrappers = [ String(mergedEnv.RUSTC_WRAPPER ?? '').trim(), String(mergedEnv.CARGO_BUILD_RUSTC_WRAPPER ?? '').trim(), ].filter(Boolean); - const customWrapper = wrappers.find((wrapper) => !isSccacheRustcWrapper(wrapper)); + const customWrapper = wrappers.find( + (wrapper) => !isSccacheRustcWrapper(wrapper), + ); if (customWrapper) { mergedEnv.RUSTC_WRAPPER = customWrapper; mergedEnv.CARGO_BUILD_RUSTC_WRAPPER = customWrapper; @@ -530,9 +541,10 @@ function buildLocalRustProcessEnv(env, options = {}) { function readWorkspaceSpacetimeVersion() { const manifestText = readFileSync(manifestPath, 'utf8'); - const match = /^spacetimedb\s*=\s*(?:"([^"]+)"|\{[^}]*version\s*=\s*"([^"]+)")/mu.exec( - manifestText, - ); + const match = + /^spacetimedb\s*=\s*(?:"([^"]+)"|\{[^}]*version\s*=\s*"([^"]+)")/mu.exec( + manifestText, + ); const version = match?.[1] ?? match?.[2] ?? ''; if (!version) { throw new Error('无法从 server-rs/Cargo.toml 读取 spacetimedb 版本'); @@ -545,7 +557,9 @@ function normalizeCargoVersionRequirement(version) { } function parseSpacetimeToolVersion(output) { - const match = /spacetimedb tool version\s+([0-9]+\.[0-9]+\.[0-9]+)/u.exec(output); + const match = /spacetimedb tool version\s+([0-9]+\.[0-9]+\.[0-9]+)/u.exec( + output, + ); return match?.[1] ?? ''; } @@ -627,12 +641,19 @@ function ensureSpacetimeToolVersionMatchesWorkspace() { function ensureRequiredFiles(command) { const requiredFiles = []; - if (command === 'api-server' || command === 'spacetime' || command === 'all') { + if ( + command === 'api-server' || + command === 'spacetime' || + command === 'all' + ) { requiredFiles.push([manifestPath, 'server-rs/Cargo.toml']); } if (command === 'spacetime' || command === 'all') { - requiredFiles.push([resolve(modulePath, 'Cargo.toml'), 'spacetime-module Cargo.toml']); + requiredFiles.push([ + resolve(modulePath, 'Cargo.toml'), + 'spacetime-module Cargo.toml', + ]); } if (command === 'web' || command === 'admin-web' || command === 'all') { @@ -640,7 +661,10 @@ function ensureRequiredFiles(command) { } if (command === 'admin-web' || command === 'all') { - requiredFiles.push([resolve(adminWebDir, 'package.json'), 'apps/admin-web/package.json']); + requiredFiles.push([ + resolve(adminWebDir, 'package.json'), + 'apps/admin-web/package.json', + ]); } for (const [path, label] of requiredFiles) { @@ -689,11 +713,15 @@ class DevService { return; } - this.updateRuntimeState({status: 'starting', exitCode: null, signal: null}); + this.updateRuntimeState({ + status: 'starting', + exitCode: null, + signal: null, + }); try { await this.startFn(this); } catch (error) { - this.updateRuntimeState({status: 'failed', pid: null}); + this.updateRuntimeState({ status: 'failed', pid: null }); throw error; } } @@ -738,7 +766,7 @@ class DevService { const processes = [this.child, ...this.children].filter(Boolean); this.stopping = processes.length > 0; if (processes.length > 0) { - this.updateRuntimeState({status: 'stopping'}); + this.updateRuntimeState({ status: 'stopping' }); } this.child = null; this.children = []; @@ -753,7 +781,7 @@ class DevService { this.logStream = null; this.stopping = false; if (processes.length > 0) { - this.updateRuntimeState({status: 'stopped', pid: null}); + this.updateRuntimeState({ status: 'stopped', pid: null }); } } @@ -772,7 +800,9 @@ class DevService { await this.restart(); } catch (error) { - console.error(`[dev:${this.name}] ${actionLabel}失败: ${error.message}`); + console.error( + `[dev:${this.name}] ${actionLabel}失败: ${error.message}`, + ); } }, delayMs); } @@ -858,7 +888,10 @@ function isStaleExternalGenerationWorkerProcess({ if (env.GENARRATIVE_PROCESS_ROLE !== 'external-generation-worker') { return false; } - if (String(env.GENARRATIVE_SPACETIME_SERVER_URL ?? '') !== expectedSpacetimeServer) { + if ( + String(env.GENARRATIVE_SPACETIME_SERVER_URL ?? '') !== + expectedSpacetimeServer + ) { return false; } if (String(env.GENARRATIVE_SPACETIME_DATABASE ?? '') !== expectedDatabase) { @@ -941,7 +974,9 @@ async function stopLinuxProcessId(pid, label) { if (error?.code === 'ESRCH') { return true; } - console.warn(`[dev:${label}] 强制停止旧进程失败 pid=${pid}: ${error.message}`); + console.warn( + `[dev:${label}] 强制停止旧进程失败 pid=${pid}: ${error.message}`, + ); return false; } return waitForLinuxProcessExit(pid, 1000); @@ -961,7 +996,10 @@ async function stopStaleLocalExternalGenerationWorkers({ return []; } - const expectedExePath = resolve(repoRootPath, 'server-rs/target/debug/api-server'); + const expectedExePath = resolve( + repoRootPath, + 'server-rs/target/debug/api-server', + ); const stopped = []; for (const pid of listLinuxProcessIds()) { const snapshot = readLinuxApiServerProcessSnapshot(pid); @@ -999,48 +1037,52 @@ function stopWindowsProcessTree(pid) { return; } - spawnSync('powershell.exe', [ - '-NoProfile', - '-ExecutionPolicy', - 'Bypass', - '-Command', + spawnSync( + 'powershell.exe', [ - '$ErrorActionPreference = "SilentlyContinue"', - '$root = [int]$env:GENARRATIVE_STOP_PID', - '$all = Get-CimInstance Win32_Process', - '$childrenByParent = @{}', - 'foreach ($process in $all) {', - ' $parent = [int]$process.ParentProcessId', - ' if (-not $childrenByParent.ContainsKey($parent)) { $childrenByParent[$parent] = @() }', - ' $childrenByParent[$parent] += [int]$process.ProcessId', - '}', - '$toStop = New-Object System.Collections.Generic.List[int]', - '$queue = New-Object System.Collections.Generic.Queue[int]', - '$queue.Enqueue($root)', - 'while ($queue.Count -gt 0) {', - ' $current = $queue.Dequeue()', - ' $toStop.Add($current)', - ' if ($childrenByParent.ContainsKey($current)) {', - ' foreach ($child in $childrenByParent[$current]) { $queue.Enqueue($child) }', - ' }', - '}', - 'foreach ($id in ($toStop | Select-Object -Unique | Sort-Object -Descending)) {', - ' Stop-Process -Id $id -Force -ErrorAction SilentlyContinue', - '}', - ].join('\n'), - ], { - env: { - ...process.env, - GENARRATIVE_STOP_PID: String(pid), + '-NoProfile', + '-ExecutionPolicy', + 'Bypass', + '-Command', + [ + '$ErrorActionPreference = "SilentlyContinue"', + '$root = [int]$env:GENARRATIVE_STOP_PID', + '$all = Get-CimInstance Win32_Process', + '$childrenByParent = @{}', + 'foreach ($process in $all) {', + ' $parent = [int]$process.ParentProcessId', + ' if (-not $childrenByParent.ContainsKey($parent)) { $childrenByParent[$parent] = @() }', + ' $childrenByParent[$parent] += [int]$process.ProcessId', + '}', + '$toStop = New-Object System.Collections.Generic.List[int]', + '$queue = New-Object System.Collections.Generic.Queue[int]', + '$queue.Enqueue($root)', + 'while ($queue.Count -gt 0) {', + ' $current = $queue.Dequeue()', + ' $toStop.Add($current)', + ' if ($childrenByParent.ContainsKey($current)) {', + ' foreach ($child in $childrenByParent[$current]) { $queue.Enqueue($child) }', + ' }', + '}', + 'foreach ($id in ($toStop | Select-Object -Unique | Sort-Object -Descending)) {', + ' Stop-Process -Id $id -Force -ErrorAction SilentlyContinue', + '}', + ].join('\n'), + ], + { + env: { + ...process.env, + GENARRATIVE_STOP_PID: String(pid), + }, + stdio: 'ignore', }, - stdio: 'ignore', - }); + ); } class DevRunner { constructor(options, baseEnv = process.env, explicitOptions = new Set()) { this.options = options; - this.baseEnv = {...baseEnv}; + this.baseEnv = { ...baseEnv }; this.explicitOptions = explicitOptions; const initialSpacetimeServer = options.spacetimeServerUrl || @@ -1067,7 +1109,8 @@ class DevRunner { } if ( command === 'spacetime' || - (command === 'all' && (!this.options.skipSpacetime || !this.options.skipPublish)) + (command === 'all' && + (!this.options.skipSpacetime || !this.options.skipPublish)) ) { requireCommand('spacetime'); } @@ -1153,16 +1196,24 @@ class DevRunner { return; } - const pidState = readRecordedSpacetimePidState(this.options.spacetimeDataDir); + const pidState = readRecordedSpacetimePidState( + this.options.spacetimeDataDir, + ); const recordedUrl = readRecordedSpacetimeUrl(this.options.spacetimeDataDir); if (pidState.state === 'missing') { if (recordedUrl) { - console.log(`[dev:spacetime] 记录的 URL 缺少 spacetime.pid,跳过复用: ${recordedUrl}`); + console.log( + `[dev:spacetime] 记录的 URL 缺少 spacetime.pid,跳过复用: ${recordedUrl}`, + ); } return; } - if (pidState.state === 'invalid' || pidState.state === 'dead' || pidState.state === 'unknown') { + if ( + pidState.state === 'invalid' || + pidState.state === 'dead' || + pidState.state === 'unknown' + ) { console.log('[dev:spacetime] 检测到 spacetime.pid 但状态无效,跳过复用'); return; } @@ -1198,12 +1249,16 @@ class DevRunner { this.state.spacetimePid = Number.isInteger(pidState.pid) ? pidState.pid : null; - const pidLabel = Number.isInteger(pidState.pid) ? ` pid=${pidState.pid}` : ''; + const pidLabel = Number.isInteger(pidState.pid) + ? ` pid=${pidState.pid}` + : ''; console.log(`[dev:spacetime] 复用已启动实例${pidLabel}: ${candidate}`); return; } - const pidLabel = Number.isInteger(pidState.pid) ? ` pid=${pidState.pid}` : ''; + const pidLabel = Number.isInteger(pidState.pid) + ? ` pid=${pidState.pid}` + : ''; throw new Error( `检测到 spacetime.pid${pidLabel},但无法连接候选地址: ${candidates.join(', ')}`, ); @@ -1225,7 +1280,7 @@ class DevRunner { } async resolvePorts(command) { - const {options} = this; + const { options } = this; const portConfig = {}; const portRangeFor = (optionName) => this.explicitOptions.has(optionName) ? null : this.state.portRange; @@ -1324,7 +1379,11 @@ class DevRunner { ); this.services.set( 'web', - new DevService('web', async (service) => this.startWeb(service), onStateChange), + new DevService( + 'web', + async (service) => this.startWeb(service), + onStateChange, + ), ); this.services.set( 'admin-web', @@ -1350,7 +1409,7 @@ class DevRunner { } printSummary(command) { - const {options, state} = this; + const { options, state } = this; console.log(`[dev] repo: ${repoRoot}`); console.log(`[dev] command: ${command}`); console.log(`[dev] watch: ${options.watch ? 'on' : 'off'}`); @@ -1364,7 +1423,9 @@ class DevRunner { ); } console.log(`[dev] web: http://127.0.0.1:${options.webPort}`); - console.log(`[dev] admin web: http://${state.adminWebTargetHost}:${options.adminWebPort}/admin/`); + console.log( + `[dev] admin web: http://${state.adminWebTargetHost}:${options.adminWebPort}/admin/`, + ); console.log(`[dev] api-server: ${state.apiTarget}`); console.log(`[dev] spacetime: ${state.spacetimeServer}`); console.log(`[dev] database: ${options.database}`); @@ -1374,7 +1435,11 @@ class DevRunner { try { ensureParentDir(devStackStatePath); const snapshot = buildDevStackSnapshot(this); - writeFileSync(devStackStatePath, `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8'); + writeFileSync( + devStackStatePath, + `${JSON.stringify(snapshot, null, 2)}\n`, + 'utf8', + ); } catch (error) { console.warn(`[dev] 写入 ${devStackStatePath} 失败: ${error.message}`); } @@ -1437,20 +1502,23 @@ class DevRunner { } startSpacetime(service) { - const {options} = this; + const { options } = this; ensureParentDir(resolve(options.spacetimeDataDir, 'logs/.keep')); - const logFile = resolve(options.spacetimeDataDir, 'logs/dev-spacetime-start.log'); - const logStream = createWriteStream(logFile, {flags: 'a', encoding: 'utf8'}); + const logFile = resolve( + options.spacetimeDataDir, + 'logs/dev-spacetime-start.log', + ); + const logStream = createWriteStream(logFile, { + flags: 'a', + encoding: 'utf8', + }); service.logStream = logStream; const spacetimeToolVersion = readSpacetimeToolVersion(); assertSpacetimeToolVersionMatchesWorkspace({ toolVersion: spacetimeToolVersion, workspaceVersion: readWorkspaceSpacetimeVersion(), }); - recordSpacetimeToolVersion( - options.spacetimeDataDir, - spacetimeToolVersion, - ); + recordSpacetimeToolVersion(options.spacetimeDataDir, spacetimeToolVersion); console.log(`[dev:spacetime] log: ${logFile}`); service.updateRuntimeState({ @@ -1516,7 +1584,10 @@ class DevRunner { this.options.spacetimePort = port; this.state.spacetimeServer = `http://${this.options.spacetimeHost}:${port}`; - recordSpacetimeUrl(this.options.spacetimeDataDir, this.state.spacetimeServer); + recordSpacetimeUrl( + this.options.spacetimeDataDir, + this.state.spacetimeServer, + ); this.services.get('spacetime')?.updateRuntimeState({ host: this.options.spacetimeHost, port, @@ -1528,7 +1599,9 @@ class DevRunner { async waitForSpacetime() { const deadline = Date.now() + this.options.spacetimeTimeoutSeconds * 1000; while (Date.now() < deadline) { - if (await isHttpReady(new URL('/v1/ping', this.state.spacetimeServer).href)) { + if ( + await isHttpReady(new URL('/v1/ping', this.state.spacetimeServer).href) + ) { return; } await sleep(500); @@ -1570,23 +1643,37 @@ class DevRunner { console.log('[dev:spacetime] 未启用迁移引导密钥'); return; default: - throw new Error(`未知迁移引导密钥模式: ${this.options.migrationBootstrapSecretMode}`); + throw new Error( + `未知迁移引导密钥模式: ${this.options.migrationBootstrapSecretMode}`, + ); } env.GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET = this.options.migrationBootstrapSecret; - console.log(`[dev:spacetime] 迁移引导密钥: ${this.options.migrationBootstrapSecret}`); + console.log( + `[dev:spacetime] 迁移引导密钥: ${this.options.migrationBootstrapSecret}`, + ); } async ensureApiServerSpacetimeToken() { - const existingToken = String(this.baseEnv.GENARRATIVE_SPACETIME_TOKEN ?? '').trim(); - if (existingToken && shouldTrustExistingSpacetimeToken(existingToken, this.state.spacetimeServer)) { + const existingToken = String( + this.baseEnv.GENARRATIVE_SPACETIME_TOKEN ?? '', + ).trim(); + if ( + existingToken && + shouldTrustExistingSpacetimeToken( + existingToken, + this.state.spacetimeServer, + ) + ) { return; } const identityUrl = buildUrl(this.state.spacetimeServer, '/v1/identity'); if (!identityUrl) { - throw new Error(`无法构造 SpacetimeDB identity 地址: ${this.state.spacetimeServer}`); + throw new Error( + `无法构造 SpacetimeDB identity 地址: ${this.state.spacetimeServer}`, + ); } const response = await fetchSpacetimeIdentity(identityUrl); @@ -1608,7 +1695,10 @@ class DevRunner { const logFile = resolveApiServerLogFile(repoRoot, mergedEnv); ensureParentDir(logFile); - const logStream = createWriteStream(logFile, {flags: 'a', encoding: 'utf8'}); + const logStream = createWriteStream(logFile, { + flags: 'a', + encoding: 'utf8', + }); service.logStream = logStream; mergedEnv.GENARRATIVE_API_SERVER_LOG_FILE = logFile; @@ -1723,7 +1813,7 @@ class DevRunner { } startAdminWeb(service) { - const apiTarget = this.resolveFrontendApiTarget({admin: true}); + const apiTarget = this.resolveFrontendApiTarget({ admin: true }); const endpoint = resolveDevStackServiceEndpoint(this, 'admin-web'); const env = { ...this.baseEnv, @@ -1767,7 +1857,7 @@ class DevRunner { service.registerChild(child); } - resolveFrontendApiTarget({admin = false} = {}) { + resolveFrontendApiTarget({ admin = false } = {}) { if ( this.command === 'all' || this.explicitOptions.has('apiHost') || @@ -1909,7 +1999,10 @@ function stopExistingWindowsApiServer(logStream) { return; } - const apiServerExePath = resolve(repoRoot, 'server-rs/target/debug/api-server.exe'); + const apiServerExePath = resolve( + repoRoot, + 'server-rs/target/debug/api-server.exe', + ); const command = [ '$ErrorActionPreference = "Continue"', '$target = [System.IO.Path]::GetFullPath($env:GENARRATIVE_API_SERVER_EXE_TARGET)', @@ -1957,21 +2050,31 @@ function parseListenAddr(text) { return match?.[1] ?? ''; } -function createServiceWatcher({config, service, serviceName, restartFn, actionLabel = '重启'}) { +function createServiceWatcher({ + config, + service, + serviceName, + restartFn, + actionLabel = '重启', +}) { try { - const watcher = watch(config.path, {recursive: true}, (_event, fileName) => { - const filePath = fileName - ? resolve(config.path, String(fileName)) - : config.path; - notifyWatchedFile({ - config, - filePath, - restartFn, - service, - serviceName, - actionLabel, - }); - }); + const watcher = watch( + config.path, + { recursive: true }, + (_event, fileName) => { + const filePath = fileName + ? resolve(config.path, String(fileName)) + : config.path; + notifyWatchedFile({ + config, + filePath, + restartFn, + service, + serviceName, + actionLabel, + }); + }, + ); return { close: () => watcher.close(), @@ -1980,11 +2083,23 @@ function createServiceWatcher({config, service, serviceName, restartFn, actionLa console.warn( `[dev:watch] ${serviceName} 使用轮询监听 ${relative(repoRoot, config.path)}: ${error.message}`, ); - return createPollingWatcher({config, restartFn, service, serviceName, actionLabel}); + return createPollingWatcher({ + config, + restartFn, + service, + serviceName, + actionLabel, + }); } } -function createPollingWatcher({config, service, serviceName, restartFn, actionLabel = '重启'}) { +function createPollingWatcher({ + config, + service, + serviceName, + restartFn, + actionLabel = '重启', +}) { let snapshot = snapshotWatchedFiles(config.path, config.filter); const timer = setInterval(() => { const nextSnapshot = snapshotWatchedFiles(config.path, config.filter); @@ -2009,7 +2124,14 @@ function createPollingWatcher({config, service, serviceName, restartFn, actionLa }; } -function notifyWatchedFile({config, filePath, restartFn, service, serviceName, actionLabel}) { +function notifyWatchedFile({ + config, + filePath, + restartFn, + service, + serviceName, + actionLabel, +}) { if (!shouldAcceptWatchEvent(config, filePath)) { return; } @@ -2128,9 +2250,7 @@ function pipeChildOutput(child) { function uniqueNonEmpty(values) { return [ ...new Set( - values - .map((value) => String(value ?? '').trim()) - .filter(Boolean), + values.map((value) => String(value ?? '').trim()).filter(Boolean), ), ]; } @@ -2167,32 +2287,35 @@ function readRecordedSpacetimeToolVersion(dataDir) { function readRecordedSpacetimePidState(dataDir) { const pidPath = resolve(dataDir, 'spacetime.pid'); if (!existsSync(pidPath)) { - return {state: 'missing', pid: 0}; + return { state: 'missing', pid: 0 }; } try { - const rawPid = readFileSync(pidPath, 'utf8').split(/\r?\n/u)[0]?.trim() ?? ''; + const rawPid = + readFileSync(pidPath, 'utf8').split(/\r?\n/u)[0]?.trim() ?? ''; const pid = Number(rawPid); if (!Number.isInteger(pid) || pid <= 0) { - return {state: 'invalid', pid: 0}; + return { state: 'invalid', pid: 0 }; } try { process.kill(pid, 0); - return {state: 'alive', pid}; + return { state: 'alive', pid }; } catch (error) { if (error?.code === 'EPERM') { - return {state: 'alive', pid}; + return { state: 'alive', pid }; } - return {state: 'dead', pid: 0}; + return { state: 'dead', pid: 0 }; } } catch (error) { if (error?.code === 'EBUSY' || error?.code === 'EPERM') { - return {state: 'locked', pid: 0}; + return { state: 'locked', pid: 0 }; } - console.warn(`[dev:spacetime] 读取 PID 记录失败 ${pidPath}: ${error.message}`); - return {state: 'unknown', pid: 0}; + console.warn( + `[dev:spacetime] 读取 PID 记录失败 ${pidPath}: ${error.message}`, + ); + return { state: 'unknown', pid: 0 }; } } @@ -2202,7 +2325,9 @@ function recordSpacetimeToolVersion(dataDir, version) { try { writeFileSync(versionPath, `${version}\n`, 'utf8'); } catch (error) { - console.warn(`[dev:spacetime] 写入版本记录失败 ${versionPath}: ${error.message}`); + console.warn( + `[dev:spacetime] 写入版本记录失败 ${versionPath}: ${error.message}`, + ); } } @@ -2217,7 +2342,9 @@ function recordSpacetimeUrl(dataDir, serverUrl) { try { writeFileSync(target, `${serverUrl}\n`, 'utf8'); } catch (error) { - console.warn(`[dev:spacetime] 写入 URL 记录失败 ${target}: ${error.message}`); + console.warn( + `[dev:spacetime] 写入 URL 记录失败 ${target}: ${error.message}`, + ); } } } @@ -2242,7 +2369,7 @@ async function isHttpReady(url, timeoutMs = 1000) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { - const response = await fetch(url, {signal: controller.signal}); + const response = await fetch(url, { signal: controller.signal }); return response.status >= 200 && response.status < 500; } catch { return false; @@ -2271,7 +2398,9 @@ async function fetchSpacetimeIdentity(url) { const text = await response.text(); if (!response.ok) { - throw new Error(`SpacetimeDB identity HTTP ${response.status}: ${trimPreview(text)}`); + throw new Error( + `SpacetimeDB identity HTTP ${response.status}: ${trimPreview(text)}`, + ); } let payload; @@ -2286,22 +2415,41 @@ async function fetchSpacetimeIdentity(url) { } const identity = - payload.identity ?? payload.Identity ?? payload.identity_hex ?? payload.identityHex; + payload.identity ?? + payload.Identity ?? + payload.identity_hex ?? + payload.identityHex; const token = payload.token ?? payload.Token; if (typeof identity !== 'string' || typeof token !== 'string') { - throw new Error(`SpacetimeDB identity 响应缺少 identity/token: ${trimPreview(text)}`); + throw new Error( + `SpacetimeDB identity 响应缺少 identity/token: ${trimPreview(text)}`, + ); } - return {identity, token}; + return { identity, token }; } -function shouldTrustExistingSpacetimeToken(existingToken, serverUrl) { - const shellToken = String(process.env.GENARRATIVE_SPACETIME_TOKEN ?? '').trim(); - if (shellToken && shellToken === existingToken) { +function shouldTrustExistingSpacetimeToken( + existingToken, + serverUrl, + { env = process.env, resolveCliToken = resolveCurrentSpacetimeCliToken } = {}, +) { + const normalizedToken = String(existingToken ?? '').trim(); + if (!normalizedToken) { + return false; + } + + const shellToken = String(env.GENARRATIVE_SPACETIME_TOKEN ?? '').trim(); + if (shellToken && shellToken === normalizedToken) { return true; } - return !isLoopbackSpacetimeServer(serverUrl); + if (!isLoopbackSpacetimeServer(serverUrl)) { + return true; + } + + const cliToken = resolveCliToken(); + return Boolean(cliToken && cliToken === normalizedToken); } function isLoopbackSpacetimeServer(serverUrl) { @@ -2313,14 +2461,30 @@ function isLoopbackSpacetimeServer(serverUrl) { } } +function resolveCurrentSpacetimeCliToken() { + const result = spawnSync('spacetime', ['login', 'show', '--token'], { + cwd: repoRoot, + encoding: 'utf8', + shell: process.platform === 'win32', + }); + if (result.status !== 0) { + return ''; + } + + const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`; + return /auth token\b.*?\bis\s+(\S+)/iu.exec(output)?.[1]?.trim() ?? ''; +} + function trimPreview(text, maxLength = 300) { - const normalized = String(text ?? '').replace(/\s+/gu, ' ').trim(); + const normalized = String(text ?? '') + .replace(/\s+/gu, ' ') + .trim(); return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized; } -function runForeground(command, args, {cwd, env, label}) { +function runForeground(command, args, { cwd, env, label }) { return new Promise((resolveRun, rejectRun) => { const child = spawn(command, args, { cwd, @@ -2397,7 +2561,7 @@ function isDirectModuleExecution(argv1, moduleUrl, resolvePath = safeRealpath) { } } -function buildSpacetimePublishArgs({database, server, preserveDatabase}) { +function buildSpacetimePublishArgs({ database, server, preserveDatabase }) { const args = [ 'publish', database, @@ -2467,14 +2631,19 @@ export { parseArgs, parseSpacetimeToolVersion, resolveDevStackStatePath, + resolveCurrentSpacetimeCliToken, shouldAcceptWatchEvent, + shouldTrustExistingSpacetimeToken, }; async function main() { let runner; try { const baseEnv = mergeApiServerEnv(repoRoot, process.env); - const {command, explicitOptions, options} = parseArgs(process.argv.slice(2), baseEnv); + const { command, explicitOptions, options } = parseArgs( + process.argv.slice(2), + baseEnv, + ); runner = new DevRunner(options, baseEnv, explicitOptions); await runner.init(command); diff --git a/scripts/dev.test.ts b/scripts/dev.test.ts index 18d810a85..28f696923 100644 --- a/scripts/dev.test.ts +++ b/scripts/dev.test.ts @@ -1,8 +1,14 @@ -import {mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs'; -import {tmpdir} from 'node:os'; -import {join} from 'node:path'; +import { + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; -import {afterEach, describe, expect, test, vi} from 'vitest'; +import { afterEach, describe, expect, test, vi } from 'vitest'; import { assertReusableSpacetimeProcessVersionMatchesWorkspace, @@ -23,6 +29,7 @@ import { parseSpacetimeToolVersion, resolveDevStackStatePath, shouldAcceptWatchEvent, + shouldTrustExistingSpacetimeToken, } from './dev.mjs'; const originalFetch = globalThis.fetch; @@ -58,7 +65,7 @@ describe('dev scheduler argument routing', () => { }); test('完整 dev 栈覆盖前端代理到本次解析出的 api-server 地址', () => { - const {command, explicitOptions, options} = parseArgs([], { + const { command, explicitOptions, options } = parseArgs([], { GENARRATIVE_API_PORT: '8090', GENARRATIVE_RUNTIME_SERVER_TARGET: 'http://127.0.0.1:3100', RUST_SERVER_TARGET: 'http://127.0.0.1:3100', @@ -75,7 +82,7 @@ describe('dev scheduler argument routing', () => { RUST_SERVER_TARGET: 'http://127.0.0.1:3100', GENARRATIVE_API_PORT: '8082', }; - const {command, explicitOptions, options} = parseArgs(['web'], testEnv); + const { command, explicitOptions, options } = parseArgs(['web'], testEnv); const runner = new DevRunner(options, testEnv, explicitOptions); runner.command = command; @@ -87,7 +94,7 @@ describe('dev scheduler argument routing', () => { RUST_SERVER_TARGET: 'http://127.0.0.1:3100', GENARRATIVE_API_PORT: '8082', }; - const {command, explicitOptions, options} = parseArgs( + const { command, explicitOptions, options } = parseArgs( ['web', '--api-port', '9090'], testEnv, ); @@ -102,11 +109,14 @@ describe('dev scheduler argument routing', () => { ADMIN_API_TARGET: 'http://127.0.0.1:3100', RUST_SERVER_TARGET: 'http://127.0.0.1:8082', }; - const {command, explicitOptions, options} = parseArgs(['admin-web'], testEnv); + const { command, explicitOptions, options } = parseArgs( + ['admin-web'], + testEnv, + ); const runner = new DevRunner(options, testEnv, explicitOptions); runner.command = command; - expect(runner.resolveFrontendApiTarget({admin: true})).toBe( + expect(runner.resolveFrontendApiTarget({ admin: true })).toBe( 'http://127.0.0.1:3100', ); }); @@ -114,18 +124,22 @@ describe('dev scheduler argument routing', () => { linuxTest('Linux 启动时按系统级端口段映射四个 dev 端口', async () => { const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-dev-port-range-')); try { - const {command, explicitOptions, options} = parseArgs([], { + const { command, explicitOptions, options } = parseArgs([], { USER: 'alice', LOGNAME: 'alice', GENARRATIVE_DEV_PORT_RANGE: '22000-22099', GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir, }); - const runner = new DevRunner(options, { - USER: 'alice', - LOGNAME: 'alice', - GENARRATIVE_DEV_PORT_RANGE: '22000-22099', - GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir, - }, explicitOptions); + const runner = new DevRunner( + options, + { + USER: 'alice', + LOGNAME: 'alice', + GENARRATIVE_DEV_PORT_RANGE: '22000-22099', + GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir, + }, + explicitOptions, + ); await runner.prepareLinuxPortRange(command); @@ -137,56 +151,72 @@ describe('dev scheduler argument routing', () => { expect(runner.state.apiTarget).toBe('http://127.0.0.1:22001'); expect(runner.state.spacetimeServer).toBe('http://127.0.0.1:22002'); } finally { - rmSync(tempDir, {recursive: true, force: true}); + rmSync(tempDir, { recursive: true, force: true }); } }); - linuxTest('Linux 桌面壳显式指定 web-port 时不被系统级端口段改写', async () => { - const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-dev-port-range-')); - try { - const {command, explicitOptions, options} = parseArgs( - ['web', '--web-port', '3000', '--strict-web-port'], - { - USER: 'alice', - LOGNAME: 'alice', - GENARRATIVE_DEV_PORT_RANGE: '22000-22099', - GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir, - }, + linuxTest( + 'Linux 桌面壳显式指定 web-port 时不被系统级端口段改写', + async () => { + const tempDir = mkdtempSync( + join(tmpdir(), 'genarrative-dev-port-range-'), ); - const runner = new DevRunner(options, { - USER: 'alice', - LOGNAME: 'alice', - GENARRATIVE_DEV_PORT_RANGE: '22000-22099', - GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir, - }, explicitOptions); + try { + const { command, explicitOptions, options } = parseArgs( + ['web', '--web-port', '3000', '--strict-web-port'], + { + USER: 'alice', + LOGNAME: 'alice', + GENARRATIVE_DEV_PORT_RANGE: '22000-22099', + GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir, + }, + ); + const runner = new DevRunner( + options, + { + USER: 'alice', + LOGNAME: 'alice', + GENARRATIVE_DEV_PORT_RANGE: '22000-22099', + GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR: tempDir, + }, + explicitOptions, + ); - await runner.prepareLinuxPortRange(command); - expect(runner.state.portRange.label).toBe('22000-22099'); - expect(runner.options.webPort).toBe(3000); - expect(runner.options.apiPort).toBe(22001); - expect(runner.options.spacetimePort).toBe(22002); - expect(runner.options.adminWebPort).toBe(22003); - } finally { - rmSync(tempDir, {recursive: true, force: true}); - } - }); + await runner.prepareLinuxPortRange(command); + expect(runner.state.portRange.label).toBe('22000-22099'); + expect(runner.options.webPort).toBe(3000); + expect(runner.options.apiPort).toBe(22001); + expect(runner.options.spacetimePort).toBe(22002); + expect(runner.options.adminWebPort).toBe(22003); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }, + ); test('Windows 仍沿用原有端口解析,不启用 Linux 端口段登记', async () => { - const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); + const originalPlatform = Object.getOwnPropertyDescriptor( + process, + 'platform', + ); Object.defineProperty(process, 'platform', { configurable: true, value: 'win32', }); try { - const {command, explicitOptions, options} = parseArgs([], { + const { command, explicitOptions, options } = parseArgs([], { USER: 'alice', GENARRATIVE_DEV_PORT_RANGE: '22000-22099', }); - const runner = new DevRunner(options, { - USER: 'alice', - GENARRATIVE_DEV_PORT_RANGE: '22000-22099', - }, explicitOptions); + const runner = new DevRunner( + options, + { + USER: 'alice', + GENARRATIVE_DEV_PORT_RANGE: '22000-22099', + }, + explicitOptions, + ); await runner.prepareLinuxPortRange(command); @@ -201,29 +231,44 @@ describe('dev scheduler argument routing', () => { } } }); + + test('本地 SpacetimeDB 信任与当前 CLI 一致的 env token', () => { + expect( + shouldTrustExistingSpacetimeToken( + 'owner-cli-token', + 'http://127.0.0.1:3101', + { + env: {}, + resolveCliToken: () => 'owner-cli-token', + }, + ), + ).toBe(true); + }); }); describe('dev scheduler api-server env', () => { test('dev 脚本默认打开密码入口自动注册', () => { - const {options} = parseArgs(['api-server', '--api-port', '9091'], {}); + const { options } = parseArgs(['api-server', '--api-port', '9091'], {}); const env = buildApiServerProcessEnv({ baseEnv: {}, options, - state: {spacetimeServer: 'http://127.0.0.1:3199'}, + state: { spacetimeServer: 'http://127.0.0.1:3199' }, }); - expect(env.GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED).toBe('true'); + expect(env.GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED).toBe( + 'true', + ); expect(env.GENARRATIVE_PROCESS_ROLE).toBe('all'); expect(env.GENARRATIVE_API_PORT).toBe('9091'); expect(env.GENARRATIVE_SPACETIME_SERVER_URL).toBe('http://127.0.0.1:3199'); }); test('dev 脚本保留显式 api-server 进程角色', () => { - const {options} = parseArgs(['api-server'], {}); + const { options } = parseArgs(['api-server'], {}); const env = buildApiServerProcessEnv({ - baseEnv: {GENARRATIVE_PROCESS_ROLE: 'api'}, + baseEnv: { GENARRATIVE_PROCESS_ROLE: 'api' }, options, - state: {spacetimeServer: 'http://127.0.0.1:3199'}, + state: { spacetimeServer: 'http://127.0.0.1:3199' }, }); expect(env.GENARRATIVE_PROCESS_ROLE).toBe('api'); @@ -235,15 +280,15 @@ describe('dev scheduler api-server env', () => { const binDir = join(tempDir, 'Genarrative', 'ffmpeg', 'bin'); const ffmpegPath = join(binDir, 'ffmpeg.exe'); const ffprobePath = join(binDir, 'ffprobe.exe'); - mkdirSync(binDir, {recursive: true}); + mkdirSync(binDir, { recursive: true }); writeFileSync(ffmpegPath, '', 'utf8'); writeFileSync(ffprobePath, '', 'utf8'); - const {options} = parseArgs(['api-server'], {}); + const { options } = parseArgs(['api-server'], {}); const env = buildApiServerProcessEnv({ - baseEnv: {LOCALAPPDATA: tempDir, Path: 'C:\\Windows\\System32'}, + baseEnv: { LOCALAPPDATA: tempDir, Path: 'C:\\Windows\\System32' }, options, - state: {spacetimeServer: 'http://127.0.0.1:3199'}, + state: { spacetimeServer: 'http://127.0.0.1:3199' }, platform: 'win32', }); @@ -252,7 +297,7 @@ describe('dev scheduler api-server env', () => { expect(env.GENARRATIVE_CHARACTER_ANIMATION_FFMPEG_PATH).toBe(ffmpegPath); expect(env.Path?.split(';')[0]).toBe(binDir); } finally { - rmSync(tempDir, {recursive: true, force: true}); + rmSync(tempDir, { recursive: true, force: true }); } }); @@ -260,11 +305,11 @@ describe('dev scheduler api-server env', () => { const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-ffmpeg-')); try { const binDir = join(tempDir, 'Genarrative', 'ffmpeg', 'bin'); - mkdirSync(binDir, {recursive: true}); + mkdirSync(binDir, { recursive: true }); writeFileSync(join(binDir, 'ffmpeg.exe'), '', 'utf8'); writeFileSync(join(binDir, 'ffprobe.exe'), '', 'utf8'); - const {options} = parseArgs(['api-server'], {}); + const { options } = parseArgs(['api-server'], {}); const env = buildApiServerProcessEnv({ baseEnv: { LOCALAPPDATA: tempDir, @@ -272,14 +317,16 @@ describe('dev scheduler api-server env', () => { CHARACTER_ANIMATION_FFPROBE_PATH: 'D:\\tools\\ffprobe.exe', }, options, - state: {spacetimeServer: 'http://127.0.0.1:3199'}, + state: { spacetimeServer: 'http://127.0.0.1:3199' }, platform: 'win32', }); expect(env.CHARACTER_ANIMATION_FFMPEG_PATH).toBe('D:\\tools\\ffmpeg.exe'); - expect(env.CHARACTER_ANIMATION_FFPROBE_PATH).toBe('D:\\tools\\ffprobe.exe'); + expect(env.CHARACTER_ANIMATION_FFPROBE_PATH).toBe( + 'D:\\tools\\ffprobe.exe', + ); } finally { - rmSync(tempDir, {recursive: true, force: true}); + rmSync(tempDir, { recursive: true, force: true }); } }); }); @@ -352,7 +399,7 @@ describe('dev scheduler Rust build env', () => { RUSTC_WRAPPER: '/usr/bin/sccache', CARGO_BUILD_RUSTC_WRAPPER: 'sccache', }, - {log: false}, + { log: false }, ); expect(env.RUSTC_WRAPPER).not.toBe('/usr/bin/sccache'); @@ -366,7 +413,7 @@ describe('dev scheduler Rust build env', () => { RUSTC_WRAPPER: 'custom-wrapper', CARGO_BUILD_RUSTC_WRAPPER: 'sccache', }, - {log: false}, + { log: false }, ); expect(env.RUSTC_WRAPPER).toBe('custom-wrapper'); @@ -374,7 +421,10 @@ describe('dev scheduler Rust build env', () => { }); test('Windows 下本地 dev Rust env 用空 wrapper 覆盖项目 sccache', () => { - const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); + const originalPlatform = Object.getOwnPropertyDescriptor( + process, + 'platform', + ); Object.defineProperty(process, 'platform', { configurable: true, value: 'win32', @@ -386,7 +436,7 @@ describe('dev scheduler Rust build env', () => { RUSTC_WRAPPER: 'sccache', CARGO_BUILD_RUSTC_WRAPPER: 'sccache', }, - {log: false}, + { log: false }, ); expect(env.RUSTC_WRAPPER).toBe(''); @@ -432,7 +482,7 @@ describe('dev scheduler stack state file', () => { [ 'web', { - child: {pid: 4321}, + child: { pid: 4321 }, runtime: { status: 'running', pid: 4321, @@ -476,10 +526,16 @@ describe('dev scheduler spacetime reuse guard', () => { test('记录 URL 可 ping 但没有 spacetime.pid 时不复用宿主', async () => { const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-spacetime-reuse-')); try { - writeFileSync(join(tempDir, 'dev-spacetime-url'), 'http://127.0.0.1:3199\n', 'utf8'); - globalThis.fetch = vi.fn(async () => ({status: 200})) as unknown as typeof fetch; + writeFileSync( + join(tempDir, 'dev-spacetime-url'), + 'http://127.0.0.1:3199\n', + 'utf8', + ); + globalThis.fetch = vi.fn(async () => ({ + status: 200, + })) as unknown as typeof fetch; - const {command, explicitOptions, options} = parseArgs( + const { command, explicitOptions, options } = parseArgs( ['--spacetime-data-dir', tempDir], {}, ); @@ -490,23 +546,29 @@ describe('dev scheduler spacetime reuse guard', () => { expect(runner.state.spacetimeReused).toBeUndefined(); expect(runner.state.spacetimeServer).toBe('http://127.0.0.1:3101'); } finally { - rmSync(tempDir, {recursive: true, force: true}); + rmSync(tempDir, { recursive: true, force: true }); } }); test('记录 URL 可 ping 且 spacetime.pid 存活时复用宿主', async () => { const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-spacetime-reuse-')); try { - writeFileSync(join(tempDir, 'dev-spacetime-url'), 'http://127.0.0.1:3199\n', 'utf8'); + writeFileSync( + join(tempDir, 'dev-spacetime-url'), + 'http://127.0.0.1:3199\n', + 'utf8', + ); writeFileSync(join(tempDir, 'spacetime.pid'), `${process.pid}\n`, 'utf8'); writeFileSync( join(tempDir, 'dev-spacetime-tool-version'), `${workspaceSpacetimeVersionForTest()}\n`, 'utf8', ); - globalThis.fetch = vi.fn(async () => ({status: 200})) as unknown as typeof fetch; + globalThis.fetch = vi.fn(async () => ({ + status: 200, + })) as unknown as typeof fetch; - const {command, explicitOptions, options} = parseArgs( + const { command, explicitOptions, options } = parseArgs( ['--spacetime-data-dir', tempDir], {}, ); @@ -518,7 +580,7 @@ describe('dev scheduler spacetime reuse guard', () => { expect(runner.state.spacetimeServer).toBe('http://127.0.0.1:3199'); expect(runner.options.spacetimePort).toBe(3199); } finally { - rmSync(tempDir, {recursive: true, force: true}); + rmSync(tempDir, { recursive: true, force: true }); } }); @@ -531,9 +593,11 @@ describe('dev scheduler spacetime reuse guard', () => { `${workspaceSpacetimeVersionForTest()}\n`, 'utf8', ); - globalThis.fetch = vi.fn(async () => ({status: 200})) as unknown as typeof fetch; + globalThis.fetch = vi.fn(async () => ({ + status: 200, + })) as unknown as typeof fetch; - const {command, explicitOptions, options} = parseArgs( + const { command, explicitOptions, options } = parseArgs( ['--spacetime-data-dir', tempDir, '--spacetime-port', '3198'], {}, ); @@ -548,7 +612,7 @@ describe('dev scheduler spacetime reuse guard', () => { expect.any(Object), ); } finally { - rmSync(tempDir, {recursive: true, force: true}); + rmSync(tempDir, { recursive: true, force: true }); } }); @@ -556,9 +620,11 @@ describe('dev scheduler spacetime reuse guard', () => { const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-spacetime-reuse-')); try { writeFileSync(join(tempDir, 'spacetime.pid'), `${process.pid}\n`, 'utf8'); - globalThis.fetch = vi.fn(async () => ({status: 503})) as unknown as typeof fetch; + globalThis.fetch = vi.fn(async () => ({ + status: 503, + })) as unknown as typeof fetch; - const {command, explicitOptions, options} = parseArgs( + const { command, explicitOptions, options } = parseArgs( ['--spacetime-data-dir', tempDir, '--spacetime-port', '3198'], {}, ); @@ -569,17 +635,20 @@ describe('dev scheduler spacetime reuse guard', () => { ); expect(runner.state.spacetimeReused).toBeUndefined(); } finally { - rmSync(tempDir, {recursive: true, force: true}); + rmSync(tempDir, { recursive: true, force: true }); } }); }); describe('dev scheduler interactive input', () => { test('前端 dev server 不继承 stdin,避免吞掉 rs 重启命令', () => { - const options = createDevServerSpawnOptions({cwd: repoRootForTest(), env: {A: 'B'}}); + const options = createDevServerSpawnOptions({ + cwd: repoRootForTest(), + env: { A: 'B' }, + }); expect(options.stdio).toEqual(['ignore', 'pipe', 'pipe']); - expect(options.env).toEqual({A: 'B'}); + expect(options.env).toEqual({ A: 'B' }); }); }); @@ -602,16 +671,25 @@ describe('dev scheduler watch routing', () => { }; expect( - shouldAcceptWatchEvent(config, join(process.cwd(), 'apps/admin-web/src/App.tsx')), + shouldAcceptWatchEvent( + config, + join(process.cwd(), 'apps/admin-web/src/App.tsx'), + ), ).toBe(true); expect( shouldAcceptWatchEvent( config, - join(process.cwd(), 'apps/admin-web/node_modules/.vite/deps/_metadata.json'), + join( + process.cwd(), + 'apps/admin-web/node_modules/.vite/deps/_metadata.json', + ), ), ).toBe(false); expect( - shouldAcceptWatchEvent(config, join(process.cwd(), 'apps/admin-web/dist/assets/app.js')), + shouldAcceptWatchEvent( + config, + join(process.cwd(), 'apps/admin-web/dist/assets/app.js'), + ), ).toBe(false); }); }); @@ -641,9 +719,15 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0; }); test('复用本地 SpacetimeDB standalone 前校验启动时版本记录', () => { - const tempDir = mkdtempSync(join(tmpdir(), 'genarrative-spacetime-version-')); + const tempDir = mkdtempSync( + join(tmpdir(), 'genarrative-spacetime-version-'), + ); try { - writeFileSync(join(tempDir, 'dev-spacetime-tool-version'), '2.1.0\n', 'utf8'); + writeFileSync( + join(tempDir, 'dev-spacetime-tool-version'), + '2.1.0\n', + 'utf8', + ); expect(() => assertReusableSpacetimeProcessVersionMatchesWorkspace({ @@ -652,7 +736,7 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0; }), ).toThrow('SpacetimeDB procedure 调用超时'); } finally { - rmSync(tempDir, {recursive: true, force: true}); + rmSync(tempDir, { recursive: true, force: true }); } }); @@ -662,7 +746,11 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0; ); expect(isSpacetimePublishPermissionError(error)).toBe(true); - expect(isSpacetimePublishPermissionError(new Error('No database target matches'))).toBe(false); + expect( + isSpacetimePublishPermissionError( + new Error('No database target matches'), + ), + ).toBe(false); }); test('发布 spacetime-module 时忽略 spacetime.json 以免覆盖显式数据库', () => { @@ -685,11 +773,11 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0; }); test('手动刷新 spacetime 只重新发布模块,不重启 standalone 进程', async () => { - const {explicitOptions, options} = parseArgs([], {}); + const { explicitOptions, options } = parseArgs([], {}); const runner = new DevRunner(options, {}, explicitOptions); const restart = vi.fn(); - runner.services.set('spacetime', {restart}); + runner.services.set('spacetime', { restart }); runner.waitForSpacetime = vi.fn(async () => {}); runner.publishSpacetimeModule = vi.fn(async () => {}); @@ -701,11 +789,11 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0; }); test('skip-publish 时 spacetime 刷新不会重启或发布', async () => { - const {explicitOptions, options} = parseArgs(['--skip-publish'], {}); + const { explicitOptions, options } = parseArgs(['--skip-publish'], {}); const runner = new DevRunner(options, {}, explicitOptions); const restart = vi.fn(); - runner.services.set('spacetime', {restart}); + runner.services.set('spacetime', { restart }); runner.waitForSpacetime = vi.fn(async () => {}); runner.publishSpacetimeModule = vi.fn(async () => {}); @@ -717,7 +805,7 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0; }); test('启动 api-server 前为空 token 自动创建本地 Web identity', async () => { - const {explicitOptions, options} = parseArgs([], { + const { explicitOptions, options } = parseArgs([], { GENARRATIVE_SPACETIME_TOKEN: '', }); const runner = new DevRunner(options, {}, explicitOptions); @@ -747,10 +835,14 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0; const originalToken = process.env.GENARRATIVE_SPACETIME_TOKEN; delete process.env.GENARRATIVE_SPACETIME_TOKEN; try { - const {explicitOptions, options} = parseArgs([], { + const { explicitOptions, options } = parseArgs([], { GENARRATIVE_SPACETIME_TOKEN: 'stale-env-file-token', }); - const runner = new DevRunner(options, {GENARRATIVE_SPACETIME_TOKEN: 'stale-env-file-token'}, explicitOptions); + const runner = new DevRunner( + options, + { GENARRATIVE_SPACETIME_TOKEN: 'stale-env-file-token' }, + explicitOptions, + ); runner.state.spacetimeServer = 'http://127.0.0.1:3101'; globalThis.fetch = vi.fn(async () => ({ ok: true, @@ -764,7 +856,9 @@ spacetimedb tool version 2.5.0; spacetimedb-lib version 2.5.0; await runner.ensureApiServerSpacetimeToken(); - expect(runner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBe('fresh-web-token'); + expect(runner.baseEnv.GENARRATIVE_SPACETIME_TOKEN).toBe( + 'fresh-web-token', + ); } finally { if (originalToken === undefined) { delete process.env.GENARRATIVE_SPACETIME_TOKEN; diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index 67ced6ea9..d451c441f 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -3388,6 +3388,170 @@ mod tests { ); } + #[tokio::test] + async fn wechat_bind_start_binds_oauth_identity_to_current_phone_user() { + let config = AppConfig { + sms_auth_enabled: true, + wechat_auth_enabled: true, + ..AppConfig::default() + }; + let app = build_router(AppState::new(config).expect("state should build")); + + let phone_send_response = app + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/auth/phone/send-code") + .header("content-type", "application/json") + .body(Body::from( + serde_json::json!({ + "phone": "13800138000", + "scene": "login" + }) + .to_string(), + )) + .expect("phone send request should build"), + ) + .await + .expect("phone send request should succeed"); + assert_eq!(phone_send_response.status(), StatusCode::OK); + + let phone_login_response = app + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/auth/phone/login") + .header("content-type", "application/json") + .body(Body::from( + serde_json::json!({ + "phone": "13800138000", + "code": "123456" + }) + .to_string(), + )) + .expect("phone login request should build"), + ) + .await + .expect("phone login request should succeed"); + let phone_login_body = phone_login_response + .into_body() + .collect() + .await + .expect("phone login body should collect") + .to_bytes(); + let phone_login_payload: Value = + serde_json::from_slice(&phone_login_body).expect("phone login payload should be json"); + let phone_user_id = phone_login_payload["user"]["id"].clone(); + let phone_token = phone_login_payload["token"] + .as_str() + .expect("phone token should exist"); + + let bind_start_response = app + .clone() + .oneshot( + Request::builder() + .uri("/api/auth/wechat/bind-start?redirectPath=%2Fplay%3FclientRuntime%3Dwechat_h5") + .header("authorization", format!("Bearer {phone_token}")) + .header( + "user-agent", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148 MicroMessenger/8.0.50", + ) + .header("host", "localhost:3000") + .body(Body::empty()) + .expect("wechat bind start request should build"), + ) + .await + .expect("wechat bind start should succeed"); + assert_eq!(bind_start_response.status(), StatusCode::OK); + let bind_start_body = bind_start_response + .into_body() + .collect() + .await + .expect("wechat bind start body should collect") + .to_bytes(); + let bind_start_payload: Value = serde_json::from_slice(&bind_start_body) + .expect("wechat bind start payload should be json"); + let authorization_url = bind_start_payload["authorizationUrl"] + .as_str() + .expect("wechat bind authorization url should exist"); + let callback_state = url::Url::parse(authorization_url) + .expect("authorization url should be valid") + .query_pairs() + .find(|(key, _)| key == "state") + .map(|(_, value)| value.into_owned()) + .expect("state should exist"); + + let callback_response = app + .clone() + .oneshot( + Request::builder() + .uri(format!( + "/api/auth/wechat/callback?state={callback_state}&mock_code=wx-bind-code" + )) + .header( + "user-agent", + "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148 MicroMessenger/8.0.50", + ) + .header("host", "localhost:3000") + .body(Body::empty()) + .expect("wechat callback request should build"), + ) + .await + .expect("wechat callback request should succeed"); + assert_eq!(callback_response.status(), StatusCode::SEE_OTHER); + let location = callback_response + .headers() + .get("location") + .and_then(|value| value.to_str().ok()) + .expect("redirect location should exist"); + assert!(location.starts_with("/play?clientRuntime=wechat_h5#")); + assert!(location.contains("auth_provider=wechat")); + assert!(location.contains("auth_binding_status=active")); + + let auth_hash = location + .split('#') + .nth(1) + .expect("hash fragment should exist"); + let auth_params = url::form_urlencoded::parse(auth_hash.as_bytes()) + .into_owned() + .collect::>(); + let bound_token = auth_params + .get("auth_token") + .expect("bound auth token should exist"); + + let me_response = app + .oneshot( + Request::builder() + .uri("/api/auth/me") + .header("authorization", format!("Bearer {bound_token}")) + .body(Body::empty()) + .expect("auth me request should build"), + ) + .await + .expect("auth me request should succeed"); + assert_eq!(me_response.status(), StatusCode::OK); + let me_body = me_response + .into_body() + .collect() + .await + .expect("auth me body should collect") + .to_bytes(); + let me_payload: Value = + serde_json::from_slice(&me_body).expect("auth me payload should be json"); + assert_eq!(me_payload["user"]["id"], phone_user_id); + assert_eq!( + me_payload["user"]["loginMethod"], + Value::String("phone".to_string()) + ); + assert_eq!(me_payload["user"]["wechatBound"], Value::Bool(true)); + assert_eq!( + me_payload["user"]["wechatAccount"], + Value::String("wx-bind-code".to_string()) + ); + } + #[tokio::test] async fn auth_sessions_returns_multi_device_session_summaries() { let state = AppState::new(AppConfig::default()).expect("state should build"); diff --git a/server-rs/crates/api-server/src/main.rs b/server-rs/crates/api-server/src/main.rs index b64b8bf79..b281584d0 100644 --- a/server-rs/crates/api-server/src/main.rs +++ b/server-rs/crates/api-server/src/main.rs @@ -75,6 +75,7 @@ mod phone_auth; mod platform_errors; mod process_metrics; mod profile_identity; +mod profile_recharge_expiration_worker; mod prompt; mod public_work; mod puzzle; @@ -131,6 +132,7 @@ use crate::{ config::{AppConfig, ProcessRole}, external_generation_worker::run_external_generation_worker, external_generation_worker_controller::run_external_generation_worker_controller, + profile_recharge_expiration_worker::spawn_profile_recharge_expiration_worker, state::{AppState, AppStateInitError}, tracking_outbox::TrackingOutbox, wallet_refund_outbox::WalletRefundOutbox, @@ -408,6 +410,7 @@ async fn finalize_shutdown(context: ShutdownContext) { fn spawn_app_state_background_workers(state: &AppState) { state.puzzle_gallery_cache().spawn_cleanup_task(); + spawn_profile_recharge_expiration_worker(state.clone()); if let Some(outbox) = state.tracking_outbox() { outbox.spawn_worker(); } diff --git a/server-rs/crates/api-server/src/modules/auth.rs b/server-rs/crates/api-server/src/modules/auth.rs index 5cb67df28..fecbf4fe0 100644 --- a/server-rs/crates/api-server/src/modules/auth.rs +++ b/server-rs/crates/api-server/src/modules/auth.rs @@ -17,7 +17,8 @@ use crate::{ refresh_session::refresh_session, state::AppState, wechat::auth::{ - bind_wechat_phone, handle_wechat_callback, login_wechat_mini_program, start_wechat_login, + bind_wechat_phone, handle_wechat_callback, login_wechat_mini_program, start_wechat_bind, + start_wechat_login, }, }; @@ -72,6 +73,13 @@ pub fn router(state: AppState) -> Router { .route("/api/auth/phone/send-code", post(send_phone_code)) .route("/api/auth/phone/login", post(phone_login)) .route("/api/auth/wechat/start", get(start_wechat_login)) + .route( + "/api/auth/wechat/bind-start", + get(start_wechat_bind).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) .route("/api/auth/wechat/callback", get(handle_wechat_callback)) .route( "/api/auth/wechat/miniprogram-login", diff --git a/server-rs/crates/api-server/src/profile_recharge_expiration_worker.rs b/server-rs/crates/api-server/src/profile_recharge_expiration_worker.rs new file mode 100644 index 000000000..617e70e17 --- /dev/null +++ b/server-rs/crates/api-server/src/profile_recharge_expiration_worker.rs @@ -0,0 +1,207 @@ +use std::{process, time::Duration}; + +use module_runtime::RuntimeProfileRechargeOrderExpirationScheduleSnapshot; +use platform_wechat::pay::WechatPayError; +use shared_kernel::offset_datetime_to_unix_micros; +use time::OffsetDateTime; +use tokio::time::MissedTickBehavior; +use tracing::{debug, info, warn}; + +use crate::state::AppState; + +const PROFILE_RECHARGE_EXPIRATION_WORKER_INTERVAL: Duration = Duration::from_secs(15); +const PROFILE_RECHARGE_EXPIRATION_WORKER_LEASE: Duration = Duration::from_secs(90); +const PROFILE_RECHARGE_EXPIRATION_WORKER_CLAIM_LIMIT: u32 = 20; + +pub fn spawn_profile_recharge_expiration_worker(state: AppState) { + tokio::spawn(async move { + run_profile_recharge_expiration_worker(state).await; + }); +} + +async fn run_profile_recharge_expiration_worker(state: AppState) { + let worker_id = format!("api:{}:profile-recharge-expiration", process::id()); + let mut interval = tokio::time::interval(PROFILE_RECHARGE_EXPIRATION_WORKER_INTERVAL); + interval.set_missed_tick_behavior(MissedTickBehavior::Delay); + + loop { + interval.tick().await; + let now_micros = current_unix_micros(); + let lease_expires_at_micros = now_micros + + i64::try_from(PROFILE_RECHARGE_EXPIRATION_WORKER_LEASE.as_micros()) + .unwrap_or(90_000_000); + let schedules = match state + .spacetime_client() + .claim_profile_recharge_order_expiration_schedules( + worker_id.clone(), + now_micros, + lease_expires_at_micros, + PROFILE_RECHARGE_EXPIRATION_WORKER_CLAIM_LIMIT, + ) + .await + { + Ok(schedules) => schedules, + Err(error) => { + warn!("充值订单过期检查 claim 失败:{error}"); + continue; + } + }; + + if schedules.is_empty() { + debug!("充值订单过期检查暂无到期任务"); + continue; + } + + for schedule in schedules { + process_profile_recharge_expiration_schedule(&state, schedule).await; + } + } +} + +async fn process_profile_recharge_expiration_schedule( + state: &AppState, + schedule: RuntimeProfileRechargeOrderExpirationScheduleSnapshot, +) { + let order_id = schedule.order_id.clone(); + let wechat_order = match state + .wechat_pay_client() + .query_order_by_out_trade_no(&order_id) + .await + { + Ok(order) => order, + Err(WechatPayError::OrderNotExist(message)) => { + warn!( + order_id = order_id.as_str(), + error = message.as_str(), + "过期前微信查单确认订单不存在,关闭本地 pending 订单" + ); + close_order_not_found_profile_recharge_order(state, &order_id).await; + return; + } + Err(error) => { + warn!( + order_id = order_id.as_str(), + "过期前微信查单失败,将等待租约过期后重试:{error}" + ); + return; + } + }; + + if wechat_order.out_trade_no != order_id { + warn!( + order_id = order_id.as_str(), + provider_order_id = wechat_order.out_trade_no.as_str(), + "过期前微信查单返回的商户订单号不匹配,将等待租约过期后重试" + ); + return; + } + + match wechat_order.trade_state.as_str() { + "SUCCESS" => { + let paid_at_micros = wechat_order + .success_time + .as_deref() + .and_then(|value| shared_kernel::parse_rfc3339(value).ok()) + .map(offset_datetime_to_unix_micros) + .unwrap_or_else(current_unix_micros); + match state + .spacetime_client() + .mark_profile_recharge_order_paid( + order_id.clone(), + paid_at_micros, + wechat_order.transaction_id, + ) + .await + { + Ok(_) => { + complete_profile_recharge_expiration_schedule(state, &order_id).await; + state.publish_profile_recharge_order_update(order_id.clone()); + info!( + order_id = order_id.as_str(), + "过期检查发现微信已支付,已补确认入账" + ); + } + Err(error) => { + warn!( + order_id = order_id.as_str(), + "过期检查确认已支付订单失败:{error}" + ); + } + } + } + "NOTPAY" | "CLOSED" | "REVOKED" | "PAYERROR" => { + match state + .spacetime_client() + .close_profile_recharge_order(order_id.clone(), current_unix_micros()) + .await + { + Ok(_) => { + complete_profile_recharge_expiration_schedule(state, &order_id).await; + state.publish_profile_recharge_order_update(order_id.clone()); + info!( + order_id = order_id.as_str(), + trade_state = wechat_order.trade_state.as_str(), + "充值订单到期且微信确认未支付,已关闭本地订单" + ); + } + Err(error) => { + warn!( + order_id = order_id.as_str(), + "关闭过期充值订单失败:{error}" + ); + } + } + } + "USERPAYING" => { + info!( + order_id = order_id.as_str(), + "微信订单仍在支付中,将等待租约过期后重试" + ); + } + trade_state => { + warn!( + order_id = order_id.as_str(), + trade_state, "微信订单状态暂不适合本地关闭,将等待租约过期后重试" + ); + } + } +} + +async fn close_order_not_found_profile_recharge_order(state: &AppState, order_id: &str) { + match state + .spacetime_client() + .close_profile_recharge_order(order_id.to_string(), current_unix_micros()) + .await + { + Ok(_) => { + complete_profile_recharge_expiration_schedule(state, order_id).await; + state.publish_profile_recharge_order_update(order_id.to_string()); + info!( + order_id, + reason = "ORDER_NOT_EXIST", + "过期检查发现微信订单不存在,已关闭本地充值订单" + ); + } + Err(error) => { + warn!( + order_id, + reason = "ORDER_NOT_EXIST", + "关闭微信不存在的过期充值订单失败:{error}" + ); + } + } +} + +async fn complete_profile_recharge_expiration_schedule(state: &AppState, order_id: &str) { + if let Err(error) = state + .spacetime_client() + .complete_profile_recharge_order_expiration_schedule(order_id.to_string()) + .await + { + warn!(order_id, "删除充值订单过期检查任务失败:{error}"); + } +} + +fn current_unix_micros() -> i64 { + offset_datetime_to_unix_micros(OffsetDateTime::now_utc()) +} diff --git a/server-rs/crates/api-server/src/runtime_profile.rs b/server-rs/crates/api-server/src/runtime_profile.rs index 5bd8a8d5b..34128d3c7 100644 --- a/server-rs/crates/api-server/src/runtime_profile.rs +++ b/server-rs/crates/api-server/src/runtime_profile.rs @@ -10,7 +10,7 @@ use axum::{ use hmac::{Hmac, Mac}; use module_runtime::{ AnalyticsGranularity, PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK, - PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5, + PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_JSAPI, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE, RuntimeProfileCodeOperationRecord, @@ -240,6 +240,7 @@ pub async fn create_profile_recharge_order( let wechat_mini_program_pay_params = if payment_channel == PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM + || payment_channel == PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_JSAPI { let identity = resolve_wechat_identity_for_payment(&state, &order.user_id) .await @@ -407,6 +408,7 @@ pub async fn confirm_wechat_profile_recharge_order( map_runtime_profile_client_error(error), ) })?; + state.publish_profile_recharge_order_update(order.order_id.clone()); Ok(json_success_body( Some(&request_context), @@ -1305,18 +1307,43 @@ fn validate_recharge_device_for_payment_channel( PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL => { claims.is_wechat_mini_program_device() } - PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5 => claims.is_mobile_wechat_browser_device(), - PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE => claims.is_desktop_wechat_browser_device(), + PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_JSAPI => is_web_recharge_device(claims), + PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5 => is_web_recharge_device(claims), + PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE => is_web_recharge_device(claims), _ => false, }; if is_supported_device { return Ok(()); } + tracing::warn!( + payment_channel = payment_channel, + client_type = claims.client_type().unwrap_or(""), + client_platform = claims.client_platform().unwrap_or(""), + is_mobile_device = claims.is_mobile_device(), + is_desktop_device = claims.is_desktop_device(), + is_wechat_mini_program_device = claims.is_wechat_mini_program_device(), + "profile recharge payment channel rejected for current login device" + ); + Err(AppError::from_status(StatusCode::FORBIDDEN) .with_message("当前登录设备不支持充值,请在微信环境内登录后重试")) } +fn is_web_recharge_device(claims: &platform_auth::AccessTokenClaims) -> bool { + is_web_recharge_client_type(claims) || is_legacy_web_recharge_device(claims) +} + +fn is_web_recharge_client_type(claims: &platform_auth::AccessTokenClaims) -> bool { + matches!(claims.client_type(), Some("wechat_h5" | "web_browser")) +} + +fn is_legacy_web_recharge_device(claims: &platform_auth::AccessTokenClaims) -> bool { + let legacy_or_unknown_platform = + matches!(claims.client_platform(), None | Some("unknown" | "test")); + legacy_or_unknown_platform && matches!(claims.client_type(), None | Some("web_browser")) +} + fn validate_real_wechat_recharge_payment_provider(state: &AppState) -> Result<(), AppError> { if !state.config.wechat_pay_enabled { return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE) @@ -1353,6 +1380,7 @@ fn is_wechat_recharge_payment_channel(payment_channel: &str) -> bool { payment_channel, PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM | PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL + | PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_JSAPI | PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5 | PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE ) @@ -2352,7 +2380,7 @@ mod tests { } #[tokio::test] - async fn profile_recharge_order_rejects_non_wechat_device_before_spacetime() { + async fn profile_recharge_order_rejects_non_web_device_before_spacetime() { let state = seed_authenticated_state_with_config(AppConfig { wechat_pay_enabled: true, wechat_pay_provider: "mock".to_string(), @@ -2360,7 +2388,13 @@ mod tests { ..AppConfig::default() }) .await; - let token = issue_access_token(&state); + let token = issue_device_access_token( + &state, + "native_app", + "native_app", + "windows", + "sess_runtime_profile_native_app", + ); let app = build_router(state); let response = app @@ -2394,7 +2428,93 @@ mod tests { } #[tokio::test] - async fn profile_recharge_order_rejects_mismatched_wechat_channel_before_spacetime() { + async fn profile_recharge_order_allows_legacy_web_session_before_provider_check() { + let state = seed_authenticated_state_with_config(AppConfig { + wechat_pay_enabled: true, + wechat_pay_provider: "mock".to_string(), + spacetime_procedure_timeout: Duration::from_secs(1), + ..AppConfig::default() + }) + .await; + let token = issue_access_token(&state); + let app = build_router(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/api/profile/recharge/orders") + .header("authorization", format!("Bearer {token}")) + .header("content-type", "application/json") + .body(Body::from( + r#"{"productId":"points_60","paymentChannel":"wechat_native"}"#, + )) + .expect("request should build"), + ) + .await + .expect("request should succeed"); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + let body = response + .into_body() + .collect() + .await + .expect("body should collect") + .to_bytes(); + let payload: Value = + serde_json::from_slice(&body).expect("response body should be valid json"); + assert_eq!( + payload["error"]["message"], + "真实微信支付渠道不能使用 mock 支付配置" + ); + } + + #[tokio::test] + async fn profile_recharge_order_allows_legacy_web_unknown_platform_before_provider_check() { + let state = seed_authenticated_state_with_config(AppConfig { + wechat_pay_enabled: true, + wechat_pay_provider: "mock".to_string(), + spacetime_procedure_timeout: Duration::from_secs(1), + ..AppConfig::default() + }) + .await; + let token = + issue_web_browser_access_token(&state, "test", "sess_runtime_profile_legacy_web"); + let app = build_router(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/api/profile/recharge/orders") + .header("authorization", format!("Bearer {token}")) + .header("content-type", "application/json") + .body(Body::from( + r#"{"productId":"points_60","paymentChannel":"wechat_native"}"#, + )) + .expect("request should build"), + ) + .await + .expect("request should succeed"); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + let body = response + .into_body() + .collect() + .await + .expect("body should collect") + .to_bytes(); + let payload: Value = + serde_json::from_slice(&body).expect("response body should be valid json"); + assert_eq!( + payload["error"]["message"], + "真实微信支付渠道不能使用 mock 支付配置" + ); + } + + #[tokio::test] + async fn profile_recharge_order_rejects_web_device_using_mini_program_channel_before_spacetime() + { let state = seed_authenticated_state_with_config(AppConfig { wechat_pay_enabled: true, wechat_pay_provider: "mock".to_string(), @@ -2413,7 +2533,7 @@ mod tests { .header("authorization", format!("Bearer {token}")) .header("content-type", "application/json") .body(Body::from( - r#"{"productId":"points_60","paymentChannel":"wechat_native"}"#, + r#"{"productId":"points_60","paymentChannel":"wechat_mp_virtual"}"#, )) .expect("request should build"), ) @@ -2478,6 +2598,181 @@ mod tests { ); } + #[tokio::test] + async fn profile_recharge_order_allows_mobile_web_h5_channel_before_provider_check() { + let state = seed_authenticated_state_with_config(AppConfig { + wechat_pay_enabled: true, + wechat_pay_provider: "mock".to_string(), + spacetime_procedure_timeout: Duration::from_secs(1), + ..AppConfig::default() + }) + .await; + let token = + issue_web_browser_access_token(&state, "ios", "sess_runtime_profile_mobile_web"); + let app = build_router(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/api/profile/recharge/orders") + .header("authorization", format!("Bearer {token}")) + .header("content-type", "application/json") + .body(Body::from( + r#"{"productId":"points_60","paymentChannel":"wechat_h5"}"#, + )) + .expect("request should build"), + ) + .await + .expect("request should succeed"); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + let body = response + .into_body() + .collect() + .await + .expect("body should collect") + .to_bytes(); + let payload: Value = + serde_json::from_slice(&body).expect("response body should be valid json"); + assert_eq!( + payload["error"]["message"], + "真实微信支付渠道不能使用 mock 支付配置" + ); + } + + #[tokio::test] + async fn profile_recharge_order_allows_desktop_web_native_channel_before_provider_check() { + let state = seed_authenticated_state_with_config(AppConfig { + wechat_pay_enabled: true, + wechat_pay_provider: "mock".to_string(), + spacetime_procedure_timeout: Duration::from_secs(1), + ..AppConfig::default() + }) + .await; + let token = + issue_web_browser_access_token(&state, "windows", "sess_runtime_profile_desktop_web"); + let app = build_router(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/api/profile/recharge/orders") + .header("authorization", format!("Bearer {token}")) + .header("content-type", "application/json") + .body(Body::from( + r#"{"productId":"points_60","paymentChannel":"wechat_native"}"#, + )) + .expect("request should build"), + ) + .await + .expect("request should succeed"); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + let body = response + .into_body() + .collect() + .await + .expect("body should collect") + .to_bytes(); + let payload: Value = + serde_json::from_slice(&body).expect("response body should be valid json"); + assert_eq!( + payload["error"]["message"], + "真实微信支付渠道不能使用 mock 支付配置" + ); + } + + #[tokio::test] + async fn profile_recharge_order_allows_mobile_web_native_channel_before_provider_check() { + let state = seed_authenticated_state_with_config(AppConfig { + wechat_pay_enabled: true, + wechat_pay_provider: "mock".to_string(), + spacetime_procedure_timeout: Duration::from_secs(1), + ..AppConfig::default() + }) + .await; + let token = + issue_web_browser_access_token(&state, "ios", "sess_runtime_profile_mobile_web_native"); + let app = build_router(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/api/profile/recharge/orders") + .header("authorization", format!("Bearer {token}")) + .header("content-type", "application/json") + .body(Body::from( + r#"{"productId":"points_60","paymentChannel":"wechat_native"}"#, + )) + .expect("request should build"), + ) + .await + .expect("request should succeed"); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + let body = response + .into_body() + .collect() + .await + .expect("body should collect") + .to_bytes(); + let payload: Value = + serde_json::from_slice(&body).expect("response body should be valid json"); + assert_eq!( + payload["error"]["message"], + "真实微信支付渠道不能使用 mock 支付配置" + ); + } + + #[tokio::test] + async fn profile_recharge_order_allows_desktop_web_h5_channel_before_provider_check() { + let state = seed_authenticated_state_with_config(AppConfig { + wechat_pay_enabled: true, + wechat_pay_provider: "mock".to_string(), + spacetime_procedure_timeout: Duration::from_secs(1), + ..AppConfig::default() + }) + .await; + let token = issue_web_browser_access_token( + &state, + "windows", + "sess_runtime_profile_desktop_web_h5", + ); + let app = build_router(state); + + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/api/profile/recharge/orders") + .header("authorization", format!("Bearer {token}")) + .header("content-type", "application/json") + .body(Body::from( + r#"{"productId":"points_60","paymentChannel":"wechat_h5"}"#, + )) + .expect("request should build"), + ) + .await + .expect("request should succeed"); + + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + let body = response + .into_body() + .collect() + .await + .expect("body should collect") + .to_bytes(); + let payload: Value = + serde_json::from_slice(&body).expect("response body should be valid json"); + assert_eq!( + payload["error"]["message"], + "真实微信支付渠道不能使用 mock 支付配置" + ); + } + #[tokio::test] async fn profile_recharge_order_rejects_real_wechat_channel_when_pay_provider_is_mock() { let state = seed_authenticated_state_with_config(AppConfig { @@ -3203,6 +3498,68 @@ mod tests { sign_access_token(&claims, state.auth_jwt_config()).expect("token should sign") } + fn issue_device_access_token( + state: &AppState, + client_type: &str, + client_runtime: &str, + client_platform: &str, + session_id: &str, + ) -> String { + let user_id = test_authenticated_user_id(state); + let claims = AccessTokenClaims::from_input_with_device( + AccessTokenClaimsInput { + user_id: user_id.clone(), + session_id: state.seed_test_refresh_session_for_user_id(&user_id, session_id), + provider: AuthProvider::Password, + roles: vec!["user".to_string()], + token_version: 2, + phone_verified: true, + binding_status: BindingStatus::Active, + display_name: Some("设备资料页用户".to_string()), + }, + Some(platform_auth::AccessTokenDeviceInfo { + client_type: client_type.to_string(), + client_runtime: client_runtime.to_string(), + client_platform: client_platform.to_string(), + }), + state.auth_jwt_config(), + OffsetDateTime::now_utc(), + ) + .expect("claims should build"); + + sign_access_token(&claims, state.auth_jwt_config()).expect("token should sign") + } + + fn issue_web_browser_access_token( + state: &AppState, + client_platform: &str, + session_id: &str, + ) -> String { + let user_id = test_authenticated_user_id(state); + let claims = AccessTokenClaims::from_input_with_device( + AccessTokenClaimsInput { + user_id: user_id.clone(), + session_id: state.seed_test_refresh_session_for_user_id(&user_id, session_id), + provider: AuthProvider::Password, + roles: vec!["user".to_string()], + token_version: 2, + phone_verified: true, + binding_status: BindingStatus::Active, + display_name: Some("网页资料页用户".to_string()), + }, + Some(platform_auth::AccessTokenDeviceInfo { + client_type: "web_browser".to_string(), + client_runtime: "web".to_string(), + client_platform: client_platform.to_string(), + }), + state.auth_jwt_config(), + OffsetDateTime::now_utc(), + ) + .expect("claims should build"); + + sign_access_token(&claims, state.auth_jwt_config()).expect("token should sign") + } + fn test_authenticated_user_id(state: &AppState) -> String { state .auth_user_service() diff --git a/server-rs/crates/api-server/src/wechat/auth.rs b/server-rs/crates/api-server/src/wechat/auth.rs index c5d8437d3..b2571903c 100644 --- a/server-rs/crates/api-server/src/wechat/auth.rs +++ b/server-rs/crates/api-server/src/wechat/auth.rs @@ -5,7 +5,7 @@ response::{IntoResponse, Redirect, Response}, }; use module_auth::{ - AuthLoginMethod, BindWechatPhoneInput, BindWechatVerifiedPhoneInput, + AuthLoginMethod, BindWechatIdentityInput, BindWechatPhoneInput, BindWechatVerifiedPhoneInput, CreateWechatAuthStateInput, WechatAuthError, }; use platform_auth::WechatAuthScene; @@ -57,6 +57,52 @@ pub async fn start_wechat_login( ), scene: map_wechat_scene_to_domain(&scene), request_user_agent: user_agent.clone(), + bind_user_id: None, + }, + OffsetDateTime::now_utc(), + ) + .map_err(map_wechat_auth_error)?; + let authorization_url = state + .wechat_provider() + .build_authorization_url( + &resolve_wechat_callback_url(&state, &headers)?, + &state_record.state.state_token, + &scene, + ) + .map_err(map_wechat_provider_error)?; + + Ok(json_success_body( + Some(&request_context), + WechatStartResponse { authorization_url }, + )) +} + +pub async fn start_wechat_bind( + State(state): State, + Extension(request_context): Extension, + Extension(authenticated): Extension, + headers: HeaderMap, + Query(query): Query, +) -> Result, AppError> { + if !state.config.wechat_auth_enabled { + return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("微信登录暂未启用")); + } + let user_agent = headers + .get("user-agent") + .and_then(|value| value.to_str().ok()) + .map(|value| value.to_string()); + let scene = resolve_wechat_scene(user_agent.as_deref())?; + let state_record = state + .wechat_auth_state_service() + .create_state( + CreateWechatAuthStateInput { + redirect_path: normalize_redirect_path( + query.redirect_path.as_deref(), + &state.config.wechat_redirect_path, + ), + scene: map_wechat_scene_to_domain(&scene), + request_user_agent: user_agent.clone(), + bind_user_id: Some(authenticated.claims().user_id().to_string()), }, OffsetDateTime::now_utc(), ) @@ -128,23 +174,39 @@ pub async fn handle_wechat_callback( .resolve_callback_profile(query.code.as_deref(), query.mock_code.as_deref()) .await { - Ok(profile) => state - .wechat_auth_service() - .resolve_login(module_auth::ResolveWechatLoginInput { - profile: map_wechat_profile_to_domain(profile), - }) - .await - .map_err(map_wechat_auth_error), + Ok(profile) => { + let profile = map_wechat_profile_to_domain(profile); + if let Some(bind_user_id) = consumed.state.bind_user_id.as_deref() { + state + .wechat_auth_service() + .bind_identity_to_user(BindWechatIdentityInput { + user_id: bind_user_id.to_string(), + profile, + }) + .map(|user| module_auth::ResolveWechatLoginResult { + user, + created: false, + }) + .map_err(map_wechat_auth_error) + } else { + state + .wechat_auth_service() + .resolve_login(module_auth::ResolveWechatLoginInput { profile }) + .await + .map_err(map_wechat_auth_error) + } + } Err(error) => Err(map_wechat_provider_error(error)), }; match result { Ok(result) => { + let session_provider = result.user.login_method.clone(); let signed_session = create_auth_session( &state, &result.user, &session_client, - AuthLoginMethod::Wechat, + session_provider.clone(), )?; state .sync_auth_store_tables_to_spacetime() @@ -157,7 +219,7 @@ pub async fn handle_wechat_callback( &state, &request_context, &result.user.id, - AuthLoginMethod::Wechat, + session_provider, ) .await; let mut response = Redirect::to(&build_auth_result_redirect_url( @@ -478,7 +540,8 @@ fn map_wechat_auth_error(error: WechatAuthError) -> AppError { | WechatAuthError::StateNotFound | WechatAuthError::StateExpired | WechatAuthError::StateConsumed - | WechatAuthError::MissingWechatIdentity => { + | WechatAuthError::MissingWechatIdentity + | WechatAuthError::WechatIdentityAlreadyBound => { AppError::from_status(StatusCode::BAD_REQUEST).with_message(error.to_string()) } WechatAuthError::UserNotFound => { diff --git a/server-rs/crates/api-server/src/wechat/pay.rs b/server-rs/crates/api-server/src/wechat/pay.rs index 438a439d0..151ba77c0 100644 --- a/server-rs/crates/api-server/src/wechat/pay.rs +++ b/server-rs/crates/api-server/src/wechat/pay.rs @@ -70,6 +70,7 @@ pub async fn handle_wechat_pay_notify( AppError::from_status(StatusCode::BAD_GATEWAY) .with_message(format!("确认微信支付订单失败:{error}")) })?; + state.publish_profile_recharge_order_update(notify.out_trade_no.clone()); info!( order_id = notify.out_trade_no.as_str(), "微信支付通知已确认订单入账" @@ -260,6 +261,9 @@ pub fn map_wechat_pay_error(error: WechatPayError) -> AppError { WechatPayError::InvalidRequest(message) => AppError::from_status(StatusCode::BAD_REQUEST) .with_message(message) .with_details(json!({ "provider": "wechat_pay" })), + WechatPayError::OrderNotExist(message) => AppError::from_status(StatusCode::NOT_FOUND) + .with_message(message) + .with_details(json!({ "provider": "wechat_pay", "code": "ORDER_NOT_EXIST" })), WechatPayError::RequestFailed(message) | WechatPayError::Upstream(message) | WechatPayError::Deserialize(message) @@ -331,6 +335,7 @@ fn build_wechat_message_push_verify_error_response(error: WechatPayError) -> Res WechatPayError::Disabled => "微信消息推送暂未启用".to_string(), WechatPayError::InvalidConfig(message) | WechatPayError::InvalidRequest(message) + | WechatPayError::OrderNotExist(message) | WechatPayError::RequestFailed(message) | WechatPayError::Upstream(message) | WechatPayError::Deserialize(message) @@ -349,6 +354,7 @@ fn build_virtual_payment_notify_error_response( WechatPayError::Disabled => "微信虚拟支付暂未启用".to_string(), WechatPayError::InvalidConfig(message) | WechatPayError::InvalidRequest(message) + | WechatPayError::OrderNotExist(message) | WechatPayError::RequestFailed(message) | WechatPayError::Upstream(message) | WechatPayError::Deserialize(message) diff --git a/server-rs/crates/module-auth/src/commands.rs b/server-rs/crates/module-auth/src/commands.rs index e7e67fb9b..6706afe65 100644 --- a/server-rs/crates/module-auth/src/commands.rs +++ b/server-rs/crates/module-auth/src/commands.rs @@ -51,11 +51,18 @@ pub struct ResolveWechatLoginInput { pub profile: WechatIdentityProfile, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BindWechatIdentityInput { + pub user_id: String, + pub profile: WechatIdentityProfile, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct CreateWechatAuthStateInput { pub redirect_path: String, pub scene: WechatAuthScene, pub request_user_agent: Option, + pub bind_user_id: Option, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/server-rs/crates/module-auth/src/domain.rs b/server-rs/crates/module-auth/src/domain.rs index d9563a1b8..1321b9824 100644 --- a/server-rs/crates/module-auth/src/domain.rs +++ b/server-rs/crates/module-auth/src/domain.rs @@ -142,6 +142,7 @@ pub struct WechatAuthStateRecord { pub redirect_path: String, pub scene: WechatAuthScene, pub request_user_agent: Option, + pub bind_user_id: Option, pub expires_at: String, pub consumed_at: Option, pub created_at: String, diff --git a/server-rs/crates/module-auth/src/errors.rs b/server-rs/crates/module-auth/src/errors.rs index 2cb61cb35..1757606ca 100644 --- a/server-rs/crates/module-auth/src/errors.rs +++ b/server-rs/crates/module-auth/src/errors.rs @@ -42,6 +42,7 @@ pub enum WechatAuthError { StateConsumed, UserNotFound, MissingWechatIdentity, + WechatIdentityAlreadyBound, Store(String), PasswordHash(String), } @@ -109,6 +110,7 @@ impl fmt::Display for WechatAuthError { Self::StateConsumed => f.write_str("微信登录状态已被消费,请重新发起登录"), Self::UserNotFound => f.write_str("用户不存在"), Self::MissingWechatIdentity => f.write_str("当前账号缺少微信身份"), + Self::WechatIdentityAlreadyBound => f.write_str("该微信身份已绑定其他账号"), Self::Store(message) | Self::PasswordHash(message) => f.write_str(message), } } diff --git a/server-rs/crates/module-auth/src/lib.rs b/server-rs/crates/module-auth/src/lib.rs index 0b9c93a83..6682c8406 100644 --- a/server-rs/crates/module-auth/src/lib.rs +++ b/server-rs/crates/module-auth/src/lib.rs @@ -793,6 +793,7 @@ impl WechatAuthStateService { redirect_path: normalize_required_string(&input.redirect_path).unwrap_or_default(), scene: input.scene, request_user_agent: normalize_optional_string(input.request_user_agent), + bind_user_id: normalize_optional_string(input.bind_user_id), expires_at, consumed_at: None, created_at: created_at.clone(), @@ -858,6 +859,17 @@ impl WechatAuthService { }) } + pub fn bind_identity_to_user( + &self, + input: BindWechatIdentityInput, + ) -> Result { + if input.profile.provider_uid.trim().is_empty() { + return Err(WechatAuthError::MissingProfile); + } + self.store + .bind_wechat_identity_to_user(&input.user_id, input.profile) + } + pub fn get_identity_by_user_id( &self, user_id: &str, @@ -1691,6 +1703,103 @@ impl InMemoryAuthStore { })) } + fn bind_wechat_identity_to_user( + &self, + user_id: &str, + profile: WechatIdentityProfile, + ) -> Result { + let user_id = normalize_required_string(user_id).ok_or(WechatAuthError::UserNotFound)?; + let provider_uid = normalize_required_string(&profile.provider_uid) + .ok_or(WechatAuthError::MissingProfile)?; + let provider_union_id = normalize_optional_string(profile.provider_union_id); + let display_name = normalize_optional_string(profile.display_name); + let avatar_url = normalize_optional_string(profile.avatar_url); + let session_key = normalize_optional_string(profile.session_key); + + let mut state = self + .inner + .lock() + .map_err(|_| WechatAuthError::Store("鐢ㄦ埛浠撳偍閿佸凡涓瘨".to_string()))?; + + if !state + .users_by_username + .values() + .any(|stored_user| stored_user.user.id == user_id) + { + return Err(WechatAuthError::UserNotFound); + } + + if let Some(existing) = state.wechat_identity_by_provider_uid.get(&provider_uid) + && existing.user_id != user_id + { + return Err(WechatAuthError::WechatIdentityAlreadyBound); + } + if let Some(provider_union_id) = provider_union_id.as_deref() + && let Some(existing_user_id) = + state.user_id_by_provider_union_id.get(provider_union_id) + && existing_user_id != &user_id + { + return Err(WechatAuthError::WechatIdentityAlreadyBound); + } + + if let Some(existing_identity) = state + .wechat_identity_by_provider_uid + .values() + .find(|identity| identity.user_id == user_id) + .cloned() + { + state + .wechat_identity_by_provider_uid + .remove(&existing_identity.provider_uid); + if let Some(existing_union_id) = existing_identity.provider_union_id + && provider_union_id.as_deref() != Some(existing_union_id.as_str()) + { + state + .user_id_by_provider_union_id + .remove(&existing_union_id); + } + } + + state.wechat_identity_by_provider_uid.insert( + provider_uid.clone(), + StoredWechatIdentity { + user_id: user_id.clone(), + provider_uid: provider_uid.clone(), + provider_union_id: provider_union_id.clone(), + display_name: display_name.clone(), + avatar_url: avatar_url.clone(), + session_key, + }, + ); + if let Some(provider_union_id) = provider_union_id { + state + .user_id_by_provider_union_id + .insert(provider_union_id, user_id.clone()); + } + + let next_user = { + let stored_user = state + .users_by_username + .values_mut() + .find(|stored_user| stored_user.user.id == user_id) + .ok_or(WechatAuthError::UserNotFound)?; + stored_user.user.wechat_bound = true; + stored_user.user.wechat_account = Some(provider_uid); + if let Some(display_name) = display_name { + stored_user.user.wechat_display_name = Some(display_name); + } + if stored_user.user.avatar_url.is_none() + && let Some(avatar_url) = avatar_url + { + stored_user.user.avatar_url = Some(avatar_url); + } + stored_user.user.clone() + }; + self.persist_wechat_state(&state)?; + + Ok(next_user) + } + fn refresh_wechat_identity_profile( &self, user_id: &str, diff --git a/server-rs/crates/module-runtime/src/commands.rs b/server-rs/crates/module-runtime/src/commands.rs index 496a246ca..70fb73958 100644 --- a/server-rs/crates/module-runtime/src/commands.rs +++ b/server-rs/crates/module-runtime/src/commands.rs @@ -397,6 +397,42 @@ pub fn build_runtime_profile_recharge_order_paid_input( }) } +pub fn build_runtime_profile_recharge_order_close_input( + order_id: String, + closed_at_micros: i64, +) -> Result { + let order_id = + normalize_required_string(order_id).ok_or(RuntimeProfileFieldError::MissingOrderId)?; + Ok(RuntimeProfileRechargeOrderCloseInput { + order_id, + closed_at_micros, + }) +} + +pub fn build_runtime_profile_recharge_order_expiration_claim_input( + worker_id: String, + now_micros: i64, + lease_expires_at_micros: i64, + limit: u32, +) -> Result { + let worker_id = + normalize_required_string(worker_id).ok_or(RuntimeProfileFieldError::MissingWorkerId)?; + Ok(RuntimeProfileRechargeOrderExpirationClaimInput { + worker_id, + now_micros, + lease_expires_at_micros, + limit, + }) +} + +pub fn build_runtime_profile_recharge_order_expiration_complete_input( + order_id: String, +) -> Result { + let order_id = + normalize_required_string(order_id).ok_or(RuntimeProfileFieldError::MissingOrderId)?; + Ok(RuntimeProfileRechargeOrderExpirationCompleteInput { order_id }) +} + pub fn build_runtime_profile_feedback_submission_input( user_id: String, description: String, diff --git a/server-rs/crates/module-runtime/src/domain.rs b/server-rs/crates/module-runtime/src/domain.rs index e2487f580..928a83252 100644 --- a/server-rs/crates/module-runtime/src/domain.rs +++ b/server-rs/crates/module-runtime/src/domain.rs @@ -36,8 +36,10 @@ pub const DEFAULT_SAVE_ARCHIVE_SUMMARY_TEXT: &str = "继续推进上一次保存 pub const PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK: &str = "mock"; pub const PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM: &str = "wechat_mp"; pub const PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL: &str = "wechat_mp_virtual"; +pub const PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_JSAPI: &str = "wechat_jsapi"; pub const PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_H5: &str = "wechat_h5"; pub const PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_NATIVE: &str = "wechat_native"; +pub const PROFILE_RECHARGE_ORDER_EXPIRATION_SECONDS: i64 = 5 * 60; pub const PROFILE_FEEDBACK_DESCRIPTION_MIN_CHARS: usize = 10; pub const PROFILE_FEEDBACK_DESCRIPTION_MAX_CHARS: usize = 200; pub const PROFILE_FEEDBACK_CONTACT_PHONE_MAX_CHARS: usize = 40; @@ -1358,6 +1360,55 @@ pub struct RuntimeProfileRechargeOrderPaidInput { pub provider_transaction_id: Option, } +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RuntimeProfileRechargeOrderCloseInput { + pub order_id: String, + pub closed_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RuntimeProfileRechargeOrderExpirationClaimInput { + pub worker_id: String, + pub now_micros: i64, + pub lease_expires_at_micros: i64, + pub limit: u32, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RuntimeProfileRechargeOrderExpirationCompleteInput { + pub order_id: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RuntimeProfileRechargeOrderExpirationScheduleSnapshot { + pub order_id: String, + pub user_id: String, + pub scheduled_at_micros: i64, + pub lease_owner: Option, + pub lease_expires_at_micros: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RuntimeProfileRechargeOrderExpirationClaimProcedureResult { + pub ok: bool, + pub entries: Vec, + pub error_message: Option, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RuntimeProfileRechargeOrderExpirationCompleteProcedureResult { + pub ok: bool, + pub error_message: Option, +} + #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct RuntimeProfileWalletLedgerEntrySnapshot { diff --git a/server-rs/crates/module-runtime/src/errors.rs b/server-rs/crates/module-runtime/src/errors.rs index 4154505cb..c98e43eb5 100644 --- a/server-rs/crates/module-runtime/src/errors.rs +++ b/server-rs/crates/module-runtime/src/errors.rs @@ -74,6 +74,7 @@ pub enum RuntimeProfileFieldError { TaskDisabled, TaskNotClaimable, TaskAlreadyClaimed, + MissingWorkerId, MissingOrderId, MissingProductId, MissingProductTitle, @@ -149,6 +150,7 @@ impl std::fmt::Display for RuntimeProfileFieldError { Self::TaskDisabled => f.write_str("任务已停用"), Self::TaskNotClaimable => f.write_str("任务尚未达成"), Self::TaskAlreadyClaimed => f.write_str("任务奖励已领取"), + Self::MissingWorkerId => f.write_str("worker_id 不能为空"), Self::MissingOrderId => f.write_str("recharge.order_id 不能为空"), Self::MissingProductId => f.write_str("recharge.product_id 不能为空"), Self::MissingProductTitle => f.write_str("recharge.product_title 不能为空"), diff --git a/server-rs/crates/platform-wechat/Cargo.toml b/server-rs/crates/platform-wechat/Cargo.toml index 5d1dd7b8b..760bb2387 100644 --- a/server-rs/crates/platform-wechat/Cargo.toml +++ b/server-rs/crates/platform-wechat/Cargo.toml @@ -16,7 +16,7 @@ serde_json = { workspace = true } sha1 = { workspace = true } sha2 = { workspace = true } shared-contracts = { workspace = true } -time = { workspace = true } +time = { workspace = true, features = ["formatting"] } tracing = { workspace = true } url = { workspace = true } urlencoding = { workspace = true } diff --git a/server-rs/crates/platform-wechat/src/pay.rs b/server-rs/crates/platform-wechat/src/pay.rs index 44c5aa518..bcd897440 100644 --- a/server-rs/crates/platform-wechat/src/pay.rs +++ b/server-rs/crates/platform-wechat/src/pay.rs @@ -24,7 +24,7 @@ use shared_contracts::runtime::{ WechatH5PaymentResponse, WechatMiniProgramPayParamsResponse, WechatNativePaymentResponse, }; use std::convert::TryInto; -use time::OffsetDateTime; +use time::{Duration as TimeDuration, OffsetDateTime, format_description::well_known::Rfc3339}; use tracing::warn; use url::Url; @@ -47,6 +47,7 @@ const WECHAT_PAY_CLIENT_IP_MAX_CHARS: usize = 45; const WECHAT_PAY_JSAPI_PATH: &str = "/v3/pay/transactions/jsapi"; const WECHAT_PAY_H5_PATH: &str = "/v3/pay/transactions/h5"; const WECHAT_PAY_NATIVE_PATH: &str = "/v3/pay/transactions/native"; +const WECHAT_NATIVE_PAY_EXPIRE_SECONDS: i64 = 5 * 60; const WECHAT_MINIPROGRAM_MESSAGE_ENCODING_AES_KEY_BYTES: usize = 43; const WECHAT_MINIPROGRAM_MESSAGE_AES_KEY_BYTES: usize = 32; const WECHAT_MINIPROGRAM_MESSAGE_RANDOM_BYTES: usize = 16; @@ -134,6 +135,7 @@ pub enum WechatPayError { Disabled, InvalidConfig(String), InvalidRequest(String), + OrderNotExist(String), RequestFailed(String), Upstream(String), Deserialize(String), @@ -192,6 +194,7 @@ struct WechatNativeOrderRequest<'a> { mchid: &'a str, description: &'a str, out_trade_no: &'a str, + time_expire: &'a str, notify_url: &'a str, amount: WechatJsapiAmount, scene_info: WechatNativeSceneInfo<'a>, @@ -257,6 +260,14 @@ struct WechatPayQueryOrderResponse { success_time: Option, } +#[derive(Deserialize)] +struct WechatPayErrorResponse { + #[serde(default)] + code: Option, + #[serde(default)] + message: Option, +} + #[derive(Deserialize)] struct WechatVirtualPaymentNotifyBody { #[serde(rename = "Event", alias = "event")] @@ -569,11 +580,16 @@ impl RealWechatPayClient { validate_web_order_request(self, &request)?; let amount_total = i64::try_from(request.amount_cents) .map_err(|_| WechatPayError::InvalidRequest("微信支付金额超出 i64 范围".to_string()))?; + let expires_at = + OffsetDateTime::now_utc() + TimeDuration::seconds(WECHAT_NATIVE_PAY_EXPIRE_SECONDS); + let expires_at_text = + format_wechat_pay_rfc3339_seconds(expires_at, "微信支付 Native 过期时间")?; let body = serde_json::to_string(&WechatNativeOrderRequest { appid: &self.app_id, mchid: &self.mch_id, description: &request.description, out_trade_no: &request.order_id, + time_expire: &expires_at_text, notify_url: &self.notify_url, amount: WechatJsapiAmount { total: amount_total, @@ -611,7 +627,10 @@ impl RealWechatPayClient { ) })?; - Ok(WechatNativePaymentResponse { code_url }) + Ok(WechatNativePaymentResponse { + code_url, + expires_at: expires_at_text, + }) } async fn post_wechat_json( @@ -680,6 +699,7 @@ impl RealWechatPayClient { let pay_sign = self.sign_message(&message)?; Ok(WechatMiniProgramPayParamsResponse { + app_id: Some(self.app_id.clone()), time_stamp, nonce_str, package, @@ -755,6 +775,17 @@ impl RealWechatPayClient { WechatPayError::Deserialize(format!("微信支付查单响应读取失败:{error}")) })?; if !status.is_success() { + if let Ok(payload) = serde_json::from_str::(&response_text) + && payload.code.as_deref() == Some("ORDER_NOT_EXIST") + { + return Err(WechatPayError::OrderNotExist( + payload + .message + .filter(|message| !message.trim().is_empty()) + .unwrap_or_else(|| "微信支付订单不存在".to_string()), + )); + } + return Err(WechatPayError::Upstream(format!( "微信支付查单失败:HTTP {status},{response_text}" ))); @@ -856,6 +887,7 @@ fn build_mock_pay_params(order_id: &str) -> WechatMiniProgramPayParamsResponse { let pay_sign = hex_sha256(format!("{time_stamp}\n{nonce_str}\n{package}\n").as_bytes()); WechatMiniProgramPayParamsResponse { + app_id: Some("wx-mock-app".to_string()), time_stamp, nonce_str, package, @@ -873,12 +905,28 @@ fn build_mock_h5_payment(order_id: &str) -> WechatH5PaymentResponse { } } +fn format_wechat_pay_rfc3339_seconds( + value: OffsetDateTime, + context: &str, +) -> Result { + let value = value.replace_nanosecond(0).map_err(|error| { + WechatPayError::InvalidRequest(format!("{context} 秒级时间规整失败:{error}")) + })?; + value + .format(&Rfc3339) + .map_err(|error| WechatPayError::InvalidRequest(format!("{context} 格式化失败:{error}"))) +} + fn build_mock_native_payment(order_id: &str) -> WechatNativePaymentResponse { + let expires_at = + OffsetDateTime::now_utc() + TimeDuration::seconds(WECHAT_NATIVE_PAY_EXPIRE_SECONDS); WechatNativePaymentResponse { code_url: format!( "weixin://pay.weixin.qq.com/bizpayurl/up?pr=mock-{}", hex_sha256(order_id.as_bytes()) ), + expires_at: format_wechat_pay_rfc3339_seconds(expires_at, "mock 微信支付 Native 过期时间") + .unwrap_or_else(|_| expires_at.to_string()), } } @@ -1552,6 +1600,7 @@ impl std::fmt::Display for WechatPayError { Self::Disabled => formatter.write_str("微信支付暂未启用"), Self::InvalidConfig(message) | Self::InvalidRequest(message) + | Self::OrderNotExist(message) | Self::RequestFailed(message) | Self::Upstream(message) | Self::Deserialize(message) @@ -1639,6 +1688,7 @@ mod tests { mchid: "1900000001", description: "陶泥儿 - 60泥点", out_trade_no: "rcgtest001", + time_expire: "2026-05-15T10:05:00Z", notify_url: "https://api.example.com/api/profile/recharge/wechat/notify", amount: WechatJsapiAmount { total: 600, @@ -1655,12 +1705,27 @@ mod tests { .expect("Native order response should deserialize"); assert_eq!(body["scene_info"]["payer_client_ip"], "203.0.113.10"); + assert_eq!(body["time_expire"], "2026-05-15T10:05:00Z"); assert_eq!( response.code_url.as_deref(), Some("weixin://pay.weixin.qq.com/bizpayurl/up?pr=test") ); } + #[test] + fn wechat_pay_expire_time_uses_rfc3339_without_fractional_seconds() { + let value = OffsetDateTime::from_unix_timestamp(1_768_398_476) + .expect("timestamp should be valid") + .replace_nanosecond(395_297_400) + .expect("nanosecond should be valid"); + + let formatted = + format_wechat_pay_rfc3339_seconds(value, "测试时间").expect("time should format"); + + assert_eq!(formatted, "2026-01-14T13:47:56Z"); + assert!(!formatted.contains('.')); + } + #[test] fn transaction_endpoints_reuse_configured_wechat_pay_origin() { let h5_endpoint = resolve_wechat_pay_transaction_endpoint( diff --git a/server-rs/crates/shared-contracts/src/runtime.rs b/server-rs/crates/shared-contracts/src/runtime.rs index c8b99dee6..6e9a96bf1 100644 --- a/server-rs/crates/shared-contracts/src/runtime.rs +++ b/server-rs/crates/shared-contracts/src/runtime.rs @@ -238,6 +238,8 @@ pub struct ProfileRechargeOrderResponse { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct WechatMiniProgramPayParamsResponse { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub app_id: Option, pub time_stamp: String, pub nonce_str: String, pub package: String, @@ -271,6 +273,7 @@ pub struct WechatH5PaymentResponse { #[serde(rename_all = "camelCase")] pub struct WechatNativePaymentResponse { pub code_url: String, + pub expires_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -1496,6 +1499,7 @@ mod tests { }), wechat_native_payment: Some(WechatNativePaymentResponse { code_url: "weixin://pay.weixin.qq.com/bizpayurl/up?pr=test".to_string(), + expires_at: "2026-05-15T10:05:00Z".to_string(), }), }) .expect("payload should serialize"); @@ -1508,6 +1512,10 @@ mod tests { payload["wechatNativePayment"]["codeUrl"], json!("weixin://pay.weixin.qq.com/bizpayurl/up?pr=test") ); + assert_eq!( + payload["wechatNativePayment"]["expiresAt"], + json!("2026-05-15T10:05:00Z") + ); } #[test] diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index 1977dcc76..aa2977862 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -231,7 +231,10 @@ use module_runtime::{ build_runtime_profile_invite_code_admin_upsert_input, build_runtime_profile_invite_code_record, build_runtime_profile_play_stats_get_input, build_runtime_profile_play_stats_record, build_runtime_profile_recharge_center_get_input, build_runtime_profile_recharge_center_record, + build_runtime_profile_recharge_order_close_input, build_runtime_profile_recharge_order_create_input, + build_runtime_profile_recharge_order_expiration_claim_input, + build_runtime_profile_recharge_order_expiration_complete_input, build_runtime_profile_recharge_order_get_input, build_runtime_profile_recharge_product_admin_list_input, build_runtime_profile_recharge_product_admin_upsert_input, diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index cf4462144..b67319bdf 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -291,6 +291,8 @@ pub(crate) use self::runtime_profile::{ map_runtime_profile_invite_code_admin_procedure_result, map_runtime_profile_play_stats_procedure_result, map_runtime_profile_recharge_center_procedure_result, + map_runtime_profile_recharge_order_expiration_claim_procedure_result, + map_runtime_profile_recharge_order_expiration_complete_procedure_result, map_runtime_profile_recharge_order_procedure_result, map_runtime_profile_recharge_product_admin_list_procedure_result, map_runtime_profile_recharge_product_admin_procedure_result, 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 8fbad4d71..0a032ebed 100644 --- a/server-rs/crates/spacetime-client/src/mapper/runtime_profile.rs +++ b/server-rs/crates/spacetime-client/src/mapper/runtime_profile.rs @@ -210,6 +210,40 @@ impl From } } +impl From + for RuntimeProfileRechargeOrderCloseInput +{ + fn from(input: module_runtime::RuntimeProfileRechargeOrderCloseInput) -> Self { + Self { + order_id: input.order_id, + closed_at_micros: input.closed_at_micros, + } + } +} + +impl From + for RuntimeProfileRechargeOrderExpirationClaimInput +{ + fn from(input: module_runtime::RuntimeProfileRechargeOrderExpirationClaimInput) -> Self { + Self { + worker_id: input.worker_id, + now_micros: input.now_micros, + lease_expires_at_micros: input.lease_expires_at_micros, + limit: input.limit, + } + } +} + +impl From + for RuntimeProfileRechargeOrderExpirationCompleteInput +{ + fn from(input: module_runtime::RuntimeProfileRechargeOrderExpirationCompleteInput) -> Self { + Self { + order_id: input.order_id, + } + } +} + impl From for RuntimeProfileRechargeProductAdminListInput { @@ -453,6 +487,33 @@ pub(crate) fn map_runtime_profile_recharge_order_procedure_result( )) } +pub(crate) fn map_runtime_profile_recharge_order_expiration_claim_procedure_result( + result: RuntimeProfileRechargeOrderExpirationClaimProcedureResult, +) -> Result< + Vec, + SpacetimeClientError, +> { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + + Ok(result + .entries + .into_iter() + .map(map_runtime_profile_recharge_order_expiration_schedule_snapshot) + .collect()) +} + +pub(crate) fn map_runtime_profile_recharge_order_expiration_complete_procedure_result( + result: RuntimeProfileRechargeOrderExpirationCompleteProcedureResult, +) -> Result<(), SpacetimeClientError> { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + + Ok(()) +} + pub(crate) fn map_runtime_profile_feedback_submission_procedure_result( result: RuntimeProfileFeedbackSubmissionProcedureResult, ) -> Result { @@ -967,6 +1028,20 @@ pub(crate) fn map_runtime_profile_recharge_order_snapshot( } } +pub(crate) fn map_runtime_profile_recharge_order_expiration_schedule_snapshot( + snapshot: RuntimeProfileRechargeOrderExpirationScheduleSnapshot, +) -> module_runtime::RuntimeProfileRechargeOrderExpirationScheduleSnapshot { + module_runtime::RuntimeProfileRechargeOrderExpirationScheduleSnapshot { + order_id: snapshot.order_id, + user_id: snapshot.user_id, + scheduled_at_micros: snapshot.scheduled_at_micros, + lease_owner: snapshot.lease_owner, + lease_expires_at_micros: snapshot.lease_expires_at_micros, + created_at_micros: snapshot.created_at_micros, + updated_at_micros: snapshot.updated_at_micros, + } +} + pub(crate) fn map_runtime_profile_feedback_submission_snapshot( snapshot: RuntimeProfileFeedbackSubmissionSnapshot, ) -> module_runtime::RuntimeProfileFeedbackSubmissionSnapshot { diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index 2b3821204..37ddd4a5e 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.rs @@ -214,12 +214,14 @@ pub mod chapter_progression_table; pub mod chapter_progression_type; pub mod checkpoint_wooden_fish_run_procedure; pub mod claim_external_generation_jobs_and_return_procedure; +pub mod claim_profile_recharge_order_expiration_schedule_and_return_procedure; pub mod claim_profile_task_reward_and_return_procedure; pub mod claim_puzzle_background_compile_task_procedure; pub mod claim_puzzle_work_point_incentive_procedure; pub mod clear_database_migration_import_chunks_procedure; pub mod clear_platform_browse_history_and_return_procedure; pub mod click_match_3_d_item_procedure; +pub mod close_profile_recharge_order_and_return_procedure; pub mod combat_outcome_type; pub mod compile_big_fish_draft_procedure; pub mod compile_custom_world_published_profile_procedure; @@ -233,6 +235,7 @@ pub mod compile_wooden_fish_draft_procedure; pub mod complete_ai_stage_and_return_procedure; pub mod complete_ai_task_and_return_procedure; pub mod complete_external_generation_job_and_return_procedure; +pub mod complete_profile_recharge_order_expiration_schedule_and_return_procedure; pub mod confirm_asset_object_and_return_procedure; pub mod confirm_asset_object_reducer; pub mod consume_inventory_item_input_type; @@ -714,6 +717,8 @@ pub mod profile_membership_table; pub mod profile_membership_type; pub mod profile_played_world_table; pub mod profile_played_world_type; +pub mod profile_recharge_order_expiration_schedule_table; +pub mod profile_recharge_order_expiration_schedule_type; pub mod profile_recharge_order_table; pub mod profile_recharge_order_type; pub mod profile_recharge_product_config_table; @@ -997,7 +1002,13 @@ pub mod runtime_profile_played_world_snapshot_type; pub mod runtime_profile_recharge_center_get_input_type; pub mod runtime_profile_recharge_center_procedure_result_type; pub mod runtime_profile_recharge_center_snapshot_type; +pub mod runtime_profile_recharge_order_close_input_type; pub mod runtime_profile_recharge_order_create_input_type; +pub mod runtime_profile_recharge_order_expiration_claim_input_type; +pub mod runtime_profile_recharge_order_expiration_claim_procedure_result_type; +pub mod runtime_profile_recharge_order_expiration_complete_input_type; +pub mod runtime_profile_recharge_order_expiration_complete_procedure_result_type; +pub mod runtime_profile_recharge_order_expiration_schedule_snapshot_type; pub mod runtime_profile_recharge_order_get_input_type; pub mod runtime_profile_recharge_order_paid_input_type; pub mod runtime_profile_recharge_order_snapshot_type; @@ -1500,12 +1511,14 @@ pub use chapter_progression_table::*; pub use chapter_progression_type::ChapterProgression; pub use checkpoint_wooden_fish_run_procedure::checkpoint_wooden_fish_run; pub use claim_external_generation_jobs_and_return_procedure::claim_external_generation_jobs_and_return; +pub use claim_profile_recharge_order_expiration_schedule_and_return_procedure::claim_profile_recharge_order_expiration_schedule_and_return; pub use claim_profile_task_reward_and_return_procedure::claim_profile_task_reward_and_return; pub use claim_puzzle_background_compile_task_procedure::claim_puzzle_background_compile_task; pub use claim_puzzle_work_point_incentive_procedure::claim_puzzle_work_point_incentive; pub use clear_database_migration_import_chunks_procedure::clear_database_migration_import_chunks; pub use clear_platform_browse_history_and_return_procedure::clear_platform_browse_history_and_return; pub use click_match_3_d_item_procedure::click_match_3_d_item; +pub use close_profile_recharge_order_and_return_procedure::close_profile_recharge_order_and_return; pub use combat_outcome_type::CombatOutcome; pub use compile_big_fish_draft_procedure::compile_big_fish_draft; pub use compile_custom_world_published_profile_procedure::compile_custom_world_published_profile; @@ -1519,6 +1532,7 @@ pub use compile_wooden_fish_draft_procedure::compile_wooden_fish_draft; pub use complete_ai_stage_and_return_procedure::complete_ai_stage_and_return; pub use complete_ai_task_and_return_procedure::complete_ai_task_and_return; pub use complete_external_generation_job_and_return_procedure::complete_external_generation_job_and_return; +pub use complete_profile_recharge_order_expiration_schedule_and_return_procedure::complete_profile_recharge_order_expiration_schedule_and_return; pub use confirm_asset_object_and_return_procedure::confirm_asset_object_and_return; pub use confirm_asset_object_reducer::confirm_asset_object; pub use consume_inventory_item_input_type::ConsumeInventoryItemInput; @@ -2000,6 +2014,8 @@ pub use profile_membership_table::*; pub use profile_membership_type::ProfileMembership; pub use profile_played_world_table::*; pub use profile_played_world_type::ProfilePlayedWorld; +pub use profile_recharge_order_expiration_schedule_table::*; +pub use profile_recharge_order_expiration_schedule_type::ProfileRechargeOrderExpirationSchedule; pub use profile_recharge_order_table::*; pub use profile_recharge_order_type::ProfileRechargeOrder; pub use profile_recharge_product_config_table::*; @@ -2283,7 +2299,13 @@ pub use runtime_profile_played_world_snapshot_type::RuntimeProfilePlayedWorldSna pub use runtime_profile_recharge_center_get_input_type::RuntimeProfileRechargeCenterGetInput; pub use runtime_profile_recharge_center_procedure_result_type::RuntimeProfileRechargeCenterProcedureResult; pub use runtime_profile_recharge_center_snapshot_type::RuntimeProfileRechargeCenterSnapshot; +pub use runtime_profile_recharge_order_close_input_type::RuntimeProfileRechargeOrderCloseInput; pub use runtime_profile_recharge_order_create_input_type::RuntimeProfileRechargeOrderCreateInput; +pub use runtime_profile_recharge_order_expiration_claim_input_type::RuntimeProfileRechargeOrderExpirationClaimInput; +pub use runtime_profile_recharge_order_expiration_claim_procedure_result_type::RuntimeProfileRechargeOrderExpirationClaimProcedureResult; +pub use runtime_profile_recharge_order_expiration_complete_input_type::RuntimeProfileRechargeOrderExpirationCompleteInput; +pub use runtime_profile_recharge_order_expiration_complete_procedure_result_type::RuntimeProfileRechargeOrderExpirationCompleteProcedureResult; +pub use runtime_profile_recharge_order_expiration_schedule_snapshot_type::RuntimeProfileRechargeOrderExpirationScheduleSnapshot; pub use runtime_profile_recharge_order_get_input_type::RuntimeProfileRechargeOrderGetInput; pub use runtime_profile_recharge_order_paid_input_type::RuntimeProfileRechargeOrderPaidInput; pub use runtime_profile_recharge_order_snapshot_type::RuntimeProfileRechargeOrderSnapshot; @@ -2923,6 +2945,8 @@ pub struct DbUpdate { profile_membership: __sdk::TableUpdate, profile_played_world: __sdk::TableUpdate, profile_recharge_order: __sdk::TableUpdate, + profile_recharge_order_expiration_schedule: + __sdk::TableUpdate, profile_recharge_product_config: __sdk::TableUpdate, profile_redeem_code: __sdk::TableUpdate, profile_redeem_code_usage: __sdk::TableUpdate, @@ -3219,6 +3243,13 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "profile_recharge_order" => db_update.profile_recharge_order.append( profile_recharge_order_table::parse_table_update(table_update)?, ), + "profile_recharge_order_expiration_schedule" => { + db_update.profile_recharge_order_expiration_schedule.append( + profile_recharge_order_expiration_schedule_table::parse_table_update( + table_update, + )?, + ) + } "profile_recharge_product_config" => { db_update.profile_recharge_product_config.append( profile_recharge_product_config_table::parse_table_update(table_update)?, @@ -3784,6 +3815,12 @@ impl __sdk::DbUpdate for DbUpdate { &self.profile_recharge_order, ) .with_updates_by_pk(|row| &row.order_id); + diff.profile_recharge_order_expiration_schedule = cache + .apply_diff_to_table::( + "profile_recharge_order_expiration_schedule", + &self.profile_recharge_order_expiration_schedule, + ) + .with_updates_by_pk(|row| &row.order_id); diff.profile_recharge_product_config = cache .apply_diff_to_table::( "profile_recharge_product_config", @@ -4320,6 +4357,9 @@ impl __sdk::DbUpdate for DbUpdate { "profile_recharge_order" => db_update .profile_recharge_order .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "profile_recharge_order_expiration_schedule" => db_update + .profile_recharge_order_expiration_schedule + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "profile_recharge_product_config" => db_update .profile_recharge_product_config .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -4729,6 +4769,9 @@ impl __sdk::DbUpdate for DbUpdate { "profile_recharge_order" => db_update .profile_recharge_order .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "profile_recharge_order_expiration_schedule" => db_update + .profile_recharge_order_expiration_schedule + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "profile_recharge_product_config" => db_update .profile_recharge_product_config .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -4996,6 +5039,8 @@ pub struct AppliedDiff<'r> { profile_membership: __sdk::TableAppliedDiff<'r, ProfileMembership>, profile_played_world: __sdk::TableAppliedDiff<'r, ProfilePlayedWorld>, profile_recharge_order: __sdk::TableAppliedDiff<'r, ProfileRechargeOrder>, + profile_recharge_order_expiration_schedule: + __sdk::TableAppliedDiff<'r, ProfileRechargeOrderExpirationSchedule>, profile_recharge_product_config: __sdk::TableAppliedDiff<'r, ProfileRechargeProductConfig>, profile_redeem_code: __sdk::TableAppliedDiff<'r, ProfileRedeemCode>, profile_redeem_code_usage: __sdk::TableAppliedDiff<'r, ProfileRedeemCodeUsage>, @@ -5422,6 +5467,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.profile_recharge_order, event, ); + callbacks.invoke_table_row_callbacks::( + "profile_recharge_order_expiration_schedule", + &self.profile_recharge_order_expiration_schedule, + event, + ); callbacks.invoke_table_row_callbacks::( "profile_recharge_product_config", &self.profile_recharge_product_config, @@ -5964,19 +6014,19 @@ impl __sdk::SubscriptionHandle for SubscriptionHandle { /// either a [`DbConnection`] or an [`EventContext`] and operate on either. pub trait RemoteDbContext: __sdk::DbContext< - DbView = RemoteTables, - Reducers = RemoteReducers, - SubscriptionBuilder = __sdk::SubscriptionBuilder, -> + DbView = RemoteTables, + Reducers = RemoteReducers, + SubscriptionBuilder = __sdk::SubscriptionBuilder, + > { } impl< - Ctx: __sdk::DbContext< + Ctx: __sdk::DbContext< DbView = RemoteTables, Reducers = RemoteReducers, SubscriptionBuilder = __sdk::SubscriptionBuilder, >, - > RemoteDbContext for Ctx +> RemoteDbContext for Ctx { } @@ -6442,6 +6492,7 @@ impl __sdk::SpacetimeModule for RemoteModule { profile_membership_table::register_table(client_cache); profile_played_world_table::register_table(client_cache); profile_recharge_order_table::register_table(client_cache); + profile_recharge_order_expiration_schedule_table::register_table(client_cache); profile_recharge_product_config_table::register_table(client_cache); profile_redeem_code_table::register_table(client_cache); profile_redeem_code_usage_table::register_table(client_cache); @@ -6576,6 +6627,7 @@ impl __sdk::SpacetimeModule for RemoteModule { "profile_membership", "profile_played_world", "profile_recharge_order", + "profile_recharge_order_expiration_schedule", "profile_recharge_product_config", "profile_redeem_code", "profile_redeem_code_usage", diff --git a/server-rs/crates/spacetime-client/src/module_bindings/accept_quest_reducer.rs b/server-rs/crates/spacetime-client/src/module_bindings/accept_quest_reducer.rs index dfebf9039..61e6b9c53 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/accept_quest_reducer.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/accept_quest_reducer.rs @@ -47,9 +47,11 @@ pub trait accept_quest { &self, input: QuestRecordInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()>; } @@ -58,9 +60,11 @@ impl accept_quest for super::RemoteReducers { &self, input: QuestRecordInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()> { self.imp .invoke_reducer_with_callback(AcceptQuestArgs { input }, callback) diff --git a/server-rs/crates/spacetime-client/src/module_bindings/acknowledge_external_generation_jobs_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/acknowledge_external_generation_jobs_and_return_procedure.rs index e9b1c18fe..4c83af9ca 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/acknowledge_external_generation_jobs_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/acknowledge_external_generation_jobs_and_return_procedure.rs @@ -34,10 +34,10 @@ pub trait acknowledge_external_generation_jobs_and_return { input: ExternalGenerationJobAcknowledgeInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -47,10 +47,10 @@ impl acknowledge_external_generation_jobs_and_return for super::RemoteProcedures input: ExternalGenerationJobAcknowledgeInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, ExternalGenerationJobProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/acknowledge_quest_completion_reducer.rs b/server-rs/crates/spacetime-client/src/module_bindings/acknowledge_quest_completion_reducer.rs index 6ae2fd100..b1419fb7e 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/acknowledge_quest_completion_reducer.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/acknowledge_quest_completion_reducer.rs @@ -47,9 +47,11 @@ pub trait acknowledge_quest_completion { &self, input: QuestCompletionAckInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()>; } @@ -58,9 +60,11 @@ impl acknowledge_quest_completion for super::RemoteReducers { &self, input: QuestCompletionAckInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()> { self.imp .invoke_reducer_with_callback(AcknowledgeQuestCompletionArgs { input }, callback) diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_disable_profile_redeem_code_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_disable_profile_redeem_code_procedure.rs index bbdaab4f3..9865ace59 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_disable_profile_redeem_code_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_disable_profile_redeem_code_procedure.rs @@ -31,10 +31,10 @@ pub trait admin_disable_profile_redeem_code { input: RuntimeProfileRedeemCodeAdminDisableInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl admin_disable_profile_redeem_code for super::RemoteProcedures { input: RuntimeProfileRedeemCodeAdminDisableInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_disable_profile_task_config_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_disable_profile_task_config_procedure.rs index c968f950d..0417bd2e9 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_disable_profile_task_config_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_disable_profile_task_config_procedure.rs @@ -31,10 +31,10 @@ pub trait admin_disable_profile_task_config { input: RuntimeProfileTaskConfigAdminDisableInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl admin_disable_profile_task_config for super::RemoteProcedures { input: RuntimeProfileTaskConfigAdminDisableInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, RuntimeProfileTaskConfigAdminProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_get_profile_wallet_config_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_get_profile_wallet_config_procedure.rs index c7c836f1b..1f48e0770 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_get_profile_wallet_config_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_get_profile_wallet_config_procedure.rs @@ -31,10 +31,10 @@ pub trait admin_get_profile_wallet_config { input: RuntimeProfileWalletConfigAdminGetInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl admin_get_profile_wallet_config for super::RemoteProcedures { input: RuntimeProfileWalletConfigAdminGetInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, RuntimeProfileWalletConfigAdminProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_editor_assets_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_editor_assets_and_return_procedure.rs index cc3c8ecd6..8e3589adb 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_editor_assets_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_editor_assets_and_return_procedure.rs @@ -31,10 +31,10 @@ pub trait admin_list_editor_assets_and_return { input: AdminEditorAssetListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl admin_list_editor_assets_and_return for super::RemoteProcedures { input: AdminEditorAssetListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, AdminEditorAssetListProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_editor_showcase_assets_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_editor_showcase_assets_and_return_procedure.rs index 69ce4beca..a740930a7 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_editor_showcase_assets_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_editor_showcase_assets_and_return_procedure.rs @@ -34,10 +34,10 @@ pub trait admin_list_editor_showcase_assets_and_return { input: EditorShowcaseAssetAdminListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -47,10 +47,10 @@ impl admin_list_editor_showcase_assets_and_return for super::RemoteProcedures { input: EditorShowcaseAssetAdminListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, EditorShowcaseAssetListProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_invite_codes_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_invite_codes_procedure.rs index cdfa27d91..96d2350f5 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_invite_codes_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_invite_codes_procedure.rs @@ -31,10 +31,10 @@ pub trait admin_list_profile_invite_codes { input: RuntimeProfileInviteCodeAdminListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl admin_list_profile_invite_codes for super::RemoteProcedures { input: RuntimeProfileInviteCodeAdminListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, RuntimeProfileInviteCodeAdminListProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_recharge_products_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_recharge_products_procedure.rs index e84d4ec62..a1deed886 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_recharge_products_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_recharge_products_procedure.rs @@ -34,10 +34,10 @@ pub trait admin_list_profile_recharge_products { input: RuntimeProfileRechargeProductAdminListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -47,10 +47,10 @@ impl admin_list_profile_recharge_products for super::RemoteProcedures { input: RuntimeProfileRechargeProductAdminListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp.invoke_procedure_with_callback::<_, RuntimeProfileRechargeProductAdminListProcedureResult>( "admin_list_profile_recharge_products", diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_redeem_codes_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_redeem_codes_procedure.rs index 2c9b9dd73..c7d6a78e8 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_redeem_codes_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_redeem_codes_procedure.rs @@ -31,10 +31,10 @@ pub trait admin_list_profile_redeem_codes { input: RuntimeProfileRedeemCodeAdminListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl admin_list_profile_redeem_codes for super::RemoteProcedures { input: RuntimeProfileRedeemCodeAdminListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminListProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_task_configs_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_task_configs_procedure.rs index 88ca28d55..a152116df 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_task_configs_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_profile_task_configs_procedure.rs @@ -31,10 +31,10 @@ pub trait admin_list_profile_task_configs { input: RuntimeProfileTaskConfigAdminListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl admin_list_profile_task_configs for super::RemoteProcedures { input: RuntimeProfileTaskConfigAdminListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, RuntimeProfileTaskConfigAdminListProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_work_visibility_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_work_visibility_procedure.rs index 72028b5e5..df222e415 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_work_visibility_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_work_visibility_procedure.rs @@ -31,10 +31,10 @@ pub trait admin_list_work_visibility { input: AdminWorkVisibilityListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl admin_list_work_visibility for super::RemoteProcedures { input: AdminWorkVisibilityListInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, AdminWorkVisibilityListProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_review_editor_showcase_asset_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_review_editor_showcase_asset_and_return_procedure.rs index 29b2ecf60..71334fc07 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_review_editor_showcase_asset_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_review_editor_showcase_asset_and_return_procedure.rs @@ -34,10 +34,10 @@ pub trait admin_review_editor_showcase_asset_and_return { input: EditorShowcaseAssetAdminReviewInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -47,10 +47,10 @@ impl admin_review_editor_showcase_asset_and_return for super::RemoteProcedures { input: EditorShowcaseAssetAdminReviewInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, EditorShowcaseAssetProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_update_work_visibility_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_update_work_visibility_procedure.rs index bbc85a892..4a88c0841 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_update_work_visibility_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_update_work_visibility_procedure.rs @@ -31,10 +31,10 @@ pub trait admin_update_work_visibility { input: AdminWorkVisibilityUpdateInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl admin_update_work_visibility for super::RemoteProcedures { input: AdminWorkVisibilityUpdateInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, AdminWorkVisibilityProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_invite_code_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_invite_code_procedure.rs index 3601be97f..2411092d2 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_invite_code_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_invite_code_procedure.rs @@ -31,10 +31,10 @@ pub trait admin_upsert_profile_invite_code { input: RuntimeProfileInviteCodeAdminUpsertInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl admin_upsert_profile_invite_code for super::RemoteProcedures { input: RuntimeProfileInviteCodeAdminUpsertInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, RuntimeProfileInviteCodeAdminProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_recharge_product_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_recharge_product_procedure.rs index e3f42278b..83941b836 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_recharge_product_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_recharge_product_procedure.rs @@ -34,10 +34,10 @@ pub trait admin_upsert_profile_recharge_product { input: RuntimeProfileRechargeProductAdminUpsertInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -47,10 +47,10 @@ impl admin_upsert_profile_recharge_product for super::RemoteProcedures { input: RuntimeProfileRechargeProductAdminUpsertInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, RuntimeProfileRechargeProductAdminProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_redeem_code_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_redeem_code_procedure.rs index 7e918220f..9c7ae92f1 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_redeem_code_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_redeem_code_procedure.rs @@ -31,10 +31,10 @@ pub trait admin_upsert_profile_redeem_code { input: RuntimeProfileRedeemCodeAdminUpsertInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl admin_upsert_profile_redeem_code for super::RemoteProcedures { input: RuntimeProfileRedeemCodeAdminUpsertInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, RuntimeProfileRedeemCodeAdminProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_task_config_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_task_config_procedure.rs index a3d3e11a4..b441a8084 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_task_config_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_task_config_procedure.rs @@ -31,10 +31,10 @@ pub trait admin_upsert_profile_task_config { input: RuntimeProfileTaskConfigAdminUpsertInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl admin_upsert_profile_task_config for super::RemoteProcedures { input: RuntimeProfileTaskConfigAdminUpsertInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, RuntimeProfileTaskConfigAdminProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_wallet_config_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_wallet_config_procedure.rs index b87b6506d..46814d669 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_wallet_config_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_wallet_config_procedure.rs @@ -34,10 +34,10 @@ pub trait admin_upsert_profile_wallet_config { input: RuntimeProfileWalletConfigAdminUpsertInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -47,10 +47,10 @@ impl admin_upsert_profile_wallet_config for super::RemoteProcedures { input: RuntimeProfileWalletConfigAdminUpsertInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, RuntimeProfileWalletConfigAdminProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_clear_next_level_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_clear_next_level_procedure.rs index 7ed1e4bc9..a75b35e46 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_clear_next_level_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_clear_next_level_procedure.rs @@ -31,10 +31,10 @@ pub trait advance_puzzle_clear_next_level { input: PuzzleClearRunNextLevelInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl advance_puzzle_clear_next_level for super::RemoteProcedures { input: PuzzleClearRunNextLevelInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, PuzzleClearRunProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_next_level_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_next_level_procedure.rs index 6d3e9f791..7cb4f8f4d 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_next_level_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_next_level_procedure.rs @@ -31,10 +31,10 @@ pub trait advance_puzzle_next_level { input: PuzzleRunNextLevelInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl advance_puzzle_next_level for super::RemoteProcedures { input: PuzzleRunNextLevelInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, PuzzleRunProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/append_ai_text_chunk_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/append_ai_text_chunk_and_return_procedure.rs index 11323392d..191e2ea7c 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/append_ai_text_chunk_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/append_ai_text_chunk_and_return_procedure.rs @@ -31,10 +31,10 @@ pub trait append_ai_text_chunk_and_return { input: AiTextChunkAppendInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl append_ai_text_chunk_and_return for super::RemoteProcedures { input: AiTextChunkAppendInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, AiTaskProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/append_visual_novel_runtime_history_entry_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/append_visual_novel_runtime_history_entry_procedure.rs index 4686ba5ca..ad1099d0f 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/append_visual_novel_runtime_history_entry_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/append_visual_novel_runtime_history_entry_procedure.rs @@ -34,10 +34,10 @@ pub trait append_visual_novel_runtime_history_entry { input: VisualNovelRuntimeHistoryAppendInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -47,10 +47,10 @@ impl append_visual_novel_runtime_history_entry for super::RemoteProcedures { input: VisualNovelRuntimeHistoryAppendInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, VisualNovelHistoryProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/apply_chapter_progression_ledger_entry_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/apply_chapter_progression_ledger_entry_and_return_procedure.rs index bba4c841b..4a9499061 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/apply_chapter_progression_ledger_entry_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/apply_chapter_progression_ledger_entry_and_return_procedure.rs @@ -34,10 +34,10 @@ pub trait apply_chapter_progression_ledger_entry_and_return { input: ChapterProgressionLedgerInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -47,10 +47,10 @@ impl apply_chapter_progression_ledger_entry_and_return for super::RemoteProcedur input: ChapterProgressionLedgerInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, ChapterProgressionProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/apply_chapter_progression_ledger_entry_reducer.rs b/server-rs/crates/spacetime-client/src/module_bindings/apply_chapter_progression_ledger_entry_reducer.rs index 98f821ef9..44596083b 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/apply_chapter_progression_ledger_entry_reducer.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/apply_chapter_progression_ledger_entry_reducer.rs @@ -50,9 +50,11 @@ pub trait apply_chapter_progression_ledger_entry { &self, input: ChapterProgressionLedgerInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()>; } @@ -61,9 +63,11 @@ impl apply_chapter_progression_ledger_entry for super::RemoteReducers { &self, input: ChapterProgressionLedgerInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()> { self.imp.invoke_reducer_with_callback( ApplyChapterProgressionLedgerEntryArgs { input }, diff --git a/server-rs/crates/spacetime-client/src/module_bindings/apply_inventory_mutation_reducer.rs b/server-rs/crates/spacetime-client/src/module_bindings/apply_inventory_mutation_reducer.rs index 91f7d2c06..d9b4240d8 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/apply_inventory_mutation_reducer.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/apply_inventory_mutation_reducer.rs @@ -47,9 +47,11 @@ pub trait apply_inventory_mutation { &self, input: InventoryMutationInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()>; } @@ -58,9 +60,11 @@ impl apply_inventory_mutation for super::RemoteReducers { &self, input: InventoryMutationInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()> { self.imp .invoke_reducer_with_callback(ApplyInventoryMutationArgs { input }, callback) diff --git a/server-rs/crates/spacetime-client/src/module_bindings/apply_quest_signal_reducer.rs b/server-rs/crates/spacetime-client/src/module_bindings/apply_quest_signal_reducer.rs index afb452b55..6b4c310ba 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/apply_quest_signal_reducer.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/apply_quest_signal_reducer.rs @@ -47,9 +47,11 @@ pub trait apply_quest_signal { &self, input: QuestSignalApplyInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()>; } @@ -58,9 +60,11 @@ impl apply_quest_signal for super::RemoteReducers { &self, input: QuestSignalApplyInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()> { self.imp .invoke_reducer_with_callback(ApplyQuestSignalArgs { input }, callback) diff --git a/server-rs/crates/spacetime-client/src/module_bindings/attach_ai_result_reference_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/attach_ai_result_reference_and_return_procedure.rs index 2f3edbe2a..94d418502 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/attach_ai_result_reference_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/attach_ai_result_reference_and_return_procedure.rs @@ -31,10 +31,10 @@ pub trait attach_ai_result_reference_and_return { input: AiResultReferenceInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl attach_ai_result_reference_and_return for super::RemoteProcedures { input: AiResultReferenceInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, AiTaskProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/authenticate_external_api_key_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/authenticate_external_api_key_and_return_procedure.rs index 4c3aec34c..279534f41 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/authenticate_external_api_key_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/authenticate_external_api_key_and_return_procedure.rs @@ -31,10 +31,10 @@ pub trait authenticate_external_api_key_and_return { input: ExternalApiKeyAuthenticateInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl authenticate_external_api_key_and_return for super::RemoteProcedures { input: ExternalApiKeyAuthenticateInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, ExternalApiKeyProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/authorize_database_migration_operator_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/authorize_database_migration_operator_procedure.rs index b58850228..ac77f7e80 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/authorize_database_migration_operator_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/authorize_database_migration_operator_procedure.rs @@ -34,10 +34,10 @@ pub trait authorize_database_migration_operator { input: DatabaseMigrationAuthorizeOperatorInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -47,10 +47,10 @@ impl authorize_database_migration_operator for super::RemoteProcedures { input: DatabaseMigrationAuthorizeOperatorInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, DatabaseMigrationOperatorProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/begin_story_session_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/begin_story_session_and_return_procedure.rs index eef3de0f7..304b2e0c2 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/begin_story_session_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/begin_story_session_and_return_procedure.rs @@ -31,10 +31,10 @@ pub trait begin_story_session_and_return { input: StorySessionInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl begin_story_session_and_return for super::RemoteProcedures { input: StorySessionInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, StorySessionProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/begin_story_session_reducer.rs b/server-rs/crates/spacetime-client/src/module_bindings/begin_story_session_reducer.rs index 6a082f419..22bc4add7 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/begin_story_session_reducer.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/begin_story_session_reducer.rs @@ -47,9 +47,11 @@ pub trait begin_story_session { &self, input: StorySessionInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()>; } @@ -58,9 +60,11 @@ impl begin_story_session for super::RemoteReducers { &self, input: StorySessionInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()> { self.imp .invoke_reducer_with_callback(BeginStorySessionArgs { input }, callback) diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bind_asset_object_to_entity_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/bind_asset_object_to_entity_and_return_procedure.rs index b709d5c2f..78c80aee5 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/bind_asset_object_to_entity_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/bind_asset_object_to_entity_and_return_procedure.rs @@ -31,10 +31,10 @@ pub trait bind_asset_object_to_entity_and_return { input: AssetEntityBindingInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl bind_asset_object_to_entity_and_return for super::RemoteProcedures { input: AssetEntityBindingInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, AssetEntityBindingProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bind_asset_object_to_entity_reducer.rs b/server-rs/crates/spacetime-client/src/module_bindings/bind_asset_object_to_entity_reducer.rs index b20bc5b23..caf48b269 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/bind_asset_object_to_entity_reducer.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/bind_asset_object_to_entity_reducer.rs @@ -47,9 +47,11 @@ pub trait bind_asset_object_to_entity { &self, input: AssetEntityBindingInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()>; } @@ -58,9 +60,11 @@ impl bind_asset_object_to_entity for super::RemoteReducers { &self, input: AssetEntityBindingInput, - callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) - + Send - + 'static, + callback: impl FnOnce( + &super::ReducerEventContext, + Result, __sdk::InternalError>, + ) + Send + + 'static, ) -> __sdk::Result<()> { self.imp .invoke_reducer_with_callback(BindAssetObjectToEntityArgs { input }, callback) diff --git a/server-rs/crates/spacetime-client/src/module_bindings/cancel_ai_task_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/cancel_ai_task_and_return_procedure.rs index b239e0600..0c5dc3eb4 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/cancel_ai_task_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/cancel_ai_task_and_return_procedure.rs @@ -31,10 +31,10 @@ pub trait cancel_ai_task_and_return { input: AiTaskCancelInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl cancel_ai_task_and_return for super::RemoteProcedures { input: AiTaskCancelInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, AiTaskProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/checkpoint_wooden_fish_run_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/checkpoint_wooden_fish_run_procedure.rs index 51aa864f5..5bd5a45c7 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/checkpoint_wooden_fish_run_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/checkpoint_wooden_fish_run_procedure.rs @@ -31,10 +31,10 @@ pub trait checkpoint_wooden_fish_run { input: WoodenFishRunCheckpointInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl checkpoint_wooden_fish_run for super::RemoteProcedures { input: WoodenFishRunCheckpointInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, WoodenFishRunProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/claim_external_generation_jobs_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/claim_external_generation_jobs_and_return_procedure.rs index 6455c7b2e..979ea56cc 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/claim_external_generation_jobs_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/claim_external_generation_jobs_and_return_procedure.rs @@ -31,10 +31,10 @@ pub trait claim_external_generation_jobs_and_return { input: ExternalGenerationJobClaimInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl claim_external_generation_jobs_and_return for super::RemoteProcedures { input: ExternalGenerationJobClaimInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, ExternalGenerationJobProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/claim_profile_recharge_order_expiration_schedule_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/claim_profile_recharge_order_expiration_schedule_and_return_procedure.rs new file mode 100644 index 000000000..93d75e737 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/claim_profile_recharge_order_expiration_schedule_and_return_procedure.rs @@ -0,0 +1,61 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::runtime_profile_recharge_order_expiration_claim_input_type::RuntimeProfileRechargeOrderExpirationClaimInput; +use super::runtime_profile_recharge_order_expiration_claim_procedure_result_type::RuntimeProfileRechargeOrderExpirationClaimProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct ClaimProfileRechargeOrderExpirationScheduleAndReturnArgs { + pub input: RuntimeProfileRechargeOrderExpirationClaimInput, +} + +impl __sdk::InModule for ClaimProfileRechargeOrderExpirationScheduleAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `claim_profile_recharge_order_expiration_schedule_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait claim_profile_recharge_order_expiration_schedule_and_return { + fn claim_profile_recharge_order_expiration_schedule_and_return( + &self, + input: RuntimeProfileRechargeOrderExpirationClaimInput, + ) { + self.claim_profile_recharge_order_expiration_schedule_and_return_then(input, |_, _| {}); + } + + fn claim_profile_recharge_order_expiration_schedule_and_return_then( + &self, + input: RuntimeProfileRechargeOrderExpirationClaimInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl claim_profile_recharge_order_expiration_schedule_and_return for super::RemoteProcedures { + fn claim_profile_recharge_order_expiration_schedule_and_return_then( + &self, + input: RuntimeProfileRechargeOrderExpirationClaimInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp.invoke_procedure_with_callback::<_, RuntimeProfileRechargeOrderExpirationClaimProcedureResult>( + "claim_profile_recharge_order_expiration_schedule_and_return", + ClaimProfileRechargeOrderExpirationScheduleAndReturnArgs { input, }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/claim_profile_task_reward_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/claim_profile_task_reward_and_return_procedure.rs index 5a386f3cd..ea5010707 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/claim_profile_task_reward_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/claim_profile_task_reward_and_return_procedure.rs @@ -31,10 +31,10 @@ pub trait claim_profile_task_reward_and_return { input: RuntimeProfileTaskClaimInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl claim_profile_task_reward_and_return for super::RemoteProcedures { input: RuntimeProfileTaskClaimInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, RuntimeProfileTaskClaimProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/claim_puzzle_background_compile_task_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/claim_puzzle_background_compile_task_procedure.rs index 45b9de6db..e43e713fe 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/claim_puzzle_background_compile_task_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/claim_puzzle_background_compile_task_procedure.rs @@ -31,10 +31,10 @@ pub trait claim_puzzle_background_compile_task { input: PuzzleBackgroundCompileTaskClaimInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl claim_puzzle_background_compile_task for super::RemoteProcedures { input: PuzzleBackgroundCompileTaskClaimInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, PuzzleBackgroundCompileTaskProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/claim_puzzle_work_point_incentive_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/claim_puzzle_work_point_incentive_procedure.rs index f7ac8d756..1d7872222 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/claim_puzzle_work_point_incentive_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/claim_puzzle_work_point_incentive_procedure.rs @@ -31,10 +31,10 @@ pub trait claim_puzzle_work_point_incentive { input: PuzzleWorkPointIncentiveClaimInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl claim_puzzle_work_point_incentive for super::RemoteProcedures { input: PuzzleWorkPointIncentiveClaimInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, PuzzleWorkProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/clear_database_migration_import_chunks_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/clear_database_migration_import_chunks_procedure.rs index 51146e998..d05fcdf27 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/clear_database_migration_import_chunks_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/clear_database_migration_import_chunks_procedure.rs @@ -34,10 +34,10 @@ pub trait clear_database_migration_import_chunks { input: DatabaseMigrationImportChunksClearInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -47,10 +47,10 @@ impl clear_database_migration_import_chunks for super::RemoteProcedures { input: DatabaseMigrationImportChunksClearInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, DatabaseMigrationProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/clear_platform_browse_history_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/clear_platform_browse_history_and_return_procedure.rs index 2e6238453..c8ba8d497 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/clear_platform_browse_history_and_return_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/clear_platform_browse_history_and_return_procedure.rs @@ -31,10 +31,10 @@ pub trait clear_platform_browse_history_and_return { input: RuntimeBrowseHistoryClearInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl clear_platform_browse_history_and_return for super::RemoteProcedures { input: RuntimeBrowseHistoryClearInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, RuntimeBrowseHistoryProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/click_match_3_d_item_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/click_match_3_d_item_procedure.rs index 6070a62d8..278845ba6 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/click_match_3_d_item_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/click_match_3_d_item_procedure.rs @@ -31,10 +31,10 @@ pub trait click_match_3_d_item { input: Match3DRunClickInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl click_match_3_d_item for super::RemoteProcedures { input: Match3DRunClickInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ) { self.imp .invoke_procedure_with_callback::<_, Match3DClickItemProcedureResult>( diff --git a/server-rs/crates/spacetime-client/src/module_bindings/close_profile_recharge_order_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/close_profile_recharge_order_and_return_procedure.rs new file mode 100644 index 000000000..984e99294 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/close_profile_recharge_order_and_return_procedure.rs @@ -0,0 +1,62 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::runtime_profile_recharge_center_procedure_result_type::RuntimeProfileRechargeCenterProcedureResult; +use super::runtime_profile_recharge_order_close_input_type::RuntimeProfileRechargeOrderCloseInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct CloseProfileRechargeOrderAndReturnArgs { + pub input: RuntimeProfileRechargeOrderCloseInput, +} + +impl __sdk::InModule for CloseProfileRechargeOrderAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `close_profile_recharge_order_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait close_profile_recharge_order_and_return { + fn close_profile_recharge_order_and_return( + &self, + input: RuntimeProfileRechargeOrderCloseInput, + ) { + self.close_profile_recharge_order_and_return_then(input, |_, _| {}); + } + + fn close_profile_recharge_order_and_return_then( + &self, + input: RuntimeProfileRechargeOrderCloseInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl close_profile_recharge_order_and_return for super::RemoteProcedures { + fn close_profile_recharge_order_and_return_then( + &self, + input: RuntimeProfileRechargeOrderCloseInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, RuntimeProfileRechargeCenterProcedureResult>( + "close_profile_recharge_order_and_return", + CloseProfileRechargeOrderAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/compile_big_fish_draft_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/compile_big_fish_draft_procedure.rs index 6eea6571a..bafe95a65 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/compile_big_fish_draft_procedure.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/compile_big_fish_draft_procedure.rs @@ -31,10 +31,10 @@ pub trait compile_big_fish_draft { input: BigFishDraftCompileInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result, - ) + Send - + 'static, + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, ); } @@ -44,10 +44,10 @@ impl compile_big_fish_draft for super::RemoteProcedures { input: BigFishDraftCompileInput, __callback: impl FnOnce( - &super::ProcedureEventContext, - Result
{JSON.stringify(detailRow.raw ?? detailRow.cells, null, 2)}