515 行
14 KiB
TypeScript
515 行
14 KiB
TypeScript
import { Provide } from '@midwayjs/core';
|
|
import { BaseService } from '@cool-midway/core';
|
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import * as moment from 'moment';
|
|
import { TeamInvoiceEntity } from '../entity/invoice';
|
|
import { TeamSubscriptionEntity } from '../entity/subscription';
|
|
import {
|
|
ManualRenewDTO,
|
|
PlanConfigItem,
|
|
PlatformOverviewResult,
|
|
RenewalOrderItem,
|
|
SubscriberItem,
|
|
TenantPlanType,
|
|
UpdateSubscriberDTO,
|
|
} from '../../../comm/types/omni';
|
|
|
|
/**
|
|
* 平台总后台超级管理员 (Platform Super Admin)
|
|
* 会员、续费与订阅全周期运营管控服务
|
|
*/
|
|
@Provide()
|
|
export class PlatformSubscriptionService extends BaseService {
|
|
@InjectEntityModel(TeamInvoiceEntity)
|
|
teamInvoiceEntity?: Repository<TeamInvoiceEntity>;
|
|
|
|
@InjectEntityModel(TeamSubscriptionEntity)
|
|
teamSubscriptionEntity?: Repository<TeamSubscriptionEntity>;
|
|
|
|
// 内存化种子会员数据池 (保障无缝离线测试与即席演示)
|
|
private subscribersStore: SubscriberItem[] = [
|
|
{
|
|
id: 1,
|
|
tenantId: 1,
|
|
tenantName: 'Vanguard Global Trade Ltd. (出海先锋外贸)',
|
|
adminName: 'David Zhang',
|
|
adminEmail: 'david@vanguard-trade.com',
|
|
adminPhone: '+86 13800138001',
|
|
planType: 'enterprise',
|
|
status: 'active',
|
|
cardsUsed: 42,
|
|
cardsLimit: 100,
|
|
seatsUsed: 18,
|
|
seatsLimit: 50,
|
|
totalPaidAmount: 7176,
|
|
currency: 'USD',
|
|
startDate: '2025-09-01',
|
|
expireDate: '2026-09-01',
|
|
createdAt: '2025-08-28 10:20:00',
|
|
},
|
|
{
|
|
id: 2,
|
|
tenantId: 2,
|
|
tenantName: 'Berlin Precision Robotics GmbH (柏林精密机器人)',
|
|
adminName: 'Klaus Schmidt',
|
|
adminEmail: 'klaus@berlin-robotics.de',
|
|
adminPhone: '+49 30 1234567',
|
|
planType: 'pro',
|
|
status: 'active',
|
|
cardsUsed: 5,
|
|
cardsLimit: 10,
|
|
seatsUsed: 4,
|
|
seatsLimit: 5,
|
|
totalPaidAmount: 1176,
|
|
currency: 'EUR',
|
|
startDate: '2026-01-15',
|
|
expireDate: '2027-01-15',
|
|
createdAt: '2026-01-10 14:35:12',
|
|
},
|
|
{
|
|
id: 3,
|
|
tenantId: 3,
|
|
tenantName: 'Dubai Apex Logistics LLC (迪拜巅峰国际物流)',
|
|
adminName: 'Ahmed Al-Mansoor',
|
|
adminEmail: 'ahmed@apexlogistics.ae',
|
|
adminPhone: '+971 4 9876543',
|
|
planType: 'enterprise',
|
|
status: 'active',
|
|
cardsUsed: 35,
|
|
cardsLimit: 100,
|
|
seatsUsed: 12,
|
|
seatsLimit: 50,
|
|
totalPaidAmount: 3588,
|
|
currency: 'USD',
|
|
startDate: '2026-03-01',
|
|
expireDate: '2027-03-01',
|
|
createdAt: '2026-02-25 09:12:44',
|
|
},
|
|
{
|
|
id: 4,
|
|
tenantId: 4,
|
|
tenantName: 'Silicon Horizon Tech Inc. (硅谷地平线科技)',
|
|
adminName: 'Sarah Jenkins',
|
|
adminEmail: 'sarah@siliconhorizon.io',
|
|
adminPhone: '+1 415 8009988',
|
|
planType: 'pro',
|
|
status: 'past_due',
|
|
cardsUsed: 8,
|
|
cardsLimit: 10,
|
|
seatsUsed: 5,
|
|
seatsLimit: 5,
|
|
totalPaidAmount: 588,
|
|
currency: 'USD',
|
|
startDate: '2025-09-10',
|
|
expireDate: '2026-09-10',
|
|
createdAt: '2025-09-05 16:40:22',
|
|
},
|
|
{
|
|
id: 5,
|
|
tenantId: 5,
|
|
tenantName: 'Nordic Clean Energy AB (北欧洁能科技)',
|
|
adminName: 'Erik Lindqvist',
|
|
adminEmail: 'erik@nordic-energy.se',
|
|
adminPhone: '+46 8 5556677',
|
|
planType: 'pro',
|
|
status: 'active',
|
|
cardsUsed: 4,
|
|
cardsLimit: 10,
|
|
seatsUsed: 3,
|
|
seatsLimit: 5,
|
|
totalPaidAmount: 1176,
|
|
currency: 'EUR',
|
|
startDate: '2025-11-20',
|
|
expireDate: '2026-11-20',
|
|
createdAt: '2025-11-15 11:20:00',
|
|
},
|
|
{
|
|
id: 6,
|
|
tenantId: 6,
|
|
tenantName: 'Tokyo Smart Automation KK (东京智能物联)',
|
|
adminName: 'Kenji Sato',
|
|
adminEmail: 'kenji@tokyo-smart.jp',
|
|
adminPhone: '+81 3 33445566',
|
|
planType: 'free',
|
|
status: 'active',
|
|
cardsUsed: 1,
|
|
cardsLimit: 1,
|
|
seatsUsed: 1,
|
|
seatsLimit: 1,
|
|
totalPaidAmount: 0,
|
|
currency: 'USD',
|
|
startDate: '2026-08-01',
|
|
expireDate: '2099-12-31',
|
|
createdAt: '2026-08-01 10:00:00',
|
|
},
|
|
];
|
|
|
|
// 内存化种子续费流水池
|
|
private renewalsStore: RenewalOrderItem[] = [
|
|
{
|
|
id: 1,
|
|
orderNo: 'ORD-20260921-0089',
|
|
tenantId: 1,
|
|
tenantName: 'Vanguard Global Trade Ltd.',
|
|
planType: 'enterprise',
|
|
billingCycle: 'yearly',
|
|
amount: 3588,
|
|
currency: 'USD',
|
|
channel: 'stripe',
|
|
status: 'paid',
|
|
paidAt: '2026-09-21 00:15:20',
|
|
createdAt: '2026-09-21 00:15:10',
|
|
},
|
|
{
|
|
id: 2,
|
|
orderNo: 'ORD-20260920-0088',
|
|
tenantId: 2,
|
|
tenantName: 'Berlin Precision Robotics GmbH',
|
|
planType: 'pro',
|
|
billingCycle: 'yearly',
|
|
amount: 588,
|
|
currency: 'EUR',
|
|
channel: 'stripe',
|
|
status: 'paid',
|
|
paidAt: '2026-09-20 18:42:10',
|
|
createdAt: '2026-09-20 18:42:00',
|
|
},
|
|
{
|
|
id: 3,
|
|
orderNo: 'ORD-20260920-0087',
|
|
tenantId: 3,
|
|
tenantName: 'Dubai Apex Logistics LLC',
|
|
planType: 'enterprise',
|
|
billingCycle: 'monthly',
|
|
amount: 299,
|
|
currency: 'USD',
|
|
channel: 'wire_transfer',
|
|
status: 'paid',
|
|
paidAt: '2026-09-20 14:22:05',
|
|
createdAt: '2026-09-20 14:10:00',
|
|
},
|
|
{
|
|
id: 4,
|
|
orderNo: 'ORD-20260919-0086',
|
|
tenantId: 4,
|
|
tenantName: 'Silicon Horizon Tech Inc.',
|
|
planType: 'pro',
|
|
billingCycle: 'monthly',
|
|
amount: 49,
|
|
currency: 'USD',
|
|
channel: 'stripe',
|
|
status: 'pending',
|
|
paidAt: '',
|
|
createdAt: '2026-09-19 22:11:34',
|
|
},
|
|
{
|
|
id: 5,
|
|
orderNo: 'ORD-20260919-0085',
|
|
tenantId: 5,
|
|
tenantName: 'Nordic Clean Energy AB',
|
|
planType: 'pro',
|
|
billingCycle: 'yearly',
|
|
amount: 588,
|
|
currency: 'EUR',
|
|
channel: 'manual_admin',
|
|
status: 'paid',
|
|
paidAt: '2026-09-19 11:05:49',
|
|
createdAt: '2026-09-19 11:05:00',
|
|
},
|
|
];
|
|
|
|
// SaaS 套餐规格与定价方案配置
|
|
private planConfigs: PlanConfigItem[] = [
|
|
{
|
|
planType: 'free',
|
|
name: 'Free 基础体验版',
|
|
description: '适合个人自由职业者与初创独立人,轻量体验出海名片微官网',
|
|
monthlyPrice: 0,
|
|
yearlyPrice: 0,
|
|
cardsLimit: 1,
|
|
seatsLimit: 1,
|
|
aiTokensPerMonth: 5000,
|
|
nfcDynamicRouting: false,
|
|
supportCustomDomain: false,
|
|
crmLeadLimit: 50,
|
|
isPopular: false,
|
|
},
|
|
{
|
|
planType: 'pro',
|
|
name: 'Pro 专业商务版',
|
|
description: '面向出海业务精英、外贸销售主管与跨国顾问的高性能全功能套件',
|
|
monthlyPrice: 49,
|
|
yearlyPrice: 490,
|
|
cardsLimit: 10,
|
|
seatsLimit: 5,
|
|
aiTokensPerMonth: 200000,
|
|
nfcDynamicRouting: true,
|
|
supportCustomDomain: true,
|
|
crmLeadLimit: 2000,
|
|
isPopular: true,
|
|
},
|
|
{
|
|
planType: 'enterprise',
|
|
name: 'Enterprise 商业企业版',
|
|
description: '为出海跨国集团、参展销售天团定制的多租户公海池与品牌统一管控',
|
|
monthlyPrice: 299,
|
|
yearlyPrice: 2990,
|
|
cardsLimit: 100,
|
|
seatsLimit: 50,
|
|
aiTokensPerMonth: 1000000,
|
|
nfcDynamicRouting: true,
|
|
supportCustomDomain: true,
|
|
crmLeadLimit: 50000,
|
|
isPopular: false,
|
|
},
|
|
];
|
|
|
|
/**
|
|
* 1. 平台总览与核心 KPI 概览
|
|
*/
|
|
async getOverview(): Promise<PlatformOverviewResult> {
|
|
const totalSubscribers = this.subscribersStore.length;
|
|
const activeSubscribers = this.subscribersStore.filter(
|
|
s => s.status === 'active' || s.status === 'trialing'
|
|
).length;
|
|
const proCount = this.subscribersStore.filter(s => s.planType === 'pro').length;
|
|
const enterpriseCount = this.subscribersStore.filter(s => s.planType === 'enterprise').length;
|
|
|
|
// 即将到期判定 (当前日期后 30 天内到期)
|
|
const now = moment();
|
|
const expiringSoonCount = this.subscribersStore.filter(s => {
|
|
if (s.planType === 'free') return false;
|
|
const exp = moment(s.expireDate);
|
|
const diffDays = exp.diff(now, 'days');
|
|
return diffDays >= -5 && diffDays <= 30;
|
|
}).length;
|
|
|
|
const totalRenewalRevenue = this.renewalsStore
|
|
.filter(r => r.status === 'paid')
|
|
.reduce((sum, r) => sum + Number(r.amount || 0), 0);
|
|
|
|
return {
|
|
totalSubscribers,
|
|
activeSubscribers,
|
|
proCount,
|
|
enterpriseCount,
|
|
expiringSoonCount,
|
|
totalRenewalRevenue,
|
|
currency: 'USD',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 2. 分页及多条件查询全网会员列表
|
|
*/
|
|
async getSubscribers(query: {
|
|
page?: number;
|
|
size?: number;
|
|
planType?: TenantPlanType;
|
|
status?: string;
|
|
keyword?: string;
|
|
} = {}): Promise<{
|
|
list: SubscriberItem[];
|
|
pagination: { page: number; size: number; total: number };
|
|
}> {
|
|
const page = Number(query.page || 1);
|
|
const size = Number(query.size || 20);
|
|
|
|
let filtered = [...this.subscribersStore];
|
|
|
|
if (query.planType) {
|
|
filtered = filtered.filter(s => s.planType === query.planType);
|
|
}
|
|
if (query.status) {
|
|
filtered = filtered.filter(s => s.status === query.status);
|
|
}
|
|
if (query.keyword) {
|
|
const kw = query.keyword.toLowerCase().trim();
|
|
filtered = filtered.filter(
|
|
s =>
|
|
String(s.tenantId).includes(kw) ||
|
|
s.tenantName.toLowerCase().includes(kw) ||
|
|
s.adminEmail.toLowerCase().includes(kw) ||
|
|
s.adminName.toLowerCase().includes(kw)
|
|
);
|
|
}
|
|
|
|
const total = filtered.length;
|
|
const start = (page - 1) * size;
|
|
const list = filtered.slice(start, start + size).map(item => ({ ...item }));
|
|
|
|
return {
|
|
list,
|
|
pagination: {
|
|
page,
|
|
size,
|
|
total,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 3. 超级管理员修改会员信息 (套餐、配额、延期、状态)
|
|
*/
|
|
async updateSubscriber(dto: UpdateSubscriberDTO): Promise<SubscriberItem> {
|
|
const item = this.subscribersStore.find(s => s.tenantId === Number(dto.tenantId));
|
|
if (!item) {
|
|
throw new Error(`未找到租户 ID 为 ${dto.tenantId} 的会员`);
|
|
}
|
|
|
|
if (dto.planType) {
|
|
item.planType = dto.planType;
|
|
// 联动规格默认配额
|
|
const targetPlan = this.planConfigs.find(p => p.planType === dto.planType);
|
|
if (targetPlan) {
|
|
item.cardsLimit = targetPlan.cardsLimit;
|
|
item.seatsLimit = targetPlan.seatsLimit;
|
|
}
|
|
}
|
|
|
|
if (typeof dto.cardsLimit === 'number') {
|
|
item.cardsLimit = dto.cardsLimit;
|
|
}
|
|
if (typeof dto.seatsLimit === 'number') {
|
|
item.seatsLimit = dto.seatsLimit;
|
|
}
|
|
if (dto.status) {
|
|
item.status = dto.status;
|
|
}
|
|
|
|
if (dto.extendDays && dto.extendDays > 0) {
|
|
const baseDate = moment(item.expireDate).isValid()
|
|
? moment(item.expireDate)
|
|
: moment();
|
|
item.expireDate = baseDate.add(dto.extendDays, 'days').format('YYYY-MM-DD');
|
|
item.status = 'active';
|
|
} else if (dto.expireDate) {
|
|
item.expireDate = dto.expireDate;
|
|
}
|
|
|
|
return { ...item };
|
|
}
|
|
|
|
/**
|
|
* 4. 查询全网续费订单流水台账
|
|
*/
|
|
async getRenewals(query: {
|
|
page?: number;
|
|
size?: number;
|
|
status?: string;
|
|
keyword?: string;
|
|
} = {}): Promise<{
|
|
list: RenewalOrderItem[];
|
|
pagination: { page: number; size: number; total: number };
|
|
}> {
|
|
const page = Number(query.page || 1);
|
|
const size = Number(query.size || 20);
|
|
|
|
let filtered = [...this.renewalsStore];
|
|
|
|
if (query.status) {
|
|
filtered = filtered.filter(r => r.status === query.status);
|
|
}
|
|
if (query.keyword) {
|
|
const kw = query.keyword.toLowerCase().trim();
|
|
filtered = filtered.filter(
|
|
r =>
|
|
r.orderNo.toLowerCase().includes(kw) ||
|
|
r.tenantName.toLowerCase().includes(kw) ||
|
|
String(r.tenantId).includes(kw)
|
|
);
|
|
}
|
|
|
|
const total = filtered.length;
|
|
const start = (page - 1) * size;
|
|
const list = filtered.slice(start, start + size);
|
|
|
|
return {
|
|
list,
|
|
pagination: {
|
|
page,
|
|
size,
|
|
total,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 5. 超管手动为会员代续费 / 补录订单
|
|
*/
|
|
async manualRenew(dto: ManualRenewDTO): Promise<{
|
|
success: boolean;
|
|
orderNo: string;
|
|
item: RenewalOrderItem;
|
|
subscriber: SubscriberItem;
|
|
}> {
|
|
const subscriber = this.subscribersStore.find(s => s.tenantId === Number(dto.tenantId));
|
|
if (!subscriber) {
|
|
throw new Error(`租户 ${dto.tenantId} 不存在`);
|
|
}
|
|
|
|
const orderNo = `ORD-${moment().format('YYYYMMDD')}-${Math.floor(
|
|
1000 + Math.random() * 9000
|
|
)}`;
|
|
const nowStr = moment().format('YYYY-MM-DD HH:mm:ss');
|
|
|
|
const newOrder: RenewalOrderItem = {
|
|
id: this.renewalsStore.length + 1,
|
|
orderNo,
|
|
tenantId: subscriber.tenantId,
|
|
tenantName: subscriber.tenantName,
|
|
planType: dto.planType,
|
|
billingCycle: dto.billingCycle,
|
|
amount: dto.amount,
|
|
currency: dto.currency || 'USD',
|
|
channel: dto.channel || 'manual_admin',
|
|
status: 'paid',
|
|
paidAt: nowStr,
|
|
createdAt: nowStr,
|
|
};
|
|
|
|
this.renewalsStore.unshift(newOrder);
|
|
|
|
// 更新会员期限和套餐
|
|
subscriber.planType = dto.planType;
|
|
const addDays = dto.billingCycle === 'yearly' ? 365 : 30;
|
|
const baseExpire = moment(subscriber.expireDate).isAfter(moment())
|
|
? moment(subscriber.expireDate)
|
|
: moment();
|
|
subscriber.expireDate = baseExpire.add(addDays, 'days').format('YYYY-MM-DD');
|
|
subscriber.status = 'active';
|
|
subscriber.totalPaidAmount = Number(subscriber.totalPaidAmount || 0) + Number(dto.amount);
|
|
|
|
return {
|
|
success: true,
|
|
orderNo,
|
|
item: newOrder,
|
|
subscriber,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 6. SaaS 套餐定价规格列表
|
|
*/
|
|
async getPlans(): Promise<PlanConfigItem[]> {
|
|
return [...this.planConfigs];
|
|
}
|
|
|
|
/**
|
|
* 7. 更新指定套餐定价与配额参数
|
|
*/
|
|
async updatePlan(
|
|
planType: TenantPlanType,
|
|
params: Partial<PlanConfigItem>
|
|
): Promise<PlanConfigItem> {
|
|
const plan = this.planConfigs.find(p => p.planType === planType);
|
|
if (!plan) {
|
|
throw new Error(`套餐类型 ${planType} 不存在`);
|
|
}
|
|
|
|
Object.assign(plan, params);
|
|
return plan;
|
|
}
|
|
}
|