Files
certd/packages/ui/certd-server/src/modules/pipeline/service/pipeline-service.ts
T

708 lines
20 KiB
TypeScript
Raw Normal View History

import {Config, Inject, Provide, Scope, ScopeEnum, sleep} from "@midwayjs/core";
import {InjectEntityModel} from "@midwayjs/typeorm";
import {In, MoreThan, Repository} from "typeorm";
2024-12-22 14:00:46 +08:00
import {
AccessService,
BaseService,
NeedSuiteException,
NeedVIPException,
PageReq,
SysPublicSettings,
SysSettingsService,
2025-03-18 00:52:50 +08:00
SysSiteInfo
} from "@certd/lib-server";
import {PipelineEntity} from "../entity/pipeline.js";
import {PipelineDetail} from "../entity/vo/pipeline-detail.js";
import {
Executor,
IAccessService,
ICnameProxyService,
INotificationService,
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";
import {HistoryService} from "./history-service.js";
import {HistoryEntity} from "../entity/history.js";
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 {CnameRecordService} from "../../cname/service/cname-record-service.js";
import {PluginConfigGetter} from "../../plugin/service/plugin-config-getter.js";
2025-03-18 00:52:50 +08:00
import dayjs from "dayjs";
import {DbAdapter} from "../../db/index.js";
import {isComm} from "@certd/plus-core";
import {logger} from "@certd/basic";
import {UrlService} from "./url-service.js";
import {NotificationService} from "./notification-service.js";
import {UserSuiteEntity, UserSuiteService} from "@certd/commercial-core";
import {CertInfoService} from "../../monitor/service/cert-info-service.js";
import {TaskServiceBuilder} from "./task-service-getter.js";
2023-01-29 13:44:19 +08:00
2023-07-03 10:54:03 +08:00
const runningTasks: Map<string | number, Executor> = new Map();
2025-03-18 00:52:50 +08:00
2023-01-29 13:44:19 +08:00
/**
* 证书申请
*/
@Provide()
2024-12-22 14:00:46 +08:00
@Scope(ScopeEnum.Request, { allowDowngrade: true })
2023-01-29 13:44:19 +08:00
export class PipelineService extends BaseService<PipelineEntity> {
@InjectEntityModel(PipelineEntity)
repository: Repository<PipelineEntity>;
2023-06-25 15:30:18 +08:00
@Inject()
emailService: EmailService;
2023-01-29 13:44:19 +08:00
@Inject()
accessService: AccessService;
@Inject()
cnameRecordService: CnameRecordService;
@Inject()
2023-01-29 13:44:19 +08:00
storageService: StorageService;
@Inject()
historyService: HistoryService;
@Inject()
historyLogService: HistoryLogService;
2024-10-13 01:27:08 +08:00
@Inject()
pluginConfigGetter: PluginConfigGetter;
2024-10-13 01:27:08 +08:00
@Inject()
taskServiceBuilder: TaskServiceBuilder;
2024-10-26 23:54:49 +08:00
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
userService: UserService;
2024-12-22 14:00:46 +08:00
@Inject()
userSuiteService: UserSuiteService;
2023-01-29 13:44:19 +08:00
@Inject()
cron: Cron;
2023-06-28 15:16:19 +08:00
@Config('certd')
private certdConfig: any;
@Inject()
urlService: UrlService;
@Inject()
notificationService: NotificationService;
2024-10-31 22:35:05 +08:00
@Inject()
dbAdapter: DbAdapter;
2024-12-22 14:00:46 +08:00
@Inject()
certInfoService: CertInfoService;
2024-10-09 02:34:28 +08:00
//@ts-ignore
2023-01-29 13:44:19 +08:00
getRepository() {
return this.repository;
}
2024-08-14 21:24:12 +08:00
async add(bean: PipelineEntity) {
await this.save(bean);
2024-08-14 21:24:12 +08:00
return bean;
}
2024-10-14 00:19:55 +08:00
async page(pageReq: PageReq<PipelineEntity>) {
const result = await super.page(pageReq);
2024-10-31 15:14:56 +08:00
await this.fillLastVars(result.records);
return result;
}
private async fillLastVars(records: PipelineEntity[]) {
2024-08-04 02:35:45 +08:00
const pipelineIds: number[] = [];
const recordMap = {};
2024-10-31 15:14:56 +08:00
for (const record of records) {
2024-08-04 02:35:45 +08:00
pipelineIds.push(record.id);
recordMap[record.id] = record;
2024-10-31 15:14:56 +08:00
record.title = record.title + '';
2024-08-04 02:35:45 +08:00
}
if (pipelineIds?.length > 0) {
const vars = await this.storageService.findPipelineVars(pipelineIds);
for (const varEntity of vars) {
const record = recordMap[varEntity.namespace];
if (record) {
const value = JSON.parse(varEntity.value);
record.lastVars = value.value;
}
2024-08-04 02:35:45 +08:00
}
}
2023-01-29 13:44:19 +08:00
}
2023-05-24 15:41:35 +08:00
public async registerTriggerById(pipelineId) {
2023-01-29 13:44:19 +08:00
if (pipelineId == null) {
return;
}
const info = await this.info(pipelineId);
if (info && !info.disabled) {
const pipeline = JSON.parse(info.content);
// 手动触发,不要await
2023-01-29 13:44:19 +08:00
this.registerTriggers(pipeline);
}
}
/**
* 获取详情
* @param id
*/
async detail(id) {
const pipeline = await this.info(id);
return new PipelineDetail(pipeline);
}
async update(bean: Partial<PipelineEntity>) {
2024-08-05 16:00:04 +08:00
//更新非trigger部分
2024-08-04 02:35:45 +08:00
await super.update(bean);
}
2023-01-29 13:44:19 +08:00
async save(bean: PipelineEntity) {
let old = null;
if (bean.id > 0) {
//修改
old = await this.info(bean.id);
}
2024-12-22 14:00:46 +08:00
const pipeline = JSON.parse(bean.content || '{}');
RunnableCollection.initPipelineRunnableType(pipeline);
const isUpdate = bean.id > 0 && old != null;
2024-12-22 14:00:46 +08:00
let domains = [];
if (pipeline.stages) {
RunnableCollection.each(pipeline.stages, (runnable: any) => {
if (runnable.runnableType === 'step' && runnable.type.startsWith('CertApply')) {
domains = runnable.input.domains || [];
}
});
}
2024-12-22 14:00:46 +08:00
2024-10-26 23:54:49 +08:00
if (!isUpdate) {
//如果是添加,校验数量
2024-12-22 14:00:46 +08:00
await this.checkMaxPipelineCount(bean, pipeline, domains);
2024-08-14 21:24:12 +08:00
}
2024-10-26 23:54:49 +08:00
if (!isUpdate) {
//如果是添加,先保存一下,获取到id,更新pipeline.id
await this.addOrUpdate(bean);
}
2024-08-04 02:35:45 +08:00
await this.clearTriggers(bean.id);
2024-12-22 14:00:46 +08:00
if (pipeline.title) {
bean.title = pipeline.title;
2024-08-05 16:00:04 +08:00
}
2024-12-22 14:00:46 +08:00
pipeline.id = bean.id;
bean.content = JSON.stringify(pipeline);
2023-01-29 13:44:19 +08:00
await this.addOrUpdate(bean);
2024-08-04 02:35:45 +08:00
await this.registerTriggerById(bean.id);
2024-12-22 14:00:46 +08:00
//保存域名信息到certInfo表
2025-03-22 02:06:02 +08:00
let fromType = 'pipeline';
if(bean.type === 'cert_upload') {
fromType = 'upload';
2025-03-18 00:52:50 +08:00
}
2025-03-22 02:06:02 +08:00
await this.certInfoService.updateDomains(pipeline.id, pipeline.userId || bean.userId, domains,fromType);
return bean;
2023-01-29 13:44:19 +08:00
}
2024-12-22 14:00:46 +08:00
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 suiteSetting = await this.userSuiteService.getSuiteSetting();
if (suiteSetting.enabled) {
const userSuite = await this.userSuiteService.getMySuiteDetail(bean.userId);
if (userSuite?.pipelineCount.max != -1 && userSuite?.pipelineCount.used + 1 > userSuite?.pipelineCount.max) {
throw new NeedSuiteException(`对不起,您最多只能创建${userSuite?.pipelineCount.max}条流水线,请购买或升级套餐`);
}
2024-12-22 14:00:46 +08:00
if (userSuite.domainCount.max != -1 && userSuite.domainCount.used + domains.length > userSuite.domainCount.max) {
throw new NeedSuiteException(`对不起,您最多只能添加${userSuite.domainCount.max}个域名,请购买或升级套餐`);
}
2024-12-22 14:00:46 +08:00
}
}
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) {
2024-12-23 23:33:13 +08:00
throw new NeedVIPException(`普通用户最多只能创建${limitUserPipelineCount}条流水线`);
2024-12-22 14:00:46 +08:00
}
}
}
async foreachPipeline(callback: (pipeline: PipelineEntity) => void) {
2023-01-29 13:44:19 +08:00
const idEntityList = await this.repository.find({
select: {
id: true,
},
where: {
disabled: false,
},
});
const ids = idEntityList.map(item => {
return item.id;
});
//id 分段
const idsSpan = [];
let arr = [];
for (let i = 0; i < ids.length; i++) {
if (i % 20 === 0) {
arr = [];
idsSpan.push(arr);
}
arr.push(ids[i]);
}
//分段加载记录
for (const idArr of idsSpan) {
const list = await this.repository.findBy({
id: In(idArr),
});
for (const entity of list) {
await callback(entity);
2023-01-29 13:44:19 +08:00
}
}
}
async stopOtherUserPipeline(userId: number) {
await this.foreachPipeline(async entity => {
if (entity.userId !== userId) {
await this.clearTriggers(entity.id);
}
});
}
/**
* 应用启动后初始加载记录
*/
2024-08-13 20:30:42 +08:00
async onStartup(immediateTriggerOnce: boolean, onlyAdminUser: boolean) {
await this.foreachPipeline(async entity => {
2024-08-13 20:30:42 +08:00
if (onlyAdminUser && entity.userId !== 1) {
return;
}
const pipeline = JSON.parse(entity.content ?? '{}');
try {
await this.registerTriggers(pipeline, immediateTriggerOnce);
} catch (e) {
logger.error('加载定时trigger失败:', e);
}
});
logger.info('定时器数量:', this.cron.getTaskSize());
2023-01-29 13:44:19 +08:00
}
async registerTriggers(pipeline?: Pipeline, immediateTriggerOnce = false) {
2023-01-29 13:44:19 +08:00
if (pipeline?.triggers == null) {
return;
}
for (const trigger of pipeline.triggers) {
this.registerCron(pipeline.id, trigger);
}
if (immediateTriggerOnce) {
await this.trigger(pipeline.id);
await sleep(200);
}
2023-01-29 13:44:19 +08:00
}
2024-09-02 18:36:12 +08:00
async trigger(id: any, stepId?: string) {
const entity: PipelineEntity = await this.info(id);
if (isComm()) {
await this.checkHasDeployCount(id, entity.userId);
}
2023-01-29 13:44:19 +08:00
this.cron.register({
name: `pipeline.${id}.trigger.once`,
cron: null,
job: async () => {
2024-07-18 11:17:13 +08:00
logger.info('用户手动启动job');
2024-07-15 00:30:33 +08:00
try {
await this.doRun(entity, null, stepId);
2024-07-15 00:30:33 +08:00
} catch (e) {
2024-07-18 11:17:13 +08:00
logger.error('手动job执行失败:', e);
2024-07-15 00:30:33 +08:00
}
2023-01-29 13:44:19 +08:00
},
});
}
async checkHasDeployCount(pipelineId: number, userId: number) {
try {
return await this.userSuiteService.checkHasDeployCount(userId);
} catch (e) {
if (e instanceof NeedSuiteException) {
logger.error(e.message);
await this.update({
id: pipelineId,
status: 'no_deploy_count',
});
}
throw e;
}
}
2024-10-15 11:55:59 +08:00
async delete(id: any) {
2024-08-04 02:35:45 +08:00
await this.clearTriggers(id);
//TODO 删除storage
// const storage = new DbStorage(pipeline.userId, this.storageService);
// await storage.remove(pipeline.id);
await super.delete([id]);
await this.historyService.deleteByPipelineId(id);
await this.historyLogService.deleteByPipelineId(id);
2024-12-27 22:40:07 +08:00
await this.certInfoService.deleteByPipelineId(id);
2024-08-04 02:35:45 +08:00
}
async clearTriggers(id: number) {
if (id == null) {
return;
}
2023-06-28 12:46:29 +08:00
const pipeline = await this.info(id);
if (!pipeline) {
return;
}
const pipelineObj = JSON.parse(pipeline.content);
if (pipelineObj.triggers) {
for (const trigger of pipelineObj.triggers) {
this.removeCron(id, trigger);
}
}
}
removeCron(pipelineId, trigger) {
const name = this.buildCronKey(pipelineId, trigger.id);
this.cron.remove(name);
}
2023-01-29 13:44:19 +08:00
registerCron(pipelineId, trigger) {
if (pipelineId == null) {
logger.warn('pipelineId为空,无法注册定时任务');
return;
}
2023-01-29 13:44:19 +08:00
let cron = trigger.props?.cron;
if (cron == null) {
return;
}
2024-08-04 02:35:45 +08:00
cron = cron.trim();
if (cron.startsWith('* *')) {
cron = cron.replace('* *', '0 0');
}
2023-05-23 18:01:20 +08:00
if (cron.startsWith('*')) {
cron = cron.replace('*', '0');
2023-01-29 13:44:19 +08:00
}
const triggerId = trigger.id;
const name = this.buildCronKey(pipelineId, triggerId);
2023-05-24 15:41:35 +08:00
this.cron.remove(name);
2023-01-29 13:44:19 +08:00
this.cron.register({
2023-05-24 15:41:35 +08:00
name,
2024-08-04 02:35:45 +08:00
cron,
2023-01-29 13:44:19 +08:00
job: async () => {
logger.info('定时任务触发:', pipelineId, triggerId);
if (pipelineId == null) {
logger.warn('pipelineId为空,无法执行');
return;
}
2024-07-18 11:17:13 +08:00
try {
await this.run(pipelineId, triggerId);
2024-07-18 11:17:13 +08:00
} catch (e) {
logger.error('定时job执行失败:', e);
}
2023-01-29 13:44:19 +08:00
},
});
logger.info('当前定时器数量:', this.cron.getTaskSize());
2023-01-29 13:44:19 +08:00
}
2024-09-02 18:36:12 +08:00
async run(id: number, triggerId: string, stepId?: string) {
2023-01-29 13:44:19 +08:00
const entity: PipelineEntity = await this.info(id);
await this.doRun(entity, triggerId, stepId);
}
2024-08-04 02:49:40 +08:00
async doRun(entity: PipelineEntity, triggerId: string, stepId?: string) {
const id = entity.id;
let suite: UserSuiteEntity = null;
if (isComm()) {
suite = await this.checkHasDeployCount(id, entity.userId);
}
2023-01-29 13:44:19 +08:00
const pipeline = JSON.parse(entity.content);
if (!pipeline.id) {
pipeline.id = id;
}
2023-01-29 13:44:19 +08:00
if (!pipeline.stages || pipeline.stages.length === 0) {
return;
}
const triggerType = this.getTriggerType(triggerId, pipeline);
if (triggerType == null) {
return;
}
2024-08-04 02:49:40 +08:00
if (triggerType === 'timer') {
if (entity.disabled) {
return;
}
}
2023-01-29 13:44:19 +08:00
const onChanged = async (history: RunHistory) => {
//保存执行历史
2023-05-23 18:01:20 +08:00
try {
logger.info('保存执行历史:', history.id);
2023-05-23 18:01:20 +08:00
await this.saveHistory(history);
} catch (e) {
const pipelineEntity = new PipelineEntity();
pipelineEntity.id = id;
2023-05-23 18:01:20 +08:00
pipelineEntity.status = 'error';
pipelineEntity.lastHistoryTime = history.pipeline.status.startTime;
await this.update(pipelineEntity);
logger.error('保存执行历史失败:', e);
throw e;
}
2023-01-29 13:44:19 +08:00
};
const userId = entity.userId;
const historyId = await this.historyService.start(entity);
const userIsAdmin = await this.userService.isAdmin(userId);
const user: UserInfo = {
id: userId,
role: userIsAdmin ? 'admin' : 'user',
};
2024-12-04 12:36:17 +08:00
2024-12-04 12:36:17 +08:00
const sysInfo: SysInfo = {};
if (isComm()) {
const siteInfo = await this.sysSettingsService.getSetting<SysSiteInfo>(SysSiteInfo);
sysInfo.title = siteInfo.title;
}
const taskServiceGetter = this.taskServiceBuilder.create({
userId,
})
const accessGetter = await taskServiceGetter.get<IAccessService>("accessService")
const notificationGetter =await taskServiceGetter.get<INotificationService>("notificationService")
const cnameProxyService =await taskServiceGetter.get<ICnameProxyService>("cnameProxyService")
2023-01-29 13:44:19 +08:00
const executor = new Executor({
user,
2023-01-29 13:44:19 +08:00
pipeline,
onChanged,
accessService: accessGetter,
cnameProxyService,
pluginConfigService: this.pluginConfigGetter,
2023-01-29 13:44:19 +08:00
storage: new DbStorage(userId, this.storageService),
2023-06-25 15:30:18 +08:00
emailService: this.emailService,
urlService: this.urlService,
notificationService: notificationGetter,
2023-06-28 15:16:19 +08:00
fileRootDir: this.certdConfig.fileRootDir,
2024-12-04 12:36:17 +08:00
sysInfo,
serviceGetter:taskServiceGetter
2023-01-29 13:44:19 +08:00
});
2023-05-24 15:41:35 +08:00
try {
2023-07-03 11:16:46 +08:00
runningTasks.set(historyId, executor);
2023-05-24 15:41:35 +08:00
await executor.init();
2024-09-02 18:36:12 +08:00
if (stepId) {
// 清除该step的状态
executor.clearLastStatus(stepId);
}
const result = await executor.run(historyId, triggerType);
if (result === ResultType.success) {
if (isComm()) {
// 消耗成功次数
await this.userSuiteService.consumeDeployCount(suite, 1);
}
}
2023-05-24 15:41:35 +08:00
} catch (e) {
logger.error('执行失败:', e);
2024-07-15 00:30:33 +08:00
// throw e;
2023-07-03 10:54:03 +08:00
} finally {
2023-07-03 11:16:46 +08:00
runningTasks.delete(historyId);
}
}
async cancel(historyId: number) {
2023-07-03 11:16:46 +08:00
const executor = runningTasks.get(historyId);
if (executor) {
2023-07-03 11:45:32 +08:00
await executor.cancel();
2023-05-24 15:41:35 +08:00
}
2024-07-18 11:17:13 +08:00
const entity = await this.historyService.info(historyId);
if (entity == null) {
return;
}
const pipeline: Pipeline = JSON.parse(entity.pipeline);
pipeline.status.status = ResultType.canceled;
pipeline.status.result = ResultType.canceled;
const runtime = new RunHistory(historyId, null, pipeline);
await this.saveHistory(runtime);
2023-01-29 13:44:19 +08:00
}
private getTriggerType(triggerId, pipeline) {
let triggerType = 'user';
if (triggerId != null) {
//如果不是手动触发
//查找trigger
const found = this.findTrigger(pipeline, triggerId);
if (!found) {
//如果没有找到triggerId,说明被用户删掉了,这里再删除一次
this.cron.remove(this.buildCronKey(pipeline.id, triggerId));
triggerType = null;
} else {
logger.info('timer trigger:' + found.id, found.title, found.cron);
triggerType = 'timer';
}
}
return triggerType;
}
private buildCronKey(pipelineId, triggerId) {
return `pipeline.${pipelineId}.trigger.${triggerId}`;
}
private findTrigger(pipeline, triggerId) {
for (const trigger of pipeline.triggers) {
if (trigger.id === triggerId) {
return trigger;
}
}
return;
}
private async saveHistory(history: RunHistory) {
//修改pipeline状态
const pipelineEntity = new PipelineEntity();
pipelineEntity.id = parseInt(history.pipeline.id);
2023-05-24 15:41:35 +08:00
pipelineEntity.status = history.pipeline.status.status + '';
2023-01-29 13:44:19 +08:00
pipelineEntity.lastHistoryTime = history.pipeline.status.startTime;
await this.update(pipelineEntity);
const entity: HistoryEntity = new HistoryEntity();
entity.id = parseInt(history.id);
entity.userId = history.pipeline.userId;
2024-07-15 00:30:33 +08:00
entity.status = pipelineEntity.status;
2023-01-29 13:44:19 +08:00
entity.pipeline = JSON.stringify(history.pipeline);
2023-05-23 18:01:20 +08:00
entity.pipelineId = parseInt(history.pipeline.id);
2023-01-29 13:44:19 +08:00
await this.historyService.save(entity);
const logEntity: HistoryLogEntity = new HistoryLogEntity();
logEntity.id = entity.id;
logEntity.userId = entity.userId;
logEntity.pipelineId = entity.pipelineId;
logEntity.historyId = entity.id;
logEntity.logs = JSON.stringify(history.logs);
await this.historyLogService.addOrUpdate(logEntity);
}
2024-10-31 15:14:56 +08:00
2024-10-31 16:19:35 +08:00
async count(param: { userId?: any }) {
2024-10-31 15:14:56 +08:00
const count = await this.repository.count({
where: {
userId: param.userId,
},
});
return count;
}
2024-10-31 16:19:35 +08:00
async statusCount(param: { userId?: any } = {}) {
2024-10-31 15:14:56 +08:00
const statusCount = await this.repository
.createQueryBuilder()
.select('status')
.addSelect('count(1)', 'count')
.where({
userId: param.userId,
})
.groupBy('status')
.getRawMany();
return statusCount;
}
async latestExpiringList({ userId }: any) {
let list = await this.repository.find({
select: {
id: true,
title: true,
status: true,
},
where: {
userId,
},
});
await this.fillLastVars(list);
list = list.filter(item => {
return item.lastVars?.certExpiresTime != null;
});
list = list.sort((a, b) => {
return a.lastVars.certExpiresTime - b.lastVars.certExpiresTime;
});
return list.slice(0, 5);
}
2024-10-31 16:19:35 +08:00
async createCountPerDay(param: { days: number } = { days: 7 }) {
const todayEnd = dayjs().endOf('day');
const result = await this.getRepository()
.createQueryBuilder('main')
2024-10-31 22:35:05 +08:00
.select(`${this.dbAdapter.date('main.createTime')} AS date`) // 将UNIX时间戳转换为日期
.addSelect('COUNT(1) AS count')
2024-10-31 16:19:35 +08:00
.where({
// 0点
createTime: MoreThan(todayEnd.add(-param.days, 'day').toDate()),
})
.groupBy('date')
.getRawMany();
return result;
}
async batchDelete(ids: number[], userId: number) {
for (const id of ids) {
await this.checkUserId(id, userId);
await this.delete(id);
}
}
async batchUpdateGroup(ids: number[], groupId: number, userId: any) {
await this.repository.update(
{
id: In(ids),
userId,
},
{ groupId }
);
}
2024-12-22 14:00:46 +08:00
async getUserPipelineCount(userId) {
return await this.repository.count({ where: { userId } });
}
async getSimplePipelines(pipelineIds: number[], userId?: number) {
return await this.repository.find({
select: {
id: true,
title: true,
},
where: {
id: In(pipelineIds),
userId,
},
});
}
2025-03-18 00:52:50 +08:00
2023-01-29 13:44:19 +08:00
}