All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
后端 - 接入阿里云 OSS(ali-oss SDK),bucket=lnh2etest,目录 /dos/feedback/YYYY-MM-DD/ - POST /api/feedback/upload:单图 multipart 上传,限制 5MB,仅 png/jpeg/webp/gif - bi_user_feedback 增加 screenshots(JSON)、reply_content/reply_user/reply_at、user_id 索引 老表通过 try-catch 自动 ALTER 兼容 - POST /api/feedback/submit:增加 screenshots[] 字段 - GET /api/feedback/mine:当前用户自己的反馈历史 - PATCH /api/feedback/:id:更新状态 + 回复 - GET /api/feedback/list:增加 status 过滤 前端 - FeedbackFab 改为悬浮按钮 + 弹出菜单:「提个建议」/「我的反馈」 - 弹窗 Step 2 增加截图区:点击选择 / 多张 / 直接 Ctrl+V 粘贴 缩略图预览 + 单张移除,最多 6 张,上传中转圈 - FeedbackHistoryDrawer 新组件:底部抽屉展示自己的反馈 含状态徽章(待处理/处理中/已完成/已忽略)、截图缩略图、产品同学回复区 - 新增隐藏后台管理页 /admin/feedback(或 #/admin/feedback) 状态分类计数 + 列表 + 详情弹窗(改状态 + 写回复,状态选项含徽章色) 待处理项有快捷按钮(标记处理中 / 忽略) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
39 lines
1.6 KiB
TypeScript
39 lines
1.6 KiB
TypeScript
import OSS from 'ali-oss';
|
||
|
||
let client: OSS | null = null;
|
||
|
||
function getClient(): OSS {
|
||
if (client) return client;
|
||
const region = process.env.OSS_REGION || 'oss-cn-shanghai';
|
||
const accessKeyId = process.env.OSS_ACCESS_KEY_ID || '';
|
||
const accessKeySecret = process.env.OSS_ACCESS_KEY_SECRET || '';
|
||
const bucket = process.env.OSS_BUCKET || '';
|
||
if (!accessKeyId || !accessKeySecret || !bucket) {
|
||
throw new Error('OSS 未配置:OSS_ACCESS_KEY_ID / OSS_ACCESS_KEY_SECRET / OSS_BUCKET');
|
||
}
|
||
client = new OSS({ region, accessKeyId, accessKeySecret, bucket, secure: true });
|
||
return client;
|
||
}
|
||
|
||
function safeExt(filename: string, fallback = 'png'): string {
|
||
const m = /\.([a-zA-Z0-9]{1,8})$/.exec(filename);
|
||
return m ? m[1].toLowerCase() : fallback;
|
||
}
|
||
|
||
function randId(len = 8): string {
|
||
return Math.random().toString(36).slice(2, 2 + len);
|
||
}
|
||
|
||
/** 上传 buffer 到 OSS,返回公开访问的 URL */
|
||
export async function uploadFeedbackImage(filename: string, buf: Buffer, mimetype: string): Promise<string> {
|
||
const c = getClient();
|
||
const baseDir = (process.env.OSS_BASE_DIR || '/dos').replace(/^\/+|\/+$/g, '');
|
||
const ymd = new Date().toISOString().slice(0, 10);
|
||
const key = `${baseDir}/feedback/${ymd}/${Date.now().toString(36)}-${randId()}.${safeExt(filename, mimetype.split('/')[1] || 'png')}`;
|
||
await c.put(key, buf, {
|
||
headers: { 'Content-Type': mimetype, 'x-oss-object-acl': 'public-read' },
|
||
});
|
||
const host = (process.env.OSS_HOST || `https://${process.env.OSS_BUCKET}.${process.env.OSS_ENDPOINT}/`).replace(/\/+$/, '/');
|
||
return host + key;
|
||
}
|