持久化用户历史消费泥点投影
后台用户详情展示历史花费并保留手动对账 消费落账原子更新投影并提供存量初始化 为手动对账增加独立管理员操作权限 更新SpacetimeDB绑定、文档和测试
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
getAdminFeatureGateConfig,
|
||||
getAdminUserDetail,
|
||||
listAdminRechargeOrders,
|
||||
reconcileAdminUserConsumption,
|
||||
resolveAdminRechargeRefundManualReview,
|
||||
updateAdminAccount,
|
||||
uploadAdminEditorShowcaseCampaignImage,
|
||||
@@ -16,7 +17,7 @@ afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test('后台账号创建和更新携带 owner 会话与 Tab 权限', async () => {
|
||||
test('后台账号创建和更新同时携带 Tab 与独立操作权限', async () => {
|
||||
const fetchMock = vi.fn().mockImplementation(() =>
|
||||
Promise.resolve(
|
||||
new Response(JSON.stringify({ account: { accountId: 'member-1' } }), {
|
||||
@@ -31,11 +32,13 @@ test('后台账号创建和更新携带 owner 会话与 Tab 权限', async () =>
|
||||
displayName: '运营',
|
||||
password: 'secret123',
|
||||
tabPermissions: ['dashboard', 'tracking'],
|
||||
actionPermissions: ['profile-wallet-consumption-reconcile'],
|
||||
enabled: true,
|
||||
});
|
||||
await updateAdminAccount('owner-token', 'member/1', {
|
||||
displayName: '运营二组',
|
||||
tabPermissions: ['tracking'],
|
||||
actionPermissions: [],
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
@@ -44,6 +47,14 @@ test('后台账号创建和更新携带 owner 会话与 Tab 权限', async () =>
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: expect.objectContaining({ Authorization: 'Bearer owner-token' }),
|
||||
body: JSON.stringify({
|
||||
username: 'operator',
|
||||
displayName: '运营',
|
||||
password: 'secret123',
|
||||
tabPermissions: ['dashboard', 'tracking'],
|
||||
actionPermissions: ['profile-wallet-consumption-reconcile'],
|
||||
enabled: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(fetchMock.mock.calls[1]?.[0]).toBe('/admin/api/accounts/member%2F1');
|
||||
@@ -53,6 +64,7 @@ test('后台账号创建和更新携带 owner 会话与 Tab 权限', async () =>
|
||||
body: JSON.stringify({
|
||||
displayName: '运营二组',
|
||||
tabPermissions: ['tracking'],
|
||||
actionPermissions: [],
|
||||
enabled: false,
|
||||
}),
|
||||
}),
|
||||
@@ -238,6 +250,33 @@ test('用户详情只发送实际提供的用户定位字段', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('历史花费手动对账使用独立管理员写接口', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
userId: 'user-1',
|
||||
historicalConsumedPoints: 1300,
|
||||
changed: true,
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await reconcileAdminUserConsumption('token-1', { userId: 'user-1' });
|
||||
|
||||
expect(String(fetchMock.mock.calls[0]?.[0])).toBe(
|
||||
'/admin/api/profile/users/reconcile-consumption',
|
||||
);
|
||||
expect(fetchMock.mock.calls[0]?.[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: expect.objectContaining({ Authorization: 'Bearer token-1' }),
|
||||
body: JSON.stringify({ userId: 'user-1' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('退款执行使用独立 execute 管理员路由', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ outRefundNo: 'refund-1' }), {
|
||||
|
||||
@@ -48,6 +48,8 @@ import type {
|
||||
AdminUpsertProfileRedeemCodeRequest,
|
||||
AdminUpsertProfileTaskConfigRequest,
|
||||
AdminUpsertProfileWalletConfigRequest,
|
||||
AdminUserConsumptionReconcileRequest,
|
||||
AdminUserConsumptionReconcileResponse,
|
||||
AdminUserDetailQuery,
|
||||
AdminUserDetailResponse,
|
||||
AdminWalletRestrictionRequest,
|
||||
@@ -577,6 +579,16 @@ export function getAdminUserDetail(token: string, query: AdminUserDetailQuery) {
|
||||
);
|
||||
}
|
||||
|
||||
export function reconcileAdminUserConsumption(
|
||||
token: string,
|
||||
payload: AdminUserConsumptionReconcileRequest,
|
||||
) {
|
||||
return request<AdminUserConsumptionReconcileResponse>(
|
||||
'/admin/api/profile/users/reconcile-consumption',
|
||||
{ method: 'POST', token, body: payload },
|
||||
);
|
||||
}
|
||||
|
||||
export function previewAdminRechargeRefund(
|
||||
token: string,
|
||||
payload: AdminRechargeRefundPreviewRequest,
|
||||
|
||||
@@ -38,6 +38,7 @@ export interface AdminSessionPayload {
|
||||
roles: string[];
|
||||
accountRole: 'owner' | 'member';
|
||||
tabPermissions: string[];
|
||||
actionPermissions: string[];
|
||||
issuedAt: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
@@ -48,6 +49,7 @@ export interface AdminAccountPayload {
|
||||
displayName: string;
|
||||
accountRole: 'owner' | 'member';
|
||||
tabPermissions: string[];
|
||||
actionPermissions: string[];
|
||||
enabled: boolean;
|
||||
tokenVersion: number;
|
||||
createdBy: string;
|
||||
@@ -65,6 +67,7 @@ export interface AdminCreateAccountRequest {
|
||||
displayName: string;
|
||||
password: string;
|
||||
tabPermissions: string[];
|
||||
actionPermissions: string[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
@@ -76,6 +79,7 @@ export interface AdminUpdateAccountRequest {
|
||||
displayName: string;
|
||||
password?: string;
|
||||
tabPermissions: string[];
|
||||
actionPermissions: string[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
@@ -819,10 +823,24 @@ export interface AdminUserDetailResponse {
|
||||
bindingStatus: string;
|
||||
phoneBound: boolean;
|
||||
wechatBound: boolean;
|
||||
historicalConsumedPoints: number;
|
||||
canReconcileConsumption: boolean;
|
||||
wallet: AdminProfileWalletPayload;
|
||||
rechargeOrders: AdminRechargeOrderEntryPayload[];
|
||||
}
|
||||
|
||||
export interface AdminUserConsumptionReconcileRequest {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface AdminUserConsumptionReconcileResponse {
|
||||
userId: string;
|
||||
previousHistoricalConsumedPoints?: number | null;
|
||||
historicalConsumedPoints: number;
|
||||
changed: boolean;
|
||||
reconciledAtMicros: number;
|
||||
}
|
||||
|
||||
export interface AdminRechargeRefundPreviewRequest {
|
||||
orderId: string;
|
||||
refundAmountCents: number;
|
||||
|
||||
Reference in New Issue
Block a user