init
This commit is contained in:
14
api/_license/public_key.pem
Normal file
14
api/_license/public_key.pem
Normal file
@@ -0,0 +1,14 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5qcNLrwgngGJMqkHhY4b
|
||||
KCeS1HZegxM744fRtdrnWNVA3JwYASc52aokSQh0ig9SKN9k1zRs3L7N4cF4i9kE
|
||||
AfW/2c+yiaMmbX6LW3Wi+yRH2jvTbpj1GkB/9Lsa+OEvdqYaeMiBEVoHS7FZUEaV
|
||||
dzqTqrikUfql3htEhGCI9CGqZFoi8dz0GGKqDpqX7380pbST5Qgi9N4ZQLRVcmOP
|
||||
596xYMoXdfufJ4em+FftYT5Q1rDt42lJhO+UrENORwTrGwCJVmLtIWiHfiCxeUzx
|
||||
5Ft/xOKacndR86L4CmKLekVjejQSo+4Ge8j/BEdVUWY1tMlUFTC8aUTeFE2yA6dt
|
||||
FkX3dQzgEOlRUifLjalXxLmxPY77N+mcuDzjaRomdHdxoGZsRYlS8yHL74rixSRa
|
||||
U9JOVL9i8csLmJarzmYx6jsl4sSDbcDdZHxC2AbdGdDHV5/Zr+a8m8B6PW2nArgB
|
||||
bTNKVx9g8aj4n3jf3NGzRqW/TwNifY4xb6BrbeNTlhXl/9+RCvvmCZZYK8JKus55
|
||||
3cvBvrLUBQdpkk9JwIzmEQZoitD8g4CB/2tKsKvfiwlQUK44HNfWE+cxiqtyXL+I
|
||||
shRJkwYbt0CQsXmU5F5j/prOPiJZjjlk7jqSLZLyJ99vMMm0+Iw7kozweGs3zUct
|
||||
dOvKFUYgxdSaMjTiMOXdcN0CAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
||||
1
api/_license/registration.code
Normal file
1
api/_license/registration.code
Normal file
@@ -0,0 +1 @@
|
||||
eyJ2IjoiMy4xIiwiaCI6ImMwZjM0Yzg2ZDA2ZDAwZjQ3NTIzYTMwNjFhNjlmZmNlZmRkNzhjMWNlMDg4OTk1ZjE5MzQyOTE1ZjgyMmExNTgiLCJzIjoiZjY5MWIwYWUwYmQ2ODUwMTQ4NjA5YzBlMGYyNzZkZDQiLCJ0IjoxNzc1MjA3NDk1LCJuIjoiMjUxNjRiYmY0MjY5MzYwYiIsImsiOiI2NDRjZGFhMiIsImMiOiJiNmE4YmMzMzZkN2NlOTk2MDc0ODZhOGEzMjExNjlhN2YzNGYzNDFjZTUwMzgyMzA5ODBiOWYxN2I3NGYyZGZjIiwibSI6ImRkZTllMzc3Yzk2Mzg3ZDUifQ==
|
||||
49
api/controller_admin/sys_file.js
Normal file
49
api/controller_admin/sys_file.js
Normal file
@@ -0,0 +1,49 @@
|
||||
var UUID = require("uuid");
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
const ossTool = require("../service/ossTool");
|
||||
const funTool = require("../../tool/funTool");
|
||||
|
||||
module.exports = {
|
||||
"POST /sys_file/upload_img": async (ctx, next) => {
|
||||
const files = ctx.request.files; // 获取上传文件
|
||||
let fileArray = [];
|
||||
let rootPath = path.join(__dirname, "../../upload/imgs");
|
||||
for (var key in files) {
|
||||
fileArray.push(files[key]);
|
||||
}
|
||||
|
||||
//创建文件夹
|
||||
await funTool.mkdirsSync(rootPath);
|
||||
|
||||
let resArray = [];
|
||||
fileArray.forEach((file) => {
|
||||
// 创建可读流
|
||||
const reader = fs.createReadStream(file.path);
|
||||
|
||||
let filePath = `/${UUID.v1() + "_" + file.name}`;
|
||||
// 创建可写流
|
||||
const upStream = fs.createWriteStream(path.join(rootPath, filePath));
|
||||
// 可读流通过管道写入可写流
|
||||
|
||||
reader.pipe(upStream);
|
||||
|
||||
resArray.push({ name: file.name, path: path.join("/imgs", filePath) });
|
||||
});
|
||||
|
||||
ctx.success(resArray);
|
||||
},
|
||||
"POST /sys_file/upload_oos_img": async (ctx, next) => {
|
||||
let fileArray = [];
|
||||
const files = ctx.request.files; // 获取上传文件
|
||||
for (var key in files) {
|
||||
fileArray.push(files[key]);
|
||||
}
|
||||
let data = await ossTool.putImg(fileArray[0]);
|
||||
if (data.path) {
|
||||
return ctx.success(data);
|
||||
} else {
|
||||
return ctx.fail();
|
||||
}
|
||||
},
|
||||
};
|
||||
75
api/controller_admin/sys_parameter.js
Normal file
75
api/controller_admin/sys_parameter.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 后台字典/系统参数管理
|
||||
* Banner 配置存于 sys_parameter,key:bannerListImage、bannerDetailImage、bannerListIndex
|
||||
*/
|
||||
const { sys_parameter, op } = require('../../middleware/baseModel');
|
||||
|
||||
const BANNER_KEYS = ['bannerListImage', 'bannerDetailImage', 'bannerListIndex'];
|
||||
|
||||
module.exports = {
|
||||
|
||||
// all 接口
|
||||
'GET /sys_parameter/all': async (ctx, next) => {
|
||||
const rows = await sys_parameter.findAll({
|
||||
where: {},
|
||||
attributes: ['key', 'value']
|
||||
});
|
||||
return ctx.success(rows);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 获取 Banner 配置(三个 key 的 value)
|
||||
*/
|
||||
'GET /sys_parameter/banner_config': async (ctx, next) => {
|
||||
const rows = await sys_parameter.findAll({
|
||||
where: {
|
||||
key: { [op.in]: ['bannerListImage', 'bannerDetailImage', 'bannerListIndex'] }
|
||||
},
|
||||
attributes: ['key', 'value']
|
||||
});
|
||||
const data = {
|
||||
bannerListImage: '',
|
||||
bannerDetailImage: '',
|
||||
bannerListIndex: '0'
|
||||
};
|
||||
if (rows && rows.length) {
|
||||
rows.forEach((row) => {
|
||||
const key = row.key;
|
||||
if (BANNER_KEYS.includes(key)) {
|
||||
data[key] = row.value != null ? String(row.value).replace(/\n/g, '') : '';
|
||||
}
|
||||
});
|
||||
}
|
||||
return ctx.success(data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存 Banner 配置
|
||||
* body: { bannerListImage?, bannerDetailImage?, bannerListIndex? }
|
||||
*/
|
||||
'POST /sys_parameter/banner_config': async (ctx, next) => {
|
||||
const body = ctx.getBody() || {};
|
||||
const updates = [
|
||||
{ key: 'bannerListImage', value: body.bannerListImage != null ? String(body.bannerListImage).trim() : '' },
|
||||
{ key: 'bannerDetailImage', value: body.bannerDetailImage != null ? String(body.bannerDetailImage).trim() : '' },
|
||||
{ key: 'bannerListIndex', value: body.bannerListIndex != null ? String(body.bannerListIndex).trim() : '0' }
|
||||
];
|
||||
|
||||
for (const item of updates) {
|
||||
let row = await sys_parameter.findOne({ where: { key: item.key } });
|
||||
if (row) {
|
||||
await row.update({ value: item.value });
|
||||
} else {
|
||||
await sys_parameter.create({
|
||||
key: item.key,
|
||||
value: item.value,
|
||||
remark: item.key === 'bannerListIndex' ? '列表下标或关闭(0关闭)' : 'Banner图片URL',
|
||||
is_modified: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.success({}, '保存成功');
|
||||
}
|
||||
};
|
||||
61
api/controller_admin/sys_tenant.js
Normal file
61
api/controller_admin/sys_tenant.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 系统租户 sys_tenant:表单下拉(平台租户可看全部启用租户,否则仅本租户)
|
||||
*/
|
||||
const baseModel = require("../../middleware/baseModel");
|
||||
const { getAdminTenantContext } = require("../service/tenant_scope");
|
||||
|
||||
module.exports = {
|
||||
"GET /sys_tenant/form_options": async (ctx) => {
|
||||
const sys_tenant = baseModel.sys_tenant;
|
||||
if (!sys_tenant) {
|
||||
return ctx.fail("sys_tenant 模型未加载");
|
||||
}
|
||||
const { tenantId: user_tenant_id, userId } = await getAdminTenantContext(ctx);
|
||||
const uid = userId != null && userId !== "" ? Number(userId) : null;
|
||||
const has_user = Number.isFinite(uid) && uid > 0;
|
||||
if (!has_user) {
|
||||
return ctx.fail("未登录或登录已失效");
|
||||
}
|
||||
const tid = user_tenant_id != null && user_tenant_id !== "" ? Number(user_tenant_id) : null;
|
||||
const user_tenant_ok = Number.isFinite(tid) && tid > 0;
|
||||
|
||||
let is_platform = false;
|
||||
if (user_tenant_ok) {
|
||||
const row = await sys_tenant.findByPk(tid, {
|
||||
attributes: ["id", "is_platform"],
|
||||
raw: true,
|
||||
});
|
||||
if (row && Number(row.is_platform) === 1) {
|
||||
is_platform = true;
|
||||
}
|
||||
}
|
||||
|
||||
let rows;
|
||||
if (is_platform) {
|
||||
rows = await sys_tenant.findAll({
|
||||
where: { status: 1 },
|
||||
order: [["id", "ASC"]],
|
||||
limit: 500,
|
||||
raw: true,
|
||||
});
|
||||
} else if (user_tenant_ok) {
|
||||
const one = await sys_tenant.findByPk(tid, { raw: true });
|
||||
rows = one ? [one] : [];
|
||||
} else {
|
||||
rows = [];
|
||||
}
|
||||
|
||||
const list = (rows || []).map((r) => {
|
||||
const id = Number(r.id);
|
||||
const name = r.name != null ? String(r.name).trim() : "";
|
||||
const plat = Number(r.is_platform) === 1;
|
||||
return {
|
||||
key: id,
|
||||
value: plat && name ? `${name}(平台)` : name || String(id),
|
||||
};
|
||||
});
|
||||
const locked = !is_platform;
|
||||
const defaultTenantId = user_tenant_ok ? tid : null;
|
||||
return ctx.success({ list, locked, defaultTenantId });
|
||||
},
|
||||
};
|
||||
114
api/controller_admin/tpl_demo.js
Normal file
114
api/controller_admin/tpl_demo.js
Normal file
@@ -0,0 +1,114 @@
|
||||
const { tpl_demo, op } = require("../../middleware/baseModel");
|
||||
|
||||
function build_search_where(seach_option) {
|
||||
const key = seach_option && seach_option.key;
|
||||
const raw = seach_option && seach_option.value;
|
||||
if (!key || raw === undefined || raw === null) {
|
||||
return {};
|
||||
}
|
||||
const str = String(raw).trim();
|
||||
if (str === "") {
|
||||
return {};
|
||||
}
|
||||
const attr = tpl_demo.rawAttributes[key];
|
||||
if (!attr) {
|
||||
return { [key]: { [op.like]: `%${str}%` } };
|
||||
}
|
||||
const type_key = attr.type && attr.type.key;
|
||||
if (type_key === "INTEGER" || type_key === "BIGINT") {
|
||||
const n = Number(str);
|
||||
if (!Number.isNaN(n)) {
|
||||
return { [key]: n };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
return { [key]: { [op.like]: `%${str}%` } };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
"POST /tpl_demo/page": async (ctx) => {
|
||||
const body = ctx.getBody() || {};
|
||||
const param = body.param || body;
|
||||
const { limit, offset } = ctx.getPageSize();
|
||||
const base_where = { is_delete: 0 };
|
||||
const search_where = build_search_where(param.seachOption || {});
|
||||
const where = { ...base_where, ...search_where };
|
||||
const { count, rows } = await tpl_demo.findAndCountAll({
|
||||
where,
|
||||
offset,
|
||||
limit,
|
||||
order: [["id", "DESC"]],
|
||||
});
|
||||
return ctx.success({ rows, count });
|
||||
},
|
||||
|
||||
"POST /tpl_demo/add": async (ctx) => {
|
||||
const body = ctx.getBody() || {};
|
||||
const title = body.title != null ? String(body.title).trim() : "";
|
||||
if (!title) {
|
||||
return ctx.fail("请填写标题");
|
||||
}
|
||||
const row = await tpl_demo.create({
|
||||
title,
|
||||
remark: body.remark != null ? String(body.remark).trim() : "",
|
||||
});
|
||||
return ctx.success(row);
|
||||
},
|
||||
|
||||
"POST /tpl_demo/edit": async (ctx) => {
|
||||
const body = ctx.getBody() || {};
|
||||
const id = body.id;
|
||||
if (!id) {
|
||||
return ctx.fail("缺少 id");
|
||||
}
|
||||
const title = body.title != null ? String(body.title).trim() : "";
|
||||
if (!title) {
|
||||
return ctx.fail("请填写标题");
|
||||
}
|
||||
await tpl_demo.update(
|
||||
{
|
||||
title,
|
||||
remark: body.remark != null ? String(body.remark).trim() : "",
|
||||
last_modify_time: new Date(),
|
||||
},
|
||||
{ where: { id, is_delete: 0 } }
|
||||
);
|
||||
return ctx.success({});
|
||||
},
|
||||
|
||||
"POST /tpl_demo/del": async (ctx) => {
|
||||
const body = ctx.getBody() || {};
|
||||
const id = body.id;
|
||||
if (!id) {
|
||||
return ctx.fail("缺少 id");
|
||||
}
|
||||
await tpl_demo.update(
|
||||
{ is_delete: 1, last_modify_time: new Date() },
|
||||
{ where: { id, is_delete: 0 } }
|
||||
);
|
||||
return ctx.success({});
|
||||
},
|
||||
|
||||
"GET /tpl_demo/detail": async (ctx) => {
|
||||
const id = ctx.get("id");
|
||||
if (!id) {
|
||||
return ctx.fail("缺少 id");
|
||||
}
|
||||
const row = await tpl_demo.findOne({ where: { id, is_delete: 0 } });
|
||||
return ctx.success(row);
|
||||
},
|
||||
|
||||
"GET /tpl_demo/all": async (ctx) => {
|
||||
const rows = await tpl_demo.findAll({ limit: 500, order: [["id", "DESC"]] });
|
||||
return ctx.success(rows);
|
||||
},
|
||||
|
||||
"POST /tpl_demo/export": async (ctx) => {
|
||||
const body = ctx.getBody() || {};
|
||||
const param = body.param || body;
|
||||
const search_where = build_search_where(param.seachOption || {});
|
||||
const where = { is_delete: 0, ...search_where };
|
||||
const rows = await tpl_demo.findAll({ where, limit: 10000, order: [["id", "DESC"]] });
|
||||
return ctx.success({ rows });
|
||||
},
|
||||
};
|
||||
40
api/model/sys_tenant.js
Normal file
40
api/model/sys_tenant.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const Sequelize = require("sequelize");
|
||||
|
||||
/** 租户表:用户、角色按 tenant_id 隔离;is_platform=1 的租户可管理本表 */
|
||||
module.exports = (db) => {
|
||||
return db.define("sys_tenant", {
|
||||
name: {
|
||||
type: Sequelize.STRING(100),
|
||||
allowNull: false,
|
||||
defaultValue: "",
|
||||
comment: "租户名称",
|
||||
},
|
||||
code: {
|
||||
type: Sequelize.STRING(64),
|
||||
allowNull: false,
|
||||
defaultValue: "",
|
||||
unique: true,
|
||||
comment: "租户编码(登录时与账号一起使用)",
|
||||
},
|
||||
remark: {
|
||||
type: Sequelize.STRING(255),
|
||||
allowNull: false,
|
||||
defaultValue: "",
|
||||
comment: "备注",
|
||||
},
|
||||
/** 1 启用 0 停用 */
|
||||
status: {
|
||||
type: Sequelize.INTEGER(1),
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: "状态",
|
||||
},
|
||||
/** 1 平台租户:可维护租户表、可代建任意租户用户 */
|
||||
is_platform: {
|
||||
type: Sequelize.INTEGER(1),
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: "是否平台租户",
|
||||
},
|
||||
});
|
||||
};
|
||||
60
api/model/tpl_demo.js
Normal file
60
api/model/tpl_demo.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const Sequelize = require("sequelize");
|
||||
|
||||
/** 模板示例:与库表字段 create_time / last_modify_time / is_delete 一致(无 createdAt/updatedAt) */
|
||||
module.exports = (db) => {
|
||||
return db.define(
|
||||
"tpl_demo",
|
||||
{
|
||||
title: {
|
||||
type: Sequelize.STRING(200),
|
||||
allowNull: false,
|
||||
comment: "标题",
|
||||
},
|
||||
remark: {
|
||||
type: Sequelize.STRING(500),
|
||||
allowNull: true,
|
||||
comment: "备注",
|
||||
},
|
||||
create_time: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
comment: "创建时间",
|
||||
},
|
||||
last_modify_time: {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
comment: "最后修改时间",
|
||||
},
|
||||
is_delete: {
|
||||
type: Sequelize.INTEGER(1),
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: "0 正常 1 已删除",
|
||||
},
|
||||
},
|
||||
{
|
||||
comment: "前后端模板演示表",
|
||||
timestamps: false,
|
||||
defaultScope: {
|
||||
where: { is_delete: 0 },
|
||||
},
|
||||
hooks: {
|
||||
beforeCreate(instance) {
|
||||
const now = new Date();
|
||||
if (!instance.get("create_time")) {
|
||||
instance.set("create_time", now);
|
||||
}
|
||||
if (!instance.get("last_modify_time")) {
|
||||
instance.set("last_modify_time", now);
|
||||
}
|
||||
if (instance.get("is_delete") == null) {
|
||||
instance.set("is_delete", 0);
|
||||
}
|
||||
},
|
||||
beforeUpdate(instance) {
|
||||
instance.set("last_modify_time", new Date());
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
333
api/service/ossTool.js
Normal file
333
api/service/ossTool.js
Normal file
@@ -0,0 +1,333 @@
|
||||
const OSS = require('ali-oss')
|
||||
const fs = require('fs')
|
||||
const config = require('../../config/config')['aliyun']
|
||||
const uuid = require('node-uuid')
|
||||
const logs = require('../../tool/logs_proxy')
|
||||
|
||||
/**
|
||||
* OSS 文件上传工具类
|
||||
* 统一管理文件上传、存储路径、文件类型等
|
||||
*/
|
||||
class OSSTool {
|
||||
constructor() {
|
||||
this.client = new OSS({
|
||||
region: 'oss-cn-shanghai',
|
||||
accessKeyId: config.accessKeyId,
|
||||
accessKeySecret: config.accessKeySecret,
|
||||
bucket:config.bucket
|
||||
})
|
||||
|
||||
|
||||
// 基础存储路径前缀
|
||||
this.basePrefix = 'front/ball'
|
||||
|
||||
// 文件类型映射
|
||||
this.fileTypeMap = {
|
||||
// 图片类型
|
||||
'image/jpeg': 'jpg',
|
||||
'image/jpg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/gif': 'gif',
|
||||
'image/webp': 'webp',
|
||||
'image/svg+xml': 'svg',
|
||||
|
||||
// 视频类型
|
||||
'video/mp4': 'mp4',
|
||||
'video/avi': 'avi',
|
||||
'video/mov': 'mov',
|
||||
'video/wmv': 'wmv',
|
||||
'video/flv': 'flv',
|
||||
'video/webm': 'webm',
|
||||
'video/mkv': 'mkv',
|
||||
|
||||
// 音频类型
|
||||
'audio/mp3': 'mp3',
|
||||
'audio/wav': 'wav',
|
||||
'audio/aac': 'aac',
|
||||
'audio/ogg': 'ogg',
|
||||
'audio/flac': 'flac',
|
||||
|
||||
// 文档类型
|
||||
'application/pdf': 'pdf',
|
||||
'application/msword': 'doc',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
|
||||
'application/vnd.ms-excel': 'xls',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
|
||||
'application/vnd.ms-powerpoint': 'ppt',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
|
||||
'text/plain': 'txt',
|
||||
'text/html': 'html',
|
||||
'text/css': 'css',
|
||||
'application/javascript': 'js',
|
||||
'application/json': 'json'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件后缀名
|
||||
* @param {Object} file - 文件对象(兼容 formidable 格式)
|
||||
* @returns {string} 文件后缀名
|
||||
*/
|
||||
getFileSuffix(file) {
|
||||
// 优先使用 MIME 类型判断(兼容 type 和 mimetype)
|
||||
const mimeType = file.mimetype || file.type
|
||||
if (mimeType && this.fileTypeMap[mimeType]) {
|
||||
return this.fileTypeMap[mimeType]
|
||||
}
|
||||
|
||||
// 备用方案:从文件名获取(兼容 originalFilename 和 name)
|
||||
const fileName = file.originalFilename || file.name
|
||||
if (fileName) {
|
||||
const lastIndex = fileName.lastIndexOf('.')
|
||||
if (lastIndex > -1) {
|
||||
return fileName.substring(lastIndex + 1).toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
return 'bin'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件存储路径
|
||||
* @param {Object} file - 文件对象(兼容 formidable 格式)
|
||||
* @param {string} category - 存储分类
|
||||
* @returns {string} 完整的存储路径
|
||||
*/
|
||||
getStoragePath(file, category = 'files') {
|
||||
const suffix = this.getFileSuffix(file)
|
||||
const uid = uuid.v4()
|
||||
|
||||
// 根据文件类型确定子路径(兼容 mimetype 和 type)
|
||||
let subPath = category
|
||||
const mimeType = file.mimetype || file.type
|
||||
|
||||
if (mimeType) {
|
||||
if (mimeType.startsWith('image/')) {
|
||||
subPath = 'images'
|
||||
} else if (mimeType.startsWith('video/')) {
|
||||
subPath = 'videos'
|
||||
} else if (mimeType.startsWith('audio/')) {
|
||||
subPath = 'audios'
|
||||
} else if (mimeType.startsWith('application/') || mimeType.startsWith('text/')) {
|
||||
subPath = 'documents'
|
||||
}
|
||||
}
|
||||
|
||||
// 完整路径:front/ball/{subPath}/{uid}.{suffix}
|
||||
return `${this.basePrefix}/${subPath}/${uid}.${suffix}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心文件上传方法
|
||||
* @param {Object} file - 文件对象(兼容 formidable 格式)
|
||||
* @param {string} category - 存储分类
|
||||
* @returns {Object} 上传结果
|
||||
*/
|
||||
async uploadFile(file, category = 'files') {
|
||||
try {
|
||||
// 兼容不同的文件对象格式(filepath 或 path)
|
||||
const filePath = file.filepath || file.path
|
||||
|
||||
// 验证文件
|
||||
if (!file || !filePath) {
|
||||
return { success: false, error: '无效的文件对象' }
|
||||
}
|
||||
|
||||
const stream = fs.createReadStream(filePath)
|
||||
const storagePath = this.getStoragePath(file, category)
|
||||
const suffix = this.getFileSuffix(file)
|
||||
|
||||
// 设置 content-type(兼容 mimetype 和 type)
|
||||
const contentType = file.mimetype || file.type || 'application/octet-stream'
|
||||
|
||||
// 上传到 OSS
|
||||
const result = await this.client.put(storagePath, stream, {
|
||||
headers: {
|
||||
'content-disposition': 'inline',
|
||||
"content-type": contentType
|
||||
}
|
||||
})
|
||||
|
||||
if (result.res.status === 200) {
|
||||
const ossPath = config.ossUrl + '/' + result.name
|
||||
|
||||
// 上传成功后删除临时文件
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath)
|
||||
}
|
||||
} catch (unlinkError) {
|
||||
logs.error('删除临时文件失败:', unlinkError)
|
||||
}
|
||||
|
||||
// 使用 ossPath(https)作为 path,确保返回 https 格式
|
||||
const path = ossPath
|
||||
|
||||
return {
|
||||
success: true,
|
||||
name: result.name,
|
||||
path: path,
|
||||
ossPath,
|
||||
fileType: file.mimetype || file.type,
|
||||
fileSize: file.size,
|
||||
originalName: file.originalFilename || file.name,
|
||||
suffix: suffix,
|
||||
storagePath: storagePath
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: 'OSS 上传失败' }
|
||||
}
|
||||
} catch (error) {
|
||||
logs.error('文件上传错误:', error)
|
||||
|
||||
// 上传失败也要清理临时文件
|
||||
try {
|
||||
const filePath = file.filepath || file.path
|
||||
if (filePath && fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath)
|
||||
}
|
||||
} catch (unlinkError) {
|
||||
logs.error('删除临时文件失败:', unlinkError)
|
||||
}
|
||||
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传流数据
|
||||
* @param {Stream} stream - 文件流
|
||||
* @param {string} contentType - 内容类型
|
||||
* @param {string} suffix - 文件后缀
|
||||
* @returns {Object} 上传结果
|
||||
*/
|
||||
async uploadStream(stream, contentType, suffix) {
|
||||
try {
|
||||
const uid = uuid.v4()
|
||||
const storagePath = `${this.basePrefix}/files/${uid}.${suffix}`
|
||||
|
||||
const result = await this.client.put(storagePath, stream, {
|
||||
headers: {
|
||||
'content-disposition': 'inline',
|
||||
"content-type": contentType
|
||||
}
|
||||
})
|
||||
|
||||
if (result.res.status === 200) {
|
||||
const ossPath = config.ossUrl + '/' + result.name
|
||||
// 使用 ossPath(https)作为 path,确保返回 https 格式
|
||||
const path = ossPath
|
||||
return {
|
||||
success: true,
|
||||
name: result.name,
|
||||
path: path,
|
||||
ossPath,
|
||||
storagePath: storagePath
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: 'OSS 上传失败' }
|
||||
}
|
||||
} catch (error) {
|
||||
logs.error('流上传错误:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {Object} 删除结果
|
||||
*/
|
||||
async deleteFile(filePath) {
|
||||
try {
|
||||
if (!filePath) {
|
||||
return { success: false, error: '文件路径不能为空' }
|
||||
}
|
||||
|
||||
// 从完整 URL 中提取相对路径
|
||||
const relativePath = filePath.replace(config.ossUrl + '/', '')
|
||||
const result = await this.client.delete(relativePath)
|
||||
|
||||
if (result.res.status === 204) {
|
||||
return { success: true, message: '文件删除成功' }
|
||||
} else {
|
||||
return { success: false, error: '文件删除失败' }
|
||||
}
|
||||
} catch (error) {
|
||||
logs.error('文件删除错误:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件信息
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {Object} 文件信息
|
||||
*/
|
||||
async getFileInfo(filePath) {
|
||||
try {
|
||||
if (!filePath) {
|
||||
return { success: false, error: '文件路径不能为空' }
|
||||
}
|
||||
|
||||
const relativePath = filePath.replace(config.ossUrl + '/', '')
|
||||
const result = await this.client.head(relativePath)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
size: result.res.headers['content-length'],
|
||||
type: result.res.headers['content-type'],
|
||||
lastModified: result.res.headers['last-modified'],
|
||||
etag: result.res.headers['etag']
|
||||
}
|
||||
} catch (error) {
|
||||
logs.error('获取文件信息错误:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 便捷方法 ====================
|
||||
|
||||
/**
|
||||
* 上传图片文件(保持向后兼容)
|
||||
* @param {Object} file - 图片文件
|
||||
* @returns {Object} 上传结果
|
||||
*/
|
||||
async putImg(file) {
|
||||
return await this.uploadFile(file, 'images')
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 上传视频文件
|
||||
* @param {Object} file - 视频文件
|
||||
* @returns {Object} 上传结果
|
||||
*/
|
||||
async uploadVideo(file) {
|
||||
return await this.uploadFile(file, 'videos')
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传音频文件
|
||||
* @param {Object} file - 音频文件
|
||||
* @returns {Object} 上传结果
|
||||
*/
|
||||
async uploadAudio(file) {
|
||||
return await this.uploadFile(file, 'audios')
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文档文件
|
||||
* @param {Object} file - 文档文件
|
||||
* @returns {Object} 上传结果
|
||||
*/
|
||||
async uploadDocument(file) {
|
||||
return await this.uploadFile(file, 'documents')
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const ossTool = new OSSTool()
|
||||
|
||||
// 导出实例(保持向后兼容)
|
||||
module.exports = ossTool
|
||||
89
api/service/tenant_scope.js
Normal file
89
api/service/tenant_scope.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 管理端多租户:从当前登录 sys_user 解析 tenant_id
|
||||
*/
|
||||
const { sys_user } = require("../../middleware/baseModel");
|
||||
|
||||
/**
|
||||
* @param {import('koa').Context} ctx
|
||||
* @returns {Promise<{ tenantId: number|null, userId: number|null }>}
|
||||
*/
|
||||
async function getAdminTenantContext(ctx) {
|
||||
let uid_raw = null;
|
||||
try {
|
||||
uid_raw = ctx.getAdminUserId && ctx.getAdminUserId();
|
||||
} catch (e) {
|
||||
uid_raw = null;
|
||||
}
|
||||
if (uid_raw === null || uid_raw === undefined || uid_raw === "") {
|
||||
return { tenantId: null, userId: null };
|
||||
}
|
||||
const user_id = Number(uid_raw);
|
||||
if (!Number.isFinite(user_id) || user_id <= 0) {
|
||||
return { tenantId: null, userId: null };
|
||||
}
|
||||
if (!sys_user) {
|
||||
return { tenantId: null, userId: user_id };
|
||||
}
|
||||
const row = await sys_user.findByPk(user_id, {
|
||||
attributes: ["id", "tenant_id"],
|
||||
raw: true,
|
||||
});
|
||||
const tid_raw = row && row.tenant_id;
|
||||
const tenant_id = tid_raw != null && tid_raw !== "" ? Number(tid_raw) : null;
|
||||
const tenant_ok = tenant_id != null && Number.isFinite(tenant_id) && tenant_id > 0;
|
||||
return { tenantId: tenant_ok ? tenant_id : null, userId: user_id };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('sequelize').Model} model
|
||||
* @param {object} where
|
||||
* @param {number} tenant_id
|
||||
*/
|
||||
function mergeTenantWhere(model, where, tenant_id) {
|
||||
if (tenant_id == null || tenant_id === "" || !model || !model.rawAttributes || !model.rawAttributes.tenant_id) {
|
||||
return where || {};
|
||||
}
|
||||
const w = where && typeof where === "object" ? { ...where } : {};
|
||||
w.tenant_id = tenant_id;
|
||||
return w;
|
||||
}
|
||||
|
||||
function mergeTenantWhereById(model, id, tenant_id) {
|
||||
return mergeTenantWhere(model, { id }, tenant_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('koa').Context} ctx
|
||||
* @returns {Promise<{ tenantId: number, userId: number }|null>}
|
||||
*/
|
||||
async function requireTenantContext(ctx) {
|
||||
const { tenantId, userId } = await getAdminTenantContext(ctx);
|
||||
if (!tenantId) {
|
||||
ctx.fail("当前用户未绑定租户,无法操作业务数据");
|
||||
return null;
|
||||
}
|
||||
if (!userId) {
|
||||
ctx.fail("未登录或登录已失效");
|
||||
return null;
|
||||
}
|
||||
return { tenantId, userId };
|
||||
}
|
||||
|
||||
function stampTenantCreate(body, tenant_id, user_id, opts = {}) {
|
||||
const out = body && typeof body === "object" ? { ...body } : {};
|
||||
if (tenant_id != null) {
|
||||
out.tenant_id = tenant_id;
|
||||
}
|
||||
if (opts.setCreateUser !== false && user_id != null) {
|
||||
out.create_user_id = user_id;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAdminTenantContext,
|
||||
requireTenantContext,
|
||||
stampTenantCreate,
|
||||
mergeTenantWhere,
|
||||
mergeTenantWhereById,
|
||||
};
|
||||
Reference in New Issue
Block a user