Merge branch 'dev' of codeup.aliyun.com:61c14a7efa282c88e103c23f/oak-general-business into dev

This commit is contained in:
Xu Chang 2022-05-31 19:07:18 +08:00
commit 5bb19d3dfb
15 changed files with 455 additions and 356 deletions

View File

@ -1,4 +1,4 @@
import { String, Text } from 'oak-domain/lib/types/DataType'; import { String, Int, Text } from 'oak-domain/lib/types/DataType';
import { FileCarrierEntityShape } from 'oak-domain/lib/types/Entity'; import { FileCarrierEntityShape } from 'oak-domain/lib/types/Entity';
export interface Schema extends FileCarrierEntityShape { export interface Schema extends FileCarrierEntityShape {
origin: 'qiniu' | 'unknown'; origin: 'qiniu' | 'unknown';
@ -12,4 +12,6 @@ export interface Schema extends FileCarrierEntityShape {
entity: String<32>; entity: String<32>;
entityId: String<64>; entityId: String<64>;
extra1?: Text; extra1?: Text;
extension: String<16>;
size?: Int<4>;
} }

View File

@ -15,7 +15,9 @@ class ExtraFile extends oak_frontend_base_1.Feature {
} }
async upload(extraFile, scene) { async upload(extraFile, scene) {
try { try {
const { origin, extra1: filePath, filename: fileName } = extraFile; const { origin, extra1: filePath, filename, objectId, extension, entity, } = extraFile;
// 构造文件上传所需的fileName
const fileName = `${entity}/${objectId}.${extension}`;
const uploadInfo = await this.getAspectProxy().getUploadInfo({ const uploadInfo = await this.getAspectProxy().getUploadInfo({
origin, origin,
fileName, fileName,

View File

@ -11,7 +11,7 @@ export default class qiniuInstance {
bucket: string; bucket: string;
domain: string; domain: string;
}); });
getUploadInfo(fileName: string): Promise<{ getUploadInfo(key: string): Promise<{
key: string; key: string;
uploadToken: string; uploadToken: string;
uploadHost: string; uploadHost: string;

View File

@ -19,10 +19,9 @@ class qiniuInstance {
this.bucket = bucket; this.bucket = bucket;
this.domain = domain; this.domain = domain;
} }
async getUploadInfo(fileName) { async getUploadInfo(key) {
try { try {
const { uploadHost, domain, bucket } = this; const { uploadHost, domain, bucket } = this;
const key = `${Date.now()}/${fileName}`;
const scope = `${bucket}:${key}`; const scope = `${bucket}:${key}`;
const uploadToken = this.getToken(scope); const uploadToken = this.getToken(scope);
return { return {

View File

@ -1,3 +1,3 @@
import { OpSchema as ExtraFile } from 'oak-app-domain/ExtraFile/Schema'; import { OpSchema as ExtraFile } from 'oak-app-domain/ExtraFile/Schema';
export declare function composeFileUrl(extraFile: Pick<ExtraFile, 'type' | 'bucket' | 'filename' | 'origin' | 'extra1'>): string; export declare function composeFileUrl(extraFile: Pick<ExtraFile, 'type' | 'bucket' | 'filename' | 'origin' | 'extra1' | 'objectId' | 'extension' | 'entity'>): string;
export declare function decomposeFileUrl(url: string): Pick<ExtraFile, 'bucket' | 'filename' | 'origin' | 'type' | 'extra1'>; export declare function decomposeFileUrl(url: string): Pick<ExtraFile, 'bucket' | 'filename' | 'origin' | 'type' | 'extra1'>;

View File

@ -2,12 +2,13 @@
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.decomposeFileUrl = exports.composeFileUrl = void 0; exports.decomposeFileUrl = exports.composeFileUrl = void 0;
function composeFileUrl(extraFile) { function composeFileUrl(extraFile) {
const { type, bucket, filename, origin, extra1 } = extraFile; const { type, bucket, filename, origin, extra1, objectId, extension, entity } = extraFile;
if (extra1) { if (extra1) {
// 有extra1就用extra1 // 有extra1就用extra1
return extra1; return extra1;
} }
return ''; // 缺少https和域名
return `${entity}/${objectId}.${extension}`;
} }
exports.composeFileUrl = composeFileUrl; exports.composeFileUrl = composeFileUrl;
function decomposeFileUrl(url) { function decomposeFileUrl(url) {

View File

@ -13,4 +13,6 @@ export interface Schema extends FileCarrierEntityShape {
entity: String<32>; entity: String<32>;
entityId: String<64>; entityId: String<64>;
extra1?: Text; extra1?: Text;
extension: String<16>;
size?: Int<4>;
}; };

View File

@ -15,25 +15,36 @@ export class ExtraFile<
@Action @Action
async upload(extraFile: DeduceCreateOperationData<EntityDict['extraFile']['OpSchema']>, scene: string) { async upload(extraFile: DeduceCreateOperationData<EntityDict['extraFile']['OpSchema']>, scene: string) {
try { try {
const { origin, extra1: filePath, filename: fileName } = extraFile; const {
const uploadInfo = origin,
await this.getAspectProxy().getUploadInfo( extra1: filePath,
{ filename,
origin, objectId,
fileName, extension,
}, entity,
scene } = extraFile;
); // 构造文件上传所需的fileName
const fileName = `${entity}/${objectId}.${extension}`;
const uploadInfo = await this.getAspectProxy().getUploadInfo(
{
origin,
fileName,
},
scene
);
if (process.env.OAK_PLATFORM === 'wechatMp') { if (process.env.OAK_PLATFORM === 'wechatMp') {
// 微信小程序使用wx.uploadFile, 封装upload上传源为origin // 微信小程序使用wx.uploadFile, 封装upload上传源为origin
const up = new Upload(); const up = new Upload();
const result = await up.uploadFile(origin, filePath!, uploadInfo); const result = await up.uploadFile(
origin,
filePath!,
uploadInfo
);
return result; return result;
} } else {
else {
throw new Error('not implemented yet'); throw new Error('not implemented yet');
} }
} catch (err) { } catch (err) {
throw err; throw err;
} }

View File

@ -22,10 +22,9 @@ export default class qiniuInstance {
this.domain = domain; this.domain = domain;
} }
async getUploadInfo(fileName: string) { async getUploadInfo(key: string) {
try { try {
const { uploadHost, domain, bucket } = this; const { uploadHost, domain, bucket } = this;
const key = `${Date.now()}/${fileName}`;
const scope = `${bucket}:${key}`; const scope = `${bucket}:${key}`;
const uploadToken = this.getToken(scope); const uploadToken = this.getToken(scope);
return { return {
@ -47,10 +46,13 @@ export default class qiniuInstance {
deadline: 3600 + Math.floor(Date.now() / 1000), deadline: 3600 + Math.floor(Date.now() / 1000),
}; };
// 构造凭证 // 构造凭证
const encodedFlags = this.urlSafeBase64Encode(JSON.stringify(putPolicy)); const encodedFlags = this.urlSafeBase64Encode(
JSON.stringify(putPolicy)
);
const encoded = this.hmacSha1(encodedFlags, this.secretKey); const encoded = this.hmacSha1(encodedFlags, this.secretKey);
const encodedSign = this.base64ToUrlSafe(encoded); const encodedSign = this.base64ToUrlSafe(encoded);
const uploadToken = this.accessKey + ':' + encodedSign + ':' + encodedFlags; const uploadToken =
this.accessKey + ':' + encodedSign + ':' + encodedFlags;
return uploadToken; return uploadToken;
} }

View File

@ -1,12 +1,26 @@
import { OpSchema as ExtraFile } from 'oak-app-domain/ExtraFile/Schema'; import { OpSchema as ExtraFile } from 'oak-app-domain/ExtraFile/Schema';
export function composeFileUrl(extraFile: Pick<ExtraFile, 'type' | 'bucket' | 'filename' | 'origin' | 'extra1'>) { export function composeFileUrl(
const { type, bucket, filename, origin, extra1 } = extraFile; extraFile: Pick<
ExtraFile,
| 'type'
| 'bucket'
| 'filename'
| 'origin'
| 'extra1'
| 'objectId'
| 'extension'
| 'entity'
>
) {
const { type, bucket, filename, origin, extra1, objectId, extension, entity } =
extraFile;
if (extra1) { if (extra1) {
// 有extra1就用extra1 // 有extra1就用extra1
return extra1!; return extra1!;
} }
return ''; // 缺少https和域名
return `${entity}/${objectId}.${extension}`;
} }
export function decomposeFileUrl(url: string): Pick<ExtraFile, 'bucket' | 'filename' | 'origin' | 'type' | 'extra1'> { export function decomposeFileUrl(url: string): Pick<ExtraFile, 'bucket' | 'filename' | 'origin' | 'type' | 'extra1'> {

View File

@ -71,6 +71,7 @@ OakComponent({
origin: String, origin: String,
tag1: String, tag1: String,
tag2: String, tag2: String,
entity: String,
}, },
methods: { methods: {
@ -100,8 +101,16 @@ OakComponent({
return (750 / windowWidth) * px; return (750 / windowWidth) * px;
}, },
async onPick() { async onPick() {
const { selectCount, mediaType, sourceType, type, origin, tag1, tag2} = const {
this.data; selectCount,
mediaType,
sourceType,
type,
origin,
tag1,
tag2,
entity,
} = this.data;
try { try {
const { errMsg, tempFiles } = await wx.chooseMedia({ const { errMsg, tempFiles } = await wx.chooseMedia({
count: selectCount, count: selectCount,
@ -116,30 +125,45 @@ OakComponent({
} else { } else {
await Promise.all(tempFiles.map( await Promise.all(tempFiles.map(
async (tempExtraFile) => { async (tempExtraFile) => {
const { tempFilePath, thumbTempFilePath } = tempExtraFile; const { tempFilePath, thumbTempFilePath, fileType, size } = tempExtraFile;
const filePath = tempFilePath || thumbTempFilePath; const filePath = tempFilePath || thumbTempFilePath;
const filename = filePath.match(/[^/]+(?!.*\/)/g)![0]; const fileFullName = filePath.match(/[^/]+(?!.*\/)/g)![0];
const extension = fileFullName.substring(
assert(origin === 'qiniu'); // 目前只支持七牛上传 fileFullName.lastIndexOf('.') + 1
const ele: Parameters<typeof this['pushNode']>[1] = { );
updateData: { const filename = fileFullName.substring(
extra1: filePath, 0, fileFullName.lastIndexOf('.')
origin, );
type, assert(entity, '必须传入entity');
tag1, assert(origin === 'qiniu', '目前只支持七牛上传'); // 目前只支持七牛上传
tag2, const ele: Parameters<typeof this['pushNode']>[1] =
objectId: await generateNewId(), {
filename: filename, updateData: {
}, extra1: filePath,
beforeExecute: async (updateData) => { origin,
const { url, bucket } = await this.features.extraFile.upload( type: type || fileType,
updateData as DeduceCreateOperationData<EntityDict['extraFile']['Schema']>, "extraFile:gallery:upload"); tag1,
Object.assign(updateData, { tag2,
bucket, objectId: await generateNewId(),
extra1: url, entity,
}); filename: filename,
}, size: size,
}; extension,
},
beforeExecute: async (updateData) => {
const { url, bucket } =
await this.features.extraFile.upload(
updateData as DeduceCreateOperationData<
EntityDict['extraFile']['Schema']
>,
'extraFile:gallery:upload'
);
Object.assign(updateData, {
bucket,
extra1: url,
});
},
};
this.pushNode(undefined, ele); this.pushNode(undefined, ele);
} }

View File

@ -1,118 +1,124 @@
import { ROOT_ROLE_ID } from '../../../../src/constants'; import { ROOT_ROLE_ID } from '../../../../src/constants';
import { composeFileUrl } from '../../../../src/utils/extraFile'; import { composeFileUrl } from '../../../../src/utils/extraFile';
OakPage({ OakPage(
path: 'token:me', {
entity: 'token', path: 'token:me',
isList: true, entity: 'token',
projection: { isList: true,
id: 1, projection: {
userId: 1,
playerId: 1,
user: {
id: 1, id: 1,
nickname: 1, userId: 1,
name: 1, playerId: 1,
extraFile$entity: { user: {
$entity: 'extraFile', id: 1,
data: { nickname: 1,
id: 1, name: 1,
tag1: 1, extraFile$entity: {
origin: 1, $entity: 'extraFile',
bucket: 1, data: {
objectId: 1, id: 1,
filename: 1, tag1: 1,
extra1: 1, origin: 1,
type: 1, bucket: 1,
objectId: 1,
filename: 1,
extra1: 1,
type: 1,
entity: 1,
extension: 1,
},
filter: {
tag1: 'avatar',
},
indexFrom: 0,
count: 1,
}, },
filter: { mobile$user: {
tag1: 'avatar', $entity: 'mobile',
data: {
id: 1,
mobile: 1,
},
}, },
indexFrom: 0,
count: 1,
}, },
mobile$user: { player: {
$entity: 'mobile', id: 1,
data: { userRole$user: {
id: 1, $entity: 'userRole',
mobile: 1, data: {
id: 1,
userId: 1,
roleId: 1,
},
}, },
}, },
}, },
player: { formData: async ({ data: [token] }) => {
id: 1, const user = token?.user;
userRole$user: { const player = token?.player;
$entity: 'userRole', const avatarFile =
data: { user && user.extraFile$entity && user.extraFile$entity[0];
id: 1, const avatar = avatarFile && composeFileUrl(avatarFile);
userId: 1, const nickname = user && user.nickname;
roleId: 1, const mobileData = user && user.mobile$user && user.mobile$user[0];
} const { mobile } = mobileData || {};
}, const mobileCount = user?.mobile$user?.length || 0;
}
},
formData: async ({ data: [ token ] }) => {
const user = token?.user;
const player = token?.player;
const avatarFile = user && user.extraFile$entity && user.extraFile$entity[0];
const avatar = avatarFile && composeFileUrl(avatarFile);
const nickname = user && user.nickname;
const mobileData = user && user.mobile$user && user.mobile$user[0];
const { mobile } = mobileData || {};
const mobileCount = user?.mobile$user?.length || 0;
const isLoggedIn = !!token; const isLoggedIn = !!token;
const isPlayingAnother = token && token.userId !== token.playerId; const isPlayingAnother = token && token.userId !== token.playerId;
const isRoot = player?.userRole$user && player.userRole$user[0].roleId === ROOT_ROLE_ID; const isRoot =
return { player?.userRole$user &&
avatar, player.userRole$user[0].roleId === ROOT_ROLE_ID;
nickname, return {
mobile, avatar,
mobileCount, nickname,
isLoggedIn, mobile,
isPlayingAnother, mobileCount,
isRoot, isLoggedIn,
}; isPlayingAnother,
isRoot,
};
},
}, },
}, { {
methods: { methods: {
async onRefresh() { async onRefresh() {
this.setData({ this.setData({
refreshing: true, refreshing: true,
}); });
try { try {
await this.features.token.syncUserInfoWechatMp('token:me'); await this.features.token.syncUserInfoWechatMp('token:me');
} } catch (err) {
catch (err) { console.error(err);
console.error(err); }
} this.setData({
this.setData({ refreshing: false,
refreshing: false, });
}); },
async doLogin() {
this.setData({
refreshing: true,
});
try {
await this.features.token.loginWechatMp('token:me');
} catch (err) {
console.error(err);
}
this.setData({
refreshing: false,
});
},
goMyMobile() {
this.navigateTo({
url: '../../mobile/me/index',
});
},
goUserManage() {
this.navigateTo({
url: '../../user/manage/index',
});
},
}, },
async doLogin() {
this.setData({
refreshing: true,
});
try {
await this.features.token.loginWechatMp('token:me');
}
catch (err) {
console.error(err);
}
this.setData({
refreshing: false,
});
},
goMyMobile() {
this.navigateTo({
url: '../../mobile/me/index',
});
},
goUserManage() {
this.navigateTo({
url: '../../user/manage/index',
});
}
} }
}); );

View File

@ -2,150 +2,175 @@
import { composeFileUrl } from "../../../../../src/utils/extraFile"; import { composeFileUrl } from "../../../../../src/utils/extraFile";
OakPage({ OakPage(
path: 'user:manage:detail', {
entity: 'user', path: 'user:manage:detail',
projection: { entity: 'user',
id: 1, projection: {
nickname: 1, id: 1,
name: 1, nickname: 1,
userState: 1, name: 1,
idState: 1, userState: 1,
extraFile$entity: { idState: 1,
$entity: 'extraFile', extraFile$entity: {
data: { $entity: 'extraFile',
id: 1, data: {
tag1: 1, id: 1,
origin: 1, tag1: 1,
bucket: 1, origin: 1,
objectId: 1, bucket: 1,
filename: 1, objectId: 1,
extra1: 1, filename: 1,
type: 1, extra1: 1,
type: 1,
entity: 1,
extension: 1,
},
filter: {
tag1: 'avatar',
},
indexFrom: 0,
count: 1,
}, },
filter: { mobile$user: {
tag1: 'avatar', $entity: 'mobile',
}, data: {
indexFrom: 0, id: 1,
count: 1, mobile: 1,
}, },
mobile$user: {
$entity: 'mobile',
data: {
id: 1,
mobile: 1,
}, },
}, },
isList: false,
formData: async ({ data: user }) => {
const {
id,
nickname,
idState,
userState,
name,
mobile$user,
extraFile$entity,
} = user || {};
const mobile = mobile$user && mobile$user[0]?.mobile;
const avatar =
extraFile$entity &&
extraFile$entity[0] &&
composeFileUrl(extraFile$entity[0]);
return {
id,
nickname,
name,
mobile,
avatar,
userState,
idState,
};
},
actions: [
'accept',
'activate',
'disable',
'enable',
'remove',
'update',
'verify',
'play',
],
}, },
isList: false, {
formData: async ({ data: user }) => { data: {
const { id, nickname, idState, userState, name, mobile$user, extraFile$entity } = user || {}; show: false,
const mobile = mobile$user && mobile$user[0]?.mobile; actionDescriptions: {
const avatar = extraFile$entity && extraFile$entity[0] && composeFileUrl(extraFile$entity[0]); accept: {
return { icon: {
id, name: 'pan_tool',
nickname, },
name, label: '通过',
mobile, },
avatar, activate: {
userState, icon: {
idState, name: 'check',
}; },
}, label: '激活',
actions: ['accept', 'activate', 'disable', 'enable', 'remove', 'update', 'verify', 'play'], },
}, { disable: {
data: { icon: {
show: false, name: 'flash_off',
actionDescriptions: { },
accept: { label: '禁用',
icon: { },
name: 'pan_tool', enable: {
icon: {
name: 'flash_on',
},
label: '启用',
},
remove: {
icon: {
name: 'clear',
},
label: '删除',
},
update: {
icon: {
name: 'edit',
},
label: '更新',
},
verify: {
icon: {
name: 'how_to_reg',
},
label: '验证',
},
play: {
icon: {
name: 'play_circle',
},
label: '切换',
}, },
label: '通过',
}, },
activate: {
icon: {
name: 'check',
},
label: '激活',
},
disable: {
icon: {
name: 'flash_off',
},
label: '禁用',
},
enable: {
icon: {
name: 'flash_on',
},
label: '启用',
},
remove: {
icon: {
name: 'clear',
},
label: '删除',
},
update: {
icon: {
name: 'edit',
},
label: '更新',
},
verify: {
icon: {
name: 'how_to_reg',
},
label: '验证',
},
play: {
icon: {
name: 'play_circle',
},
label: '切换',
}
}
},
methods: {
openDrawer() {
this.setData({
show: true,
});
}, },
closeDrawer() { methods: {
this.setData({ openDrawer() {
show: false, this.setData({
}); show: true,
},
async onActionClick({ detail }: WechatMiniprogram.CustomEvent) {
const { action } = detail;
switch (action) {
case 'update': {
this.navigateTo({
url: '../upsert/index',
oakId: this.data.oakId,
});
return;
}
case 'enable':
case 'disable':
case 'accept':
case 'verify':
case 'activate':
case 'play': {
await this.execute(action);
break;
}
default: {
console.error(`尚未实现的action: ${action}`)
}
}
if (action === 'play') {
wx.navigateBack({
delta: 2,
}); });
} },
} closeDrawer() {
this.setData({
show: false,
});
},
async onActionClick({ detail }: WechatMiniprogram.CustomEvent) {
const { action } = detail;
switch (action) {
case 'update': {
this.navigateTo({
url: '../upsert/index',
oakId: this.data.oakId,
});
return;
}
case 'enable':
case 'disable':
case 'accept':
case 'verify':
case 'activate':
case 'play': {
await this.execute(action);
break;
}
default: {
console.error(`尚未实现的action: ${action}`);
}
}
if (action === 'play') {
wx.navigateBack({
delta: 2,
});
}
},
},
} }
}); );

View File

@ -2,47 +2,59 @@
import { composeFileUrl } from "../../../../src/utils/extraFile"; import { composeFileUrl } from "../../../../src/utils/extraFile";
OakPage({ OakPage(
path: 'user:manage', {
entity: 'user', path: 'user:manage',
projection: { entity: 'user',
id: 1, projection: {
nickname: 1, id: 1,
name: 1, nickname: 1,
userState: 1, name: 1,
extraFile$entity: { userState: 1,
$entity: 'extraFile', extraFile$entity: {
data: { $entity: 'extraFile',
id: 1, data: {
tag1: 1, id: 1,
origin: 1, tag1: 1,
bucket: 1, origin: 1,
objectId: 1, bucket: 1,
filename: 1, objectId: 1,
extra1: 1, filename: 1,
type: 1, extra1: 1,
type: 1,
entity: 1,
extension: 1,
},
filter: {
tag1: 'avatar',
},
indexFrom: 0,
count: 1,
}, },
filter: { mobile$user: {
tag1: 'avatar', $entity: 'mobile',
}, data: {
indexFrom: 0, id: 1,
count: 1, mobile: 1,
}, },
mobile$user: {
$entity: 'mobile',
data: {
id: 1,
mobile: 1,
}, },
}, },
}, isList: true,
isList: true, formData: async ({ data: users }) => {
formData: async ({ data: users }) => { const userData = users.map((user) => {
const userData = users.map( const {
(user) => { id,
const { id, nickname, userState, name, mobile$user, extraFile$entity } = user || {}; nickname,
userState,
name,
mobile$user,
extraFile$entity,
} = user || {};
const mobile = mobile$user && mobile$user[0]?.mobile; const mobile = mobile$user && mobile$user[0]?.mobile;
const avatar = extraFile$entity && extraFile$entity[0] && composeFileUrl(extraFile$entity[0]); const avatar =
extraFile$entity &&
extraFile$entity[0] &&
composeFileUrl(extraFile$entity[0]);
return { return {
id, id,
nickname, nickname,
@ -51,25 +63,26 @@ OakPage({
avatar, avatar,
userState, userState,
}; };
}
);
return {
userData,
};
},
}, {
methods: {
goUserManageDetail(options: WechatMiniprogram.Touch) {
const { id } = options.currentTarget.dataset;
this.navigateTo({
url: 'detail/index',
oakId: id,
}); });
return {
userData,
};
}, },
goNewUser() { },
this.navigateTo({ {
url: 'upsert/index', methods: {
}); goUserManageDetail(options: WechatMiniprogram.Touch) {
const { id } = options.currentTarget.dataset;
this.navigateTo({
url: 'detail/index',
oakId: id,
});
},
goNewUser() {
this.navigateTo({
url: 'upsert/index',
});
},
}, },
} }
}); );

View File

@ -32,22 +32,20 @@ OakPage({
}, },
isList: false, isList: false,
formData: async ({ data: userEntityGrant }) => { formData: async ({ data: userEntityGrant }) => {
let qrcodeUrl; let qrCodeUrl;
const str = userEntityGrant?.wechatQrCode$entity[0]?.buffer; const str = userEntityGrant?.wechatQrCode$entity[0]?.buffer;
console.log('str', str);
if (str) { if (str) {
const buf = new ArrayBuffer(str.length * 2); const buf = new ArrayBuffer(str.length * 2);
const buf2 = new Uint16Array(buf); const buf2 = new Uint16Array(buf);
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
buf2[i] = str.charCodeAt(i); buf2[i] = str.charCodeAt(i);
} }
qrcodeUrl = 'data:image/jpeg;base64,' + wx.arrayBufferToBase64(buf2); qrCodeUrl = 'data:image/jpeg;base64,' + wx.arrayBufferToBase64(buf2);
console.log('url', qrcodeUrl);
} }
return { return {
relation: userEntityGrant?.relation, relation: userEntityGrant?.relation,
entity: userEntityGrant?.entity, entity: userEntityGrant?.entity,
url: qrcodeUrl || userEntityGrant?.wechatQrCode$entity[0]?.url url: qrCodeUrl || userEntityGrant?.wechatQrCode$entity[0]?.url,
}; };
}, },
}, { }, {