Files
guoju0808/ry-h5/utils/upload.ts
T
郭庆泰 5a0a892574 feat: 邀请函邮件发送 + 首次设密/密码为空 + 姓名单一可信源
邀请发送 (菜单暂隐藏, 待确认参数见 发送邀请-待确认参数.md):
- 后端: BizInvite / BizInviteRecipient + Controller/Service/Mapper/XML
- 邮件: InviteMailSender (spring-boot-starter-mail SMTP) + application*.yml 邮件配置
- 上传进度: UploadProgressRegistry + UploadProgressController
- 前端: InviteList / InviteNew / InviteDetail / InviteView + api/business/invite.js
- 原型: proto/html/components/invite-detail / new-invitation / send-invitation

登录/账号:
- 首次设密: /getInfo 返回 isPasswordEmpty, SysProfileController 密码为空时跳过旧密码校验, ForcePasswordDialog 强制弹窗
- 姓名单一可信源 resolveDisplayName: doctor→biz_expert.name, sponsor/executor→biz_person.name, 其余回退 nick_name
- OA compliance 门禁改为按手机号查 ecology 视图 (不再限定 manager/leader)

其它:
- OSS zip 在线查看 (列清单+取单文件, 公开只读) + SecurityConfig permitAll
- doctor 项目详情 ProjectDetail.vue
- 数据库/测试/设计文档 (md) 入库
2026-09-10 20:40:49 +08:00

135 lines
5.1 KiB
TypeScript

/**
* 扫码拍照回传工具 (H5 only)
*
* 二维码把会议上下文塞进 URL query (ry-h5 hash 路由, base=/camera/):
* http://host:8090/camera/#/pages/panorama/index?meetingId=1&subType=L_PANORAMA_FRONT&apiBase=http%3A%2F%2Fhost%3A5173%2Fdev-api
*
* - getCameraParams(): 从 hash 里解析 meetingId / subType / apiBase
* - uploadCameraPhoto(base64): 拍照 Base64 → 直传 OSS → 回传 URL 到后端 /business/meetingMaterial/cameraUpload
*
* 公开上传, 无需 token (后端 SecurityConfig 已 permitAll + 白名单 subType 兜底).
*/
export type CameraParams = {
apiBase: string
meetingId: string
subType: string
}
/** 从 hash 路由的 query 里解析会议上下文 */
export function getCameraParams(): CameraParams {
const hash = window.location.hash || ''
const qs = hash.includes('?') ? hash.split('?')[1] : ''
const search = qs || (window.location.search || '').replace(/^\?/, '')
const sp = new URLSearchParams(search)
return {
apiBase: sp.get('apiBase') || '',
meetingId: sp.get('meetingId') || '',
subType: sp.get('subType') || '',
}
}
type OssSign = {
code: number
msg: string
host: string
dir: string
accessKeyId: string
policy: string
signature: string
expire: number
}
/** 拿 OSS 直传签名 (GET /common/oss/sign?dir=...) */
async function getOssSign(apiBase: string, dir: string): Promise<OssSign> {
const url = `${apiBase}/common/oss/sign?dir=${encodeURIComponent(dir)}`
const resp = await fetch(url)
const data = (await resp.json()) as OssSign
if (data.code !== 200) throw new Error(data.msg || 'OSS 签名失败')
return data
}
/** Base64 JPEG → 直传 OSS bucket, 返回完整 URL */
async function uploadBase64ToOss(apiBase: string, base64: string, dir: string): Promise<string> {
const sign = await getOssSign(apiBase, dir)
const key = sign.dir + Date.now() + '_' + Math.random().toString(36).slice(2, 8) + '.jpg'
// data:image/jpeg;base64,... → Blob (fetch data URL 最省事, 免手写 base64 解码)
const blobResp = await fetch(base64)
const blob = await blobResp.blob()
const fd = new FormData()
fd.append('key', key)
fd.append('policy', sign.policy)
fd.append('OSSAccessKeyId', sign.accessKeyId)
fd.append('signature', sign.signature)
fd.append('success_action_status', '200')
fd.append('Content-Type', 'image/jpeg')
fd.append('file', blob, 'camera.jpg')
const resp = await fetch(sign.host, { method: 'POST', body: fd })
if (!resp.ok) throw new Error('OSS 上传失败: HTTP ' + resp.status)
return sign.host + '/' + key
}
/** 回传 OSS URL 数组到后端 (POST /business/meetingMaterial/cameraUpload) */
async function cameraUpload(
apiBase: string,
meetingId: string,
subType: string,
ossUrls: string[],
extraOssUrls: string[]
): Promise<void> {
const resp = await fetch(`${apiBase}/business/meetingMaterial/cameraUpload`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ meetingId: Number(meetingId), subType, ossUrls, extraOssUrls }),
})
const data = (await resp.json()) as { code: number; msg: string }
if (data.code !== 200) throw new Error(data.msg || '照片回传失败')
}
/** 连拍多张: 每张 sharp + 可选高斯模糊版 (签到表). 批量直传 OSS 后一次性回传数组存库. */
export type CameraShot = { sharp: string; blurred: string }
/** 拍照后调用 (单张): 直传 OSS + 回传 URL 存库. 签到表额外上传高斯模糊版作为 extraOssUrl. */
export async function uploadCameraPhoto(base64: string, blurredBase64: string): Promise<void> {
const p = getCameraParams()
if (!p.apiBase) throw new Error('缺少 apiBase 参数')
if (!p.meetingId) throw new Error('缺少 meetingId 参数')
if (!p.subType) throw new Error('缺少 subType 参数')
const dir = `ry8080/meeting/${p.meetingId}/camera/`
const ossUrl = await uploadBase64ToOss(p.apiBase, base64, dir)
// 签到表: 额外上传脱敏版 (A4 70%~85% 高斯模糊), sponsor 只看这个隐藏手机号/身份证号
let extraOssUrl = ''
if (p.subType === 'L_SIGN_IN' && blurredBase64) {
extraOssUrl = await uploadBase64ToOss(p.apiBase, blurredBase64, dir + 'masked/')
}
await cameraUpload(p.apiBase, p.meetingId, p.subType, [ossUrl], extraOssUrl ? [extraOssUrl] : [])
}
/** 拍照后调用 (连拍多张): 每张直传 OSS + 签到表额外上传高斯模糊版, 一次性回传数组存库. */
export async function uploadCameraPhotos(list: CameraShot[]): Promise<void> {
const p = getCameraParams()
if (!p.apiBase) throw new Error('缺少 apiBase 参数')
if (!p.meetingId) throw new Error('缺少 meetingId 参数')
if (!p.subType) throw new Error('缺少 subType 参数')
if (!list || !list.length) throw new Error('没有可上传的照片')
const dir = `ry8080/meeting/${p.meetingId}/camera/`
const ossUrls: string[] = []
const extraOssUrls: string[] = []
for (const s of list) {
ossUrls.push(await uploadBase64ToOss(p.apiBase, s.sharp, dir))
if (p.subType === 'L_SIGN_IN' && s.blurred) {
extraOssUrls.push(await uploadBase64ToOss(p.apiBase, s.blurred, dir + 'masked/'))
} else {
extraOssUrls.push('')
}
}
await cameraUpload(p.apiBase, p.meetingId, p.subType, ossUrls, extraOssUrls)
}