feat(订阅): 添加WooCommerce订阅管理功能
实现订阅模块的完整功能,包括: - 添加订阅状态枚举 - 创建订阅实体和DTO - 实现订阅同步和查询服务 - 添加订阅控制器提供API接口 - 配置订阅实体到数据库连接
This commit is contained in:
parent
b8290d0cda
commit
795b13ce31
File diff suppressed because it is too large
Load Diff
|
|
@ -32,6 +32,7 @@ import { CustomerTag } from '../entity/customer_tag.entity';
|
|||
import { Customer } from '../entity/customer.entity';
|
||||
import { DeviceWhitelist } from '../entity/device_whitelist';
|
||||
import { AuthCode } from '../entity/auth_code';
|
||||
import { Subscription } from '../entity/subscription.entity';
|
||||
|
||||
export default {
|
||||
// use for cookie sign key, should change to your own and keep security
|
||||
|
|
@ -72,6 +73,7 @@ export default {
|
|||
Customer,
|
||||
DeviceWhitelist,
|
||||
AuthCode,
|
||||
Subscription,
|
||||
],
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import { Controller, Inject, Param, Post, Get, Query } from '@midwayjs/core';
|
||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||
import { SubscriptionService } from '../service/subscription.service';
|
||||
import { errorResponse, successResponse } from '../utils/response.util';
|
||||
import { BooleanRes, SubscriptionListRes } from '../dto/reponse.dto';
|
||||
import { QuerySubscriptionDTO } from '../dto/subscription.dto';
|
||||
|
||||
@Controller('/subscription')
|
||||
export class SubscriptionController {
|
||||
@Inject()
|
||||
subscriptionService: SubscriptionService;
|
||||
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Post('/sync/:siteId')
|
||||
async sync(@Param('siteId') siteId: string) {
|
||||
try {
|
||||
await this.subscriptionService.syncSubscriptions(siteId);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || '同步失败');
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: SubscriptionListRes })
|
||||
@Get('/list')
|
||||
async list(@Query() query: QuerySubscriptionDTO) {
|
||||
try {
|
||||
const data = await this.subscriptionService.getSubscriptionList(query);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || '获取失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import { OrderNote } from '../entity/order_note.entity';
|
|||
import { PaymentMethodDTO } from './logistics.dto';
|
||||
import { Flavors } from '../entity/flavors.entity';
|
||||
import { Strength } from '../entity/strength.entity';
|
||||
import { Subscription } from '../entity/subscription.entity';
|
||||
|
||||
export class BooleanRes extends SuccessWrapper(Boolean) {}
|
||||
//网站配置返回数据
|
||||
|
|
@ -117,3 +118,8 @@ export class OrderDetailRes extends SuccessWrapper(OrderDetail) {}
|
|||
export class PaymentMethodListRes extends SuccessArrayWrapper(
|
||||
PaymentMethodDTO
|
||||
) {}
|
||||
|
||||
// 订阅分页数据
|
||||
export class SubscriptionPaginatedResponse extends PaginatedWrapper(Subscription) {}
|
||||
// 订阅分页返回数据
|
||||
export class SubscriptionListRes extends SuccessWrapper(SubscriptionPaginatedResponse) {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { Rule, RuleType } from '@midwayjs/validate';
|
||||
import { SubscriptionStatus } from '../enums/base.enum';
|
||||
|
||||
export class QuerySubscriptionDTO {
|
||||
@ApiProperty({ example: 1, description: '页码' })
|
||||
@Rule(RuleType.number().default(1))
|
||||
current: number;
|
||||
|
||||
@ApiProperty({ example: 10, description: '每页大小' })
|
||||
@Rule(RuleType.number().default(10))
|
||||
pageSize: number;
|
||||
|
||||
@ApiProperty({ description: '站点ID' })
|
||||
@Rule(RuleType.string().allow(''))
|
||||
siteId: string;
|
||||
|
||||
@ApiProperty({ description: '订阅状态', enum: SubscriptionStatus })
|
||||
@Rule(RuleType.string().valid(...Object.values(SubscriptionStatus)).allow(''))
|
||||
status: SubscriptionStatus | '';
|
||||
|
||||
@ApiProperty({ description: '客户邮箱' })
|
||||
@Rule(RuleType.string().allow(''))
|
||||
customer_email: string;
|
||||
|
||||
@ApiProperty({ description: '关键字(订阅ID、邮箱等)' })
|
||||
@Rule(RuleType.string().allow(''))
|
||||
keyword: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { Exclude, Expose } from 'class-transformer';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { SubscriptionStatus } from '../enums/base.enum';
|
||||
|
||||
@Entity('subscription')
|
||||
@Exclude()
|
||||
export class Subscription {
|
||||
@ApiProperty()
|
||||
@PrimaryGeneratedColumn()
|
||||
@Expose()
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: '来源站点唯一标识' })
|
||||
@Column()
|
||||
@Expose()
|
||||
siteId: string;
|
||||
|
||||
@ApiProperty({ description: 'WooCommerce 订阅 ID' })
|
||||
@Column()
|
||||
@Expose()
|
||||
externalSubscriptionId: string;
|
||||
|
||||
@ApiProperty({ type: SubscriptionStatus })
|
||||
@Column({ type: 'enum', enum: SubscriptionStatus })
|
||||
@Expose()
|
||||
status: SubscriptionStatus;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ default: '' })
|
||||
@Expose()
|
||||
currency: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Column('decimal', { precision: 10, scale: 2, default: 0 })
|
||||
@Expose()
|
||||
total: number;
|
||||
|
||||
@ApiProperty({ description: '计费周期 e.g. day/week/month/year' })
|
||||
@Column({ default: '' })
|
||||
@Expose()
|
||||
billing_period: string;
|
||||
|
||||
@ApiProperty({ description: '计费周期间隔 e.g. 1/3/12' })
|
||||
@Column({ type: 'int', default: 0 })
|
||||
@Expose()
|
||||
billing_interval: number;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ type: 'int', default: 0 })
|
||||
@Expose()
|
||||
customer_id: number;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ default: '' })
|
||||
@Expose()
|
||||
customer_email: string;
|
||||
|
||||
@ApiProperty({ description: '父订单/父订阅ID(如有)' })
|
||||
@Column({ type: 'int', default: 0 })
|
||||
@Expose()
|
||||
parent_id: number;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
@Expose()
|
||||
start_date: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
@Expose()
|
||||
trial_end: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
@Expose()
|
||||
next_payment_date: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ type: 'timestamp', nullable: true })
|
||||
@Expose()
|
||||
end_date: Date;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ type: 'json', nullable: true })
|
||||
@Expose()
|
||||
line_items: any[];
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ type: 'json', nullable: true })
|
||||
@Expose()
|
||||
meta_data: any[];
|
||||
|
||||
@ApiProperty({ example: '2022-12-12 11:11:11', description: '创建时间', required: true })
|
||||
@CreateDateColumn()
|
||||
@Expose()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({ example: '2022-12-12 11:11:11', description: '更新时间', required: true })
|
||||
@UpdateDateColumn()
|
||||
@Expose()
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
@ -72,3 +72,14 @@ export enum ShipmentType {
|
|||
export enum staticValue {
|
||||
STATIC_CAPTCHA = 'yoone2025!@YOONE0923'
|
||||
}
|
||||
|
||||
// WooCommerce Subscription status
|
||||
// Reference: https://woocommerce.com/document/subscriptions/statuses/
|
||||
export enum SubscriptionStatus {
|
||||
ACTIVE = 'active', // 活跃
|
||||
PENDING = 'pending', // 待处理/待激活
|
||||
ON_HOLD = 'on-hold', // 暂停
|
||||
CANCELLED = 'cancelled', // 已取消
|
||||
EXPIRED = 'expired', // 已过期
|
||||
PENDING_CANCELLATION = 'pending-cancel', // 待取消
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import { Inject, Provide } from '@midwayjs/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Repository, Like } from 'typeorm';
|
||||
import { WPService } from './wp.service';
|
||||
import { Subscription } from '../entity/subscription.entity';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { SubscriptionStatus } from '../enums/base.enum';
|
||||
import { QuerySubscriptionDTO } from '../dto/subscription.dto';
|
||||
|
||||
@Provide()
|
||||
export class SubscriptionService {
|
||||
@Inject()
|
||||
wPService: WPService;
|
||||
|
||||
@InjectEntityModel(Subscription)
|
||||
subscriptionModel: Repository<Subscription>;
|
||||
|
||||
async syncSubscriptions(siteId: string) {
|
||||
const subs = await this.wPService.getSubscriptions(siteId);
|
||||
for (const sub of subs) {
|
||||
await this.syncSingleSubscription(siteId, sub);
|
||||
}
|
||||
}
|
||||
|
||||
async syncSingleSubscription(siteId: string, sub: any) {
|
||||
const { line_items, ...raw } = sub;
|
||||
const entity: Partial<Subscription> = {
|
||||
...raw,
|
||||
externalSubscriptionId: String(raw.id),
|
||||
siteId,
|
||||
status: raw.status as SubscriptionStatus,
|
||||
customer_email: raw?.billing?.email || raw?.customer_email || '',
|
||||
line_items,
|
||||
};
|
||||
delete (entity as any).id;
|
||||
const existing = await this.subscriptionModel.findOne({
|
||||
where: { externalSubscriptionId: String(sub.id), siteId },
|
||||
});
|
||||
const saveEntity = plainToClass(Subscription, entity);
|
||||
if (existing) {
|
||||
await this.subscriptionModel.update({ id: existing.id }, saveEntity);
|
||||
} else {
|
||||
await this.subscriptionModel.save(saveEntity);
|
||||
}
|
||||
}
|
||||
|
||||
async getSubscriptionList({
|
||||
current = 1,
|
||||
pageSize = 10,
|
||||
siteId,
|
||||
status,
|
||||
customer_email,
|
||||
keyword,
|
||||
}: QuerySubscriptionDTO) {
|
||||
const where: any = {};
|
||||
if (siteId) where.siteId = siteId;
|
||||
if (status) where.status = status;
|
||||
if (customer_email) where.customer_email = Like(`%${customer_email}%`);
|
||||
if (keyword) {
|
||||
where.externalSubscriptionId = Like(`%${keyword}%`);
|
||||
}
|
||||
const [list, total] = await this.subscriptionModel.findAndCount({
|
||||
where,
|
||||
order: { id: 'DESC' },
|
||||
skip: (current - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
return {
|
||||
list,
|
||||
total,
|
||||
current,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -134,6 +134,26 @@ export class WPService {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WooCommerce Subscriptions
|
||||
* 优先尝试 wc/v1/subscriptions (Subscriptions 插件提供), 如失败则回退 wc/v3/subscriptions(部分版本提供)。
|
||||
*/
|
||||
async getSubscriptions(siteId: string): Promise<Record<string, any>[]> {
|
||||
const site = this.geSite(siteId);
|
||||
try {
|
||||
return await this.fetchPagedData<Record<string, any>>(
|
||||
'/wc/v1/subscriptions',
|
||||
site
|
||||
);
|
||||
} catch (e) {
|
||||
// fallback
|
||||
return await this.fetchPagedData<Record<string, any>>(
|
||||
'/wc/v3/subscriptions',
|
||||
site
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getOrderRefund(
|
||||
siteId: string,
|
||||
orderId: string,
|
||||
|
|
|
|||
Loading…
Reference in New Issue