|
|
@@ -20,6 +20,28 @@ interface PriceTier {
|
|
|
price: number;
|
|
|
}
|
|
|
|
|
|
+interface WorkshopAllocation {
|
|
|
+ apigId: string;
|
|
|
+ title: string;
|
|
|
+ count: number;
|
|
|
+ authId?: string;
|
|
|
+ oldCount?: number;
|
|
|
+ balance?: number;
|
|
|
+}
|
|
|
+
|
|
|
+interface WorkshopPackage {
|
|
|
+ slug: string;
|
|
|
+ title: string;
|
|
|
+ badge: string;
|
|
|
+ subtitle: string;
|
|
|
+ scene: string;
|
|
|
+ price: number;
|
|
|
+ visible: boolean;
|
|
|
+ primaryApigId: string;
|
|
|
+ allocations: WorkshopAllocation[];
|
|
|
+ includes: string[];
|
|
|
+}
|
|
|
+
|
|
|
interface ApigData {
|
|
|
objectId: string;
|
|
|
title: string;
|
|
|
@@ -32,7 +54,102 @@ interface ApigData {
|
|
|
}
|
|
|
|
|
|
// VOC 技能体系相关的 APIG — 一键切换
|
|
|
-const VOC_APIG_IDS = ['Vo3ROWEvDy', '7HwdQZk55B'];
|
|
|
+const SOCIAL_APIG_ID = 'Vo3ROWEvDy';
|
|
|
+const ECOMMERCE_APIG_ID = '7HwdQZk55B';
|
|
|
+const VOC_APIG_IDS = [SOCIAL_APIG_ID, ECOMMERCE_APIG_ID];
|
|
|
+
|
|
|
+const WORKSHOP_PACKAGES: Record<string, WorkshopPackage> = {
|
|
|
+ '19-9': {
|
|
|
+ slug: '19-9',
|
|
|
+ title: '课程特惠体验包',
|
|
|
+ badge: '课程专享',
|
|
|
+ subtitle: '给课程学员使用的课堂练习包,适合跟着老师完成基础实操。',
|
|
|
+ scene: '课程学习',
|
|
|
+ price: 19.9,
|
|
|
+ visible: false,
|
|
|
+ primaryApigId: SOCIAL_APIG_ID,
|
|
|
+ allocations: [
|
|
|
+ { apigId: SOCIAL_APIG_ID, title: '国内外社媒数据中台', count: 100 },
|
|
|
+ { apigId: ECOMMERCE_APIG_ID, title: '电商数据中台', count: 100 }
|
|
|
+ ],
|
|
|
+ includes: ['课堂练习额度', '基础情报官模板', '不含复盘服务']
|
|
|
+ },
|
|
|
+ '29-9': {
|
|
|
+ slug: '29-9',
|
|
|
+ title: '课程加量体验包',
|
|
|
+ badge: '课程专享',
|
|
|
+ subtitle: '给课程学员使用的加量练习包,适合课上多跑几轮真实案例。',
|
|
|
+ scene: '课程学习',
|
|
|
+ price: 29.9,
|
|
|
+ visible: false,
|
|
|
+ primaryApigId: SOCIAL_APIG_ID,
|
|
|
+ allocations: [
|
|
|
+ { apigId: SOCIAL_APIG_ID, title: '国内外社媒数据中台', count: 150 },
|
|
|
+ { apigId: ECOMMERCE_APIG_ID, title: '电商数据中台', count: 150 }
|
|
|
+ ],
|
|
|
+ includes: ['课堂练习额度', '基础情报官模板', '不含复盘服务']
|
|
|
+ },
|
|
|
+ '99': {
|
|
|
+ slug: '99',
|
|
|
+ title: '纯享练习包',
|
|
|
+ badge: '低门槛',
|
|
|
+ subtitle: '适合公域课程和社群用户自己练习,不附加人工服务。',
|
|
|
+ scene: '自助学习',
|
|
|
+ price: 99,
|
|
|
+ visible: true,
|
|
|
+ primaryApigId: SOCIAL_APIG_ID,
|
|
|
+ allocations: [
|
|
|
+ { apigId: SOCIAL_APIG_ID, title: '国内外社媒数据中台', count: 500 },
|
|
|
+ { apigId: ECOMMERCE_APIG_ID, title: '电商数据中台', count: 500 }
|
|
|
+ ],
|
|
|
+ includes: ['自助练习额度', '情报官模板', '不含答疑/复盘']
|
|
|
+ },
|
|
|
+ '299': {
|
|
|
+ slug: '299',
|
|
|
+ title: '现场实训包',
|
|
|
+ badge: '现场实操',
|
|
|
+ subtitle: '适合线下课程后继续实操,现场开通额度并训练基础情报官。',
|
|
|
+ scene: '线下课程',
|
|
|
+ price: 299,
|
|
|
+ visible: true,
|
|
|
+ primaryApigId: SOCIAL_APIG_ID,
|
|
|
+ allocations: [
|
|
|
+ { apigId: SOCIAL_APIG_ID, title: '国内外社媒数据中台', count: 1000 },
|
|
|
+ { apigId: ECOMMERCE_APIG_ID, title: '电商数据中台', count: 1000 }
|
|
|
+ ],
|
|
|
+ includes: ['基础情报官训练', '龙虾场景案例测试', '使用模板']
|
|
|
+ },
|
|
|
+ '499': {
|
|
|
+ slug: '499',
|
|
|
+ title: '情报官实战包',
|
|
|
+ badge: '实战训练',
|
|
|
+ subtitle: '基于龙虾业务经验训练 1 个情报官角色,现场完成真实任务测试。',
|
|
|
+ scene: '企业实训',
|
|
|
+ price: 499,
|
|
|
+ visible: false,
|
|
|
+ primaryApigId: SOCIAL_APIG_ID,
|
|
|
+ allocations: [
|
|
|
+ { apigId: SOCIAL_APIG_ID, title: '国内外社媒数据中台', count: 2000 },
|
|
|
+ { apigId: ECOMMERCE_APIG_ID, title: '电商数据中台', count: 2000 }
|
|
|
+ ],
|
|
|
+ includes: ['情报官角色设定', '龙虾业务场景调优', '3 个真实任务测试', '使用 SOP 模板']
|
|
|
+ },
|
|
|
+ '699': {
|
|
|
+ slug: '699',
|
|
|
+ title: '情报官团队包',
|
|
|
+ badge: '交付服务',
|
|
|
+ subtitle: '适合想直接解决问题的重度用户,含复盘或辅助产出一份专业报告。',
|
|
|
+ scene: '企业/重度用户',
|
|
|
+ price: 699,
|
|
|
+ visible: true,
|
|
|
+ primaryApigId: SOCIAL_APIG_ID,
|
|
|
+ allocations: [
|
|
|
+ { apigId: SOCIAL_APIG_ID, title: '国内外社媒数据中台', count: 3000 },
|
|
|
+ { apigId: ECOMMERCE_APIG_ID, title: '电商数据中台', count: 3000 }
|
|
|
+ ],
|
|
|
+ includes: ['情报官角色设定', '龙虾业务场景调优', '团队共用额度', '1 次线上复盘', '情报官模板二次优化']
|
|
|
+ }
|
|
|
+};
|
|
|
|
|
|
interface OrderData {
|
|
|
objectId: string;
|
|
|
@@ -54,12 +171,16 @@ export class App implements OnInit {
|
|
|
@ViewChild('qrCanvas', { static: false }) qrCanvasRef!: ElementRef<HTMLCanvasElement>;
|
|
|
|
|
|
constructor(private cdr: ChangeDetectorRef) {}
|
|
|
+ private readonly routeChangeHandler = () => this.syncWorkshopRouteFromUrl();
|
|
|
|
|
|
// URL params
|
|
|
authId = '';
|
|
|
userId = '';
|
|
|
apigId = '';
|
|
|
funId = DEFAULT_FUN_ID;
|
|
|
+ workshopPackage: WorkshopPackage | null = null;
|
|
|
+ workshopPortal = false;
|
|
|
+ workshopPreparing = false;
|
|
|
|
|
|
// Login state
|
|
|
needLogin = false;
|
|
|
@@ -94,10 +215,29 @@ export class App implements OnInit {
|
|
|
orderSkip = 0;
|
|
|
|
|
|
get selectedTier(): PriceTier | null {
|
|
|
+ if (this.workshopPackage) {
|
|
|
+ return { price: this.workshopPackage.price, count: this.workshopTotalCount };
|
|
|
+ }
|
|
|
if (!this.apig?.priceStep || this.apig.priceStep.length === 0) return null;
|
|
|
return this.apig.priceStep[this.selectedIndex] || null;
|
|
|
}
|
|
|
|
|
|
+ get workshopTotalCount(): number {
|
|
|
+ return this.workshopPackage?.allocations.reduce((sum, item) => sum + item.count, 0) || 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ get visibleWorkshopPackages(): WorkshopPackage[] {
|
|
|
+ return Object.values(WORKSHOP_PACKAGES).filter(pkg => pkg.visible);
|
|
|
+ }
|
|
|
+
|
|
|
+ get isWorkshopMode(): boolean {
|
|
|
+ return !!this.workshopPackage || this.workshopPortal;
|
|
|
+ }
|
|
|
+
|
|
|
+ get pageSubtitle(): string {
|
|
|
+ return this.isWorkshopMode ? '情报官技能套餐' : 'API 服务充值';
|
|
|
+ }
|
|
|
+
|
|
|
get unitLabel(): string {
|
|
|
if (!this.apig) return '次';
|
|
|
return (this.apig.objectId === 'MYM5zJBKgw' || this.apig.objectId === 'FQtTgjcqIZ') ? 'token' : '次';
|
|
|
@@ -105,9 +245,12 @@ export class App implements OnInit {
|
|
|
|
|
|
ngOnInit(): void {
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
+ this.syncWorkshopRouteFromUrl(false);
|
|
|
+ window.addEventListener('hashchange', this.routeChangeHandler);
|
|
|
+ window.addEventListener('popstate', this.routeChangeHandler);
|
|
|
this.authId = params.get('authid') || '';
|
|
|
this.userId = params.get('user') || params.get('userid') || '';
|
|
|
- this.apigId = params.get('apigid') || 'Vo3ROWEvDy';
|
|
|
+ this.apigId = params.get('apigid') || this.workshopPackage?.primaryApigId || SOCIAL_APIG_ID;
|
|
|
this.funId = params.get('fun_id') || DEFAULT_FUN_ID;
|
|
|
const urlToken = params.get('token') || '';
|
|
|
|
|
|
@@ -144,6 +287,11 @@ export class App implements OnInit {
|
|
|
this.needLogin = true;
|
|
|
}
|
|
|
|
|
|
+ ngOnDestroy(): void {
|
|
|
+ window.removeEventListener('hashchange', this.routeChangeHandler);
|
|
|
+ window.removeEventListener('popstate', this.routeChangeHandler);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 通过 URL 中的 token 调用 Parse Server 的 become(fetch /parse/users/me)完成登录
|
|
|
*/
|
|
|
@@ -315,6 +463,7 @@ export class App implements OnInit {
|
|
|
}
|
|
|
|
|
|
this.apigList = apigs;
|
|
|
+ this.syncWorkshopAllocationsFromList();
|
|
|
console.log('[APIG] 获取到', apigs.length, '个可用 API');
|
|
|
this.cdr.detectChanges();
|
|
|
} catch (e: any) {
|
|
|
@@ -397,6 +546,7 @@ export class App implements OnInit {
|
|
|
this.selectTier(0);
|
|
|
this.cdr.detectChanges();
|
|
|
this.loadOrderHistory();
|
|
|
+ this.loadApigList();
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
@@ -478,13 +628,177 @@ export class App implements OnInit {
|
|
|
this.selectedIndex = idx;
|
|
|
}
|
|
|
|
|
|
+ syncWorkshopRouteFromUrl(shouldDetectChanges = true): void {
|
|
|
+ const params = new URLSearchParams(window.location.search);
|
|
|
+ const nextPackage = this.resolveWorkshopPackage(params);
|
|
|
+ this.workshopPackage = nextPackage;
|
|
|
+ this.workshopPortal = !nextPackage && this.isWorkshopPortal(params);
|
|
|
+
|
|
|
+ if (nextPackage) {
|
|
|
+ this.apigId = nextPackage.primaryApigId;
|
|
|
+ this.authId = '';
|
|
|
+ this.selectedIndex = 0;
|
|
|
+ this.showSuccess = false;
|
|
|
+ this.errorMsg = '';
|
|
|
+ this.syncWorkshopAllocationsFromList();
|
|
|
+ } else if (this.workshopPortal) {
|
|
|
+ this.showSuccess = false;
|
|
|
+ this.errorMsg = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (shouldDetectChanges) this.cdr.detectChanges();
|
|
|
+ }
|
|
|
+
|
|
|
+ getRouteParts(): string[] {
|
|
|
+ const pathParts = window.location.pathname.toLowerCase().split('/').filter(Boolean);
|
|
|
+ const hashParts = window.location.hash.replace(/^#\/?/, '').toLowerCase().split('/').filter(Boolean);
|
|
|
+ return [...pathParts, ...hashParts];
|
|
|
+ }
|
|
|
+
|
|
|
+ isWorkshopPortal(params: URLSearchParams): boolean {
|
|
|
+ const parts = this.getRouteParts();
|
|
|
+ return parts.includes('workshop') ||
|
|
|
+ parts.includes('packages') ||
|
|
|
+ params.get('workshop') === '1' ||
|
|
|
+ params.get('packages') === '1';
|
|
|
+ }
|
|
|
+
|
|
|
+ resolveWorkshopPackage(params: URLSearchParams): WorkshopPackage | null {
|
|
|
+ const candidates = [
|
|
|
+ params.get('workshop'),
|
|
|
+ params.get('package'),
|
|
|
+ params.get('pkg'),
|
|
|
+ ...this.getRouteParts()
|
|
|
+ ].filter((item): item is string => !!item);
|
|
|
+ const slug = candidates.find(item => !!WORKSHOP_PACKAGES[item]);
|
|
|
+ if (!slug) return null;
|
|
|
+ const pkg = WORKSHOP_PACKAGES[slug];
|
|
|
+ if (!pkg) return null;
|
|
|
+ return {
|
|
|
+ ...pkg,
|
|
|
+ allocations: pkg.allocations.map(item => ({ ...item }))
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ getPackageTotalCount(pkg: WorkshopPackage): number {
|
|
|
+ return pkg.allocations.reduce((sum, item) => sum + item.count, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ navigateToWorkshopPackage(slug: string): void {
|
|
|
+ const url = new URL(window.location.href);
|
|
|
+ url.searchParams.delete('workshop');
|
|
|
+ url.searchParams.delete('packages');
|
|
|
+ url.searchParams.delete('package');
|
|
|
+ url.searchParams.delete('pkg');
|
|
|
+ url.hash = '/workshop/' + slug;
|
|
|
+ window.history.pushState({}, '', url.toString());
|
|
|
+ this.syncWorkshopRouteFromUrl();
|
|
|
+ }
|
|
|
+
|
|
|
+ openWorkshopPortal(): void {
|
|
|
+ const url = new URL(window.location.href);
|
|
|
+ url.searchParams.delete('workshop');
|
|
|
+ url.searchParams.delete('packages');
|
|
|
+ url.searchParams.delete('package');
|
|
|
+ url.searchParams.delete('pkg');
|
|
|
+ url.hash = '/workshop';
|
|
|
+ window.history.pushState({}, '', url.toString());
|
|
|
+ this.syncWorkshopRouteFromUrl();
|
|
|
+ }
|
|
|
+
|
|
|
+ syncWorkshopAllocationsFromList(): void {
|
|
|
+ if (!this.workshopPackage || this.apigList.length === 0) return;
|
|
|
+ for (const allocation of this.workshopPackage.allocations) {
|
|
|
+ const apig = this.apigList.find(item => item.objectId === allocation.apigId);
|
|
|
+ if (!apig) continue;
|
|
|
+ allocation.title = apig.title || allocation.title;
|
|
|
+ allocation.authId = apig.authId || allocation.authId;
|
|
|
+ if (apig.userBalance != null) allocation.balance = apig.userBalance;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getWorkshopBalance(allocation: WorkshopAllocation): string {
|
|
|
+ return allocation.balance != null ? String(allocation.balance) : '—';
|
|
|
+ }
|
|
|
+
|
|
|
+ getPaymentBody(): string {
|
|
|
+ if (this.workshopPackage) {
|
|
|
+ return this.workshopPackage.title + '(双数据中台额度)';
|
|
|
+ }
|
|
|
+ return this.apig!.title + ' 接口充值';
|
|
|
+ }
|
|
|
+
|
|
|
+ getPrimaryWorkshopAllocation(): WorkshopAllocation | null {
|
|
|
+ if (!this.workshopPackage) return null;
|
|
|
+ return this.workshopPackage.allocations.find(item => item.apigId === this.workshopPackage!.primaryApigId) || null;
|
|
|
+ }
|
|
|
+
|
|
|
+ getOrderRechargeCount(): number {
|
|
|
+ const primary = this.getPrimaryWorkshopAllocation();
|
|
|
+ if (this.workshopPackage && primary) return primary.count;
|
|
|
+ return this.selectedTier?.count || 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ getOrderOldCount(): number {
|
|
|
+ const primary = this.getPrimaryWorkshopAllocation();
|
|
|
+ if (this.workshopPackage && primary) return primary.oldCount ?? primary.balance ?? this.apig!.count ?? 0;
|
|
|
+ return this.apig!.count || 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ async prepareWorkshopPackage(captureOldCounts = true): Promise<void> {
|
|
|
+ if (!this.workshopPackage) return;
|
|
|
+ if (!this.userId) throw new Error('请先登录后再购买技能套餐。');
|
|
|
+
|
|
|
+ this.workshopPreparing = true;
|
|
|
+ try {
|
|
|
+ for (const allocation of this.workshopPackage.allocations) {
|
|
|
+ const authId = await this.resolveAuthId(this.userId, allocation.apigId);
|
|
|
+ if (!authId) throw new Error('无法创建 ' + allocation.title + ' 计费账套');
|
|
|
+ allocation.authId = authId;
|
|
|
+
|
|
|
+ const data = await this.getApigByAuthId(authId);
|
|
|
+ if (data) {
|
|
|
+ allocation.title = data.title || allocation.title;
|
|
|
+ allocation.balance = data.count || 0;
|
|
|
+ if (captureOldCounts || allocation.oldCount == null) allocation.oldCount = allocation.balance;
|
|
|
+ if (allocation.apigId === this.workshopPackage.primaryApigId) {
|
|
|
+ this.authId = authId;
|
|
|
+ this.apigId = allocation.apigId;
|
|
|
+ this.apig = data;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (captureOldCounts || allocation.oldCount == null) allocation.oldCount = allocation.balance || 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.cdr.detectChanges();
|
|
|
+ } finally {
|
|
|
+ this.workshopPreparing = false;
|
|
|
+ this.cdr.detectChanges();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async getApigByAuthId(authId: string): Promise<ApigData | null> {
|
|
|
+ try {
|
|
|
+ const resp = await this.postJSON(API_BASE + '/api/apig/getApig', { authid: authId });
|
|
|
+ if (resp.code === 200 && resp.data) return resp.data;
|
|
|
+ } catch (e: any) {
|
|
|
+ console.warn('[APIG] 获取账套信息失败:', e.message);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
// ─── Payment Flow ───
|
|
|
async startPayment(): Promise<void> {
|
|
|
- if (!this.apig?.priceStep || this.paying) return;
|
|
|
+ const tier = this.selectedTier;
|
|
|
+ if (!this.apig || !tier || this.paying || this.workshopPreparing) return;
|
|
|
this.paying = true;
|
|
|
this.errorMsg = '';
|
|
|
|
|
|
try {
|
|
|
+ if (this.workshopPackage) {
|
|
|
+ await this.prepareWorkshopPackage();
|
|
|
+ }
|
|
|
+
|
|
|
// 1. Generate trade number
|
|
|
const now = new Date();
|
|
|
this.tradeNo = 'C' + (this.userId || 'U') +
|
|
|
@@ -496,8 +810,6 @@ export class App implements OnInit {
|
|
|
String(now.getSeconds()).padStart(2, '0') +
|
|
|
now.getMilliseconds();
|
|
|
|
|
|
- const tier = this.apig.priceStep[this.selectedIndex];
|
|
|
-
|
|
|
// 2. Create order
|
|
|
console.log('[PAY] Creating order...', { tradeNo: this.tradeNo, tier: tier.price, funId: this.funId });
|
|
|
await this.createOrder(tier);
|
|
|
@@ -509,7 +821,7 @@ export class App implements OnInit {
|
|
|
company: PAY_COMPANY,
|
|
|
out_trade_no: this.tradeNo,
|
|
|
total_fee: +tier.price,
|
|
|
- body: this.apig.title + ' 接口充值'
|
|
|
+ body: this.getPaymentBody()
|
|
|
};
|
|
|
if (this.funId) {
|
|
|
payParams.fun_id = this.funId;
|
|
|
@@ -568,17 +880,20 @@ export class App implements OnInit {
|
|
|
|
|
|
async createOrder(tier: PriceTier): Promise<void> {
|
|
|
try {
|
|
|
+ const orderCount = this.getOrderRechargeCount();
|
|
|
+ const oldCount = this.getOrderOldCount();
|
|
|
+ const bodyText = this.getPaymentBody();
|
|
|
const body: any = {
|
|
|
type: 'wxpay',
|
|
|
authid: this.authId,
|
|
|
params: {
|
|
|
out_trade_no: this.tradeNo,
|
|
|
total_fee: +tier.price,
|
|
|
- body: this.apig!.title + ' 接口充值'
|
|
|
+ body: bodyText
|
|
|
},
|
|
|
apigid: this.apig!.objectId,
|
|
|
- oldCount: this.apig!.count || 0,
|
|
|
- count: tier.count,
|
|
|
+ oldCount,
|
|
|
+ count: orderCount,
|
|
|
user: this.userId
|
|
|
};
|
|
|
// 仅在拿到真实 Company objectId 时才传 fcompany,避免把 userId 误写成 Company 指针
|
|
|
@@ -616,6 +931,11 @@ export class App implements OnInit {
|
|
|
}
|
|
|
|
|
|
async doRecharge(): Promise<void> {
|
|
|
+ if (this.workshopPackage) {
|
|
|
+ await this.doWorkshopRecharge();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
this.showQrModal = false;
|
|
|
this.showSuccess = true;
|
|
|
this.cdr.detectChanges();
|
|
|
@@ -673,6 +993,115 @@ export class App implements OnInit {
|
|
|
this.loadOrderHistory();
|
|
|
}
|
|
|
|
|
|
+ async doWorkshopRecharge(): Promise<void> {
|
|
|
+ if (!this.workshopPackage) return;
|
|
|
+
|
|
|
+ this.showQrModal = false;
|
|
|
+ this.showSuccess = true;
|
|
|
+ this.successDetailHtml = '正在开通 <strong style="color:var(--neon);">' + this.workshopPackage.title + '</strong>,请稍候...';
|
|
|
+ this.cdr.detectChanges();
|
|
|
+
|
|
|
+ try {
|
|
|
+ await this.prepareWorkshopPackage(false);
|
|
|
+ } catch (e: any) {
|
|
|
+ this.successDetailHtml =
|
|
|
+ '支付已完成,但双数据中台额度开通需要人工确认。<br>' +
|
|
|
+ '<span style="color:var(--text-dim);">原因:' + (e.message || '账套确认失败') + '</span>';
|
|
|
+ this.cdr.detectChanges();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const primary = this.getPrimaryWorkshopAllocation();
|
|
|
+ const primaryTarget = primary ? (primary.oldCount || 0) + primary.count : 0;
|
|
|
+
|
|
|
+ if (primary) {
|
|
|
+ for (let i = 1; i <= 8; i++) {
|
|
|
+ await this.sleep(3000);
|
|
|
+ await this.refreshWorkshopBalances();
|
|
|
+ if ((primary.balance || 0) >= primaryTarget) break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const allocation of this.workshopPackage.allocations) {
|
|
|
+ const oldCount = allocation.oldCount || 0;
|
|
|
+ const current = allocation.balance ?? oldCount;
|
|
|
+ const target = oldCount + allocation.count;
|
|
|
+ if (current >= target) continue;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const rechargeBody: any = {
|
|
|
+ user: this.userId,
|
|
|
+ authid: allocation.authId,
|
|
|
+ apigid: allocation.apigId,
|
|
|
+ oldCount: current,
|
|
|
+ count: target - current,
|
|
|
+ orderid: this.order?.objectId || ''
|
|
|
+ };
|
|
|
+ if (this.companyId) rechargeBody.authComp = this.companyId;
|
|
|
+ console.log('[WORKSHOP_RECHARGE] saveRecharge 参数:', JSON.stringify(rechargeBody));
|
|
|
+ const rechargeResp = await this.postJSON(API_BASE + '/api/apig/saveRecharge', rechargeBody);
|
|
|
+ console.log('[WORKSHOP_RECHARGE] saveRecharge 响应:', JSON.stringify(rechargeResp));
|
|
|
+ } catch (e: any) {
|
|
|
+ console.warn('[WORKSHOP_RECHARGE] ' + allocation.title + ' 充值失败:', e.message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for (let i = 1; i <= 3; i++) {
|
|
|
+ await this.sleep(2000);
|
|
|
+ await this.refreshWorkshopBalances();
|
|
|
+ const done = this.workshopPackage.allocations.every(item => {
|
|
|
+ const target = (item.oldCount || 0) + item.count;
|
|
|
+ return (item.balance || 0) >= target;
|
|
|
+ });
|
|
|
+ if (done) break;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.successDetailHtml = this.buildWorkshopSuccessHtml();
|
|
|
+ this.cdr.detectChanges();
|
|
|
+ this.loadOrderHistory();
|
|
|
+ }
|
|
|
+
|
|
|
+ async refreshWorkshopBalances(): Promise<void> {
|
|
|
+ if (!this.workshopPackage) return;
|
|
|
+ for (const allocation of this.workshopPackage.allocations) {
|
|
|
+ if (!allocation.authId) continue;
|
|
|
+ const data = await this.getApigByAuthId(allocation.authId);
|
|
|
+ if (!data) continue;
|
|
|
+ allocation.title = data.title || allocation.title;
|
|
|
+ allocation.balance = data.count || 0;
|
|
|
+ if (allocation.apigId === this.workshopPackage.primaryApigId) {
|
|
|
+ this.apig = data;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.syncApigListBalancesFromWorkshop();
|
|
|
+ this.cdr.detectChanges();
|
|
|
+ }
|
|
|
+
|
|
|
+ syncApigListBalancesFromWorkshop(): void {
|
|
|
+ if (!this.workshopPackage || this.apigList.length === 0) return;
|
|
|
+ for (const allocation of this.workshopPackage.allocations) {
|
|
|
+ const apig = this.apigList.find(item => item.objectId === allocation.apigId);
|
|
|
+ if (!apig) continue;
|
|
|
+ apig.authId = allocation.authId || apig.authId;
|
|
|
+ apig.userBalance = allocation.balance ?? apig.userBalance;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ buildWorkshopSuccessHtml(): string {
|
|
|
+ if (!this.workshopPackage) return '';
|
|
|
+ const rows = this.workshopPackage.allocations.map(item => {
|
|
|
+ const target = (item.oldCount || 0) + item.count;
|
|
|
+ const balance = item.balance ?? 0;
|
|
|
+ const status = balance >= target ? '已到账' : '处理中';
|
|
|
+ return '<div style="margin-top:6px;">' +
|
|
|
+ '<strong style="color:var(--success);">' + item.title + '</strong> +' + item.count + ' 次' +
|
|
|
+ '<span style="color:var(--text-dim);">,当前余额 ' + balance + ' 次,' + status + '</span>' +
|
|
|
+ '</div>';
|
|
|
+ }).join('');
|
|
|
+ return '已开通 <strong style="color:var(--neon);">' + this.workshopPackage.title + '</strong><br>' +
|
|
|
+ rows +
|
|
|
+ '<span style="color:var(--text-dim);margin-top:8px;display:inline-block;">有效期 730 天,情报官训练服务请按工作坊流程交付。</span>';
|
|
|
+ }
|
|
|
+
|
|
|
async refreshBalance(): Promise<void> {
|
|
|
try {
|
|
|
const resp = await this.postJSON(API_BASE + '/api/apig/getApig', { authid: this.authId });
|