refactor: 合并 biz_support_unit/biz_execution_unit/biz_service_org 为 biz_org, 删除 2 张孤儿表, 改 biz_person.work_unit → org_id FK

主要改动:
- SQL: biz_support_unit → biz_org, 加 org_type, 加 business_nature; 删 biz_execution_unit, biz_service_org
- SQL: biz_project.support_unit_id/name + service_org_id/name → org_id/name/type
- SQL: biz_meeting.support_unit_name → org_name
- SQL: biz_person.work_unit → org_id (FK)
- 后端: 新 BizOrg entity/mapper/service/controller
- 后端: BizProject/BizMeeting 字段重命名
- 后端: 删 12 个 BizSupportUnit/BizExecutionUnit/BizServiceOrg Java 文件
- 后端: BizPersonImportVO.workUnit → orgName (导入时查 biz_org 取 org_id)
- 后端: BizAuthController.registerExecutor 完整实现 (原 registerSupplier stub)
- 前端: 新 admin/Orgs.vue + manager/Orgs.vue (原 SupportUnits)
- 前端: RegisterExecutor.vue (原 RegisterSupplier, 单页 2 步)
- 前端: sponsor/executor/manager 多个文件 workUnit → orgId/orgName 重命名
- 前端: 统一 supplier → executor, 业务命名 sponsor(赞助方) / executor(执行方=供应商)
- 前端: 全工程 execution → executor (company type / role / person unit_type)
This commit is contained in:
郭庆泰
2026-08-15 12:41:10 +08:00
commit cf5790229a
694 changed files with 102090 additions and 0 deletions
File diff suppressed because one or more lines are too long
+72
View File
@@ -0,0 +1,72 @@
// OSS 直传工具 - 配合后端 /common/oss/sign
// 流程: 1) GET /common/oss/sign?dir=xxx 拿签名
// 2) POST 到 OSS bucket, formData 携带 policy/signature/key/accessKeyId
// 3) 拿回 OSS URL 写回表单
import request from '@/utils/request'
/**
* 获取 OSS 上传签名
* @param {string} dir 上传目录 (如 'ry8080/invitation/')
* @returns {Promise<{host, dir, accessKeyId, policy, signature, expire}>}
*/
export async function getOssSign(dir) {
const res = await request({
url: '/common/oss/sign',
method: 'get',
params: { dir }
})
if (res.code !== 200) {
throw new Error(res.msg || 'OSS 签名失败')
}
return res
}
/**
* 直接上传 File 到 OSS bucket
* @param {File} file 要上传的文件
* @param {string} dir 上传目录
* @returns {Promise<string>} 完整 URL (https://bucket.xxx/key)
*/
// 根据文件扩展名返回正确的 MIME,避免 OSS 默认给 application/octet-stream
function getMimeByExt(name) {
const ext = (name || '').split('.').pop().toLowerCase()
const map = {
pdf: 'application/pdf',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
webp: 'image/webp'
}
return map[ext] || 'application/octet-stream'
}
export async function uploadToOss(file, dir) {
const sign = await getOssSign(dir)
const ext = (file.name || '').split('.').pop() || 'bin'
const key = sign.dir + Date.now() + '_' + Math.random().toString(36).slice(2, 8) + '.' + ext
// 显式传 Content-Type,让 OSS 按此存文件元数据, 浏览器拿到 application/pdf 即可内嵌预览
const mime = getMimeByExt(file.name)
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', mime)
fd.append('file', file)
// ⚠️ 不要手动设置 Content-Type header,会让 multipart/form-data boundary 丢失
// FormData 会自动生成正确的 multipart 格式
const resp = await fetch(sign.host, {
method: 'POST',
body: fd
})
if (!resp.ok) {
throw new Error('OSS 上传失败: HTTP ' + resp.status)
}
return sign.host + '/' + key
}
+46
View File
@@ -0,0 +1,46 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
const request = axios.create({
baseURL: '/dev-api',
timeout: 15000
})
request.interceptors.request.use((cfg) => {
const t = localStorage.getItem('ry_token')
if (t) cfg.headers.Authorization = `Bearer ${t}`
return cfg
})
request.interceptors.response.use(
(res) => {
// blob/file 类型的响应 (如 Excel 下载/导出) 不走 code 检查, 直接放行
const rt = res.config?.responseType
if (rt === 'blob' || rt === 'arraybuffer') {
return res
}
const data = res.data
if (!data) return data
if (data.code === 200) {
// TableDataInfo 风格: {code, msg, total, rows} -> 归一化为 {code, msg, data: {total, rows}}
if (data.rows !== undefined && data.total !== undefined && data.data === undefined) {
return { code: 200, msg: data.msg, data: { total: data.total, rows: data.rows } }
}
// AjaxResult 风格: {code, msg, data} 直接返回
return data
}
if (data.code === 401) {
localStorage.removeItem('ry_token')
localStorage.removeItem('ry_user')
window.location.href = '/#/login'
}
ElMessage.error(data?.msg || '请求失败')
return Promise.reject(new Error(data?.msg || '请求失败'))
},
(err) => {
ElMessage.error(err.message || '网络错误')
return Promise.reject(err)
}
)
export default request
+65
View File
@@ -0,0 +1,65 @@
// 雪花 ID 53-bit 压缩版 (JS Number 安全)
// 移植自 hwt-code (IdGenerator.java) - 与后端 com.ruoyi.common.utils.id.IdGenerator 完全一致
// 53-bit 容量: 2^53-1 = 9007199254740991, JS Number max safe = 2^53-1
//
// 算法:
// timestamp 40 bits (per 10ms, 2300+ 年可用)
// machineId 5 bits (0~31)
// sequence 8 bits (0~255, 每 10ms 重置)
//
// 注意: JS bit-shift 对 32-bit 安全, 53-bit 不会溢出 (32-bit long 范围更大)
const MACHINE_BIT = 5
const SEQUENCE_BIT = 8
const MAX_MACHINE_NUM = -1 ^ (-1 << MACHINE_BIT) // 31
const MAX_SEQUENCE = -1 ^ (-1 << SEQUENCE_BIT) // 255
const MACHINE_LEFT = SEQUENCE_BIT
const TIMESTMP_LEFT = MACHINE_BIT + SEQUENCE_BIT
let machineId = 0
let sequence = 0
let lastStmp = -1
function initDefaultInstance(id) {
machineId = id
sequence = 0
lastStmp = -1
}
function getTimestamp() {
return Math.floor(Date.now() / 10) // per 10ms (与后端一致)
}
function getNextTimestamp() {
let mill = getTimestamp()
while (mill <= lastStmp) mill = getTimestamp()
return mill
}
function nextId() {
let currStmp = getTimestamp()
if (currStmp < lastStmp) throw new Error('Clock moved backwards. Refusing to generate id')
if (currStmp === lastStmp) {
sequence = (sequence + 1) & MAX_SEQUENCE
if (sequence === 0) currStmp = getNextTimestamp()
} else {
sequence = 0
}
lastStmp = currStmp
// JS Number 53-bit 安全
return Number(BigInt(currStmp) << BigInt(TIMESTMP_LEFT) | BigInt(machineId) << BigInt(MACHINE_LEFT) | BigInt(sequence))
}
function generateId() {
return nextId()
}
function parseIdTimestamp(id) {
return new Date(Number(BigInt(id) >> BigInt(TIMESTMP_LEFT)) * 10)
}
export default {
initDefaultInstance,
generateId,
parseIdTimestamp,
}
+41
View File
@@ -0,0 +1,41 @@
// 投稿状态字典 - 与后端 SubmissionStatus 枚举同步
// value 用英文 code (PENDING/APPROVED/REJECTED/DRAFT), 前端通过 label 映射中文
// locked=true 表示锁定(不可修改/提交)
export const SUBMISSION_STATUS = {
PENDING: { code: 'PENDING', label: '待审核', type: 'warning', locked: true },
APPROVED: { code: 'APPROVED', label: '审核通过', type: 'success', locked: true },
REJECTED: { code: 'REJECTED', label: '已退回', type: 'danger', locked: false },
DRAFT: { code: 'DRAFT', label: '待提交', type: 'info', locked: false }
}
// 容错: 把数据库返回的 status (字符串) 规范成 enum, 找不到原样返回
export function parseStatus(val) {
if (val === null || val === undefined || val === '') return null
const key = String(val).trim().toUpperCase()
return SUBMISSION_STATUS[key] ? key : null
}
// 获取状态 label, 容错返回 val 原值
export function statusLabel(val) {
const key = parseStatus(val)
return key ? SUBMISSION_STATUS[key].label : (val ?? '')
}
// 获取 el-tag type
export function statusType(val) {
const key = parseStatus(val)
return key ? SUBMISSION_STATUS[key].type : 'info'
}
// 是否锁定 (待审核/审核通过不可修改/提交)
export function isLocked(val) {
const key = parseStatus(val)
if (!key) return false
return SUBMISSION_STATUS[key].locked === true
}
// 筛选项列表 (用于 el-select options)
export const STATUS_OPTIONS = Object.values(SUBMISSION_STATUS).map(s => ({
label: s.label,
value: s.code
}))