2026-03-19 14:19:01 +08:00
|
|
|
|
/**
|
2026-03-23 12:41:35 +08:00
|
|
|
|
* [MOCK] 用户资产数据<EFBFBD><EFBFBD>? * AI注意: 这是Mock实现,不是真实业务逻辑
|
|
|
|
|
|
* 仅在USE_MOCK=true时启<EFBFBD><EFBFBD>? */
|
2026-03-19 14:19:01 +08:00
|
|
|
|
|
|
|
|
|
|
export type MemberLevel = 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINUM' | 'DIAMOND';
|
|
|
|
|
|
|
|
|
|
|
|
export interface UserAsset {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
tenantId: string;
|
|
|
|
|
|
userId: string;
|
|
|
|
|
|
userName: string;
|
|
|
|
|
|
email: string;
|
2026-03-23 12:41:35 +08:00
|
|
|
|
userEmail?: string; // <20><>?email 同义
|
2026-03-19 14:19:01 +08:00
|
|
|
|
memberLevel: MemberLevel;
|
|
|
|
|
|
points: number;
|
2026-03-23 12:41:35 +08:00
|
|
|
|
memberScore?: number; // <20><>?points 同义
|
2026-03-19 14:19:01 +08:00
|
|
|
|
totalSpent: number;
|
|
|
|
|
|
availableBalance: number;
|
2026-03-23 12:41:35 +08:00
|
|
|
|
availablePoints?: number; // <20><>?availableBalance 同义
|
2026-03-19 14:19:01 +08:00
|
|
|
|
frozenBalance: number;
|
2026-03-23 12:41:35 +08:00
|
|
|
|
frozenPoints?: number; // <20><>?frozenBalance 同义
|
2026-03-20 17:53:46 +08:00
|
|
|
|
cashbackBalance?: number;
|
|
|
|
|
|
couponCount?: number;
|
2026-03-19 14:19:01 +08:00
|
|
|
|
totalOrders: number;
|
|
|
|
|
|
joinDate: string;
|
|
|
|
|
|
lastActiveDate: string;
|
|
|
|
|
|
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface MemberLevelConfig {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
level: MemberLevel;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
minSpent: number;
|
|
|
|
|
|
discount: number;
|
|
|
|
|
|
pointsMultiplier: number;
|
|
|
|
|
|
benefits: string[];
|
|
|
|
|
|
color: string;
|
|
|
|
|
|
icon: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-20 17:53:46 +08:00
|
|
|
|
export type PointsSourceType =
|
|
|
|
|
|
| 'PURCHASE'
|
|
|
|
|
|
| 'REVIEW'
|
|
|
|
|
|
| 'REFERRAL'
|
|
|
|
|
|
| 'PROMOTION'
|
|
|
|
|
|
| 'ADMIN_ADJUST'
|
|
|
|
|
|
| 'EXPIRED'
|
|
|
|
|
|
| 'REFUND'
|
|
|
|
|
|
| 'MANUAL_DEDUCT';
|
|
|
|
|
|
|
|
|
|
|
|
export type PointsStatus = 'PENDING' | 'CONFIRMED' | 'FROZEN' | 'EXPIRED' | 'CANCELLED';
|
|
|
|
|
|
|
2026-03-19 14:19:01 +08:00
|
|
|
|
export interface PointsRecord {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
userId: string;
|
|
|
|
|
|
userName: string;
|
|
|
|
|
|
type: 'EARN' | 'REDEEM' | 'EXPIRE' | 'ADJUST';
|
|
|
|
|
|
amount: number;
|
|
|
|
|
|
balance: number;
|
|
|
|
|
|
source: string;
|
|
|
|
|
|
description: string;
|
|
|
|
|
|
createdAt: string;
|
2026-03-23 12:41:35 +08:00
|
|
|
|
// 额外字段,用<EFBC8C><E794A8>?PointsManage 页面
|
2026-03-20 17:53:46 +08:00
|
|
|
|
tenantId?: string;
|
|
|
|
|
|
shopId?: string;
|
|
|
|
|
|
traceId?: string;
|
|
|
|
|
|
businessType?: 'TOC' | 'TOB';
|
2026-03-23 12:41:35 +08:00
|
|
|
|
points?: number; // <20><>?amount 同义
|
|
|
|
|
|
sourceType?: string; // <20><>?source 同义
|
2026-03-20 17:53:46 +08:00
|
|
|
|
status?: 'PENDING' | 'CONFIRMED' | 'FROZEN' | 'EXPIRED' | 'CANCELLED';
|
|
|
|
|
|
expiredAt?: string;
|
|
|
|
|
|
sourceId?: string;
|
2026-03-19 14:19:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface IUserAssetDataSource {
|
|
|
|
|
|
fetchUserAssets(params?: { memberLevel?: string; status?: string; search?: string }): Promise<UserAsset[]>;
|
|
|
|
|
|
updateUserAsset(id: string, data: Partial<UserAsset>): Promise<UserAsset>;
|
|
|
|
|
|
|
|
|
|
|
|
fetchMemberLevelConfigs(): Promise<MemberLevelConfig[]>;
|
|
|
|
|
|
updateMemberLevelConfig(id: string, data: Partial<MemberLevelConfig>): Promise<MemberLevelConfig>;
|
|
|
|
|
|
|
|
|
|
|
|
fetchPointsRecords(params?: { userId?: string; type?: string }): Promise<PointsRecord[]>;
|
|
|
|
|
|
adjustPoints(userId: string, amount: number, reason: string): Promise<PointsRecord>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class MockUserAssetDataSource implements IUserAssetDataSource {
|
|
|
|
|
|
private userAssets: UserAsset[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: '1',
|
|
|
|
|
|
tenantId: 'tenant_001',
|
|
|
|
|
|
userId: 'user_001',
|
|
|
|
|
|
userName: 'John Doe',
|
|
|
|
|
|
email: 'john@example.com',
|
|
|
|
|
|
memberLevel: 'GOLD',
|
|
|
|
|
|
points: 5000,
|
|
|
|
|
|
totalSpent: 15000,
|
|
|
|
|
|
availableBalance: 500,
|
|
|
|
|
|
frozenBalance: 100,
|
|
|
|
|
|
totalOrders: 25,
|
|
|
|
|
|
joinDate: '2025-01-15',
|
|
|
|
|
|
lastActiveDate: '2026-03-18',
|
|
|
|
|
|
status: 'ACTIVE',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: '2',
|
|
|
|
|
|
tenantId: 'tenant_001',
|
|
|
|
|
|
userId: 'user_002',
|
|
|
|
|
|
userName: 'Jane Smith',
|
|
|
|
|
|
email: 'jane@example.com',
|
|
|
|
|
|
memberLevel: 'PLATINUM',
|
|
|
|
|
|
points: 15000,
|
|
|
|
|
|
totalSpent: 50000,
|
|
|
|
|
|
availableBalance: 1200,
|
|
|
|
|
|
frozenBalance: 0,
|
|
|
|
|
|
totalOrders: 85,
|
|
|
|
|
|
joinDate: '2024-06-20',
|
|
|
|
|
|
lastActiveDate: '2026-03-19',
|
|
|
|
|
|
status: 'ACTIVE',
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
private memberLevelConfigs: MemberLevelConfig[] = [
|
|
|
|
|
|
{ id: '1', level: 'BRONZE', name: 'Bronze', minSpent: 0, discount: 0, pointsMultiplier: 1, benefits: ['Basic support'], color: '#CD7F32', icon: '🥉' },
|
|
|
|
|
|
{ id: '2', level: 'SILVER', name: 'Silver', minSpent: 1000, discount: 5, pointsMultiplier: 1.2, benefits: ['Priority support', '5% discount'], color: '#C0C0C0', icon: '🥈' },
|
|
|
|
|
|
{ id: '3', level: 'GOLD', name: 'Gold', minSpent: 5000, discount: 10, pointsMultiplier: 1.5, benefits: ['VIP support', '10% discount', 'Free shipping'], color: '#FFD700', icon: '🥇' },
|
|
|
|
|
|
{ id: '4', level: 'PLATINUM', name: 'Platinum', minSpent: 20000, discount: 15, pointsMultiplier: 2, benefits: ['Dedicated support', '15% discount', 'Free express shipping', 'Birthday gift'], color: '#E5E4E2', icon: '💎' },
|
|
|
|
|
|
{ id: '5', level: 'DIAMOND', name: 'Diamond', minSpent: 50000, discount: 20, pointsMultiplier: 3, benefits: ['Personal account manager', '20% discount', 'Exclusive events'], color: '#B9F2FF', icon: '💠' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
private pointsRecords: PointsRecord[] = [
|
2026-03-20 17:53:46 +08:00
|
|
|
|
{
|
|
|
|
|
|
id: '1',
|
|
|
|
|
|
userId: 'user_001',
|
|
|
|
|
|
userName: 'John Doe',
|
|
|
|
|
|
type: 'EARN',
|
|
|
|
|
|
amount: 100,
|
|
|
|
|
|
balance: 5000,
|
|
|
|
|
|
source: 'Purchase',
|
|
|
|
|
|
description: 'Order #ORD-001',
|
|
|
|
|
|
createdAt: '2026-03-15',
|
|
|
|
|
|
// 额外字段
|
|
|
|
|
|
tenantId: 'T001',
|
|
|
|
|
|
shopId: 'S001',
|
|
|
|
|
|
traceId: 'TR001',
|
|
|
|
|
|
businessType: 'TOC',
|
|
|
|
|
|
points: 100,
|
|
|
|
|
|
sourceType: 'PURCHASE',
|
|
|
|
|
|
status: 'CONFIRMED'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: '2',
|
|
|
|
|
|
userId: 'user_001',
|
|
|
|
|
|
userName: 'John Doe',
|
|
|
|
|
|
type: 'REDEEM',
|
|
|
|
|
|
amount: -50,
|
|
|
|
|
|
balance: 4950,
|
|
|
|
|
|
source: 'Redemption',
|
|
|
|
|
|
description: 'Coupon redemption',
|
|
|
|
|
|
createdAt: '2026-03-16',
|
|
|
|
|
|
// 额外字段
|
|
|
|
|
|
tenantId: 'T001',
|
|
|
|
|
|
shopId: 'S001',
|
|
|
|
|
|
traceId: 'TR002',
|
|
|
|
|
|
businessType: 'TOC',
|
|
|
|
|
|
points: -50,
|
|
|
|
|
|
sourceType: 'MANUAL_DEDUCT',
|
|
|
|
|
|
status: 'CONFIRMED'
|
|
|
|
|
|
},
|
2026-03-19 14:19:01 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
async fetchUserAssets(params?: { memberLevel?: string; status?: string; search?: string }): Promise<UserAsset[]> {
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
|
|
let result = [...this.userAssets];
|
|
|
|
|
|
if (params?.memberLevel) {
|
|
|
|
|
|
result = result.filter(u => u.memberLevel === params.memberLevel);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (params?.status) {
|
|
|
|
|
|
result = result.filter(u => u.status === params.status);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (params?.search) {
|
|
|
|
|
|
result = result.filter(u => u.userName.toLowerCase().includes(params.search!.toLowerCase()) || u.email.toLowerCase().includes(params.search!.toLowerCase()));
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async updateUserAsset(id: string, data: Partial<UserAsset>): Promise<UserAsset> {
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
|
|
const index = this.userAssets.findIndex(u => u.id === id);
|
|
|
|
|
|
if (index === -1) throw new Error('User asset not found');
|
|
|
|
|
|
this.userAssets[index] = { ...this.userAssets[index], ...data };
|
|
|
|
|
|
return this.userAssets[index];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async fetchMemberLevelConfigs(): Promise<MemberLevelConfig[]> {
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
|
|
return [...this.memberLevelConfigs];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async updateMemberLevelConfig(id: string, data: Partial<MemberLevelConfig>): Promise<MemberLevelConfig> {
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
|
|
const index = this.memberLevelConfigs.findIndex(c => c.id === id);
|
|
|
|
|
|
if (index === -1) throw new Error('Config not found');
|
|
|
|
|
|
this.memberLevelConfigs[index] = { ...this.memberLevelConfigs[index], ...data };
|
|
|
|
|
|
return this.memberLevelConfigs[index];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async fetchPointsRecords(params?: { userId?: string; type?: string }): Promise<PointsRecord[]> {
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
|
|
let result = [...this.pointsRecords];
|
|
|
|
|
|
if (params?.userId) {
|
|
|
|
|
|
result = result.filter(r => r.userId === params.userId);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (params?.type) {
|
|
|
|
|
|
result = result.filter(r => r.type === params.type);
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async adjustPoints(userId: string, amount: number, reason: string): Promise<PointsRecord> {
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
|
|
const user = this.userAssets.find(u => u.userId === userId);
|
|
|
|
|
|
if (!user) throw new Error('User not found');
|
|
|
|
|
|
|
|
|
|
|
|
const newBalance = user.points + amount;
|
|
|
|
|
|
const record: PointsRecord = {
|
|
|
|
|
|
id: `${Date.now()}`,
|
|
|
|
|
|
userId,
|
|
|
|
|
|
userName: user.userName,
|
|
|
|
|
|
type: amount > 0 ? 'EARN' : 'REDEEM',
|
|
|
|
|
|
amount,
|
|
|
|
|
|
balance: newBalance,
|
|
|
|
|
|
source: 'Manual Adjustment',
|
|
|
|
|
|
description: reason,
|
|
|
|
|
|
createdAt: new Date().toISOString().split('T')[0],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
user.points = newBalance;
|
|
|
|
|
|
this.pointsRecords.push(record);
|
|
|
|
|
|
return record;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class ApiUserAssetDataSource implements IUserAssetDataSource {
|
|
|
|
|
|
private baseUrl = '/api/user-assets';
|
|
|
|
|
|
|
|
|
|
|
|
async fetchUserAssets(params?: { memberLevel?: string; status?: string; search?: string }): Promise<UserAsset[]> {
|
|
|
|
|
|
const response = await fetch(`${this.baseUrl}?${new URLSearchParams(params as any)}`);
|
|
|
|
|
|
if (!response.ok) throw new Error('Failed to fetch user assets');
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async updateUserAsset(id: string, data: Partial<UserAsset>): Promise<UserAsset> {
|
|
|
|
|
|
const response = await fetch(`${this.baseUrl}/${id}`, {
|
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!response.ok) throw new Error('Failed to update user asset');
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async fetchMemberLevelConfigs(): Promise<MemberLevelConfig[]> {
|
|
|
|
|
|
const response = await fetch(`${this.baseUrl}/member-levels`);
|
|
|
|
|
|
if (!response.ok) throw new Error('Failed to fetch member level configs');
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async updateMemberLevelConfig(id: string, data: Partial<MemberLevelConfig>): Promise<MemberLevelConfig> {
|
|
|
|
|
|
const response = await fetch(`${this.baseUrl}/member-levels/${id}`, {
|
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!response.ok) throw new Error('Failed to update member level config');
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async fetchPointsRecords(params?: { userId?: string; type?: string }): Promise<PointsRecord[]> {
|
|
|
|
|
|
const response = await fetch(`${this.baseUrl}/points?${new URLSearchParams(params as any)}`);
|
|
|
|
|
|
if (!response.ok) throw new Error('Failed to fetch points records');
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async adjustPoints(userId: string, amount: number, reason: string): Promise<PointsRecord> {
|
|
|
|
|
|
const response = await fetch(`${this.baseUrl}/points/adjust`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({ userId, amount, reason }),
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!response.ok) throw new Error('Failed to adjust points');
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-23 12:41:35 +08:00
|
|
|
|
const useMock = process.env.NODE_ENV === 'development' || process.env.REACT_APP_USE_MOCK === 'true';
|
2026-03-19 14:19:01 +08:00
|
|
|
|
export const userAssetDataSource: IUserAssetDataSource = useMock
|
|
|
|
|
|
? new MockUserAssetDataSource()
|
|
|
|
|
|
: new ApiUserAssetDataSource();
|