feat: 用户套餐,用户支付功能

This commit is contained in:
xiaojunnuo
2024-12-22 14:00:46 +08:00
parent d70e2b66a3
commit a019956698
69 changed files with 2071 additions and 738 deletions
@@ -5,8 +5,8 @@ import { SmsServiceFactory } from '../sms/factory.js';
import { ISmsService } from '../sms/api.js';
import { CodeErrorException } from '@certd/lib-server/dist/basic/exception/code-error-exception.js';
import { EmailService } from './email-service.js';
import { AccessService } from '../../pipeline/service/access-service.js';
import { AccessSysGetter } from '../../pipeline/service/access-sys-getter.js';
import { AccessService } from '@certd/lib-server';
import { AccessSysGetter } from '@certd/lib-server';
import { isComm } from '@certd/plus-core';
// {data: '<svg.../svg>', text: 'abcd'}
@@ -7,7 +7,7 @@ import { createDnsProvider, IDnsProvider, parseDomain } from '@certd/plugin-cert
import { CnameProvider, CnameRecord } from '@certd/pipeline';
import { cache, http, logger, utils } from '@certd/basic';
import { AccessService } from '../../pipeline/service/access-service.js';
import { AccessService } from '@certd/lib-server';
import { isDev } from '@certd/basic';
import { walkTxtRecord } from '@certd/acme-client';
import { CnameProviderService } from './cname-provider-service.js';
@@ -1,36 +0,0 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
/**
* 授权配置
*/
@Entity('cd_access')
export class AccessEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'user_id', comment: '用户id' })
userId: number;
@Column({ comment: '名称', length: 100 })
name: string;
@Column({ comment: '类型', length: 100 })
type: string;
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
setting: string;
@Column({ name: 'encrypt_setting', comment: '已加密设置', length: 10240, nullable: true })
encryptSetting: string;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
})
updateTime: Date;
}
@@ -29,6 +29,14 @@ export class PipelineEntity {
@Column({ comment: '启用/禁用', nullable: true, default: false })
disabled: boolean;
// cert: 证书; backup: 备份; custom:自定义;
@Column({ comment: '类型', nullable: true, default: 'cert' })
type: string;
// custom: 自定义; monitor: 监控;
@Column({ comment: '来源', nullable: true, default: 'custom' })
from: string;
@Column({
name: 'last_history_time',
comment: '最后一次执行时间',
@@ -1,18 +0,0 @@
import { IAccessService } from '@certd/pipeline';
export class AccessGetter implements IAccessService {
userId: number;
getter: <T>(id: any, userId?: number) => Promise<T>;
constructor(userId: number, getter: (id: any, userId: number) => Promise<any>) {
this.userId = userId;
this.getter = getter;
}
async getById<T = any>(id: any) {
return await this.getter<T>(id, this.userId);
}
async getCommonById<T = any>(id: any) {
return await this.getter<T>(id, 0);
}
}
@@ -1,175 +0,0 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { BaseService, PageReq, PermissionException, ValidateException } from '@certd/lib-server';
import { AccessEntity } from '../entity/access.js';
import { AccessDefine, accessRegistry, newAccess } from '@certd/pipeline';
import { EncryptService } from './encrypt-service.js';
/**
* 授权
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class AccessService extends BaseService<AccessEntity> {
@InjectEntityModel(AccessEntity)
repository: Repository<AccessEntity>;
@Inject()
encryptService: EncryptService;
//@ts-ignore
getRepository() {
return this.repository;
}
async page(pageReq: PageReq<AccessEntity>) {
const res = await super.page(pageReq);
res.records = res.records.map(item => {
delete item.encryptSetting;
return item;
});
return res;
}
async add(param) {
this.encryptSetting(param, null);
return await super.add(param);
}
encryptSetting(param: any, oldSettingEntity?: AccessEntity) {
const accessType = param.type;
const accessDefine: AccessDefine = accessRegistry.getDefine(accessType);
if (!accessDefine) {
throw new ValidateException(`授权类型${accessType}不存在`);
}
const setting = param.setting;
if (!setting) {
return;
}
const json = JSON.parse(setting);
let oldSetting = {};
let encryptSetting = {};
const firstEncrypt = !oldSettingEntity || !oldSettingEntity.encryptSetting || oldSettingEntity.encryptSetting === '{}';
if (oldSettingEntity) {
oldSetting = JSON.parse(oldSettingEntity.setting || '{}');
encryptSetting = JSON.parse(oldSettingEntity.encryptSetting || '{}');
}
for (const key in json) {
//加密
let value = json[key];
if (value && typeof value === 'string') {
//去除前后空格
value = value.trim();
json[key] = value;
}
const accessInputDefine = accessDefine.input[key];
if (!accessInputDefine) {
continue;
}
if (!accessInputDefine.encrypt || !value || typeof value !== 'string') {
//定义无需加密、value为空、不是字符串 这些不需要加密
encryptSetting[key] = {
value: value,
encrypt: false,
};
continue;
}
if (firstEncrypt || oldSetting[key] !== value) {
//星号保护
const length = value.length;
const subIndex = Math.min(2, length);
let starLength = length - subIndex * 2;
starLength = Math.max(2, starLength);
const starString = '*'.repeat(starLength);
json[key] = value.substring(0, subIndex) + starString + value.substring(value.length - subIndex);
encryptSetting[key] = {
value: this.encryptService.encrypt(value),
encrypt: true,
};
}
//未改变情况下,不做修改
}
param.encryptSetting = JSON.stringify(encryptSetting);
param.setting = JSON.stringify(json);
}
/**
* 修改
* @param param 数据
*/
async update(param) {
const oldEntity = await this.info(param.id);
if (oldEntity == null) {
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
}
this.encryptSetting(param, oldEntity);
return await super.update(param);
}
async getSimpleInfo(id: number) {
const entity = await this.info(id);
if (entity == null) {
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
}
return {
id: entity.id,
name: entity.name,
userId: entity.userId,
};
}
async getAccessById(id: any, checkUserId: boolean, userId?: number): Promise<any> {
const entity = await this.info(id);
if (entity == null) {
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${id}`);
}
if (checkUserId) {
if (userId == null) {
throw new ValidateException('userId不能为空');
}
if (userId !== entity.userId) {
throw new PermissionException('您对该Access授权无访问权限');
}
}
// const access = accessRegistry.get(entity.type);
const setting = this.decryptAccessEntity(entity);
const input = {
id: entity.id,
...setting,
};
return newAccess(entity.type, input);
}
async getById(id: any, userId: number): Promise<any> {
return await this.getAccessById(id, true, userId);
}
decryptAccessEntity(entity: AccessEntity): any {
let setting = {};
if (entity.encryptSetting && entity.encryptSetting !== '{}') {
setting = JSON.parse(entity.encryptSetting);
for (const key in setting) {
//解密
const encryptValue = setting[key];
let value = encryptValue.value;
if (encryptValue.encrypt) {
value = this.encryptService.decrypt(value);
}
setting[key] = value;
}
} else if (entity.setting) {
setting = JSON.parse(entity.setting);
}
return setting;
}
getDefineList() {
return accessRegistry.getDefineList();
}
getDefineByType(type: string) {
return accessRegistry.getDefine(type);
}
}
@@ -1,17 +0,0 @@
import { IAccessService } from '@certd/pipeline';
import { AccessService } from './access-service.js';
export class AccessSysGetter implements IAccessService {
accessService: AccessService;
constructor(accessService: AccessService) {
this.accessService = accessService;
}
async getById<T = any>(id: any) {
return await this.accessService.getAccessById(id, false);
}
async getCommonById<T = any>(id: any) {
return await this.accessService.getAccessById(id, false);
}
}
@@ -1,44 +0,0 @@
import { Init, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import crypto from 'crypto';
import { SysSettingsService } from '@certd/lib-server';
import { SysPrivateSettings } from '@certd/lib-server';
/**
* 授权
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class EncryptService {
secretKey: Buffer;
@Inject()
sysSettingService: SysSettingsService;
@Init()
async init() {
const privateInfo: SysPrivateSettings = await this.sysSettingService.getSetting(SysPrivateSettings);
this.secretKey = Buffer.from(privateInfo.encryptSecret, 'base64');
}
// 加密函数
encrypt(text: string) {
const iv = crypto.randomBytes(16); // 初始化向量
// const secretKey = crypto.randomBytes(32);
// const key = Buffer.from(secretKey);
const cipher = crypto.createCipheriv('aes-256-cbc', this.secretKey, iv);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString('hex') + ':' + encrypted.toString('hex');
}
// 解密函数
decrypt(encryptedText: string) {
const textParts = encryptedText.split(':');
const iv = Buffer.from(textParts.shift(), 'hex');
const encrypted = Buffer.from(textParts.join(':'), 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this.secretKey), iv);
let decrypted = decipher.update(encrypted);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
}
}
@@ -1,11 +1,20 @@
import { Config, Inject, Provide, Scope, ScopeEnum, sleep } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { In, MoreThan, Repository } from 'typeorm';
import { BaseService, NeedVIPException, PageReq, SysPublicSettings, SysSettingsService, SysSiteInfo } from '@certd/lib-server';
import {
AccessGetter,
AccessService,
BaseService,
NeedSuiteException,
NeedVIPException,
PageReq,
SysPublicSettings,
SysSettingsService,
SysSiteInfo,
} from '@certd/lib-server';
import { PipelineEntity } from '../entity/pipeline.js';
import { PipelineDetail } from '../entity/vo/pipeline-detail.js';
import { Executor, Pipeline, ResultType, RunHistory, SysInfo, UserInfo } from '@certd/pipeline';
import { AccessService } from './access-service.js';
import { Executor, Pipeline, ResultType, RunHistory, RunnableCollection, SysInfo, UserInfo } from '@certd/pipeline';
import { DbStorage } from './db-storage.js';
import { StorageService } from './storage-service.js';
import { Cron } from '../../cron/cron.js';
@@ -15,26 +24,26 @@ import { HistoryLogEntity } from '../entity/history-log.js';
import { HistoryLogService } from './history-log-service.js';
import { EmailService } from '../../basic/service/email-service.js';
import { UserService } from '../../sys/authority/service/user-service.js';
import { AccessGetter } from './access-getter.js';
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
import { CnameProxyService } from './cname-proxy-service.js';
import { PluginConfigGetter } from '../../plugin/service/plugin-config-getter.js';
import dayjs from 'dayjs';
import { DbAdapter } from '../../db/index.js';
import { isComm, isPlus } from '@certd/plus-core';
import { isComm } from '@certd/plus-core';
import { logger } from '@certd/basic';
import { UrlService } from './url-service.js';
import { NotificationService } from './notification-service.js';
import { NotificationGetter } from './notification-getter.js';
import { UserSuiteService } from '@certd/commercial-core';
import { CertInfoService } from '../../monitor/service/cert-info-service.js';
const runningTasks: Map<string | number, Executor> = new Map();
const freeCount = 10;
/**
* 证书申请
*/
@Provide()
@Scope(ScopeEnum.Singleton)
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class PipelineService extends BaseService<PipelineEntity> {
@InjectEntityModel(PipelineEntity)
repository: Repository<PipelineEntity>;
@@ -60,6 +69,9 @@ export class PipelineService extends BaseService<PipelineEntity> {
@Inject()
userService: UserService;
@Inject()
userSuiteService: UserSuiteService;
@Inject()
cron: Cron;
@@ -75,6 +87,9 @@ export class PipelineService extends BaseService<PipelineEntity> {
@Inject()
dbAdapter: DbAdapter;
@Inject()
certInfoService: CertInfoService;
//@ts-ignore
getRepository() {
return this.repository;
@@ -144,26 +159,19 @@ export class PipelineService extends BaseService<PipelineEntity> {
//修改
old = await this.info(bean.id);
}
const pipeline = JSON.parse(bean.content || '{}');
const isUpdate = bean.id > 0 && old != null;
let domains = [];
RunnableCollection.each(pipeline.stages, (runnable: any) => {
if (runnable.runnableType === 'step' && runnable.type.startsWith('CertApply')) {
domains = runnable.input.domains || [];
}
});
if (!isUpdate) {
//如果是添加,校验数量
if (!isPlus()) {
const count = await this.repository.count();
if (count >= freeCount) {
throw new NeedVIPException(`基础版最多只能创建${freeCount}条流水线`);
}
}
const userId = bean.userId;
const userIsAdmin = await this.userService.isAdmin(userId);
if (!userIsAdmin) {
//非管理员用户,限制pipeline数量
const count = await this.repository.count({ where: { userId } });
const sysPublic = await this.sysSettingsService.getSetting<SysPublicSettings>(SysPublicSettings);
const limitUserPipelineCount = sysPublic.limitUserPipelineCount;
if (limitUserPipelineCount && limitUserPipelineCount > 0 && count >= limitUserPipelineCount) {
throw new NeedVIPException(`您最多只能创建${limitUserPipelineCount}条流水线`);
}
}
await this.checkMaxPipelineCount(bean, pipeline, domains);
}
if (!isUpdate) {
@@ -171,19 +179,51 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.addOrUpdate(bean);
}
await this.clearTriggers(bean.id);
if (bean.content) {
const pipeline = JSON.parse(bean.content);
if (pipeline.title) {
bean.title = pipeline.title;
}
pipeline.id = bean.id;
bean.content = JSON.stringify(pipeline);
if (pipeline.title) {
bean.title = pipeline.title;
}
pipeline.id = bean.id;
bean.content = JSON.stringify(pipeline);
await this.addOrUpdate(bean);
await this.registerTriggerById(bean.id);
//保存域名信息到certInfo表
await this.certInfoService.updateDomains(pipeline.id, domains);
return bean;
}
private async checkMaxPipelineCount(bean: PipelineEntity, pipeline: Pipeline, domains: string[]) {
// if (!isPlus()) {
// const count = await this.repository.count();
// if (count >= freeCount) {
// throw new NeedVIPException(`基础版最多只能创建${freeCount}条流水线`);
// }
// }
if (isComm()) {
//校验pipelineCount
const userSuite = await this.userSuiteService.getMySuiteDetail(bean.userId);
if (userSuite?.pipelineCount.used + 1 > userSuite?.pipelineCount.max) {
throw new NeedSuiteException(`对不起,您最多只能创建${userSuite?.pipelineCount.max}条流水线,请购买或升级套餐`);
}
if (userSuite.domainCount.used + domains.length > userSuite.domainCount.max) {
throw new NeedSuiteException(`对不起,您最多只能添加${userSuite.domainCount.max}个域名,请购买或升级套餐`);
}
}
const userId = bean.userId;
const userIsAdmin = await this.userService.isAdmin(userId);
if (!userIsAdmin) {
//非管理员用户,限制pipeline数量
const count = await this.repository.count({ where: { userId } });
const sysPublic = await this.sysSettingsService.getSetting<SysPublicSettings>(SysPublicSettings);
const limitUserPipelineCount = sysPublic.limitUserPipelineCount;
if (limitUserPipelineCount && limitUserPipelineCount > 0 && count >= limitUserPipelineCount) {
throw new NeedVIPException(`您最多只能创建${limitUserPipelineCount}条流水线`);
}
}
}
async foreachPipeline(callback: (pipeline: PipelineEntity) => void) {
const idEntityList = await this.repository.find({
select: {
@@ -578,4 +618,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
{ groupId }
);
}
async getUserPipelineCount(userId) {
return await this.repository.count({ where: { userId } });
}
}
@@ -0,0 +1,31 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { PipelineService } from '../../pipeline/service/pipeline-service.js';
import { CertInfoService } from '../../monitor/service/cert-info-service.js';
import { IUsedCountService } from '@certd/commercial-core';
import { SiteInfoService } from '../../monitor/service/site-info-service.js';
@Provide('myCountService')
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class MyCountService implements IUsedCountService {
@Inject()
pipelineService: PipelineService;
@Inject()
certInfoService: CertInfoService;
@Inject()
siteInfoService: SiteInfoService;
async getUsedCount(userId: number) {
if (!userId) {
throw new Error('userId is required');
}
const pipelineCountUsed = await this.pipelineService.getUserPipelineCount(userId);
const domainCountUsed = await this.certInfoService.getUserDomainCount(userId);
const monitorCountUsed = await this.siteInfoService.getUserMonitorCount(userId);
return {
pipelineCountUsed,
domainCountUsed,
monitorCountUsed,
};
}
}
@@ -206,6 +206,9 @@ export class UserService extends BaseService<UserEntity> {
});
delete newUser.password;
utils.mitter.emit('register', { userId: newUser.id });
return newUser;
}