fix: 改为由后端判断分片上传情况

This commit is contained in:
Pan Qiancheng 2025-12-26 16:22:32 +08:00
parent 147dc5eb12
commit 56b07dc4cb
63 changed files with 896 additions and 29 deletions

View File

@ -72,9 +72,17 @@ export async function mergeChunkedUpload(params, context) {
assert(extrafile.chunkInfo, `extraFile ${extraFileId} 的chunkInfo信息缺失`);
assert(!extrafile.chunkInfo.merged, `extraFile ${extraFileId} 已经合并过分片,无需重复合并`);
// 必须保证所有分片都有上传完成
const allPartsDone = extrafile.chunkInfo.parts.every(part => part.etag);
assert(allPartsDone, `extraFile ${extraFileId} 存在未上传完成的分片,无法合并`);
const cos = getCosBackend(extrafile.origin);
const { parts } = await cos.listMultipartUploads(extrafile.application, extrafile, context);
const allPartsDone = parts.every(part => part.etag && part.size > 0);
assert(allPartsDone, `extraFile ${extraFileId} 存在未上传完成的分片,无法合并`);
// 赋值顺带删除一些无用信息减小体积出现过mysql排序超出限制的问题
extrafile.chunkInfo.parts = parts.map((part, index) => ({
...extrafile.chunkInfo.parts[index],
partNumber: part.partNumber,
etag: part.etag,
uploadUrl: '', // 不需要保存上传链接
}));
await cos.mergeChunkedUpload(extrafile.application, extrafile, context);
// 更新chunkInfo状态
const closeRootMode = context.openRootMode();

View File

@ -1,5 +1,7 @@
import { EntityDict } from "../../oak-app-domain";
declare const _default: (props: import("oak-frontend-base").ReactComponentProps<EntityDict, "applicationPassport", true, {
import { ReactComponentProps } from "oak-frontend-base";
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends keyof ED2>(props: ReactComponentProps<ED2, T2, true, {
systemId: string;
}>) => React.ReactElement;
export default _default;

View File

@ -1,2 +1,2 @@
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "mobile", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "mobile", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
export default _default;

View File

@ -14,7 +14,7 @@ export declare function createToDo<ED extends EntityDict & BaseEntityDict, T ext
redirectTo: EntityDict['toDo']['OpSchema']['redirectTo'];
entity: any;
entityId: string;
}, userIds?: string[]): Promise<1 | 0>;
}, userIds?: string[]): Promise<0 | 1>;
/**
* todo例程entity对象上进行action操作时filtertodo完成
* entity的action的后trigger中调用

11
es/types/Cos.d.ts vendored
View File

@ -103,4 +103,15 @@ export interface CosBackend<ED extends EntityDict> {
*
*/
mergeChunkedUpload: (application: ED['application']['Schema'], extraFile: ED['extraFile']['OpSchema'], context: BRC<ED>) => Promise<void>;
/**
*
*/
listMultipartUploads: (application: ED['application']['Schema'], extraFile: ED['extraFile']['OpSchema'], context: BRC<ED>) => Promise<{
parts: Array<{
partNumber: number;
etag: string;
size: number;
lastModified: Date;
}>;
}>;
}

View File

@ -29,4 +29,12 @@ export default class ALiYunBackend extends ALiYun implements CosBackend<EntityDi
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: Array<{
partNumber: number;
etag: string;
size: number;
lastModified: Date;
}>;
}>;
}

View File

@ -144,4 +144,11 @@ export default class ALiYunBackend extends ALiYun {
assert(b, `extraFile中的bucket名称在阿里云配置中找不到「${extraFile.bucket}`);
await instance.abortMultipartUpload(extraFile.bucket, b.zone, key, extraFile.chunkInfo.uploadId);
}
async listMultipartUploads(application, extraFile, context) {
const key = this.formKey(extraFile);
const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application);
const b = aliyunCosConfig.buckets.find((ele) => ele.name === extraFile.bucket);
assert(b, `extraFile中的bucket名称在阿里云配置中找不到「${extraFile.bucket}`);
return await instance.listParts(extraFile.bucket, b.zone, key, extraFile.chunkInfo.uploadId);
}
}

View File

@ -44,7 +44,7 @@ export async function chunkUpload(options) {
// 验证上传是否成功
let isSuccess = false;
if (process.env.OAK_PLATFORM === 'wechatMp') {
if (response.errMsg === 'uploadFile:ok') {
if (response.errMsg === 'request:ok') {
const data = JSON.parse(response.data);
isSuccess = !!(data.status === 204 || data.status === 200);
}
@ -118,10 +118,10 @@ export async function chunkUpload(options) {
}
// 等待所有任务完成
await Promise.all(executing);
// 调用分片成功回调(所有分片完成后)
if (onChunkSuccess) {
await onChunkSuccess(chunkInfo);
}
// // 调用分片成功回调(所有分片完成后)
// if (onChunkSuccess) {
// await onChunkSuccess(chunkInfo);
// }
// 清理小程序环境下的临时文件
if (process.env.OAK_PLATFORM === 'wechatMp' && typeof file === 'string') {
await cleanTempFiles(chunks);

View File

@ -26,4 +26,7 @@ export default class CTYunBackend extends CTYun implements CosBackend<EntityDict
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: never[];
}>;
}

View File

@ -95,4 +95,9 @@ export default class CTYunBackend extends CTYun {
const key = this.formKey(extraFile);
const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application);
}
async listMultipartUploads(application, extraFile, context) {
return {
parts: [],
};
}
}

View File

@ -26,4 +26,7 @@ export default class LocalBackend extends Local implements CosBackend<EntityDict
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: never[];
}>;
}

View File

@ -73,5 +73,10 @@ export default class LocalBackend extends Local {
const key = this.formKey(extraFile);
const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application);
}
async listMultipartUploads(application, extraFile, context) {
return {
parts: [],
};
}
}
;

View File

@ -26,4 +26,7 @@ export default class QiniuBackend extends Qiniu implements CosBackend<EntityDict
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: never[];
}>;
}

View File

@ -114,5 +114,10 @@ export default class QiniuBackend extends Qiniu {
const key = this.formKey(extraFile);
const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application);
}
async listMultipartUploads(application, extraFile, context) {
return {
parts: [],
};
}
}
;

View File

@ -29,4 +29,12 @@ export default class S3Backend extends S3 implements CosBackend<EntityDict> {
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: Array<{
partNumber: number;
etag: string;
size: number;
lastModified: Date;
}>;
}>;
}

View File

@ -129,4 +129,18 @@ export default class S3Backend extends S3 {
const { instance, config: s3CosConfig } = this.getConfigAndInstance(application, extraFile.bucket);
await instance.abortMultipartUpload(extraFile.bucket, key, extraFile.chunkInfo.uploadId, s3CosConfig.endpoint);
}
async listMultipartUploads(application, extraFile, context) {
const key = this.formKey(extraFile);
const { instance, config: s3CosConfig } = this.getConfigAndInstance(application, extraFile.bucket);
const result = await instance.listParts(extraFile.bucket, key, extraFile.chunkInfo.uploadId, s3CosConfig.endpoint, 101);
assert(result.isTruncated === false, `分片数量超过101无法列出所有分片信息不应当出现这个情况触发器中已经限制了最大分片数量为100`);
return {
parts: result.parts.map((part) => ({
partNumber: part.partNumber,
etag: part.eTag,
size: part.size,
lastModified: part.lastModified,
}))
};
}
}

View File

@ -26,4 +26,7 @@ export default class TencentYunBackend extends TencentYun implements CosBackend<
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: never[];
}>;
}

View File

@ -93,4 +93,9 @@ export default class TencentYunBackend extends TencentYun {
}
async abortMultipartUpload(application, extraFile, context) {
}
async listMultipartUploads(application, extraFile, context) {
return {
parts: [],
};
}
}

View File

@ -26,4 +26,7 @@ export default class UnknownBackend extends Unknown implements CosBackend<Entity
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: never[];
}>;
}

View File

@ -33,5 +33,10 @@ export default class UnknownBackend extends Unknown {
}
async abortMultipartUpload(application, extraFile, context) {
}
async listMultipartUploads(application, extraFile, context) {
return {
parts: [],
};
}
}
;

View File

@ -26,4 +26,7 @@ export default class WechatBackend extends Wechat implements CosBackend<EntityDi
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: never[];
}>;
}

View File

@ -54,5 +54,10 @@ export default class WechatBackend extends Wechat {
}
async abortMultipartUpload(application, extraFile, context) {
}
async listMultipartUploads(application, extraFile, context) {
return {
parts: [],
};
}
}
;

View File

@ -67,6 +67,7 @@ export async function sliceFileForMp(filePath, chunkSize, partCount) {
chunks.push(tempFilePath);
}
catch (err) {
console.error('分片读取失败:', err);
// 清理已创建的临时文件
await cleanTempFiles(chunks);
throw new OakUploadException(`分片 ${i + 1} 读取失败: ${err}`);

View File

@ -2,6 +2,8 @@ import { assert } from 'oak-domain/lib/utils/assert';
import { getCosBackend } from '../utils/cos/index.backend';
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { groupBy } from 'oak-domain/lib/utils/lodash';
import { extraFileProjection } from '../types/Projection';
import applicationPassport from '../components/applicationPassport';
async function checkWhetherSuccess(context, applicationId, rows) {
const successIds = [];
const failedIds = [];
@ -59,6 +61,9 @@ const watchers = [
$lt: deadline,
},
uploadState: 'uploading',
enableChunkedUpload: {
$exists: false,
}
};
},
projection: {
@ -82,5 +87,38 @@ const watchers = [
};
}
},
{
name: '确定uploading的文件状态',
entity: 'extraFile',
filter: async () => {
const now = Date.now();
const deadline = process.env.NODE_ENV === 'production' ? now - 3 * 24 * 60 * 60 * 1000 : now - 60 * 1000;
return {
$$updateAt$$: {
$lt: deadline,
},
uploadState: 'uploading',
enableChunkedUpload: true
};
},
projection: {
...extraFileProjection,
application: {
...applicationPassport,
}
},
fn: async (context, data) => {
const eg = groupBy(data, 'applicationId');
for (const appId in eg) {
// 这里检查需要分片上传的文件信息,但是考虑到可能还在上传之类的,所以先不管,后面再处理
// 1. 要去查询分片信息看看是不是都有etag了如果都有etag了就说明上传完成了否则就标记为失败
}
return {
extraFile: {
update: data.length,
}
};
}
},
];
export default watchers;

View File

@ -78,9 +78,17 @@ async function mergeChunkedUpload(params, context) {
(0, assert_1.assert)(extrafile.chunkInfo, `extraFile ${extraFileId} 的chunkInfo信息缺失`);
(0, assert_1.assert)(!extrafile.chunkInfo.merged, `extraFile ${extraFileId} 已经合并过分片,无需重复合并`);
// 必须保证所有分片都有上传完成
const allPartsDone = extrafile.chunkInfo.parts.every(part => part.etag);
(0, assert_1.assert)(allPartsDone, `extraFile ${extraFileId} 存在未上传完成的分片,无法合并`);
const cos = (0, index_backend_1.getCosBackend)(extrafile.origin);
const { parts } = await cos.listMultipartUploads(extrafile.application, extrafile, context);
const allPartsDone = parts.every(part => part.etag && part.size > 0);
(0, assert_1.assert)(allPartsDone, `extraFile ${extraFileId} 存在未上传完成的分片,无法合并`);
// 赋值顺带删除一些无用信息减小体积出现过mysql排序超出限制的问题
extrafile.chunkInfo.parts = parts.map((part, index) => ({
...extrafile.chunkInfo.parts[index],
partNumber: part.partNumber,
etag: part.etag,
uploadUrl: '', // 不需要保存上传链接
}));
await cos.mergeChunkedUpload(extrafile.application, extrafile, context);
// 更新chunkInfo状态
const closeRootMode = context.openRootMode();

View File

@ -0,0 +1,7 @@
import { EntityDict } from "../../oak-app-domain";
import { ReactComponentProps } from "oak-frontend-base";
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
declare const _default: <ED2 extends EntityDict & BaseEntityDict, T2 extends keyof ED2>(props: ReactComponentProps<ED2, T2, true, {
systemId: string;
}>) => React.ReactElement;
export default _default;

View File

@ -0,0 +1,356 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const lodash_1 = require("oak-domain/lib/utils/lodash");
const uuid_1 = require("oak-domain/lib/utils/uuid");
exports.default = OakComponent({
entity: 'applicationPassport',
isList: true,
projection: {
id: 1,
applicationId: 1,
application: {
id: 1,
name: 1,
type: 1,
systemId: 1,
},
passportId: 1,
passport: {
id: 1,
type: 1,
systemId: 1,
enabled: 1,
},
isDefault: 1,
},
properties: {
systemId: '',
},
filters: [
{
filter() {
const { systemId } = this.props;
return {
application: {
systemId,
},
passport: {
systemId,
},
};
}
}
],
formData({ data }) {
const aps = data.filter((ele) => ele.$$deleteAt$$ !== 1);
return {
aps,
};
},
listeners: {
async 'aps,applications,passports'(prev, next) {
if (!this.arraysAreEqual(prev.aps, next.aps) || !this.arraysAreEqual(prev.applications, next.applications) || !this.arraysAreEqual(prev.passports, next.passports)) {
let apArray = [];
const records = (0, lodash_1.groupBy)(next.passports, 'type');
if (next.applications && next.applications.length > 0 && next.passports && next.passports.length > 0) {
for (const a of next.applications) {
let item = {
aId: a.id,
aName: a.name,
typeRecords: {},
defaultOptions: [],
defaultValue: '',
};
let typeRecords = {};
for (const key of Object.keys(records)) {
const r = records[key];
const render = this.getRender(key, r, a.type);
if (render === 'select') {
const passportOptions = r.map((ele) => {
const { disabled, disabledTip } = this.checkDisabled(a, ele);
return {
label: ele.type === 'email' ? ele.config.account : ele.config.appId,
value: ele.id,
apId: (0, uuid_1.generateNewId)(),
disabled,
disabledTip,
};
});
const d = !passportOptions.find((ele) => !ele.disabled);
let disabledTip = '';
if (d) {
disabledTip = '暂不支持该登录方式';
}
Object.assign(typeRecords, { [key]: { render, passportOptions, chekedValue: undefined, disabled: d, disabledTip } });
}
else {
const { disabled, disabledTip } = this.checkDisabled(a, r[0]);
const apId = await (0, uuid_1.generateNewIdAsync)();
Object.assign(typeRecords, { [key]: { render, pId: r[0].id, checked: false, disabled, disabledTip, apId, } });
}
}
Object.assign(item, { typeRecords });
apArray.push(item);
}
if (next.aps && next.aps.length > 0) {
for (const ap of next.aps) {
const aIdx = apArray.findIndex((ele) => ele.aId === ap.applicationId);
if (aIdx !== -1) {
const p = ap.passport;
const t = p.type;
if (apArray[aIdx].typeRecords[t].render === 'select') {
apArray[aIdx].typeRecords[t].checkedValue = p.id;
apArray[aIdx].typeRecords[t].apId = ap.id;
const option = apArray[aIdx].typeRecords[t].passportOptions?.find((ele) => ele.value === p.id);
option && Object.assign(option, { apId: ap.id });
}
else {
if (apArray[aIdx].typeRecords[t].pId === p.id) {
apArray[aIdx].typeRecords[t].checked = true;
apArray[aIdx].typeRecords[t].apId = ap.id;
}
}
apArray[aIdx].defaultOptions.push({
label: this.t(`passport:v.type.${p.type}`),
value: ap.id,
});
if (ap.isDefault) {
apArray[aIdx].defaultValue = ap.id;
}
}
}
}
}
this.setState({
apArray,
});
}
}
},
data: {
applications: [],
passports: [],
apArray: [],
types: [],
},
lifetimes: {
async ready() {
const { systemId } = this.props;
const { data: applicationDatas } = await this.features.cache.refresh('application', {
data: {
id: 1,
name: 1,
type: 1,
config: 1,
systemId: 1,
},
filter: {
systemId,
}
});
const { data: passportDatas } = await this.features.cache.refresh('passport', {
data: {
id: 1,
type: 1,
config: 1,
enabled: 1,
systemId: 1,
},
filter: {
systemId,
enabled: true,
},
sorter: [{
$attr: {
$$updateAt$$: 1,
},
$direction: 'desc'
}]
});
const applications = applicationDatas;
const passports = passportDatas;
const types = (0, lodash_1.uniq)(passports.map((ele) => ele.type));
this.setState({
applications,
passports,
types,
});
}
},
methods: {
arraysAreEqual(first, second) {
if (first?.length !== second?.length) {
return false;
}
for (let i = 0; i < first?.length; ++i) {
if (!(0, lodash_1.isEqual)(first[i], second[i])) {
return false;
}
}
return true;
},
checkDisabled(application, passport) {
const { type: aType, config: aConfig } = application;
const { type: pType, config: pConfig } = passport;
switch (pType) {
case 'sms':
if (!pConfig.mockSend) {
if (!pConfig.templateName || pConfig.templateName === '') {
return {
disabled: true,
disabledTip: '短信登录未配置验证码模板名称',
};
}
if (!pConfig.defaultOrigin) {
return {
disabled: true,
disabledTip: '短信登录未配置默认渠道',
};
}
}
break;
case 'email':
if (!pConfig.mockSend) {
if (!pConfig.account || pConfig.account === '') {
return {
disabled: true,
disabledTip: '邮箱登录未配置账号',
};
}
else if (!pConfig.subject || pConfig.subject === '') {
return {
disabled: true,
disabledTip: '邮箱登录未配置邮件主题',
};
}
else if ((!pConfig.text || pConfig.text === '' || !pConfig.text?.includes('${code}')) &&
(!pConfig.html || pConfig.html === '' || !pConfig.html?.includes('${code}'))) {
return {
disabled: true,
disabledTip: '邮箱登录未配置邮件内容模板',
};
}
}
break;
case 'wechatPublicForWeb':
if (!pConfig.appId || pConfig.appId === '') {
return {
disabled: true,
disabledTip: '公众号授权登录未配置appId',
};
}
break;
case 'wechatMpForWeb':
if (!pConfig.appId || pConfig.appId === '') {
return {
disabled: true,
disabledTip: '小程序授权登录未配置appId',
};
}
break;
default:
break;
}
switch (aType) {
case 'web':
if (pType === 'wechatWeb') {
//微信网站登录 application需配置微信网站appId
const { appId } = aConfig.wechat || {};
if (!appId || appId === '') {
return {
disabled: true,
disabledTip: '当前application未配置微信网站appId',
};
}
}
else if (pType === 'wechatMp' || pType === 'wechatPublic') {
return {
disabled: true,
disabledTip: '当前application不支持该登录方式',
};
}
break;
case 'wechatMp':
if (['wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) {
return {
disabled: true,
disabledTip: '当前application不支持该登录方式',
};
}
break;
case 'wechatPublic':
if (['wechatMp', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) {
return {
disabled: true,
disabledTip: '当前application不支持该登录方式',
};
}
break;
case 'native':
if (['wechatMp', 'wechatPublic', 'wechatWeb', 'wechatMpForWeb', 'wechatPublicForWeb'].includes(pType)) {
return {
disabled: true,
disabledTip: '当前application不支持该登录方式',
};
}
break;
default:
break;
}
return {
disabled: false,
disabledTip: undefined,
};
},
async onCheckedChange(aId, pId, checked, apId) {
if (checked) {
//create applicationPassport
this.addItem({
applicationId: aId,
passportId: pId,
isDefault: true,
});
}
else {
//remove id为apId的applicationPassport
apId && this.removeItem(apId);
}
},
checkLastOne(aId, pId) {
const { apArray } = this.state;
const idx = apArray.findIndex((ele) => ele.aId === aId);
if (idx !== -1) {
const records = apArray[idx].typeRecords;
for (const key of Object.keys(records)) {
const r = records[key];
if ((r.checkedValue && r.checkedValue !== pId && !!r.checkedValue) || (r.pId && r.pId !== pId && !!r.checked)) {
return false;
}
}
return true;
}
return false;
},
async onSelectChange(aId, addPId, removeApId) {
if (removeApId) {
removeApId && this.removeItem(removeApId);
}
if (addPId) {
this.addItem({
applicationId: aId,
passportId: addPId,
isDefault: true,
});
}
},
getRender(pType, passports, aType) {
let render = 'switch';
if (passports.length > 1) {
if (aType === 'web' || (['wechatMp', 'wechatPublic'].includes(aType) && ['email', 'sms'].includes(pType))) {
render = 'select';
}
}
return render;
}
}
});

View File

@ -1,6 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initialize = exports.create = void 0;
exports.create = create;
exports.initialize = initialize;
const tslib_1 = require("tslib");
const token_1 = require("./token");
const extraFile_1 = require("./extraFile");
@ -37,7 +38,6 @@ function create(basicFeatures) {
userWechatPublicTag,
};
}
exports.create = create;
const selectionRewriter_1 = require("../utils/selectionRewriter");
async function initialize(features, access, config, clazzes) {
features.cache.registerSelectionRewriter(selectionRewriter_1.rewriteSelection);
@ -56,4 +56,3 @@ async function initialize(features, access, config, clazzes) {
}
}
}
exports.initialize = initialize;

View File

@ -1,2 +1,2 @@
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "mobile", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
declare const _default: (import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "address", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "application", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "article", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "articleMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "extraFile", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "user", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "userEntityGrant", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatQrCode", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "message", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "notification", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatLogin", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "parasite", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "sessionMessage", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMenu", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatPublicTag", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "wechatMpJump", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "system", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "passport", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthApplication", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthProvider", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUser", import("..").BRC<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "oauthUserAuthorization", import("../context/BackendRuntimeContext").BackendRuntimeContext<import("../oak-app-domain").EntityDict>> | import("oak-domain/lib/types").Trigger<import("../oak-app-domain").EntityDict, "mobile", import("..").BRC<import("../oak-app-domain").EntityDict>>)[];
export default _default;

11
lib/types/Cos.d.ts vendored
View File

@ -103,4 +103,15 @@ export interface CosBackend<ED extends EntityDict> {
*
*/
mergeChunkedUpload: (application: ED['application']['Schema'], extraFile: ED['extraFile']['OpSchema'], context: BRC<ED>) => Promise<void>;
/**
*
*/
listMultipartUploads: (application: ED['application']['Schema'], extraFile: ED['extraFile']['OpSchema'], context: BRC<ED>) => Promise<{
parts: Array<{
partNumber: number;
etag: string;
size: number;
lastModified: Date;
}>;
}>;
}

View File

@ -29,4 +29,12 @@ export default class ALiYunBackend extends ALiYun implements CosBackend<EntityDi
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: Array<{
partNumber: number;
etag: string;
size: number;
lastModified: Date;
}>;
}>;
}

View File

@ -147,5 +147,12 @@ class ALiYunBackend extends aliyun_1.default {
(0, assert_1.assert)(b, `extraFile中的bucket名称在阿里云配置中找不到「${extraFile.bucket}`);
await instance.abortMultipartUpload(extraFile.bucket, b.zone, key, extraFile.chunkInfo.uploadId);
}
async listMultipartUploads(application, extraFile, context) {
const key = this.formKey(extraFile);
const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application);
const b = aliyunCosConfig.buckets.find((ele) => ele.name === extraFile.bucket);
(0, assert_1.assert)(b, `extraFile中的bucket名称在阿里云配置中找不到「${extraFile.bucket}`);
return await instance.listParts(extraFile.bucket, b.zone, key, extraFile.chunkInfo.uploadId);
}
}
exports.default = ALiYunBackend;

View File

@ -49,7 +49,7 @@ async function chunkUpload(options) {
// 验证上传是否成功
let isSuccess = false;
if (process.env.OAK_PLATFORM === 'wechatMp') {
if (response.errMsg === 'uploadFile:ok') {
if (response.errMsg === 'request:ok') {
const data = JSON.parse(response.data);
isSuccess = !!(data.status === 204 || data.status === 200);
}
@ -123,10 +123,10 @@ async function chunkUpload(options) {
}
// 等待所有任务完成
await Promise.all(executing);
// 调用分片成功回调(所有分片完成后)
if (onChunkSuccess) {
await onChunkSuccess(chunkInfo);
}
// // 调用分片成功回调(所有分片完成后)
// if (onChunkSuccess) {
// await onChunkSuccess(chunkInfo);
// }
// 清理小程序环境下的临时文件
if (process.env.OAK_PLATFORM === 'wechatMp' && typeof file === 'string') {
await (0, slice_1.cleanTempFiles)(chunks);

View File

@ -26,4 +26,7 @@ export default class CTYunBackend extends CTYun implements CosBackend<EntityDict
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: never[];
}>;
}

View File

@ -98,5 +98,10 @@ class CTYunBackend extends ctyun_1.default {
const key = this.formKey(extraFile);
const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application);
}
async listMultipartUploads(application, extraFile, context) {
return {
parts: [],
};
}
}
exports.default = CTYunBackend;

View File

@ -26,4 +26,7 @@ export default class LocalBackend extends Local implements CosBackend<EntityDict
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: never[];
}>;
}

View File

@ -76,6 +76,11 @@ class LocalBackend extends local_1.default {
const key = this.formKey(extraFile);
const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application);
}
async listMultipartUploads(application, extraFile, context) {
return {
parts: [],
};
}
}
exports.default = LocalBackend;
;

View File

@ -26,4 +26,7 @@ export default class QiniuBackend extends Qiniu implements CosBackend<EntityDict
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: never[];
}>;
}

View File

@ -117,6 +117,11 @@ class QiniuBackend extends qiniu_1.default {
const key = this.formKey(extraFile);
const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application);
}
async listMultipartUploads(application, extraFile, context) {
return {
parts: [],
};
}
}
exports.default = QiniuBackend;
;

View File

@ -29,4 +29,12 @@ export default class S3Backend extends S3 implements CosBackend<EntityDict> {
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: Array<{
partNumber: number;
etag: string;
size: number;
lastModified: Date;
}>;
}>;
}

View File

@ -132,5 +132,19 @@ class S3Backend extends s3_1.default {
const { instance, config: s3CosConfig } = this.getConfigAndInstance(application, extraFile.bucket);
await instance.abortMultipartUpload(extraFile.bucket, key, extraFile.chunkInfo.uploadId, s3CosConfig.endpoint);
}
async listMultipartUploads(application, extraFile, context) {
const key = this.formKey(extraFile);
const { instance, config: s3CosConfig } = this.getConfigAndInstance(application, extraFile.bucket);
const result = await instance.listParts(extraFile.bucket, key, extraFile.chunkInfo.uploadId, s3CosConfig.endpoint, 101);
(0, assert_1.assert)(result.isTruncated === false, `分片数量超过101无法列出所有分片信息不应当出现这个情况触发器中已经限制了最大分片数量为100`);
return {
parts: result.parts.map((part) => ({
partNumber: part.partNumber,
etag: part.eTag,
size: part.size,
lastModified: part.lastModified,
}))
};
}
}
exports.default = S3Backend;

View File

@ -26,4 +26,7 @@ export default class TencentYunBackend extends TencentYun implements CosBackend<
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: never[];
}>;
}

View File

@ -96,5 +96,10 @@ class TencentYunBackend extends tencent_1.default {
}
async abortMultipartUpload(application, extraFile, context) {
}
async listMultipartUploads(application, extraFile, context) {
return {
parts: [],
};
}
}
exports.default = TencentYunBackend;

View File

@ -26,4 +26,7 @@ export default class UnknownBackend extends Unknown implements CosBackend<Entity
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: never[];
}>;
}

View File

@ -36,6 +36,11 @@ class UnknownBackend extends unknown_1.default {
}
async abortMultipartUpload(application, extraFile, context) {
}
async listMultipartUploads(application, extraFile, context) {
return {
parts: [],
};
}
}
exports.default = UnknownBackend;
;

View File

@ -26,4 +26,7 @@ export default class WechatBackend extends Wechat implements CosBackend<EntityDi
*/
mergeChunkedUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
abortMultipartUpload(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<void>;
listMultipartUploads(application: EntityDict['application']['Schema'], extraFile: OpSchema, context: BRC<EntityDict>): Promise<{
parts: never[];
}>;
}

View File

@ -57,6 +57,11 @@ class WechatBackend extends wechat_1.default {
}
async abortMultipartUpload(application, extraFile, context) {
}
async listMultipartUploads(application, extraFile, context) {
return {
parts: [],
};
}
}
exports.default = WechatBackend;
;

View File

@ -73,6 +73,7 @@ async function sliceFileForMp(filePath, chunkSize, partCount) {
chunks.push(tempFilePath);
}
catch (err) {
console.error('分片读取失败:', err);
// 清理已创建的临时文件
await cleanTempFiles(chunks);
throw new Exception_1.OakUploadException(`分片 ${i + 1} 读取失败: ${err}`);

View File

@ -1,9 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const assert_1 = require("oak-domain/lib/utils/assert");
const index_backend_1 = require("../utils/cos/index.backend");
const uuid_1 = require("oak-domain/lib/utils/uuid");
const lodash_1 = require("oak-domain/lib/utils/lodash");
const Projection_1 = require("../types/Projection");
const applicationPassport_1 = tslib_1.__importDefault(require("../components/applicationPassport"));
async function checkWhetherSuccess(context, applicationId, rows) {
const successIds = [];
const failedIds = [];
@ -61,6 +64,9 @@ const watchers = [
$lt: deadline,
},
uploadState: 'uploading',
enableChunkedUpload: {
$exists: false,
}
};
},
projection: {
@ -84,5 +90,38 @@ const watchers = [
};
}
},
{
name: '确定uploading的文件状态',
entity: 'extraFile',
filter: async () => {
const now = Date.now();
const deadline = process.env.NODE_ENV === 'production' ? now - 3 * 24 * 60 * 60 * 1000 : now - 60 * 1000;
return {
$$updateAt$$: {
$lt: deadline,
},
uploadState: 'uploading',
enableChunkedUpload: true
};
},
projection: {
...Projection_1.extraFileProjection,
application: {
...applicationPassport_1.default,
}
},
fn: async (context, data) => {
const eg = (0, lodash_1.groupBy)(data, 'applicationId');
for (const appId in eg) {
// 这里检查需要分片上传的文件信息,但是考虑到可能还在上传之类的,所以先不管,后面再处理
// 1. 要去查询分片信息看看是不是都有etag了如果都有etag了就说明上传完成了否则就标记为失败
}
return {
extraFile: {
update: data.length,
}
};
}
},
];
exports.default = watchers;

View File

@ -112,10 +112,24 @@ export async function mergeChunkedUpload<ED extends EntityDict>(
assert(!extrafile.chunkInfo.merged, `extraFile ${extraFileId} 已经合并过分片,无需重复合并`);
// 必须保证所有分片都有上传完成
const allPartsDone = extrafile.chunkInfo!.parts.every(part => part.etag);
const cos = getCosBackend(extrafile.origin!);
const { parts } = await cos.listMultipartUploads(
extrafile.application!,
extrafile as any,
context as any
);
const allPartsDone = parts.every(part => part.etag && part.size > 0);
assert(allPartsDone, `extraFile ${extraFileId} 存在未上传完成的分片,无法合并`);
const cos = getCosBackend(extrafile.origin!);
// 赋值顺带删除一些无用信息减小体积出现过mysql排序超出限制的问题
extrafile.chunkInfo!.parts = parts.map((part, index) => ({
...extrafile.chunkInfo!.parts[index],
partNumber: part.partNumber,
etag: part.etag!,
uploadUrl: '', // 不需要保存上传链接
}));
await cos.mergeChunkedUpload(
extrafile.application!,

View File

@ -4,6 +4,8 @@ import { EmailConfig, PfwConfig, SmsConfig, MfwConfig } from "../../entities/Pas
import { WebConfig } from "../../entities/Application";
import { assert } from "oak-domain/lib/utils/assert";
import { generateNewId, generateNewIdAsync } from "oak-domain/lib/utils/uuid";
import { ReactComponentProps } from "oak-frontend-base";
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
type RowItem = {
aId: string;
@ -391,4 +393,13 @@ export default OakComponent({
return render;
}
}
});
}) as <ED2 extends EntityDict & BaseEntityDict, T2 extends keyof ED2>(
props: ReactComponentProps<
ED2,
T2,
true,
{
systemId: string;
}
>
) => React.ReactElement;

View File

@ -158,4 +158,20 @@ export interface CosBackend<ED extends EntityDict> {
extraFile: ED['extraFile']['OpSchema'],
context: BRC<ED>,
) => Promise<void>;
/**
*
*/
listMultipartUploads: (
application: ED['application']['Schema'],
extraFile: ED['extraFile']['OpSchema'],
context: BRC<ED>,
) => Promise<{
parts: Array<{
partNumber: number,
etag: string,
size: number,
lastModified: Date,
}>
}>;
}

View File

@ -260,4 +260,34 @@ export default class ALiYunBackend
extraFile.chunkInfo!.uploadId!
);
}
async listMultipartUploads(
application: EntityDict['application']['Schema'],
extraFile: OpSchema,
context: BRC<EntityDict>,
): Promise<{
parts: Array<{
partNumber: number,
etag: string,
size: number,
lastModified: Date,
}>
}> {
const key = this.formKey(extraFile);
const { instance, config: aliyunCosConfig } =
this.getConfigAndInstance(application);
const b = (aliyunCosConfig as ALiYunCosConfig).buckets.find(
(ele) => ele.name === extraFile.bucket
);
assert(
b,
`extraFile中的bucket名称在阿里云配置中找不到「${extraFile.bucket}`
);
return await instance.listParts(
extraFile.bucket!,
b.zone,
key,
extraFile.chunkInfo!.uploadId!
);
}
}

View File

@ -80,7 +80,7 @@ export async function chunkUpload(
// 验证上传是否成功
let isSuccess = false;
if (process.env.OAK_PLATFORM === 'wechatMp') {
if (response.errMsg === 'uploadFile:ok') {
if (response.errMsg === 'request:ok') {
const data = JSON.parse(response.data);
isSuccess = !!(data.status === 204 || data.status === 200);
}
@ -162,10 +162,10 @@ export async function chunkUpload(
// 等待所有任务完成
await Promise.all(executing);
// 调用分片成功回调(所有分片完成后)
if (onChunkSuccess) {
await onChunkSuccess(chunkInfo);
}
// // 调用分片成功回调(所有分片完成后)
// if (onChunkSuccess) {
// await onChunkSuccess(chunkInfo);
// }
// 清理小程序环境下的临时文件
if (process.env.OAK_PLATFORM === 'wechatMp' && typeof file === 'string') {

View File

@ -177,4 +177,15 @@ export default class CTYunBackend
const { instance, config: aliyunCosConfig } =
this.getConfigAndInstance(application);
}
async listMultipartUploads(
application: EntityDict['application']['Schema'],
extraFile: OpSchema,
context: BRC<EntityDict>,
){
return {
parts: [],
}
}
}

View File

@ -132,4 +132,15 @@ export default class LocalBackend extends Local implements CosBackend<EntityDict
const { instance, config: aliyunCosConfig } =
this.getConfigAndInstance(application);
}
async listMultipartUploads(
application: EntityDict['application']['Schema'],
extraFile: OpSchema,
context: BRC<EntityDict>,
){
return {
parts: [],
}
}
};

View File

@ -189,4 +189,14 @@ export default class QiniuBackend extends Qiniu implements CosBackend<EntityDict
const { instance, config: aliyunCosConfig } =
this.getConfigAndInstance(application);
}
async listMultipartUploads(
application: EntityDict['application']['Schema'],
extraFile: OpSchema,
context: BRC<EntityDict>,
){
return {
parts: [],
}
}
};

View File

@ -246,4 +246,43 @@ export default class S3Backend extends S3 implements CosBackend<EntityDict> {
s3CosConfig.endpoint!
);
}
async listMultipartUploads(
application: EntityDict['application']['Schema'],
extraFile: OpSchema,
context: BRC<EntityDict>,
): Promise<{
parts: Array<{
partNumber: number,
etag: string,
size: number,
lastModified: Date,
}>
}> {
const key = this.formKey(extraFile);
const { instance, config: s3CosConfig } =
this.getConfigAndInstance(application, extraFile.bucket!);
const result = await instance.listParts(
extraFile.bucket!,
key,
extraFile.chunkInfo!.uploadId!,
s3CosConfig.endpoint!,
101,
);
assert(
result.isTruncated === false,
`分片数量超过101无法列出所有分片信息不应当出现这个情况触发器中已经限制了最大分片数量为100`
);
return {
parts: result.parts.map((part) => ({
partNumber: part.partNumber,
etag: part.eTag,
size: part.size,
lastModified: part.lastModified!,
}))
}
}
}

View File

@ -174,4 +174,15 @@ export default class TencentYunBackend extends TencentYun implements CosBackend<
): Promise<void> {
}
async listMultipartUploads(
application: EntityDict['application']['Schema'],
extraFile: OpSchema,
context: BRC<EntityDict>,
){
return {
parts: [],
}
}
}

View File

@ -81,4 +81,15 @@ export default class UnknownBackend extends Unknown implements CosBackend<Entity
): Promise<void> {
}
async listMultipartUploads(
application: EntityDict['application']['Schema'],
extraFile: OpSchema,
context: BRC<EntityDict>,
){
return {
parts: [],
}
}
};

View File

@ -111,4 +111,15 @@ export default class WechatBackend extends Wechat implements CosBackend<EntityDi
): Promise<void> {
}
async listMultipartUploads(
application: EntityDict['application']['Schema'],
extraFile: OpSchema,
context: BRC<EntityDict>,
){
return {
parts: [],
}
}
};

View File

@ -75,6 +75,7 @@ export async function sliceFileForMp(filePath: string, chunkSize: number, partCo
chunks.push(tempFilePath);
} catch (err) {
console.error('分片读取失败:', err);
// 清理已创建的临时文件
await cleanTempFiles(chunks);
throw new OakUploadException(`分片 ${i + 1} 读取失败: ${err}`);

View File

@ -5,6 +5,8 @@ import { Watcher, BBWatcher } from 'oak-domain/lib/types/Watcher';
import { getCosBackend } from '../utils/cos/index.backend';
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { groupBy } from 'oak-domain/lib/utils/lodash';
import { extraFileProjection } from '../types/Projection';
import applicationPassport from '../components/applicationPassport';
async function checkWhetherSuccess(context: BackendRuntimeContext<EntityDict>, applicationId: string, rows: EntityDict['extraFile']['OpSchema'][]) {
const successIds: string[] = [];
@ -74,6 +76,9 @@ const watchers: Watcher<
$lt: deadline,
},
uploadState: 'uploading',
enableChunkedUpload: {
$exists: false,
}
};
},
projection: {
@ -92,6 +97,41 @@ const watchers: Watcher<
await checkWhetherSuccess(context, appId, eg[appId] as EntityDict['extraFile']['OpSchema'][]);
}
return {
extraFile: {
update: data.length,
}
};
}
},
{
name: '确定uploading的文件状态',
entity: 'extraFile',
filter: async () => {
const now = Date.now();
const deadline = process.env.NODE_ENV === 'production' ? now - 3 * 24 * 60 * 60 * 1000 : now - 60 * 1000;
return {
$$updateAt$$: {
$lt: deadline,
},
uploadState: 'uploading',
enableChunkedUpload: true
};
},
projection: {
...extraFileProjection,
application: {
...applicationPassport,
}
},
fn: async (context, data) => {
const eg = groupBy(data, 'applicationId');
for (const appId in eg) {
// 这里检查需要分片上传的文件信息,但是考虑到可能还在上传之类的,所以先不管,后面再处理
// 1. 要去查询分片信息看看是不是都有etag了如果都有etag了就说明上传完成了否则就标记为失败
}
return {
extraFile: {
update: data.length,