20260821173417
This commit is contained in:
1 parent
45854b0570
commit
2e5d532f23
19 files changed
+1495
-93
No files matched your search
@@ -0,0 +1,97 @@
|
||||
import { chromium } from 'playwright-core'
|
||||
import { PNG } from 'pngjs'
|
||||
|
||||
const BASE = 'http://localhost:5082'
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||
})
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 })
|
||||
|
||||
await page.addInitScript(() => {
|
||||
sessionStorage.setItem('fms-login', JSON.stringify({ loginInfo: { token: 'dev-token', user: { id: 1, name: 'dev', account: 'g3soft' } } }))
|
||||
localStorage.setItem('fms-user', JSON.stringify({ userInfo: { orgId: 'G3HD', account: 'g3soft' } }))
|
||||
})
|
||||
await page.route('**/api/**', async (route) => {
|
||||
const req = route.request()
|
||||
const url = req.url()
|
||||
let body = {}
|
||||
try { body = req.postDataJSON() || {} } catch {}
|
||||
if (url.includes('/data/loaddata')) {
|
||||
const rows = body.view_name === 's_module' ? [{ b_id: 1, b_code: 'b_port', b_name: '港口管理' }] :
|
||||
body.view_name === 'bf_files_cateid' ? [{ b_id: '10', b_name: '合同文件' }] : []
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: rows }) })
|
||||
}
|
||||
if (url.includes('/data/page')) {
|
||||
const files = Array.from({ length: 4 }, (_, i) => ({
|
||||
subid: String(1000 + i), father: '1001', mx_moduleid: '1', mx_cate_id: '10',
|
||||
mx_filename: `文件-${i + 1}.pdf`, mx_filesize: (i + 1) * 1024 * 1024,
|
||||
mx_fileext: 'pdf', mx_mapfilename: '',
|
||||
}))
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: { rows: files, total: files.length } }) })
|
||||
}
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: null }) })
|
||||
})
|
||||
|
||||
await page.goto(`${BASE}/file-demo`, { waitUntil: 'networkidle', timeout: 30000 })
|
||||
await page.waitForTimeout(2000)
|
||||
await page.locator('button:has-text("声明式弹窗")').click()
|
||||
await page.waitForTimeout(1500)
|
||||
|
||||
const buf = await page.screenshot({ fullPage: false })
|
||||
const png = PNG.sync.read(buf)
|
||||
|
||||
// Scan for "line-like" pixels: contiguous vertical runs of a gray border color in the splitter region.
|
||||
// Detect distinct vertical line clusters (x positions with many pixels of border color).
|
||||
const { width, height, data } = png
|
||||
const isBorderColor = (x, y) => {
|
||||
const i = (y * width + x) * 4
|
||||
const r = data[i], g = data[i + 1], b = data[i + 2]
|
||||
// fms-border rgb(229,230,235) — treat near-gray light lines
|
||||
return Math.abs(r - g) <= 6 && Math.abs(g - b) <= 6 && r >= 200 && r <= 245
|
||||
}
|
||||
const colCounts = new Array(width).fill(0)
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 200; y < 850; y++) {
|
||||
if (isBorderColor(x, y)) colCounts[x]++
|
||||
}
|
||||
}
|
||||
// Find clusters of consecutive columns with meaningful counts
|
||||
const clusters = []
|
||||
let start = -1
|
||||
for (let x = 0; x < width; x++) {
|
||||
if (colCounts[x] > 300) {
|
||||
if (start === -1) start = x
|
||||
} else if (start !== -1) {
|
||||
clusters.push({ from: start, to: x - 1, count: colCounts[start] })
|
||||
start = -1
|
||||
}
|
||||
}
|
||||
if (start !== -1) clusters.push({ from: start, to: width - 1, count: colCounts[start] })
|
||||
console.log('vertical line clusters (border-colored, >300px tall):')
|
||||
console.log(JSON.stringify(clusters, null, 2))
|
||||
|
||||
// Also check the modal resize handles positions
|
||||
const handles = await page.evaluate(() => {
|
||||
const hs = Array.from(document.querySelectorAll('.fms-modal-resizer')).map((el) => {
|
||||
const r = el.getBoundingClientRect()
|
||||
const cs = getComputedStyle(el)
|
||||
const before = getComputedStyle(el, '::before')
|
||||
return {
|
||||
cls: el.className,
|
||||
x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height),
|
||||
beforeW: before.width, beforeH: before.height, beforeOpacity: before.opacity,
|
||||
}
|
||||
})
|
||||
const bars = Array.from(document.querySelectorAll('.fms-splitter-bar')).map((el) => {
|
||||
const r = el.getBoundingClientRect()
|
||||
return { cls: el.className, x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) }
|
||||
})
|
||||
return { handles, bars }
|
||||
})
|
||||
console.log('modal resize handles + splitter bars:')
|
||||
console.log(JSON.stringify(handles, null, 2))
|
||||
|
||||
// Save the screenshot for reference
|
||||
require('fs').writeFileSync('.playwright-cli/dialog-geo.png', buf)
|
||||
await browser.close()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
@@ -0,0 +1,92 @@
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
const BASE = 'http://localhost:5082'
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||
})
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } })
|
||||
|
||||
await page.addInitScript(() => {
|
||||
sessionStorage.setItem('fms-login', JSON.stringify({ loginInfo: { token: 'dev-token', user: { id: 1, name: 'dev', account: 'g3soft' } } }))
|
||||
localStorage.setItem('fms-user', JSON.stringify({ userInfo: { orgId: 'G3HD', account: 'g3soft' } }))
|
||||
})
|
||||
await page.route('**/api/**', async (route) => {
|
||||
const req = route.request()
|
||||
const url = req.url()
|
||||
let body = {}
|
||||
try { body = req.postDataJSON() || {} } catch {}
|
||||
if (url.includes('/data/loaddata')) {
|
||||
const rows = body.view_name === 's_module' ? [{ b_id: 1, b_code: 'b_port', b_name: '港口管理' }] :
|
||||
body.view_name === 'bf_files_cateid' ? [{ b_id: '10', b_name: '合同文件' }] : []
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: rows }) })
|
||||
}
|
||||
if (url.includes('/data/page')) {
|
||||
const files = Array.from({ length: 4 }, (_, i) => ({
|
||||
subid: String(1000 + i), father: '1001', mx_moduleid: '1', mx_cate_id: '10',
|
||||
mx_filename: `文件-${i + 1}.pdf`, mx_filesize: (i + 1) * 1024 * 1024,
|
||||
mx_fileext: 'pdf', mx_mapfilename: '',
|
||||
}))
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: { rows: files, total: files.length } }) })
|
||||
}
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: null }) })
|
||||
})
|
||||
|
||||
await page.goto(`${BASE}/file-demo`, { waitUntil: 'networkidle', timeout: 30000 })
|
||||
await page.waitForTimeout(2000)
|
||||
await page.locator('button:has-text("声明式弹窗")').click()
|
||||
await page.waitForTimeout(1500)
|
||||
|
||||
const result = await page.evaluate(() => {
|
||||
const ff = document.querySelector('.ff-splitter')
|
||||
if (!ff) return { error: 'no .ff-splitter' }
|
||||
const all = ff.querySelectorAll(':scope > *')
|
||||
const items = Array.from(all).map((el, i) => {
|
||||
const cls = el.className
|
||||
const rect = el.getBoundingClientRect()
|
||||
const cs = getComputedStyle(el)
|
||||
const barInfo = cls.includes('fms-splitter-bar')
|
||||
? (() => {
|
||||
const style = getComputedStyle(el, '::before')
|
||||
const grip = el.querySelector('.fms-splitter-bar-grip')
|
||||
const gripStyle = grip ? getComputedStyle(grip) : null
|
||||
return {
|
||||
w: Math.round(rect.width),
|
||||
beforeW: style.width,
|
||||
beforeH: style.height,
|
||||
beforeBg: style.background,
|
||||
gripW: gripStyle ? gripStyle.width : null,
|
||||
gripH: gripStyle ? gripStyle.height : null,
|
||||
gripOpacity: gripStyle ? gripStyle.opacity : null,
|
||||
}
|
||||
})()
|
||||
: null
|
||||
return {
|
||||
index: i,
|
||||
class: cls,
|
||||
left: Math.round(rect.left),
|
||||
top: Math.round(rect.top),
|
||||
width: Math.round(rect.width),
|
||||
height: Math.round(rect.height),
|
||||
csWidth: cs.width,
|
||||
csMargin: cs.margin,
|
||||
csFlex: cs.flex,
|
||||
csFlexBasis: cs.flexBasis,
|
||||
barInfo,
|
||||
}
|
||||
})
|
||||
const container = ff.closest('.ff-container')
|
||||
const containerRect = container ? container.getBoundingClientRect() : null
|
||||
const containerCS = container ? getComputedStyle(container) : null
|
||||
const modalBody = document.querySelector('.fms-modal-body')
|
||||
const modalBodyRect = modalBody ? modalBody.getBoundingClientRect() : null
|
||||
const panels = Array.from(ff.querySelectorAll(':scope > .fms-splitter-panel')).map((el) => {
|
||||
const r = el.getBoundingClientRect()
|
||||
const cs = getComputedStyle(el)
|
||||
return { w: Math.round(r.width), h: Math.round(r.height), flex: cs.flex, flexBasis: cs.flexBasis, padding: cs.padding, border: cs.border }
|
||||
})
|
||||
return { items, containerRect, containerCS: containerCS ? { border: containerCS.border, padding: containerCS.padding } : null, modalBodyRect, panels }
|
||||
})
|
||||
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
await browser.close()
|
||||
@@ -0,0 +1,57 @@
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
const BASE = 'http://localhost:5082'
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||
})
|
||||
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } })
|
||||
const logs = []
|
||||
page.on('console', (m) => {
|
||||
if (m.type() === 'error') logs.push(`[console.error] ${m.text()}`)
|
||||
})
|
||||
page.on('pageerror', (e) => logs.push(`[pageerror] ${e.message}`))
|
||||
|
||||
await page.addInitScript(() => {
|
||||
sessionStorage.setItem(
|
||||
'fms-login',
|
||||
JSON.stringify({ loginInfo: { token: 'dev-token', user: { id: 1, name: 'dev' } } }),
|
||||
)
|
||||
localStorage.setItem('fms-user', JSON.stringify({ userInfo: { orgId: 'G3HD', account: 'g3soft' } }))
|
||||
})
|
||||
// mock all API calls so the fake token never gets 401'd
|
||||
await page.route('**/api/**', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ code: 0, data: null, message: '' }),
|
||||
}),
|
||||
)
|
||||
|
||||
try {
|
||||
await page.goto(`${BASE}/file-demo`, { waitUntil: 'networkidle', timeout: 30000 })
|
||||
await page.waitForTimeout(1500)
|
||||
console.log('url after load:', page.url())
|
||||
|
||||
const clickInfo = await page.evaluate(() => {
|
||||
const els = Array.from(document.querySelectorAll('.button, button'))
|
||||
const el = els.find((b) => (b.textContent || '').includes('声明式弹窗'))
|
||||
if (!el) return { found: false }
|
||||
el.click()
|
||||
return { found: true }
|
||||
})
|
||||
console.log('click:', JSON.stringify(clickInfo))
|
||||
await page.waitForTimeout(1200)
|
||||
|
||||
const state = await page.evaluate(() => ({
|
||||
url: location.href,
|
||||
modalCount: document.querySelectorAll('.fms-modal').length,
|
||||
ffCount: document.querySelectorAll('.ff-container').length,
|
||||
ffSplitter: !!document.querySelector('.ff-splitter'),
|
||||
}))
|
||||
console.log('state:', JSON.stringify(state))
|
||||
} catch (e) {
|
||||
console.log('ERROR:', String(e))
|
||||
}
|
||||
console.log('logs:', JSON.stringify(logs.slice(0, 10), null, 2))
|
||||
await browser.close()
|
||||
@@ -0,0 +1,29 @@
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
const BASE = 'http://localhost:5082'
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||
})
|
||||
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } })
|
||||
page.on('framenavigated', (f) => {
|
||||
if (f === page.mainFrame()) console.log('nav ->', f.url())
|
||||
})
|
||||
|
||||
await page.addInitScript(() => {
|
||||
sessionStorage.setItem(
|
||||
'fms-login',
|
||||
JSON.stringify({ loginInfo: { token: 'dev-token', user: { id: 1, name: 'dev' } } }),
|
||||
)
|
||||
localStorage.setItem('fms-user', JSON.stringify({ userInfo: { orgId: 'G3HD', account: 'g3soft' } }))
|
||||
})
|
||||
|
||||
await page.goto(`${BASE}/file-demo`, { waitUntil: 'domcontentloaded', timeout: 30000 })
|
||||
await page.waitForTimeout(2500)
|
||||
console.log('final url:', page.url())
|
||||
console.log('session:', await page.evaluate(() => sessionStorage.getItem('fms-login')))
|
||||
console.log(
|
||||
'body snippet:',
|
||||
await page.evaluate(() => (document.body.textContent || '').slice(0, 120).replace(/\s+/g, ' ')),
|
||||
)
|
||||
await browser.close()
|
||||
@@ -0,0 +1,97 @@
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
const BASE = 'http://localhost:5082'
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||
})
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 })
|
||||
|
||||
await page.addInitScript(() => {
|
||||
sessionStorage.setItem('fms-login', JSON.stringify({ loginInfo: { token: 'dev-token', user: { id: 1, name: 'dev', account: 'g3soft' } } }))
|
||||
localStorage.setItem('fms-user', JSON.stringify({ userInfo: { orgId: 'G3HD', account: 'g3soft' } }))
|
||||
})
|
||||
await page.route('**/api/**', async (route) => {
|
||||
const req = route.request()
|
||||
const url = req.url()
|
||||
let body = {}
|
||||
try { body = req.postDataJSON() || {} } catch {}
|
||||
if (url.includes('/data/loaddata')) {
|
||||
const rows = body.view_name === 's_module' ? [{ b_id: 1, b_code: 'b_port', b_name: '港口管理' }] :
|
||||
body.view_name === 'bf_files_cateid' ? [{ b_id: '10', b_name: '合同文件' }] : []
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: rows }) })
|
||||
}
|
||||
if (url.includes('/data/page')) {
|
||||
const files = Array.from({ length: 4 }, (_, i) => ({
|
||||
subid: String(1000 + i), father: '1001', mx_moduleid: '1', mx_cate_id: '10',
|
||||
mx_filename: `文件-${i + 1}.pdf`, mx_filesize: (i + 1) * 1024 * 1024,
|
||||
mx_fileext: 'pdf', mx_mapfilename: '',
|
||||
}))
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: { rows: files, total: files.length } }) })
|
||||
}
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: null }) })
|
||||
})
|
||||
|
||||
await page.goto(`${BASE}/file-demo`, { waitUntil: 'networkidle', timeout: 30000 })
|
||||
await page.waitForTimeout(2000)
|
||||
await page.locator('button:has-text("声明式弹窗")').click()
|
||||
await page.waitForTimeout(1500)
|
||||
|
||||
// Take screenshot as base64, load into canvas inside the page, and sample pixel columns
|
||||
const b64 = await page.screenshot({ encoding: 'base64' })
|
||||
const analysis = await page.evaluate(async (b64) => {
|
||||
const img = new Image()
|
||||
img.src = 'data:image/png;base64,' + b64
|
||||
await img.decode()
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(img, 0, 0)
|
||||
const { width, height } = canvas
|
||||
|
||||
// get color at pixel
|
||||
const px = (x, y) => {
|
||||
const d = ctx.getImageData(x, y, 1, 1).data
|
||||
return `${d[0]},${d[1]},${d[2]}`
|
||||
}
|
||||
|
||||
// Scan a horizontal strip at y = 500 (mid of dialog) from x=570 to x=1410
|
||||
// and report runs of distinct colors
|
||||
const y = 500
|
||||
const runs = []
|
||||
let cur = null
|
||||
for (let x = 570; x < 1410; x++) {
|
||||
const c = px(x, y)
|
||||
if (cur && cur.color === c) {
|
||||
cur.to = x
|
||||
} else {
|
||||
if (cur) runs.push(cur)
|
||||
cur = { color: c, from: x, to: x }
|
||||
}
|
||||
}
|
||||
if (cur) runs.push(cur)
|
||||
|
||||
// Vertical line detection: for a given x, count how many y in [215, 850] have the same color
|
||||
const lineAt = (x) => {
|
||||
const counts = new Map()
|
||||
for (let yy = 215; yy < 850; yy += 2) {
|
||||
const c = px(x, yy)
|
||||
counts.set(c, (counts.get(c) || 0) + 1)
|
||||
}
|
||||
const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3)
|
||||
return sorted.map(([c, n]) => ({ color: c, count: n }))
|
||||
}
|
||||
|
||||
// Check the two bar regions: every x in the bar area, list dominant colors
|
||||
const bars = []
|
||||
for (const [x0, x1] of [[795, 806], [1301, 1312]]) {
|
||||
const cols = []
|
||||
for (let x = x0; x <= x1; x++) cols.push({ x, colors: lineAt(x) })
|
||||
bars.push(cols)
|
||||
}
|
||||
return { runs, bars, imgW: width, imgH: height }
|
||||
}, b64)
|
||||
|
||||
console.log(JSON.stringify(analysis, null, 2))
|
||||
await browser.close()
|
||||
@@ -0,0 +1,72 @@
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
const BASE = 'http://localhost:5082'
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||
})
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 })
|
||||
|
||||
await page.addInitScript(() => {
|
||||
sessionStorage.setItem('fms-login', JSON.stringify({ loginInfo: { token: 'dev-token', user: { id: 1, name: 'dev', account: 'g3soft' } } }))
|
||||
localStorage.setItem('fms-user', JSON.stringify({ userInfo: { orgId: 'G3HD', account: 'g3soft' } }))
|
||||
})
|
||||
await page.route('**/api/**', async (route) => {
|
||||
const req = route.request()
|
||||
const url = req.url()
|
||||
let body = {}
|
||||
try { body = req.postDataJSON() || {} } catch {}
|
||||
if (url.includes('/data/loaddata')) {
|
||||
const rows = body.view_name === 's_module' ? [{ b_id: 1, b_code: 'b_port', b_name: '港口管理' }] :
|
||||
body.view_name === 'bf_files_cateid' ? [{ b_id: '10', b_name: '合同文件' }] : []
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: rows }) })
|
||||
}
|
||||
if (url.includes('/data/page')) {
|
||||
const files = Array.from({ length: 4 }, (_, i) => ({
|
||||
subid: String(1000 + i), father: '1001', mx_moduleid: '1', mx_cate_id: '10',
|
||||
mx_filename: `文件-${i + 1}.pdf`, mx_filesize: (i + 1) * 1024 * 1024,
|
||||
mx_fileext: 'pdf', mx_mapfilename: '',
|
||||
}))
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: { rows: files, total: files.length } }) })
|
||||
}
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: null }) })
|
||||
})
|
||||
|
||||
await page.goto(`${BASE}/file-demo`, { waitUntil: 'networkidle', timeout: 30000 })
|
||||
await page.waitForTimeout(2000)
|
||||
await page.locator('button:has-text("声明式弹窗")').click()
|
||||
await page.waitForTimeout(1500)
|
||||
|
||||
const b64 = await page.screenshot({ encoding: 'base64' })
|
||||
const analysis = await page.evaluate(async (b64) => {
|
||||
const bin = atob(b64)
|
||||
const bytes = new Uint8Array(bin.length)
|
||||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
|
||||
const blob = new Blob([bytes], { type: 'image/png' })
|
||||
const bmp = await createImageBitmap(blob)
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = bmp.width
|
||||
canvas.height = bmp.height
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(bmp, 0, 0)
|
||||
const { width, height } = canvas
|
||||
|
||||
const px = (x, y) => {
|
||||
const d = ctx.getImageData(x, y, 1, 1).data
|
||||
return `${d[0]},${d[1]},${d[2]}`
|
||||
}
|
||||
|
||||
// For each x in the splitter region, count how many sample rows are the border-gray color
|
||||
// and report a compact "row" profile at several y positions
|
||||
const yPositions = [220, 300, 400, 500, 600, 700]
|
||||
const profile = []
|
||||
for (let x = 370; x <= 930; x++) {
|
||||
const row = {}
|
||||
for (const y of yPositions) row[`y${y}`] = px(x, y)
|
||||
profile.push({ x, ...row })
|
||||
}
|
||||
return { imgW: width, imgH: height, profile }
|
||||
}, b64)
|
||||
|
||||
console.log(JSON.stringify(analysis, null, 2))
|
||||
await browser.close()
|
||||
@@ -0,0 +1,123 @@
|
||||
import { chromium } from 'playwright-core'
|
||||
import zlib from 'node:zlib'
|
||||
|
||||
const BASE = 'http://localhost:5082'
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||
})
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 })
|
||||
|
||||
await page.addInitScript(() => {
|
||||
sessionStorage.setItem('fms-login', JSON.stringify({ loginInfo: { token: 'dev-token', user: { id: 1, name: 'dev', account: 'g3soft' } } }))
|
||||
localStorage.setItem('fms-user', JSON.stringify({ userInfo: { orgId: 'G3HD', account: 'g3soft' } }))
|
||||
})
|
||||
await page.route('**/api/**', async (route) => {
|
||||
const req = route.request()
|
||||
const url = req.url()
|
||||
let body = {}
|
||||
try { body = req.postDataJSON() || {} } catch {}
|
||||
if (url.includes('/data/loaddata')) {
|
||||
const rows = body.view_name === 's_module' ? [{ b_id: 1, b_code: 'b_port', b_name: '港口管理' }] :
|
||||
body.view_name === 'bf_files_cateid' ? [{ b_id: '10', b_name: '合同文件' }] : []
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: rows }) })
|
||||
}
|
||||
if (url.includes('/data/page')) {
|
||||
const files = Array.from({ length: 4 }, (_, i) => ({
|
||||
subid: String(1000 + i), father: '1001', mx_moduleid: '1', mx_cate_id: '10',
|
||||
mx_filename: `文件-${i + 1}.pdf`, mx_filesize: (i + 1) * 1024 * 1024,
|
||||
mx_fileext: 'pdf', mx_mapfilename: '',
|
||||
}))
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: { rows: files, total: files.length } }) })
|
||||
}
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: null }) })
|
||||
})
|
||||
|
||||
await page.goto(`${BASE}/file-demo`, { waitUntil: 'networkidle', timeout: 30000 })
|
||||
await page.waitForTimeout(2000)
|
||||
await page.locator('button:has-text("声明式弹窗")').click()
|
||||
await page.waitForTimeout(1500)
|
||||
|
||||
const buf = await page.screenshot()
|
||||
|
||||
// ---- minimal PNG decoder ----
|
||||
function decodePNG(buf) {
|
||||
if (buf.readUInt32BE(0) !== 0x89504e47) throw new Error('not png')
|
||||
let pos = 8
|
||||
let width = 0, height = 0, bitDepth = 0, colorType = 0
|
||||
const idat = []
|
||||
while (pos < buf.length) {
|
||||
const len = buf.readUInt32BE(pos)
|
||||
const type = buf.toString('ascii', pos + 4, pos + 8)
|
||||
const data = buf.subarray(pos + 8, pos + 8 + len)
|
||||
if (type === 'IHDR') {
|
||||
width = data.readUInt32BE(0)
|
||||
height = data.readUInt32BE(4)
|
||||
bitDepth = data[8]
|
||||
colorType = data[9]
|
||||
} else if (type === 'IDAT') {
|
||||
idat.push(data)
|
||||
} else if (type === 'IEND') break
|
||||
pos += 12 + len
|
||||
}
|
||||
const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : colorType === 0 ? 1 : 4
|
||||
const bpp = Math.max(1, (channels * bitDepth) / 8)
|
||||
const raw = zlib.inflateSync(Buffer.concat(idat))
|
||||
const stride = width * channels
|
||||
const out = Buffer.alloc(height * stride)
|
||||
const paeth = (a, b, c) => {
|
||||
const p = a + b - c
|
||||
const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c)
|
||||
if (pa <= pb && pa <= pc) return a
|
||||
if (pb <= pc) return b
|
||||
return c
|
||||
}
|
||||
for (let y = 0; y < height; y++) {
|
||||
const filter = raw[y * (stride + 1)]
|
||||
const row = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1))
|
||||
const prev = y > 0 ? out.subarray((y - 1) * stride, y * stride) : null
|
||||
const cur = out.subarray(y * stride, (y + 1) * stride)
|
||||
for (let x = 0; x < stride; x++) {
|
||||
const a = x >= bpp ? cur[x - bpp] : 0
|
||||
const b = prev ? prev[x] : 0
|
||||
const c = x >= bpp && prev ? prev[x - bpp] : 0
|
||||
let v = row[x]
|
||||
if (filter === 1) v = (v + a) & 0xff
|
||||
else if (filter === 2) v = (v + b) & 0xff
|
||||
else if (filter === 3) v = (v + Math.floor((a + b) / 2)) & 0xff
|
||||
else if (filter === 4) v = (v + paeth(a, b, c)) & 0xff
|
||||
cur[x] = v
|
||||
}
|
||||
}
|
||||
return { width, height, channels, data: out }
|
||||
}
|
||||
|
||||
const png = decodePNG(buf)
|
||||
const { width, height, channels, data } = png
|
||||
const px = (x, y) => {
|
||||
const i = (y * width + x) * channels
|
||||
return channels === 4
|
||||
? `${data[i]},${data[i + 1]},${data[i + 2]}`
|
||||
: `${data[i]},${data[i + 1]},${data[i + 2]}`
|
||||
}
|
||||
|
||||
// Profile horizontal strip at several y's around the two bars (x 380..940)
|
||||
const yPositions = [220, 300, 400, 500, 600, 700, 800]
|
||||
const compact = []
|
||||
for (let x = 385; x <= 940; x++) {
|
||||
const row = { x }
|
||||
for (const y of yPositions) row[`y${y}`] = px(x, y)
|
||||
compact.push(row)
|
||||
}
|
||||
|
||||
// Also sample a vertical strip through both bars at mid height to show line thickness
|
||||
const vertical = []
|
||||
for (let y = 215; y <= 720; y += 10) {
|
||||
vertical.push({ y, bar1: px(400, y), bar2: px(906, y), gap1: px(397, y), gap2: px(403, y) })
|
||||
}
|
||||
|
||||
console.log('HORIZONTAL PROFILE (x around both bars):')
|
||||
for (const r of compact) console.log(JSON.stringify(r))
|
||||
console.log('\nVERTICAL PROFILE:')
|
||||
for (const r of vertical) console.log(JSON.stringify(r))
|
||||
await browser.close()
|
||||
@@ -0,0 +1,68 @@
|
||||
import { chromium } from 'playwright-core'
|
||||
|
||||
const BASE = 'http://localhost:5082'
|
||||
const browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||
})
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1 })
|
||||
|
||||
await page.addInitScript(() => {
|
||||
sessionStorage.setItem('fms-login', JSON.stringify({ loginInfo: { token: 'dev-token', user: { id: 1, name: 'dev', account: 'g3soft' } } }))
|
||||
localStorage.setItem('fms-user', JSON.stringify({ userInfo: { orgId: 'G3HD', account: 'g3soft' } }))
|
||||
})
|
||||
await page.route('**/api/**', async (route) => {
|
||||
const req = route.request()
|
||||
const url = req.url()
|
||||
let body = {}
|
||||
try { body = req.postDataJSON() || {} } catch {}
|
||||
if (url.includes('/data/loaddata')) {
|
||||
const rows = body.view_name === 's_module' ? [{ b_id: 1, b_code: 'b_port', b_name: '港口管理' }] :
|
||||
body.view_name === 'bf_files_cateid' ? [{ b_id: '10', b_name: '合同文件' }] : []
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: rows }) })
|
||||
}
|
||||
if (url.includes('/data/page')) {
|
||||
const files = Array.from({ length: 4 }, (_, i) => ({
|
||||
subid: String(1000 + i), father: '1001', mx_moduleid: '1', mx_cate_id: '10',
|
||||
mx_filename: `文件-${i + 1}.pdf`, mx_filesize: (i + 1) * 1024 * 1024,
|
||||
mx_fileext: 'pdf', mx_mapfilename: '',
|
||||
}))
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: { rows: files, total: files.length } }) })
|
||||
}
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ code: 0, data: null }) })
|
||||
})
|
||||
|
||||
await page.goto(`${BASE}/file-demo`, { waitUntil: 'networkidle', timeout: 30000 })
|
||||
await page.waitForTimeout(2000)
|
||||
await page.locator('button:has-text("声明式弹窗")').click()
|
||||
await page.waitForTimeout(1500)
|
||||
|
||||
const result = await page.evaluate(() => {
|
||||
const lines = []
|
||||
function info(el) {
|
||||
if (!el) return null
|
||||
const r = el.getBoundingClientRect()
|
||||
const cs = getComputedStyle(el)
|
||||
return {
|
||||
tag: el.tagName.toLowerCase(),
|
||||
cls: String(el.className),
|
||||
x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height),
|
||||
display: cs.display,
|
||||
padding: cs.padding,
|
||||
border: cs.border,
|
||||
overflow: cs.overflow,
|
||||
}
|
||||
}
|
||||
const walk = (el, depth) => {
|
||||
if (!el || depth > 12) return
|
||||
lines.push(' '.repeat(depth * 2) + JSON.stringify(info(el)))
|
||||
for (const child of el.children) walk(child, depth + 1)
|
||||
}
|
||||
const modal = document.querySelector('.fms-modal')
|
||||
if (!modal) return { error: 'no modal' }
|
||||
walk(modal, 0)
|
||||
return lines
|
||||
})
|
||||
|
||||
console.log('RESULT:', JSON.stringify(result))
|
||||
await browser.close()
|
||||
+260
-81
@@ -8,17 +8,17 @@
|
||||
本方案坚持五项原则:
|
||||
|
||||
1. **沿用模块主体**:`s_module` 继续作为旧系统的数据配置主体,分类节点和数据节点在同一棵树中维护,不再增加 `s_entity`。
|
||||
2. **模块与字段分离**:`data/query` 模块声明查询源、保存源和主键字段;字段挂模块保存数据库列定义,列表、表单、查询行为由字段配置子表承载。
|
||||
2. **模块与字段分离**:`data/query` 模块声明查询源、保存源和主键字段;字段挂模块保存物理列名、字段类型和数据库列约束,列表、表单、查询行为由字段配置子表承载。
|
||||
3. **菜单与模块分离**:`s_menu` 是纯导航树,只引用数据模块或固定页面;本方案只恢复旧系统的模块数据主体,不把菜单、路由和角色授权重新塞回 `s_module`。
|
||||
4. **字段定义与视图配置分责**:字段定义、列表、表单、查询是四类不同职责;本方案保留 `s_field` 作为字段定义表,并将列表、表单、查询行为分别放入 `s_field_list`、`s_field_edit`、`s_field_query`,避免一张宽表充满互不相关的空字段。
|
||||
4. **字段定义与视图配置分责**:字段定义、列表、表单、查询是四类不同职责;本方案保留 `s_field` 作为字段定义表,并将列表、表单、查询行为分别放入 `s_field_view`、`s_field_edit`、`s_field_query`,避免一张宽表充满互不相关的空字段。
|
||||
5. **配置入口与存储职责分离**:管理员可从模块树或菜单树定位数据模块,在一个配置工作台内完成数据源、字段、列表、表单、查询、编码和权限配置;底层仍按模块、菜单和权限分别存储。
|
||||
|
||||
其余约定:
|
||||
|
||||
- 内部主键 `b_id bigint identity(1, 1)`,跨环境稳定标识使用 `b_code`(参考旧系统)。
|
||||
- 元数据表保留物理外键(低频、高一致性,防脏数据);若担心部署顺序,可退化为逻辑外键并保留索引。
|
||||
- 全系统不建立物理外键;关联字段保留索引,引用完整性、跨模块一致性、停用与删除保护统一由服务层维护。
|
||||
- 布尔语义使用 `int`(0/1),与旧系统 `s_module` 风格一致。
|
||||
- 时间使用 `datetime2`,默认 `sysdatetime()`。
|
||||
- 时间统一使用 UTC:字段类型为 `datetime2`,默认 `sysutcdatetime()`;前端按用户时区转换显示。
|
||||
- 审计字段:定义类表保留 `b_inputuser_id / b_inputdatetime / b_updateuser_id / b_updatedatetime`;关联类表只保留 `b_inputdatetime`。
|
||||
- `s_module.b_parent_id` 只表达模块树位置;模块间真实的引用、主从、外键和级联关系由 `s_relation` 表达。
|
||||
- `b_module_type` 使用 `category/data/query`:分类节点不配置数据源,数据节点可查可存,查询节点只读。
|
||||
@@ -31,21 +31,22 @@
|
||||
| 1 | `s_module` | 模块树(分类/数据/查询节点,数据所有者) |
|
||||
| 2 | `s_field` | 字段定义(数据库列与通用控件元数据) |
|
||||
| 3 | `s_field_group` | 字段分组(模块级) |
|
||||
| 4 | `s_field_list` | 字段列表配置(显示、排序、列宽、分组) |
|
||||
| 4 | `s_field_view` | 字段视图配置(列表显示、排序、列宽、分组) |
|
||||
| 5 | `s_field_edit` | 字段表单配置(显示、排序、分组、必填、只读、禁用) |
|
||||
| 6 | `s_field_query` | 字段查询配置(启用、排序、组件、操作符、默认值) |
|
||||
| 7 | `s_relation` | 模块关系(引用/主从,驱动外键校验与级联) |
|
||||
| 6 | `s_field_query` | 字段查询配置(关联字段、操作符、默认值) |
|
||||
| 7 | `s_relation` | 模块关系(引用/主从/多对多,驱动外键校验与级联) |
|
||||
| 8 | `s_menu` | 菜单/页面导航(data 菜单直接绑定数据模块) |
|
||||
| 9 | `s_autocode` | 自动编码规则 |
|
||||
| 10 | `s_role` | 角色 |
|
||||
| 11 | `s_user_role` | 用户-角色 |
|
||||
| 12 | `s_power` | 权限点(menu/module/action/field) |
|
||||
| 13 | `s_role_power` | 角色-权限 |
|
||||
| 14 | `s_i18n` | 多语言资源 |
|
||||
| 15 | `s_log_technical` | 技术日志(请求链路/SQL/异常/性能) |
|
||||
| 16 | `s_log_login` | 登录日志(登录/退出/失败/锁定) |
|
||||
| 17 | `s_log_audit` | 业务审计事件 |
|
||||
| 18 | `s_log_audit_field` | 审计字段变更明细 |
|
||||
| 13 | `s_role_power` | 角色-权限(允许/拒绝) |
|
||||
| 14 | `s_role_scope` | 角色-模块数据范围 |
|
||||
| 15 | `s_i18n` | 多语言资源 |
|
||||
| 16 | `s_log_technical` | 技术日志(请求链路/SQL/异常/性能) |
|
||||
| 17 | `s_log_login` | 登录日志(登录/退出/失败/锁定) |
|
||||
| 18 | `s_log_audit` | 业务审计事件 |
|
||||
| 19 | `s_log_audit_field` | 审计字段变更明细 |
|
||||
## 3. 各表结构
|
||||
|
||||
### 3.1 s_module 模块
|
||||
@@ -64,18 +65,23 @@ create table s_module (
|
||||
b_savetable varchar(128) null, -- 保存目标(data 可用,query 必须为空)
|
||||
b_keyfield varchar(50) null, -- 主键字段(data/query 使用)
|
||||
b_orderfield varchar(500) null, -- 默认排序表达式
|
||||
b_delete_policy varchar(20) null, -- restrict/lifecycle/cascade/hard
|
||||
b_delete_policy varchar(20) null, -- restrict/lifecycle/hard;级联由 s_relation 表达
|
||||
b_canuse int not null default 1, -- 是否启用
|
||||
b_xh int not null default 0, -- 排序号
|
||||
b_icon varchar(50) null, -- 图标
|
||||
b_bz nvarchar(1000) null, -- 描述
|
||||
b_inputuser_id bigint null,
|
||||
b_inputdatetime datetime2 null default sysdatetime(),
|
||||
b_inputdatetime datetime2 null default sysutcdatetime(),
|
||||
b_updateuser_id bigint null,
|
||||
b_updatedatetime datetime2 null,
|
||||
constraint pk_s_module primary key (b_id),
|
||||
constraint ux_s_module_code unique (b_code),
|
||||
constraint ck_s_module_type check (b_module_type in ('category', 'data', 'query')),
|
||||
constraint ck_s_module_delete_policy check (b_delete_policy is null or b_delete_policy in ('restrict', 'lifecycle', 'hard')),
|
||||
constraint ck_s_module_config check (
|
||||
(b_module_type = 'category' and b_viewtable is null and b_savetable is null and b_keyfield is null and b_orderfield is null and b_delete_policy is null)
|
||||
or (b_module_type = 'data' and b_viewtable is not null and b_savetable is not null and b_keyfield is not null and b_delete_policy is not null)
|
||||
or (b_module_type = 'query' and b_viewtable is not null and b_savetable is null and b_delete_policy is null)
|
||||
),
|
||||
constraint ck_s_module_parent check (b_parent_id is null or b_parent_id <> b_id),
|
||||
constraint fk_s_module_parent foreign key (b_parent_id) references s_module (b_id)
|
||||
);
|
||||
@@ -87,20 +93,23 @@ create index ix_s_module_savetable on s_module (b_savetable);
|
||||
模块的查/存配置方式:
|
||||
|
||||
```text
|
||||
分类模块: b_module_type = category, 查/存字段全部为空
|
||||
数据模块: b_module_type = data, b_viewtable 为查询源, b_savetable 为保存表
|
||||
查询模块: b_module_type = query, b_viewtable 为查询源, b_savetable 必须为空
|
||||
分类模块: b_module_type = category, 查/存/主键/排序/删除策略全部为空
|
||||
数据模块: b_module_type = data, b_viewtable/b_savetable/b_keyfield/b_delete_policy 必填
|
||||
查询模块: b_module_type = query, b_viewtable 必填, b_savetable/b_delete_policy 必须为空;汇总查询可不填 b_keyfield
|
||||
```
|
||||
|
||||
数据库约束只能判断空值,服务层还必须拒绝空字符串,并校验表、视图、主键和排序字段真实存在。模块树、菜单树和字段分组树在修改父节点时必须检查完整祖先链,禁止 `A -> B -> C -> A` 形式的间接循环。
|
||||
|
||||
删除策略取值(`b_delete_policy`):
|
||||
|
||||
```text
|
||||
restrict 主数据:有引用时拒绝删除,只能停用或合并
|
||||
lifecycle 业务单据:草稿且无下游时可删,正式后只能取消/作废
|
||||
cascade 主表拥有的从数据:随主表同一事务级联删除
|
||||
hard 临时记录:校验权限后直接物理删除
|
||||
```
|
||||
|
||||
模块删除策略只控制“直接删除当前模块记录”是否允许;主记录删除时如何处理从记录由具体 `s_relation.b_delete_policy` 决定,因此模块层不再重复设置 `cascade`。
|
||||
|
||||
### 3.2 s_field 字段
|
||||
|
||||
只保存字段本身的规范定义,不混入列表、表单和查询布局。字段同步、数据库列校验和业务审计均以本表为准;三类界面行为分别放在后续配置表中。
|
||||
@@ -112,31 +121,69 @@ create table s_field (
|
||||
b_field varchar(50) not null, -- 物理列名(与真实表列一致)
|
||||
b_name nvarchar(100) not null, -- 字段名称
|
||||
b_i18n varchar(150) null, -- 显示名多语言 key
|
||||
b_type varchar(30) not null, -- 存储类型:varchar/decimal/datetime/int/bit/text
|
||||
b_type varchar(30) not null, -- 字段类型(input/textarea/number/money/date/datetime/checkbox/select/selectId...)
|
||||
b_dbtype varchar(30) not null, -- 数据库存储类型(varchar/nvarchar/int/bigint/decimal/date/datetime2/bit/text)
|
||||
b_length int null, -- 长度
|
||||
b_precision int null, -- 数值精度(总位数)
|
||||
b_scale int null, -- 数值小数位
|
||||
b_nullable int not null default 1, -- 数据库层是否可空
|
||||
b_default_value nvarchar(500) null, -- 默认值(文本表达)
|
||||
b_options nvarchar(max) null, -- 下拉/单选选项(JSON 文本)
|
||||
b_default_value nvarchar(500) null, -- 固定业务/UI 默认值(文本或 JSON 表达)
|
||||
b_db_default nvarchar(500) null, -- 数据库列默认表达式(结构同步得到)
|
||||
b_writemode varchar(20) not null default 'direct', -- direct/createOnly/readOnly/computed/system
|
||||
b_options nvarchar(max) null, -- 字段类型配置(JSON;静态选项或 selectId 选择器配置)
|
||||
b_canuse int not null default 1, -- 是否启用(停用隐藏并保留数据)
|
||||
b_inputuser_id bigint null,
|
||||
b_inputdatetime datetime2 null default sysdatetime(),
|
||||
b_inputdatetime datetime2 null default sysutcdatetime(),
|
||||
b_updateuser_id bigint null,
|
||||
b_updatedatetime datetime2 null,
|
||||
constraint pk_s_field primary key (b_id),
|
||||
constraint ux_s_field_module_field unique (b_module_id, b_field),
|
||||
constraint fk_s_field_module foreign key (b_module_id) references s_module (b_id)
|
||||
constraint ux_s_field_module_id unique (b_module_id, b_id),
|
||||
constraint fk_s_field_module foreign key (b_module_id) references s_module (b_id),
|
||||
constraint ck_s_field_type check (b_type in ('input', 'textarea', 'number', 'money', 'date', 'datetime', 'checkbox', 'select', 'selectId')),
|
||||
constraint ck_s_field_dbtype check (b_dbtype in ('varchar', 'nvarchar', 'char', 'nchar', 'int', 'bigint', 'smallint', 'tinyint', 'decimal', 'numeric', 'float', 'real', 'date', 'datetime', 'datetime2', 'bit', 'text', 'ntext', 'uniqueidentifier')),
|
||||
constraint ck_s_field_writemode check (b_writemode in ('direct', 'createOnly', 'readOnly', 'computed', 'system'))
|
||||
);
|
||||
|
||||
create index ix_s_field_module on s_field (b_module_id, b_canuse, b_id);
|
||||
```
|
||||
|
||||
字段是否允许用户写入属于表单配置,由 `s_field_edit` 表达;如果需要声明数据库计算列、系统维护列等全局写入语义,可在本表增加 `b_writemode varchar(20)`(direct/createOnly/readOnly/computed/actionOnly/system)。
|
||||
`b_type` 沿用旧系统语义,直接指定字段的业务/控件类型,而不是数据库字段类型。数据库真实列类型记录在 `b_dbtype`,用于同步和结构校验;前端默认控件、值转换和展示规则由 `b_type` 决定。服务层必须执行类型兼容校验:`checkbox` 只能映射 `bit/int`;`number` 映射整数或数值类型;`money` 映射 `decimal/numeric`;`date` 映射 `date/datetime/datetime2`;`datetime` 映射 `datetime/datetime2`;`selectId` 映射可承载目标 ID 的整数、字符或 `uniqueidentifier`;`input/textarea` 映射字符类型;`select` 可映射字符或整数类型。`b_default_value` 只表示新增表单时可直接使用的固定默认值,`b_db_default` 是数据库列默认表达式,两者不能混用。
|
||||
|
||||
`b_writemode` 是通用保存服务的强制规则:`direct` 可新增可修改,`createOnly` 仅新增时写入,`readOnly/computed` 永不接收客户端写入,`system` 仅允许受信任的系统逻辑写入。查询视图中的显示列、计算列和 `v_xxx` 字段必须标记为 `readOnly/computed`,即使它们出现在 `s_field_edit` 中也不能被保存接口写入。
|
||||
|
||||
依赖其他业务数据的默认值不在 `s_field` 中配置通用规则,也不增加默认值类型字段。此类值由具体业务模块的服务逻辑处理:例如新增业务单据时,根据单据币种和业务日期查询汇率模块,将汇率写入单据作为当时的业务快照;用户修改币种或日期时,由该业务服务重新计算,是否覆盖人工修改由具体业务规则决定。
|
||||
|
||||
第一阶段沿用旧系统已有类型:
|
||||
|
||||
```text
|
||||
input 单行文本
|
||||
textarea 多行文本
|
||||
number 数字
|
||||
money 金额
|
||||
date 日期
|
||||
datetime 日期时间
|
||||
checkbox 布尔/复选框
|
||||
select 静态枚举下拉选择,直接保存选项值
|
||||
selectId 关联模块选择器,保存目标记录 ID
|
||||
```
|
||||
|
||||
`select` 的静态选项直接放在 `b_options`。`selectId` 必须对应一条以当前字段作为外键端的启用 `s_relation`;`b_options` 只保存选择器展示配置,例如 `relationCode`、`labelField`、`searchFields` 和是否允许清空,不保存目标模块的动态数据。服务层保存配置时必须校验 relation、目标模块以及显示/搜索字段真实存在且已启用。后续增加人员选择、附件、级联选择等自定义类型时,继续扩展 `b_type` 的受控枚举,并在 `b_options` 保存该类型的专用配置。
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"relationCode": "booking.customer",
|
||||
"labelField": "b_name",
|
||||
"searchFields": ["b_code", "b_name"],
|
||||
"allowClear": true
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 s_field_group 字段分组
|
||||
|
||||
分组是模块级独立定义,支持分组树。列表和表单可以分别通过自己的配置表引用同一个分组,也可以选择不同分组。
|
||||
分组是模块级独立定义,支持分组树。列表和表单可以分别通过自己的配置表引用同一个分组,也可以选择不同分组。配置表冗余保存 `b_module_id`,通过复合外键保证字段、分组和配置始终属于同一个模块。
|
||||
|
||||
```sql
|
||||
create table s_field_group (
|
||||
@@ -148,107 +195,168 @@ create table s_field_group (
|
||||
b_xh int not null default 0, -- 分组排序
|
||||
b_canuse int not null default 1, -- 是否启用(停用整组隐藏)
|
||||
constraint pk_s_field_group primary key (b_id),
|
||||
constraint ux_s_field_group_module_id unique (b_module_id, b_id),
|
||||
constraint ck_s_field_group_parent check (b_parent_id is null or b_parent_id <> b_id),
|
||||
constraint fk_s_field_group_module foreign key (b_module_id) references s_module (b_id),
|
||||
constraint fk_s_field_group_parent foreign key (b_parent_id) references s_field_group (b_id)
|
||||
constraint fk_s_field_group_parent foreign key (b_module_id, b_parent_id) references s_field_group (b_module_id, b_id)
|
||||
);
|
||||
|
||||
create index ix_s_field_group_module on s_field_group (b_module_id, b_xh, b_canuse);
|
||||
```
|
||||
|
||||
### 3.4 s_field_list 字段列表配置
|
||||
### 3.4 s_field_view 字段视图配置
|
||||
|
||||
保存字段在列表中的显示、顺序、宽度和分组。每个字段最多一条列表配置;未创建配置记录表示该字段不进入列表。
|
||||
保存字段在列表中的显示、顺序、宽度和分组。每个模块字段最多一条列表配置;未创建配置记录表示该字段不进入列表。
|
||||
|
||||
```sql
|
||||
create table s_field_list (
|
||||
create table s_field_view (
|
||||
b_id bigint identity(1, 1) not null,
|
||||
b_module_id bigint not null, -- 所属数据模块(与字段、分组一致)
|
||||
b_field_id bigint not null, -- 字段
|
||||
b_group_id bigint null, -- 列表分组
|
||||
b_i18n varchar(150) null, -- 列表标题多语言 key(空则继承字段)
|
||||
b_visible int not null default 1, -- 是否显示
|
||||
b_xh int not null default 0, -- 列表排序
|
||||
b_width int null, -- 列宽(px)
|
||||
constraint pk_s_field_list primary key (b_id),
|
||||
constraint ux_s_field_list_field unique (b_field_id),
|
||||
constraint fk_s_field_list_field foreign key (b_field_id) references s_field (b_id),
|
||||
constraint fk_s_field_list_group foreign key (b_group_id) references s_field_group (b_id)
|
||||
constraint pk_s_field_view primary key (b_id),
|
||||
constraint ux_s_field_view_field unique (b_module_id, b_field_id),
|
||||
constraint fk_s_field_view_module foreign key (b_module_id) references s_module (b_id),
|
||||
constraint fk_s_field_view_field foreign key (b_module_id, b_field_id) references s_field (b_module_id, b_id),
|
||||
constraint fk_s_field_view_group foreign key (b_module_id, b_group_id) references s_field_group (b_module_id, b_id)
|
||||
);
|
||||
|
||||
create index ix_s_field_list_order on s_field_list (b_xh, b_id);
|
||||
create index ix_s_field_view_order on s_field_view (b_xh, b_id);
|
||||
```
|
||||
|
||||
### 3.5 s_field_edit 字段表单配置
|
||||
|
||||
保存字段在新增、编辑表单中的布局和交互行为。控件类型属于表单行为,因此放在本表,而不是放在字段物理定义中。
|
||||
保存字段在新增、编辑表单中的布局和交互行为。每个模块字段最多一条表单配置;字段的默认控件类型由 `s_field.b_type` 决定,本表只配置当前表单中的显示、必填、只读和禁用状态。
|
||||
|
||||
```sql
|
||||
create table s_field_edit (
|
||||
b_id bigint identity(1, 1) not null,
|
||||
b_module_id bigint not null, -- 所属数据模块(与字段、分组一致)
|
||||
b_field_id bigint not null, -- 字段
|
||||
b_group_id bigint null, -- 表单分组
|
||||
b_component varchar(30) null, -- input/select/date/datetime/textarea/number/checkbox/radio
|
||||
b_visible int not null default 1, -- 是否显示
|
||||
b_required int not null default 0, -- 是否必填
|
||||
b_readonly int not null default 0, -- 是否只读
|
||||
b_disabled int not null default 0, -- 是否禁用
|
||||
b_xh int not null default 0, -- 表单排序
|
||||
constraint pk_s_field_edit primary key (b_id),
|
||||
constraint ux_s_field_edit_field unique (b_field_id),
|
||||
constraint fk_s_field_edit_field foreign key (b_field_id) references s_field (b_id),
|
||||
constraint fk_s_field_edit_group foreign key (b_group_id) references s_field_group (b_id)
|
||||
constraint ux_s_field_edit_field unique (b_module_id, b_field_id),
|
||||
constraint fk_s_field_edit_module foreign key (b_module_id) references s_module (b_id),
|
||||
constraint fk_s_field_edit_field foreign key (b_module_id, b_field_id) references s_field (b_module_id, b_id),
|
||||
constraint fk_s_field_edit_group foreign key (b_module_id, b_group_id) references s_field_group (b_module_id, b_id)
|
||||
);
|
||||
|
||||
create index ix_s_field_edit_order on s_field_edit (b_xh, b_id);
|
||||
```
|
||||
|
||||
数据库可空性以 `s_field.b_nullable` 为准,表单必填以 `s_field_edit.b_required` 为准。元数据校验器应保证:数据库不可空且无数据库默认值的可编辑字段,表单不能配置为非必填。
|
||||
数据库可空性以 `s_field.b_nullable` 为准,数据库默认值以 `s_field.b_db_default` 为准,表单必填以 `s_field_edit.b_required` 为准;表单组件直接按 `s_field.b_type` 选择,不在 `s_field_edit` 重复配置。元数据校验器应保证:数据库不可空、无数据库默认值且允许客户端写入的字段,新增表单不能配置为非必填;`readOnly/computed/system` 字段不能配置为客户端必填。
|
||||
|
||||
### 3.6 s_field_query 字段查询配置
|
||||
|
||||
查询条件是可选集合,只有需要进入查询区的字段才创建记录。查询组件、操作符和默认查询值独立于字段默认值。
|
||||
查询条件是可选集合,只有需要进入查询区的字段才创建记录。第一阶段所有启用条件按 `b_xh` 排序,并统一使用 `AND` 组合;暂不支持 `OR`、嵌套括号和条件组。同一字段可以配置多个不同操作符,也允许通过 `b_condition_no` 配置多个相同操作符的条件。`b_module_id` 表示查询所属模块,`b_field_module_id` 表示条件字段实际所属模块;查询本模块字段时二者相同,关联查询时必须通过 `b_relation_id` 指向一条从查询模块可达的启用关系。查询组件优先使用配置中的 `b_component`;为空时再根据 `s_field.b_type` 和操作符推导。
|
||||
|
||||
```sql
|
||||
create table s_field_query (
|
||||
b_id bigint identity(1, 1) not null,
|
||||
b_field_id bigint not null, -- 字段
|
||||
b_component varchar(30) not null, -- input/select/date-picker/number-range/radio-group
|
||||
b_module_id bigint not null, -- 查询所属模块
|
||||
b_field_module_id bigint not null, -- 条件字段所属模块
|
||||
b_field_id bigint not null, -- 条件字段
|
||||
b_relation_id bigint null, -- 关联字段使用的模块关系
|
||||
b_component varchar(30) null, -- 查询控件覆盖(input/select/date/datetime/number/checkbox...)
|
||||
b_condition_no int not null default 1, -- 同模块同字段同操作符的条件序号
|
||||
b_operator varchar(20) not null, -- eq/like/gt/ge/lt/le/in/between
|
||||
b_default_value nvarchar(500) null, -- 默认查询值
|
||||
b_canuse int not null default 1, -- 是否启用
|
||||
b_xh int not null default 0, -- 查询条件排序
|
||||
constraint pk_s_field_query primary key (b_id),
|
||||
constraint ux_s_field_query_field unique (b_field_id),
|
||||
constraint fk_s_field_query_field foreign key (b_field_id) references s_field (b_id)
|
||||
constraint ux_s_field_query_condition unique (b_module_id, b_field_module_id, b_field_id, b_relation_id, b_operator, b_condition_no),
|
||||
constraint fk_s_field_query_module foreign key (b_module_id) references s_module (b_id),
|
||||
constraint fk_s_field_query_field foreign key (b_field_module_id, b_field_id) references s_field (b_module_id, b_id)
|
||||
);
|
||||
|
||||
create index ix_s_field_query_order on s_field_query (b_canuse, b_xh, b_id);
|
||||
```
|
||||
|
||||
`b_operator` 表达查询条件的比较语义,默认控件由字段定义和操作符组合推导:`b_options` 非空时优先使用下拉/多选,`datetime` 使用日期或日期时间控件,数值和日期字段在 `between` 操作符下使用范围控件。服务层必须校验操作符与字段类型匹配,并将 `in`/`between` 的默认值按约定 JSON/数组格式解析。若 `b_relation_id` 为空,字段必须属于查询模块;若不为空,关系必须连接查询模块与字段模块,并验证字段确实位于关系指定的一侧。第一阶段运行时对所有启用条件追加 `AND`,未来如需 `OR` 或括号,再增加独立的条件组模型。
|
||||
|
||||
### 3.7 s_relation 模块关系
|
||||
|
||||
描述模块间的引用与主子关系,运行时据此执行外键校验、主子事务与级联删除。
|
||||
描述模块间的引用与主子关系,运行时据此执行外键校验、主子事务与级联删除。`b_module_id` 和 `b_target_module_id` 是关系两端,不预设哪一端保存外键;`b_foreign_side` 明确外键位于哪一端,两个端点字段都使用 `s_field` 主键引用,避免只写物理列名导致跨模块歧义。
|
||||
|
||||
```sql
|
||||
create table s_relation (
|
||||
b_id bigint identity(1, 1) not null, -- 内部主键
|
||||
b_module_id bigint not null, -- 主模块
|
||||
b_target_module_id bigint not null, -- 目标模块
|
||||
b_code varchar(50) not null, -- 跨环境稳定关系编码
|
||||
b_module_id bigint not null, -- 关系端 A 模块
|
||||
b_target_module_id bigint not null, -- 关系端 B 模块
|
||||
b_relation_type varchar(20) not null, -- one-to-one/one-to-many/many-to-one/many-to-many
|
||||
b_foreign_field varchar(50) null, -- 子表侧外键字段
|
||||
b_link_field varchar(50) null, -- 主模块侧关联字段
|
||||
b_module_field_id bigint not null, -- 端 A 关联字段
|
||||
b_target_field_id bigint not null, -- 端 B 关联字段
|
||||
b_foreign_side varchar(10) null, -- 普通关系外键所在端:module/target
|
||||
b_junction_module_id bigint null, -- many-to-many 中间模块
|
||||
b_junction_module_field_id bigint null, -- 中间模块指向端 A 字段
|
||||
b_junction_target_field_id bigint null, -- 中间模块指向端 B 字段
|
||||
b_ownership varchar(20) not null default 'reference', -- reference 引用/owned 主从
|
||||
b_owner_side varchar(10) null, -- owned 时主/拥有者端:module/target
|
||||
b_delete_policy varchar(20) null, -- restrict/set-null/cascade/archive
|
||||
b_orderfield varchar(500) null, -- 子表排序字段表达式
|
||||
b_order_module_id bigint null, -- 从/被拥有模块
|
||||
b_order_field_id bigint null, -- 从模块排序字段
|
||||
b_canuse int not null default 1, -- 是否启用
|
||||
b_xh int not null default 0, -- 排序号
|
||||
constraint pk_s_relation primary key (b_id),
|
||||
constraint ux_s_relation_code unique (b_code),
|
||||
constraint fk_s_relation_module foreign key (b_module_id) references s_module (b_id),
|
||||
constraint fk_s_relation_target_module foreign key (b_target_module_id) references s_module (b_id)
|
||||
constraint fk_s_relation_target_module foreign key (b_target_module_id) references s_module (b_id),
|
||||
constraint fk_s_relation_module_field foreign key (b_module_id, b_module_field_id) references s_field (b_module_id, b_id),
|
||||
constraint fk_s_relation_target_field foreign key (b_target_module_id, b_target_field_id) references s_field (b_module_id, b_id),
|
||||
constraint fk_s_relation_junction_module foreign key (b_junction_module_id) references s_module (b_id),
|
||||
constraint fk_s_relation_junction_module_field foreign key (b_junction_module_id, b_junction_module_field_id) references s_field (b_module_id, b_id),
|
||||
constraint fk_s_relation_junction_target_field foreign key (b_junction_module_id, b_junction_target_field_id) references s_field (b_module_id, b_id),
|
||||
constraint fk_s_relation_order_module foreign key (b_order_module_id) references s_module (b_id),
|
||||
constraint fk_s_relation_order_field foreign key (b_order_module_id, b_order_field_id) references s_field (b_module_id, b_id),
|
||||
constraint ux_s_relation_key unique (b_module_id, b_target_module_id, b_module_field_id, b_target_field_id),
|
||||
constraint ck_s_relation_self_fields check (b_module_id <> b_target_module_id or b_module_field_id <> b_target_field_id),
|
||||
constraint ck_s_relation_type check (b_relation_type in ('one-to-one', 'one-to-many', 'many-to-one', 'many-to-many')),
|
||||
constraint ck_s_relation_foreign_side check (b_foreign_side is null or b_foreign_side in ('module', 'target')),
|
||||
constraint ck_s_relation_ownership check (b_ownership in ('reference', 'owned')),
|
||||
constraint ck_s_relation_owner_side check (b_owner_side is null or b_owner_side in ('module', 'target')),
|
||||
constraint ck_s_relation_shape check (
|
||||
(b_relation_type = 'many-to-many' and b_foreign_side is null and b_junction_module_id is not null and b_junction_module_field_id is not null and b_junction_target_field_id is not null)
|
||||
or (b_relation_type <> 'many-to-many' and b_foreign_side is not null and b_junction_module_id is null and b_junction_module_field_id is null and b_junction_target_field_id is null)
|
||||
),
|
||||
constraint ck_s_relation_owned check (
|
||||
(b_ownership = 'reference' and b_owner_side is null)
|
||||
or (b_ownership = 'owned' and b_owner_side is not null and b_relation_type <> 'many-to-many')
|
||||
),
|
||||
constraint ck_s_relation_order check (
|
||||
(b_order_module_id is null and b_order_field_id is null)
|
||||
or (b_order_module_id is not null and b_order_field_id is not null)
|
||||
),
|
||||
constraint ck_s_relation_delete_policy check (b_delete_policy is null or b_delete_policy in ('restrict', 'set-null', 'cascade', 'archive'))
|
||||
);
|
||||
|
||||
create index ix_s_relation_target on s_relation (b_target_module_id);
|
||||
|
||||
alter table s_field_query add constraint fk_s_field_query_relation
|
||||
foreign key (b_relation_id) references s_relation (b_id);
|
||||
```
|
||||
|
||||
关系配置必须满足以下规则:
|
||||
|
||||
- `b_code` 是跨环境稳定标识,`selectId.b_options.relationCode` 和配置导入导出都使用该编码;运行时内部关联仍可使用 `b_id`。
|
||||
- 允许同一模块自关联,用于组织树、父子记录等场景;自关联时两端字段不能相同,并仍需通过 `b_foreign_side` 明确父键和外键端。
|
||||
- `one-to-one/one-to-many/many-to-one` 不填写中间模块字段且必须明确 `b_foreign_side`;`many-to-many` 必须同时填写中间模块及两个中间外键字段,且不填写 `b_foreign_side`。
|
||||
- 普通关系由 `b_foreign_side` 指定哪一端保存外键;外键字段与另一端被引用字段类型必须兼容;`one-to-one` 还要求外键字段具有唯一约束。
|
||||
- `one-to-many`/`many-to-one` 必须与 `b_foreign_side` 的方向一致:外键在端 B 时端 A 为“一”,外键在端 A 时端 B 为“一”。`set-null` 仅允许实际外键字段可空时使用。
|
||||
- `reference` 不填写 `b_owner_side`,默认删除策略为 `restrict/set-null`;`owned` 必须明确 `b_owner_side`,另一端才是从/被拥有端,只有 owned 关系允许 `cascade/archive`、主从事务保存和从表排序。
|
||||
- `b_order_module_id/b_order_field_id` 必须同时为空或同时有值;有值时模块必须是 owned 关系的从端,字段必须属于该模块。
|
||||
- 模块自身的 `b_delete_policy` 决定该模块记录能否进入删除流程;关系的 `b_delete_policy` 决定删除发生后如何处理关联记录,两者都必须通过才可执行。
|
||||
- 同一对模块可以存在多条关系,但“两端模块 + 两侧字段”必须唯一;配置、导入导出使用关系 `b_code`,运行时和关联查询使用关系 `b_id`。
|
||||
|
||||
### 3.8 s_menu 菜单/页面
|
||||
|
||||
菜单是独立导航树,不保存字段、列表或表单配置。`data` 菜单通过 `b_module_id` 绑定一个 `data/query` 模块,并直接使用该模块的字段、列表、表单和查询配置。
|
||||
@@ -265,11 +373,11 @@ create table s_menu (
|
||||
b_module_id bigint null, -- data 页面绑定的数据/查询模块
|
||||
b_power_code varchar(50) null, -- 页面访问权限编码(对应 s_power.b_code)
|
||||
b_icon varchar(50) null, -- 图标
|
||||
b_canmenu int not null default 1, -- 是否显示在菜单
|
||||
b_xh int not null default 0, -- 排序号
|
||||
b_canuse int not null default 1, -- 是否启用
|
||||
b_canuse int not null default 1, -- 是否启用(停用后不显示且不可访问)
|
||||
constraint pk_s_menu primary key (b_id),
|
||||
constraint ux_s_menu_code unique (b_code),
|
||||
constraint ck_s_menu_parent check (b_parent_id is null or b_parent_id <> b_id),
|
||||
constraint fk_s_menu_parent foreign key (b_parent_id) references s_menu (b_id),
|
||||
constraint fk_s_menu_module foreign key (b_module_id) references s_module (b_id)
|
||||
);
|
||||
@@ -292,7 +400,7 @@ create index ix_s_menu_module on s_menu (b_module_id);
|
||||
create table s_autocode (
|
||||
b_id bigint identity(1, 1) not null, -- 内部主键
|
||||
b_module_id bigint not null, -- 所属数据模块
|
||||
b_field varchar(50) not null, -- 目标字段(物理列名)
|
||||
b_field_id bigint not null, -- 目标字段
|
||||
b_prefix nvarchar(100) not null default '', -- 前缀
|
||||
b_dateformat varchar(30) not null default 'yyyyMM', -- 日期格式
|
||||
b_separator nvarchar(10) not null default '-', -- 分隔符
|
||||
@@ -303,11 +411,16 @@ create table s_autocode (
|
||||
b_currentvalue bigint not null default 0, -- 当前流水值
|
||||
b_canuse int not null default 1, -- 是否启用
|
||||
constraint pk_s_autocode primary key (b_id),
|
||||
constraint ux_s_autocode_module_field unique (b_module_id, b_field),
|
||||
constraint fk_s_autocode_module foreign key (b_module_id) references s_module (b_id)
|
||||
constraint ux_s_autocode_module_field unique (b_module_id, b_field_id),
|
||||
constraint fk_s_autocode_module foreign key (b_module_id) references s_module (b_id),
|
||||
constraint fk_s_autocode_field foreign key (b_module_id, b_field_id) references s_field (b_module_id, b_id),
|
||||
constraint ck_s_autocode_resettype check (b_resettype in ('none', 'day', 'month', 'year')),
|
||||
constraint ck_s_autocode_values check (b_seqwidth > 0 and b_startvalue >= 0 and b_currentvalue >= 0)
|
||||
);
|
||||
```
|
||||
|
||||
自动编码目标字段必须是当前模块的可写字符字段;服务层还需校验目标字段未配置为 `computed/system/readOnly`,并保证业务字段上的唯一性约束。规则周期切换、取号和业务保存必须在同一事务内完成。
|
||||
|
||||
### 3.10 s_role 角色
|
||||
|
||||
```sql
|
||||
@@ -317,13 +430,16 @@ create table s_role (
|
||||
b_name nvarchar(100) not null, -- 角色名称
|
||||
b_i18n varchar(150) null, -- 显示名多语言 key
|
||||
b_remark nvarchar(500) null, -- 描述
|
||||
b_is_system int not null default 0, -- 系统内置角色(禁止普通管理员删除)
|
||||
b_bypass_power int not null default 0, -- 是否绕过业务权限校验(仅超级管理员)
|
||||
b_canuse int not null default 1, -- 是否启用
|
||||
b_inputuser_id bigint null,
|
||||
b_inputdatetime datetime2 null default sysdatetime(),
|
||||
b_inputdatetime datetime2 null default sysutcdatetime(),
|
||||
b_updateuser_id bigint null,
|
||||
b_updatedatetime datetime2 null,
|
||||
constraint pk_s_role primary key (b_id),
|
||||
constraint ux_s_role_code unique (b_code)
|
||||
constraint ux_s_role_code unique (b_code),
|
||||
constraint ck_s_role_system_bypass check (b_bypass_power = 0 or b_is_system = 1)
|
||||
);
|
||||
```
|
||||
|
||||
@@ -335,7 +451,7 @@ create table s_user_role (
|
||||
b_user_id bigint not null, -- 用户(逻辑外键,当前身份系统)
|
||||
b_role_id bigint not null, -- 角色
|
||||
b_canuse int not null default 1, -- 是否启用该关系
|
||||
b_inputdatetime datetime2 null default sysdatetime(),
|
||||
b_inputdatetime datetime2 null default sysutcdatetime(),
|
||||
constraint pk_s_user_role primary key (b_id),
|
||||
constraint ux_s_user_role unique (b_user_id, b_role_id),
|
||||
constraint fk_s_user_role_role foreign key (b_role_id) references s_role (b_id)
|
||||
@@ -353,16 +469,30 @@ create table s_power (
|
||||
b_name nvarchar(100) not null, -- 权限名称
|
||||
b_i18n varchar(150) null, -- 显示名多语言 key
|
||||
b_power_type varchar(20) not null, -- menu/module/action/field
|
||||
b_resource varchar(128) null, -- 资源标识(菜单 code/模块 code/动作 code/字段)
|
||||
b_resource varchar(128) null, -- 菜单 code/固定页面或扩展资源 code
|
||||
b_module_id bigint null, -- module/action/field 所属模块
|
||||
b_field_id bigint null, -- field 权限对应字段
|
||||
b_operation varchar(30) not null, -- access/read/create/update/delete/export/execute/view/edit
|
||||
b_canuse int not null default 1, -- 是否启用
|
||||
b_xh int not null default 0, -- 排序号
|
||||
constraint pk_s_power primary key (b_id),
|
||||
constraint ux_s_power_code unique (b_code)
|
||||
constraint ux_s_power_code unique (b_code),
|
||||
constraint fk_s_power_module foreign key (b_module_id) references s_module (b_id),
|
||||
constraint fk_s_power_field foreign key (b_module_id, b_field_id) references s_field (b_module_id, b_id),
|
||||
constraint ck_s_power_type check (b_power_type in ('menu', 'module', 'action', 'field')),
|
||||
constraint ck_s_power_shape check (
|
||||
(b_power_type = 'menu' and b_resource is not null and b_module_id is null and b_field_id is null and b_operation = 'access')
|
||||
or (b_power_type = 'module' and b_resource is null and b_module_id is not null and b_field_id is null and b_operation in ('read', 'create', 'update', 'delete', 'export'))
|
||||
or (b_power_type = 'action' and b_resource is not null and b_module_id is not null and b_field_id is null and b_operation = 'execute')
|
||||
or (b_power_type = 'field' and b_resource is null and b_module_id is not null and b_field_id is not null and b_operation in ('view', 'edit'))
|
||||
)
|
||||
);
|
||||
|
||||
create index ix_s_power_resource on s_power (b_power_type, b_resource, b_canuse);
|
||||
```
|
||||
|
||||
权限点的资源约束由服务层补充:`menu` 使用菜单编码且操作只能为 `access`;`module` 必须填写 `b_module_id` 且操作为 `read/create/update/delete/export`;`action` 必须填写模块和动作编码且操作为 `execute`;`field` 必须同时填写模块、字段且操作为 `view/edit`。权限编码 `b_code` 在全库唯一,资源停用后对应权限自动失效。
|
||||
|
||||
### 3.13 s_role_power 角色-权限
|
||||
|
||||
```sql
|
||||
@@ -370,20 +500,65 @@ create table s_role_power (
|
||||
b_id bigint identity(1, 1) not null, -- 内部主键
|
||||
b_role_id bigint not null, -- 角色
|
||||
b_power_id bigint not null, -- 权限点
|
||||
b_effect varchar(10) not null default 'allow', -- allow/deny
|
||||
b_canuse int not null default 1, -- 是否启用
|
||||
b_inputdatetime datetime2 null default sysdatetime(),
|
||||
b_inputdatetime datetime2 null default sysutcdatetime(),
|
||||
constraint pk_s_role_power primary key (b_id),
|
||||
constraint ux_s_role_power unique (b_role_id, b_power_id),
|
||||
constraint fk_s_role_power_role foreign key (b_role_id) references s_role (b_id),
|
||||
constraint fk_s_role_power_power foreign key (b_power_id) references s_power (b_id)
|
||||
constraint fk_s_role_power_power foreign key (b_power_id) references s_power (b_id),
|
||||
constraint ck_s_role_power_effect check (b_effect in ('allow', 'deny'))
|
||||
);
|
||||
|
||||
create index ix_s_role_power_power on s_role_power (b_power_id, b_canuse);
|
||||
```
|
||||
|
||||
### 3.14 s_role_scope 角色-模块数据范围
|
||||
|
||||
数据范围控制用户通过某个模块操作时可访问哪些业务记录。范围按角色、模块和操作配置,不承担租户隔离;本系统采用一个租户一个数据库,数据范围只解决租户内部的本人、部门、负责人等业务授权。
|
||||
|
||||
```sql
|
||||
create table s_role_scope (
|
||||
b_id bigint identity(1, 1) not null,
|
||||
b_role_id bigint not null, -- 角色
|
||||
b_module_id bigint not null, -- 数据模块
|
||||
b_operation varchar(20) not null, -- read/update/delete/export
|
||||
b_scope_type varchar(30) not null, -- all/creator/owner/department/department-tree/custom
|
||||
b_scope_no int not null default 1, -- 同角色同模块同操作的范围序号
|
||||
b_scope_field_id bigint null, -- owner/department/custom 用于匹配的业务字段
|
||||
b_scope_value nvarchar(max) null, -- 固定值或自定义条件(JSON)
|
||||
b_canuse int not null default 1,
|
||||
b_inputdatetime datetime2 null default sysutcdatetime(),
|
||||
constraint pk_s_role_scope primary key (b_id),
|
||||
constraint ux_s_role_scope unique (b_role_id, b_module_id, b_operation, b_scope_type, b_scope_field_id, b_scope_no),
|
||||
constraint fk_s_role_scope_role foreign key (b_role_id) references s_role (b_id),
|
||||
constraint fk_s_role_scope_module foreign key (b_module_id) references s_module (b_id),
|
||||
constraint fk_s_role_scope_field foreign key (b_module_id, b_scope_field_id) references s_field (b_module_id, b_id),
|
||||
constraint ck_s_role_scope_operation check (b_operation in ('read', 'update', 'delete', 'export')),
|
||||
constraint ck_s_role_scope_type check (b_scope_type in ('all', 'creator', 'owner', 'department', 'department-tree', 'custom')),
|
||||
constraint ck_s_role_scope_no check (b_scope_no > 0),
|
||||
constraint ck_s_role_scope_shape check (
|
||||
(b_scope_type in ('all', 'creator') and b_scope_field_id is null and b_scope_value is null)
|
||||
or (b_scope_type in ('owner', 'department', 'department-tree') and b_scope_field_id is not null)
|
||||
or (b_scope_type = 'custom' and b_scope_value is not null)
|
||||
)
|
||||
);
|
||||
|
||||
create index ix_s_role_scope_role on s_role_scope (b_role_id, b_module_id, b_operation, b_canuse);
|
||||
```
|
||||
|
||||
权限运行规则:
|
||||
|
||||
- 默认拒绝:用户必须通过至少一个启用角色取得权限;`b_bypass_power = 1` 的受保护系统角色除外。
|
||||
- 多角色权限先合并,任一有效 `deny` 优先于所有 `allow`;没有 `allow` 视为无权。超级管理员角色只能由受保护的系统管理接口分配,不能通过普通角色维护接口创建或提权。
|
||||
- 访问数据页需要同时具备菜单 `access`、模块 `read` 和对应操作权限;前端只负责隐藏,后端接口必须按同一权限编码再次校验。
|
||||
- 字段权限区分 `view` 与 `edit`。未配置字段权限时继承模块权限;存在字段级 `deny` 时,查询结果必须移除或脱敏该字段,保存接口必须忽略并拒绝提交无编辑权限的字段。
|
||||
- 数据范围在操作权限通过后追加。`creator` 匹配记录创建人,`owner/department/department-tree` 通过 `b_scope_field_id` 与当前登录上下文匹配;`custom` 使用结构化 JSON 条件并只允许引用当前模块字段。
|
||||
- 用户拥有多个角色时,同一操作的数据范围取并集;同一角色内多个范围记录也取并集,任一角色/范围为 `all` 即不再追加范围条件。没有有效范围记录时,即使拥有操作权限也默认无数据,避免误放开全表。
|
||||
|
||||
菜单显示继承页面访问权限:用户拥有 `s_menu.b_power_code` 对应权限时菜单才显示;前端只控制显示,后端运行时必须再次校验同一个权限编码。
|
||||
|
||||
### 3.14 s_i18n 多语言
|
||||
### 3.15 s_i18n 多语言
|
||||
|
||||
```sql
|
||||
create table s_i18n (
|
||||
@@ -400,7 +575,7 @@ create table s_i18n (
|
||||
create index ix_s_i18n_locale on s_i18n (b_locale, b_canuse);
|
||||
```
|
||||
|
||||
### 3.15 s_log_technical 技术日志
|
||||
### 3.16 s_log_technical 技术日志
|
||||
|
||||
用于请求链路、SQL、异常、性能与外部接口排查。高频日志推荐写入 Loki/OpenSearch/Elasticsearch 等日志平台,本表用于集中日志库或短期数据库兜底,不建议长期写入租户业务库。
|
||||
|
||||
@@ -424,7 +599,7 @@ create table s_log_technical (
|
||||
b_error_message nvarchar(2000) null, -- 错误信息
|
||||
b_exception_text nvarchar(max) null, -- 异常堆栈(文本)
|
||||
b_context_text nvarchar(max) null, -- 上下文信息(文本)
|
||||
b_occurdatetime datetime2 null default sysdatetime(), -- 发生时间(UTC)
|
||||
b_occurdatetime datetime2 null default sysutcdatetime(), -- 发生时间(UTC)
|
||||
constraint pk_s_log_technical primary key (b_id)
|
||||
);
|
||||
|
||||
@@ -433,7 +608,7 @@ create index ix_s_log_technical_org on s_log_technical (b_org_id, b_occurdatetim
|
||||
create index ix_s_log_technical_level on s_log_technical (b_loglevel, b_occurdatetime desc);
|
||||
```
|
||||
|
||||
### 3.16 s_log_login 登录日志
|
||||
### 3.17 s_log_login 登录日志
|
||||
|
||||
记录登录成功、失败、退出、刷新令牌与账号锁定等安全事件,可通过 `b_request_id` / `b_trace_id` 与技术日志关联。
|
||||
|
||||
@@ -457,7 +632,7 @@ create table s_log_login (
|
||||
b_session_id varchar(50) null, -- 会话 ID
|
||||
b_request_id varchar(64) null, -- 请求 ID
|
||||
b_trace_id varchar(64) null, -- 追踪 ID
|
||||
b_occurdatetime datetime2 null default sysdatetime(), -- 登录时间(UTC)
|
||||
b_occurdatetime datetime2 null default sysutcdatetime(), -- 登录时间(UTC)
|
||||
constraint pk_s_log_login primary key (b_id)
|
||||
);
|
||||
|
||||
@@ -466,7 +641,7 @@ create index ix_s_log_login_account on s_log_login (b_account, b_occurdatetime d
|
||||
create index ix_s_log_login_result on s_log_login (b_login_result, b_occurdatetime desc);
|
||||
```
|
||||
|
||||
### 3.17 业务审计事件与字段变更
|
||||
### 3.18 业务审计事件与字段变更
|
||||
|
||||
审计事件与字段变更分表存储:事件一条、字段变更多条,通过 `b_event_id` 关联。字段级明细是**面向客户的硬需求**(客户可在门户查看某个单号发生过哪些字段变更),**字段一旦发生修改即写入明细**,不按字段配置开关;`b_visibility` 控制内部/客户可见。
|
||||
|
||||
@@ -485,7 +660,7 @@ create table s_log_audit (
|
||||
b_request_id varchar(64) null, -- 请求 ID
|
||||
b_trace_id varchar(64) null, -- 追踪 ID(关联技术日志)
|
||||
b_payload_text nvarchar(max) null, -- 事件载荷(文本,如前后快照)
|
||||
b_occurdatetime datetime2 null default sysdatetime(), -- 发生时间(UTC)
|
||||
b_occurdatetime datetime2 null default sysutcdatetime(), -- 发生时间(UTC)
|
||||
constraint pk_s_log_audit primary key (b_id)
|
||||
);
|
||||
|
||||
@@ -525,7 +700,7 @@ create index ix_s_log_audit_field_customer on s_log_audit_field (b_module_code,
|
||||
当前 `s_module` 不是可独立安装、发布的软件包,因此不额外维护模块依赖 DAG。数据模块之间的引用和主从关系由 `s_relation` 表达,数据库对象依赖由实际表、视图和 Schema Compiler 校验,部署依赖由构建或插件系统管理。
|
||||
|
||||
### 4.3 为什么字段定义与页面行为分表
|
||||
旧系统的字段、列表、编辑、查询虽然都挂在 `b_module_id` 下,但运行时和管理端都把它们当作四类独立配置。列表与表单的显示、排序、分组并不相同,查询条件也只覆盖部分字段。强行并入 `s_field` 会形成宽表,并混淆数据库列定义与页面行为。因此本方案保留四张职责明确的表,但统一改为通过 `b_field_id` 关联字段,字段归属再由 `s_field.b_module_id` 确定。
|
||||
旧系统的字段、列表、编辑、查询虽然都挂在 `b_module_id` 下,但运行时和管理端都把它们当作四类独立配置。列表与表单的显示、排序、分组并不相同,查询条件也只覆盖部分字段。强行并入 `s_field` 会形成宽表,并混淆数据库列定义与页面行为。因此本方案保留字段定义、列表、表单和查询条件等职责明确的表;列表、表单通过模块 + 字段/分组复合外键保证同模块,查询条件允许通过明确的 `s_relation` 引用关联模块字段。第一阶段查询条件统一使用 `AND`,暂不增加条件组。
|
||||
|
||||
### 4.4 为什么菜单独立且引用模块
|
||||
导航是展示层概念,数据模块是业务数据概念。`s_menu` 引用 `b_module_id`,不重复保存数据源;同一数据模块可以被多个菜单入口引用,但默认共享模块上的同一套字段、列表、表单和查询配置。
|
||||
@@ -534,7 +709,7 @@ create index ix_s_log_audit_field_customer on s_log_audit_field (b_module_code,
|
||||
旧系统实际以一个 `data` 模块承载一套字段、列表、编辑和查询配置,没有菜单级字段覆盖机制。当前若再增加 `s_page_profile`、`s_page_field`,会与字段配置表重复,并引入菜单、配置档、模块三者的一致性维护成本。因此第一阶段规定一个数据模块只有一套页面配置。
|
||||
|
||||
### 4.6 为什么权限引入角色层
|
||||
旧系统 `b_user_power` 是用户直挂权限,用户一多授权矩阵爆炸。本方案 `s_role` + `s_user_role` + `s_role_power` 三层,权限点 `s_power` 支持到 menu/module/action/field 四级粒度,为后续数据范围、字段脱敏策略留出扩展位。
|
||||
旧系统 `b_user_power` 是用户直挂权限,用户一多授权矩阵爆炸。本方案使用 `s_role` + `s_user_role` + `s_role_power` 管理角色权限,并通过 `s_role_scope` 补充模块级数据范围。权限点 `s_power` 支持 menu/module/action/field 四级粒度,角色权限支持 allow/deny,字段权限支持 view/edit;数据范围按角色取并集,显式 deny 优先,后端在每次请求中重新计算有效权限。
|
||||
|
||||
|
||||
### 4.7 自动编码的并发处理
|
||||
@@ -554,9 +729,12 @@ create index ix_s_log_audit_field_customer on s_log_audit_field (b_module_code,
|
||||
| --- | --- | --- |
|
||||
| `s_user_role` | `b_user_id` | 当前身份系统的用户 `b_id` |
|
||||
| `s_menu` | `b_module_id` | 绑定的数据/查询模块 `s_module.b_id` |
|
||||
| `s_field_list` | `b_group_id` | 分组必须与 `b_field_id` 所属字段位于同一数据模块 |
|
||||
| `s_field_edit` | `b_group_id` | 分组必须与 `b_field_id` 所属字段位于同一数据模块 |
|
||||
| `s_field_group` | `b_parent_id` | 父分组必须与当前分组属于同一数据模块 |
|
||||
| `s_field_query` | `b_relation_id` | 关联关系必须从查询模块可达条件字段所属模块 |
|
||||
| `s_menu` | `b_power_code` | `s_power.b_code` |
|
||||
| `s_power` | `b_resource` / `b_operation` | 权限类型、资源和操作必须匹配;field 权限必须引用对应模块字段 |
|
||||
| `s_role_scope` | `b_scope_field_id` / `b_scope_value` | 数据范围字段和范围值必须与模块及范围类型匹配 |
|
||||
| `s_relation` | 关系类型与字段组合 | 多对多必须有中间模块;两侧字段必须属于声明的模块 |
|
||||
| `s_module` | `b_viewtable` / `b_savetable` | Schema Compiler 生成的物理表/视图 |
|
||||
| `s_log_technical` | `b_user_id` | 当前身份系统的用户 `b_id` |
|
||||
| `s_log_login` | `b_user_id` | 当前身份系统的用户 `b_id` |
|
||||
@@ -567,7 +745,8 @@ create index ix_s_log_audit_field_customer on s_log_audit_field (b_module_code,
|
||||
```text
|
||||
引用是否已启用
|
||||
-> 目标资源是否存在且类型匹配
|
||||
-> 字段、分组和模块关系是否属于正确的数据模块
|
||||
-> 字段、分组和关系是否属于正确的数据模块
|
||||
-> 权限类型、操作、字段和数据范围规则是否匹配
|
||||
-> 删除或停用前是否仍被其他资源引用
|
||||
```
|
||||
|
||||
@@ -577,12 +756,12 @@ create index ix_s_log_audit_field_customer on s_log_audit_field (b_module_code,
|
||||
| --- | --- | --- |
|
||||
| `s_module`(含表/路由/菜单/权限) | `s_module` + `s_menu` | 保留模块数据主体,拆出导航与角色权限 |
|
||||
| `s_module_field` | `s_field` | 字段挂模块 |
|
||||
| `s_module_field_list` | `s_field_list` | 改为通过字段关联模块 |
|
||||
| `s_module_field_list` | `s_field_view` | 改为通过字段关联模块 |
|
||||
| `s_module_field_edit` | `s_field_edit` | 改为通过字段关联模块 |
|
||||
| `s_module_field_query` | `s_field_query` | 改为通过字段关联模块 |
|
||||
| `s_module_field_query` | `s_field_query` | 保留平铺查询配置,增加关联字段与同字段多条件 |
|
||||
| `s_module_field_group` | `s_field_group` | 分组挂模块 |
|
||||
| `s_module_auto_code` | `s_autocode` | 挂模块+字段,原子取号 |
|
||||
| `s_module_power` + `b_user_power` | `s_power` + `s_role` + `s_user_role` + `s_role_power` | 引入角色层 |
|
||||
| `s_module_power` + `b_user_power` | `s_power` + `s_role` + `s_user_role` + `s_role_power` + `s_role_scope` | 引入角色、允许/拒绝和数据范围 |
|
||||
| `b_i18n` | `s_i18n` | 增加资源类型维度 |
|
||||
| `s_login_log` | `s_log_login` | 字段扩充(设备/UA/请求链路) |
|
||||
| (无) | `s_relation` | 新增模块关系 |
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
IF DB_NAME() <> N'FMS-NEW'
|
||||
THROW 51000, N'This migration can only run against FMS-NEW.', 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
IF OBJECT_ID(N'dbo.b_user', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.b_user (
|
||||
b_id bigint NOT NULL,
|
||||
b_account nvarchar(50) NOT NULL,
|
||||
b_name nvarchar(50) NULL,
|
||||
b_department nvarchar(50) NULL,
|
||||
b_subcode nvarchar(50) NULL,
|
||||
b_machineid nvarchar(50) NULL,
|
||||
b_bz nvarchar(200) NULL,
|
||||
b_password nvarchar(255) NOT NULL,
|
||||
b_canuse tinyint NOT NULL CONSTRAINT DF_b_user_canuse DEFAULT (1),
|
||||
b_othermachinelogin tinyint NOT NULL CONSTRAINT DF_b_user_othermachinelogin DEFAULT (0),
|
||||
b_salesman tinyint NOT NULL CONSTRAINT DF_b_user_salesman DEFAULT (0),
|
||||
b_docman tinyint NOT NULL CONSTRAINT DF_b_user_docman DEFAULT (0),
|
||||
b_dealman tinyint NOT NULL CONSTRAINT DF_b_user_dealman DEFAULT (0),
|
||||
b_receivelevel tinyint NULL,
|
||||
b_paylevel tinyint NULL,
|
||||
b_visible_customer tinyint NOT NULL CONSTRAINT DF_b_user_visible_customer DEFAULT (0),
|
||||
b_visible_supply tinyint NOT NULL CONSTRAINT DF_b_user_visible_supply DEFAULT (0),
|
||||
b_levelvisible tinyint NOT NULL CONSTRAINT DF_b_user_levelvisible DEFAULT (0),
|
||||
b_fromcustomer tinyint NOT NULL CONSTRAINT DF_b_user_fromcustomer DEFAULT (0),
|
||||
b_logintime tinyint NOT NULL CONSTRAINT DF_b_user_logintime DEFAULT (0),
|
||||
b_checkxy tinyint NOT NULL CONSTRAINT DF_b_user_checkxy DEFAULT (0),
|
||||
b_loginfee tinyint NOT NULL CONSTRAINT DF_b_user_loginfee DEFAULT (0),
|
||||
b_phone nvarchar(50) NULL,
|
||||
CONSTRAINT PK_b_user PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_b_user_account UNIQUE (b_account)
|
||||
);
|
||||
END;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.b_user)
|
||||
BEGIN
|
||||
INSERT INTO dbo.b_user (
|
||||
b_id, b_account, b_name, b_department, b_subcode, b_machineid, b_bz,
|
||||
b_password, b_canuse, b_othermachinelogin, b_salesman, b_docman, b_dealman,
|
||||
b_receivelevel, b_paylevel, b_visible_customer, b_visible_supply, b_levelvisible,
|
||||
b_fromcustomer, b_logintime, b_checkxy, b_loginfee, b_phone
|
||||
)
|
||||
SELECT
|
||||
b_id, b_account, b_name, b_department, b_subcode, b_machineid, b_bz,
|
||||
b_password, b_canuse, b_othermachinelogin, b_salesman, b_docman, b_dealman,
|
||||
b_receivelevel, b_paylevel, b_visible_customer, b_visible_supply, b_levelvisible,
|
||||
b_fromcustomer, b_logintime, b_checkxy, b_loginfee, b_phone
|
||||
FROM [FMS].dbo.b_user;
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_module', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_module (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_code varchar(50) NOT NULL,
|
||||
b_parent_id bigint NULL,
|
||||
b_name nvarchar(100) NOT NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
b_module_type varchar(20) NOT NULL,
|
||||
b_viewtable varchar(128) NULL,
|
||||
b_savetable varchar(128) NULL,
|
||||
b_keyfield varchar(50) NULL,
|
||||
b_orderfield varchar(500) NULL,
|
||||
b_delete_policy varchar(20) NULL,
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_module_canuse DEFAULT (1),
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_module_xh DEFAULT (0),
|
||||
b_bz nvarchar(1000) NULL,
|
||||
b_inputuser_id bigint NULL,
|
||||
b_inputdatetime datetime2 NULL CONSTRAINT DF_s_module_inputdatetime DEFAULT (SYSUTCDATETIME()),
|
||||
b_updateuser_id bigint NULL,
|
||||
b_updatedatetime datetime2 NULL,
|
||||
CONSTRAINT PK_s_module PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_module_code UNIQUE (b_code),
|
||||
CONSTRAINT CK_s_module_type CHECK (b_module_type IN ('category','data','query')),
|
||||
CONSTRAINT CK_s_module_delete_policy CHECK (b_delete_policy IS NULL OR b_delete_policy IN ('restrict','lifecycle','hard')),
|
||||
CONSTRAINT CK_s_module_config CHECK (
|
||||
(b_module_type = 'category' AND b_viewtable IS NULL AND b_savetable IS NULL AND b_keyfield IS NULL AND b_orderfield IS NULL AND b_delete_policy IS NULL)
|
||||
OR (b_module_type = 'data' AND b_viewtable IS NOT NULL AND b_savetable IS NOT NULL AND b_keyfield IS NOT NULL AND b_delete_policy IS NOT NULL)
|
||||
OR (b_module_type = 'query' AND b_viewtable IS NOT NULL AND b_savetable IS NULL AND b_delete_policy IS NULL)
|
||||
),
|
||||
CONSTRAINT CK_s_module_parent CHECK (b_parent_id IS NULL OR b_parent_id <> b_id)
|
||||
);
|
||||
CREATE INDEX IX_s_module_parent ON dbo.s_module (b_parent_id, b_xh, b_canuse, b_id);
|
||||
CREATE INDEX IX_s_module_savetable ON dbo.s_module (b_savetable);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_field', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_field (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_module_id bigint NOT NULL,
|
||||
b_field varchar(50) NOT NULL,
|
||||
b_name nvarchar(100) NOT NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
b_type varchar(30) NOT NULL,
|
||||
b_dbtype varchar(30) NOT NULL,
|
||||
b_length int NULL,
|
||||
b_precision int NULL,
|
||||
b_scale int NULL,
|
||||
b_nullable int NOT NULL CONSTRAINT DF_s_field_nullable DEFAULT (1),
|
||||
b_default_value nvarchar(500) NULL,
|
||||
b_db_default nvarchar(500) NULL,
|
||||
b_writemode varchar(20) NOT NULL CONSTRAINT DF_s_field_writemode DEFAULT ('direct'),
|
||||
b_options nvarchar(max) NULL,
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_field_canuse DEFAULT (1),
|
||||
b_inputuser_id bigint NULL,
|
||||
b_inputdatetime datetime2 NULL CONSTRAINT DF_s_field_inputdatetime DEFAULT (SYSUTCDATETIME()),
|
||||
b_updateuser_id bigint NULL,
|
||||
b_updatedatetime datetime2 NULL,
|
||||
CONSTRAINT PK_s_field PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_field_module_field UNIQUE (b_module_id, b_field),
|
||||
CONSTRAINT UQ_s_field_module_id UNIQUE (b_module_id, b_id),
|
||||
CONSTRAINT CK_s_field_type CHECK (b_type IN ('input','textarea','number','money','date','datetime','checkbox','select','selectId')),
|
||||
CONSTRAINT CK_s_field_dbtype CHECK (b_dbtype IN ('varchar','nvarchar','char','nchar','int','bigint','smallint','tinyint','decimal','numeric','float','real','date','datetime','datetime2','bit','text','ntext','uniqueidentifier')),
|
||||
CONSTRAINT CK_s_field_writemode CHECK (b_writemode IN ('direct','createOnly','readOnly','computed','system'))
|
||||
);
|
||||
CREATE INDEX IX_s_field_module ON dbo.s_field (b_module_id, b_canuse, b_id);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_field_group', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_field_group (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_module_id bigint NOT NULL,
|
||||
b_parent_id bigint NULL,
|
||||
b_title nvarchar(100) NOT NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_field_group_xh DEFAULT (0),
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_field_group_canuse DEFAULT (1),
|
||||
CONSTRAINT PK_s_field_group PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_field_group_module_id UNIQUE (b_module_id, b_id),
|
||||
CONSTRAINT CK_s_field_group_parent CHECK (b_parent_id IS NULL OR b_parent_id <> b_id)
|
||||
);
|
||||
CREATE INDEX IX_s_field_group_module ON dbo.s_field_group (b_module_id, b_xh, b_canuse);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_field_view', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_field_view (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_module_id bigint NOT NULL,
|
||||
b_field_id bigint NOT NULL,
|
||||
b_group_id bigint NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
b_visible int NOT NULL CONSTRAINT DF_s_field_view_visible DEFAULT (1),
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_field_view_xh DEFAULT (0),
|
||||
b_width int NULL,
|
||||
CONSTRAINT PK_s_field_view PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_field_view_field UNIQUE (b_module_id, b_field_id)
|
||||
);
|
||||
CREATE INDEX IX_s_field_view_order ON dbo.s_field_view (b_xh, b_id);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_field_edit', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_field_edit (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_module_id bigint NOT NULL,
|
||||
b_field_id bigint NOT NULL,
|
||||
b_group_id bigint NULL,
|
||||
b_visible int NOT NULL CONSTRAINT DF_s_field_edit_visible DEFAULT (1),
|
||||
b_required int NOT NULL CONSTRAINT DF_s_field_edit_required DEFAULT (0),
|
||||
b_readonly int NOT NULL CONSTRAINT DF_s_field_edit_readonly DEFAULT (0),
|
||||
b_disabled int NOT NULL CONSTRAINT DF_s_field_edit_disabled DEFAULT (0),
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_field_edit_xh DEFAULT (0),
|
||||
CONSTRAINT PK_s_field_edit PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_field_edit_field UNIQUE (b_module_id, b_field_id)
|
||||
);
|
||||
CREATE INDEX IX_s_field_edit_order ON dbo.s_field_edit (b_xh, b_id);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_field_query', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_field_query (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_module_id bigint NOT NULL,
|
||||
b_field_module_id bigint NOT NULL,
|
||||
b_field_id bigint NOT NULL,
|
||||
b_relation_id bigint NULL,
|
||||
b_component varchar(30) NULL,
|
||||
b_condition_no int NOT NULL CONSTRAINT DF_s_field_query_condition_no DEFAULT (1),
|
||||
b_operator varchar(20) NOT NULL,
|
||||
b_default_value nvarchar(500) NULL,
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_field_query_canuse DEFAULT (1),
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_field_query_xh DEFAULT (0),
|
||||
CONSTRAINT PK_s_field_query PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_field_query_condition UNIQUE (b_module_id, b_field_module_id, b_field_id, b_relation_id, b_operator, b_condition_no),
|
||||
CONSTRAINT CK_s_field_query_condition_no CHECK (b_condition_no > 0)
|
||||
);
|
||||
CREATE INDEX IX_s_field_query_order ON dbo.s_field_query (b_canuse, b_xh, b_id);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_relation', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_relation (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_code varchar(50) NOT NULL,
|
||||
b_module_id bigint NOT NULL,
|
||||
b_target_module_id bigint NOT NULL,
|
||||
b_relation_type varchar(20) NOT NULL,
|
||||
b_module_field_id bigint NOT NULL,
|
||||
b_target_field_id bigint NOT NULL,
|
||||
b_foreign_side varchar(10) NULL,
|
||||
b_junction_module_id bigint NULL,
|
||||
b_junction_module_field_id bigint NULL,
|
||||
b_junction_target_field_id bigint NULL,
|
||||
b_ownership varchar(20) NOT NULL CONSTRAINT DF_s_relation_ownership DEFAULT ('reference'),
|
||||
b_owner_side varchar(10) NULL,
|
||||
b_delete_policy varchar(20) NULL,
|
||||
b_order_module_id bigint NULL,
|
||||
b_order_field_id bigint NULL,
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_relation_canuse DEFAULT (1),
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_relation_xh DEFAULT (0),
|
||||
CONSTRAINT PK_s_relation PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_relation_code UNIQUE (b_code),
|
||||
CONSTRAINT UQ_s_relation_key UNIQUE (b_module_id, b_target_module_id, b_module_field_id, b_target_field_id),
|
||||
CONSTRAINT CK_s_relation_self_fields CHECK (b_module_id <> b_target_module_id OR b_module_field_id <> b_target_field_id),
|
||||
CONSTRAINT CK_s_relation_type CHECK (b_relation_type IN ('one-to-one','one-to-many','many-to-one','many-to-many')),
|
||||
CONSTRAINT CK_s_relation_foreign_side CHECK (b_foreign_side IS NULL OR b_foreign_side IN ('module','target')),
|
||||
CONSTRAINT CK_s_relation_ownership CHECK (b_ownership IN ('reference','owned')),
|
||||
CONSTRAINT CK_s_relation_owner_side CHECK (b_owner_side IS NULL OR b_owner_side IN ('module','target')),
|
||||
CONSTRAINT CK_s_relation_shape CHECK (
|
||||
(b_relation_type = 'many-to-many' AND b_foreign_side IS NULL AND b_junction_module_id IS NOT NULL AND b_junction_module_field_id IS NOT NULL AND b_junction_target_field_id IS NOT NULL)
|
||||
OR (b_relation_type <> 'many-to-many' AND b_foreign_side IS NOT NULL AND b_junction_module_id IS NULL AND b_junction_module_field_id IS NULL AND b_junction_target_field_id IS NULL)
|
||||
),
|
||||
CONSTRAINT CK_s_relation_owned CHECK ((b_ownership = 'reference' AND b_owner_side IS NULL) OR (b_ownership = 'owned' AND b_owner_side IS NOT NULL AND b_relation_type <> 'many-to-many')),
|
||||
CONSTRAINT CK_s_relation_order CHECK ((b_order_module_id IS NULL AND b_order_field_id IS NULL) OR (b_order_module_id IS NOT NULL AND b_order_field_id IS NOT NULL)),
|
||||
CONSTRAINT CK_s_relation_delete_policy CHECK (b_delete_policy IS NULL OR b_delete_policy IN ('restrict','set-null','cascade','archive'))
|
||||
);
|
||||
CREATE INDEX IX_s_relation_target ON dbo.s_relation (b_target_module_id);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_menu', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_menu (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_parent_id bigint NULL,
|
||||
b_code varchar(50) NOT NULL,
|
||||
b_name nvarchar(100) NOT NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
b_menu_type varchar(20) NOT NULL,
|
||||
b_route varchar(200) NULL,
|
||||
b_module_id bigint NULL,
|
||||
b_power_code varchar(50) NULL,
|
||||
b_icon varchar(50) NULL,
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_menu_xh DEFAULT (0),
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_menu_canuse DEFAULT (1),
|
||||
CONSTRAINT PK_s_menu PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_menu_code UNIQUE (b_code),
|
||||
CONSTRAINT CK_s_menu_parent CHECK (b_parent_id IS NULL OR b_parent_id <> b_id),
|
||||
CONSTRAINT CK_s_menu_type CHECK (b_menu_type IN ('directory','page','data','external'))
|
||||
);
|
||||
CREATE INDEX IX_s_menu_parent ON dbo.s_menu (b_parent_id, b_xh, b_canuse);
|
||||
CREATE INDEX IX_s_menu_module ON dbo.s_menu (b_module_id);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_autocode', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_autocode (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_module_id bigint NOT NULL,
|
||||
b_field_id bigint NOT NULL,
|
||||
b_prefix nvarchar(100) NOT NULL CONSTRAINT DF_s_autocode_prefix DEFAULT (N''),
|
||||
b_dateformat varchar(30) NOT NULL CONSTRAINT DF_s_autocode_dateformat DEFAULT ('yyyyMM'),
|
||||
b_separator nvarchar(10) NOT NULL CONSTRAINT DF_s_autocode_separator DEFAULT (N'-'),
|
||||
b_seqwidth int NOT NULL CONSTRAINT DF_s_autocode_seqwidth DEFAULT (5),
|
||||
b_resettype varchar(10) NOT NULL CONSTRAINT DF_s_autocode_resettype DEFAULT ('month'),
|
||||
b_startvalue bigint NOT NULL CONSTRAINT DF_s_autocode_startvalue DEFAULT (1),
|
||||
b_currentperiod varchar(8) NULL,
|
||||
b_currentvalue bigint NOT NULL CONSTRAINT DF_s_autocode_currentvalue DEFAULT (0),
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_autocode_canuse DEFAULT (1),
|
||||
CONSTRAINT PK_s_autocode PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_autocode_module_field UNIQUE (b_module_id, b_field_id),
|
||||
CONSTRAINT CK_s_autocode_resettype CHECK (b_resettype IN ('none','day','month','year')),
|
||||
CONSTRAINT CK_s_autocode_values CHECK (b_seqwidth > 0 AND b_startvalue >= 0 AND b_currentvalue >= 0)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_role', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_role (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_code varchar(50) NOT NULL,
|
||||
b_name nvarchar(100) NOT NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
b_remark nvarchar(500) NULL,
|
||||
b_is_system int NOT NULL CONSTRAINT DF_s_role_is_system DEFAULT (0),
|
||||
b_bypass_power int NOT NULL CONSTRAINT DF_s_role_bypass_power DEFAULT (0),
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_role_canuse DEFAULT (1),
|
||||
b_inputuser_id bigint NULL,
|
||||
b_inputdatetime datetime2 NULL CONSTRAINT DF_s_role_inputdatetime DEFAULT (SYSUTCDATETIME()),
|
||||
b_updateuser_id bigint NULL,
|
||||
b_updatedatetime datetime2 NULL,
|
||||
CONSTRAINT PK_s_role PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_role_code UNIQUE (b_code),
|
||||
CONSTRAINT CK_s_role_system_bypass CHECK (b_bypass_power = 0 OR b_is_system = 1)
|
||||
);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_user_role', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_user_role (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_user_id bigint NOT NULL,
|
||||
b_role_id bigint NOT NULL,
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_user_role_canuse DEFAULT (1),
|
||||
b_inputdatetime datetime2 NULL CONSTRAINT DF_s_user_role_inputdatetime DEFAULT (SYSUTCDATETIME()),
|
||||
CONSTRAINT PK_s_user_role PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_user_role UNIQUE (b_user_id, b_role_id)
|
||||
);
|
||||
CREATE INDEX IX_s_user_role_role ON dbo.s_user_role (b_role_id, b_canuse);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_power', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_power (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_code varchar(50) NOT NULL,
|
||||
b_name nvarchar(100) NOT NULL,
|
||||
b_i18n varchar(150) NULL,
|
||||
b_power_type varchar(20) NOT NULL,
|
||||
b_resource varchar(128) NULL,
|
||||
b_module_id bigint NULL,
|
||||
b_field_id bigint NULL,
|
||||
b_operation varchar(30) NOT NULL,
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_power_canuse DEFAULT (1),
|
||||
b_xh int NOT NULL CONSTRAINT DF_s_power_xh DEFAULT (0),
|
||||
CONSTRAINT PK_s_power PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_power_code UNIQUE (b_code),
|
||||
CONSTRAINT CK_s_power_type CHECK (b_power_type IN ('menu','module','action','field')),
|
||||
CONSTRAINT CK_s_power_shape CHECK (
|
||||
(b_power_type = 'menu' AND b_resource IS NOT NULL AND b_module_id IS NULL AND b_field_id IS NULL AND b_operation = 'access')
|
||||
OR (b_power_type = 'module' AND b_resource IS NULL AND b_module_id IS NOT NULL AND b_field_id IS NULL AND b_operation IN ('read','create','update','delete','export'))
|
||||
OR (b_power_type = 'action' AND b_resource IS NOT NULL AND b_module_id IS NOT NULL AND b_field_id IS NULL AND b_operation = 'execute')
|
||||
OR (b_power_type = 'field' AND b_resource IS NULL AND b_module_id IS NOT NULL AND b_field_id IS NOT NULL AND b_operation IN ('view','edit'))
|
||||
)
|
||||
);
|
||||
CREATE INDEX IX_s_power_resource ON dbo.s_power (b_power_type, b_resource, b_canuse);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_role_power', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_role_power (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_role_id bigint NOT NULL,
|
||||
b_power_id bigint NOT NULL,
|
||||
b_effect varchar(10) NOT NULL CONSTRAINT DF_s_role_power_effect DEFAULT ('allow'),
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_role_power_canuse DEFAULT (1),
|
||||
b_inputdatetime datetime2 NULL CONSTRAINT DF_s_role_power_inputdatetime DEFAULT (SYSUTCDATETIME()),
|
||||
CONSTRAINT PK_s_role_power PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_role_power UNIQUE (b_role_id, b_power_id),
|
||||
CONSTRAINT CK_s_role_power_effect CHECK (b_effect IN ('allow','deny'))
|
||||
);
|
||||
CREATE INDEX IX_s_role_power_power ON dbo.s_role_power (b_power_id, b_canuse);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_role_scope', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_role_scope (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_role_id bigint NOT NULL,
|
||||
b_module_id bigint NOT NULL,
|
||||
b_operation varchar(20) NOT NULL,
|
||||
b_scope_type varchar(30) NOT NULL,
|
||||
b_scope_no int NOT NULL CONSTRAINT DF_s_role_scope_no DEFAULT (1),
|
||||
b_scope_field_id bigint NULL,
|
||||
b_scope_value nvarchar(max) NULL,
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_role_scope_canuse DEFAULT (1),
|
||||
b_inputdatetime datetime2 NULL CONSTRAINT DF_s_role_scope_inputdatetime DEFAULT (SYSUTCDATETIME()),
|
||||
CONSTRAINT PK_s_role_scope PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_role_scope UNIQUE (b_role_id, b_module_id, b_operation, b_scope_type, b_scope_field_id, b_scope_no),
|
||||
CONSTRAINT CK_s_role_scope_operation CHECK (b_operation IN ('read','update','delete','export')),
|
||||
CONSTRAINT CK_s_role_scope_type CHECK (b_scope_type IN ('all','creator','owner','department','department-tree','custom')),
|
||||
CONSTRAINT CK_s_role_scope_no CHECK (b_scope_no > 0),
|
||||
CONSTRAINT CK_s_role_scope_shape CHECK (
|
||||
(b_scope_type IN ('all','creator') AND b_scope_field_id IS NULL AND b_scope_value IS NULL)
|
||||
OR (b_scope_type IN ('owner','department','department-tree') AND b_scope_field_id IS NOT NULL)
|
||||
OR (b_scope_type = 'custom' AND b_scope_value IS NOT NULL)
|
||||
)
|
||||
);
|
||||
CREATE INDEX IX_s_role_scope_role ON dbo.s_role_scope (b_role_id, b_module_id, b_operation, b_canuse);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_i18n', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_i18n (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_key varchar(150) NOT NULL,
|
||||
b_locale varchar(20) NOT NULL,
|
||||
b_value nvarchar(500) NOT NULL,
|
||||
b_i18ntype varchar(20) NULL,
|
||||
b_canuse int NOT NULL CONSTRAINT DF_s_i18n_canuse DEFAULT (1),
|
||||
CONSTRAINT PK_s_i18n PRIMARY KEY (b_id),
|
||||
CONSTRAINT UQ_s_i18n_key_locale UNIQUE (b_key, b_locale)
|
||||
);
|
||||
CREATE INDEX IX_s_i18n_locale ON dbo.s_i18n (b_locale, b_canuse);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_log_technical', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_log_technical (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_org_id varchar(50) NULL,
|
||||
b_user_id bigint NULL,
|
||||
b_account varchar(50) NULL,
|
||||
b_sourcetype varchar(20) NULL,
|
||||
b_loglevel varchar(16) NOT NULL,
|
||||
b_logcategory varchar(64) NULL,
|
||||
b_operation varchar(100) NULL,
|
||||
b_module_code varchar(50) NULL,
|
||||
b_request_id varchar(64) NULL,
|
||||
b_trace_id varchar(64) NULL,
|
||||
b_server_node varchar(128) NULL,
|
||||
b_duration_ms bigint NULL,
|
||||
b_result_code varchar(50) NULL,
|
||||
b_error_code varchar(50) NULL,
|
||||
b_error_message nvarchar(2000) NULL,
|
||||
b_exception_text nvarchar(max) NULL,
|
||||
b_context_text nvarchar(max) NULL,
|
||||
b_occurdatetime datetime2 NULL CONSTRAINT DF_s_log_technical_occurdatetime DEFAULT (SYSUTCDATETIME()),
|
||||
CONSTRAINT PK_s_log_technical PRIMARY KEY (b_id)
|
||||
);
|
||||
CREATE INDEX IX_s_log_technical_trace ON dbo.s_log_technical (b_trace_id, b_occurdatetime DESC);
|
||||
CREATE INDEX IX_s_log_technical_org ON dbo.s_log_technical (b_org_id, b_occurdatetime DESC);
|
||||
CREATE INDEX IX_s_log_technical_level ON dbo.s_log_technical (b_loglevel, b_occurdatetime DESC);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_log_login', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_log_login (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_org_id varchar(50) NULL,
|
||||
b_user_id bigint NULL,
|
||||
b_account varchar(50) NULL,
|
||||
b_login_type varchar(20) NOT NULL,
|
||||
b_login_result int NOT NULL CONSTRAINT DF_s_log_login_result DEFAULT (0),
|
||||
b_fail_reason nvarchar(200) NULL,
|
||||
b_ip varchar(50) NULL,
|
||||
b_ip_location nvarchar(200) NULL,
|
||||
b_browser varchar(100) NULL,
|
||||
b_browser_version varchar(50) NULL,
|
||||
b_os varchar(100) NULL,
|
||||
b_os_version varchar(50) NULL,
|
||||
b_device_type varchar(20) NULL,
|
||||
b_user_agent nvarchar(max) NULL,
|
||||
b_session_id varchar(50) NULL,
|
||||
b_request_id varchar(64) NULL,
|
||||
b_trace_id varchar(64) NULL,
|
||||
b_occurdatetime datetime2 NULL CONSTRAINT DF_s_log_login_occurdatetime DEFAULT (SYSUTCDATETIME()),
|
||||
CONSTRAINT PK_s_log_login PRIMARY KEY (b_id)
|
||||
);
|
||||
CREATE INDEX IX_s_log_login_user ON dbo.s_log_login (b_user_id, b_occurdatetime DESC);
|
||||
CREATE INDEX IX_s_log_login_account ON dbo.s_log_login (b_account, b_occurdatetime DESC);
|
||||
CREATE INDEX IX_s_log_login_result ON dbo.s_log_login (b_login_result, b_occurdatetime DESC);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_log_audit', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_log_audit (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_event_code varchar(50) NOT NULL,
|
||||
b_module_code varchar(50) NULL,
|
||||
b_data_id varchar(50) NULL,
|
||||
b_business_no varchar(50) NULL,
|
||||
b_operation varchar(100) NULL,
|
||||
b_operator_id bigint NULL,
|
||||
b_operator_name nvarchar(100) NULL,
|
||||
b_source_type varchar(20) NOT NULL CONSTRAINT DF_s_log_audit_source_type DEFAULT ('user'),
|
||||
b_visibility varchar(20) NOT NULL CONSTRAINT DF_s_log_audit_visibility DEFAULT ('internal'),
|
||||
b_request_id varchar(64) NULL,
|
||||
b_trace_id varchar(64) NULL,
|
||||
b_payload_text nvarchar(max) NULL,
|
||||
b_occurdatetime datetime2 NULL CONSTRAINT DF_s_log_audit_occurdatetime DEFAULT (SYSUTCDATETIME()),
|
||||
CONSTRAINT PK_s_log_audit PRIMARY KEY (b_id)
|
||||
);
|
||||
CREATE INDEX IX_s_log_audit_module ON dbo.s_log_audit (b_module_code, b_data_id, b_occurdatetime DESC);
|
||||
CREATE INDEX IX_s_log_audit_trace ON dbo.s_log_audit (b_trace_id);
|
||||
END;
|
||||
|
||||
IF OBJECT_ID(N'dbo.s_log_audit_field', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.s_log_audit_field (
|
||||
b_id bigint IDENTITY(1,1) NOT NULL,
|
||||
b_event_id bigint NOT NULL,
|
||||
b_module_code varchar(50) NULL,
|
||||
b_data_id varchar(50) NULL,
|
||||
b_field varchar(50) NOT NULL,
|
||||
b_field_label nvarchar(100) NULL,
|
||||
b_before_value nvarchar(max) NULL,
|
||||
b_after_value nvarchar(max) NULL,
|
||||
b_before_display nvarchar(500) NULL,
|
||||
b_after_display nvarchar(500) NULL,
|
||||
b_visibility varchar(20) NOT NULL CONSTRAINT DF_s_log_audit_field_visibility DEFAULT ('internal'),
|
||||
CONSTRAINT PK_s_log_audit_field PRIMARY KEY (b_id)
|
||||
);
|
||||
CREATE INDEX IX_s_log_audit_field_event ON dbo.s_log_audit_field (b_event_id);
|
||||
CREATE INDEX IX_s_log_audit_field_customer ON dbo.s_log_audit_field (b_module_code, b_data_id, b_visibility, b_event_id);
|
||||
END;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
|
||||
SELECT DB_NAME() AS database_name,
|
||||
(SELECT COUNT(*) FROM dbo.b_user) AS user_count,
|
||||
(SELECT COUNT(*) FROM sys.tables WHERE is_ms_shipped = 0) AS user_table_count;
|
||||
@@ -0,0 +1,44 @@
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
IF DB_NAME() <> N'FMS-NEW'
|
||||
THROW 51001, N'This migration can only run against FMS-NEW.', 1;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DECLARE @sql nvarchar(max) = N'';
|
||||
SELECT @sql = @sql
|
||||
+ N'ALTER TABLE ' + QUOTENAME(OBJECT_SCHEMA_NAME(parent_object_id)) + N'.'
|
||||
+ QUOTENAME(OBJECT_NAME(parent_object_id)) + N' DROP CONSTRAINT ' + QUOTENAME(name) + N';'
|
||||
FROM sys.foreign_keys
|
||||
ORDER BY object_id;
|
||||
|
||||
IF @sql <> N''
|
||||
EXEC sys.sp_executesql @sql;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.s_field_group') AND name = N'IX_s_field_group_parent')
|
||||
CREATE INDEX IX_s_field_group_parent ON dbo.s_field_group (b_module_id, b_parent_id);
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.s_field_view') AND name = N'IX_s_field_view_group')
|
||||
CREATE INDEX IX_s_field_view_group ON dbo.s_field_view (b_module_id, b_group_id);
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.s_field_edit') AND name = N'IX_s_field_edit_group')
|
||||
CREATE INDEX IX_s_field_edit_group ON dbo.s_field_edit (b_module_id, b_group_id);
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.s_field_query') AND name = N'IX_s_field_query_module')
|
||||
CREATE INDEX IX_s_field_query_module ON dbo.s_field_query (b_module_id, b_canuse, b_xh, b_id);
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.s_field_query') AND name = N'IX_s_field_query_field')
|
||||
CREATE INDEX IX_s_field_query_field ON dbo.s_field_query (b_field_module_id, b_field_id);
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.s_field_query') AND name = N'IX_s_field_query_relation')
|
||||
CREATE INDEX IX_s_field_query_relation ON dbo.s_field_query (b_relation_id) WHERE b_relation_id IS NOT NULL;
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.s_relation') AND name = N'IX_s_relation_module')
|
||||
CREATE INDEX IX_s_relation_module ON dbo.s_relation (b_module_id, b_canuse, b_xh, b_id);
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.s_relation') AND name = N'IX_s_relation_junction')
|
||||
CREATE INDEX IX_s_relation_junction ON dbo.s_relation (b_junction_module_id) WHERE b_junction_module_id IS NOT NULL;
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.s_relation') AND name = N'IX_s_relation_order_module')
|
||||
CREATE INDEX IX_s_relation_order_module ON dbo.s_relation (b_order_module_id) WHERE b_order_module_id IS NOT NULL;
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.s_power') AND name = N'IX_s_power_module')
|
||||
CREATE INDEX IX_s_power_module ON dbo.s_power (b_module_id, b_field_id, b_canuse);
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE object_id = OBJECT_ID(N'dbo.s_role_scope') AND name = N'IX_s_role_scope_field')
|
||||
CREATE INDEX IX_s_role_scope_field ON dbo.s_role_scope (b_module_id, b_scope_field_id) WHERE b_scope_field_id IS NOT NULL;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
|
||||
SELECT COUNT(*) AS foreign_key_count FROM sys.foreign_keys;
|
||||
@@ -250,7 +250,8 @@ async function submitUpload() {
|
||||
emit('uploaded')
|
||||
await loadFiles(1)
|
||||
} catch (e) {
|
||||
Message.error('上传失败:' + (e?.message || e))
|
||||
// 服务端不可达 / 5xx 已由 http 拦截器统一弹 Message,这里去重
|
||||
if (!e?.toasted) Message.error('上传失败:' + (e?.message || e))
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
@@ -305,7 +306,8 @@ async function deleteSelected() {
|
||||
emit('deleted')
|
||||
await loadFiles(currentPage.value)
|
||||
} catch (e) {
|
||||
Message.error('删除失败:' + (e?.message || e))
|
||||
// 服务端不可达 / 5xx 已由 http 拦截器统一弹 Message,这里去重
|
||||
if (!e?.toasted) Message.error('删除失败:' + (e?.message || e))
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -330,7 +332,8 @@ async function deleteSingle(file) {
|
||||
emit('deleted')
|
||||
await loadFiles(currentPage.value)
|
||||
} catch (e) {
|
||||
Message.error('删除失败:' + (e?.message || e))
|
||||
// 服务端不可达 / 5xx 已由 http 拦截器统一弹 Message,这里去重
|
||||
if (!e?.toasted) Message.error('删除失败:' + (e?.message || e))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -213,6 +213,30 @@ defineExpose({ moduleId, refreshCategories: loadCategories })
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 分隔条:宽 0 + 无负 margin,不占 flex 宽度,面板在交界处无缝相接,
|
||||
消除分割线左右两侧的空白条(对比:DefaultLayout 用负 margin 让面板互相
|
||||
覆盖,但附件三栏背景色不同——白列表 / 灰预览——覆盖会露底,这里改用
|
||||
::after 扩展命中区,不挤压面板也不遮挡相邻面板背景)。 */
|
||||
.ff-splitter :deep(> .fms-splitter-bar) {
|
||||
width: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 命中区:从宽 0 的分隔条向两侧各伸出 3px(共 6px,与 --fms-splitter-trigger-size
|
||||
一致),保持拖拽热区不变;伪元素的事件命中会冒泡到 .fms-splitter-bar 本体,
|
||||
pointerdown / 键盘聚焦仍由原逻辑处理 */
|
||||
.ff-splitter :deep(> .fms-splitter-bar::after) {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: var(--fms-splitter-trigger-size);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.ff-center {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
@@ -69,10 +69,6 @@ const { state } = useFileModal()
|
||||
/* Modal 根节点是 Teleport,class 无法透传(Vue 会告警且不生效),
|
||||
因此通过内容锚点 .ff-container(FmsFile 根元素)用 :has() 定位附件弹窗。
|
||||
打开中的附件弹窗体内必然渲染 .ff-container,其他弹窗不受影响。 */
|
||||
.fms-modal:has(.ff-container) {
|
||||
max-width: 1440px; /* 超宽屏封顶,避免三栏被拉得过宽 */
|
||||
min-width: 960px; /* 极小屏保底,三栏不挤 */
|
||||
}
|
||||
|
||||
.fms-modal-body:has(.ff-container) {
|
||||
padding: 0;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { Message } from '@/components/ui/message/message-manager'
|
||||
|
||||
const AUTH_ERROR_CODE = 401
|
||||
const SESSION_REPLACED_CODE = 40101
|
||||
@@ -81,13 +82,23 @@ http.interceptors.response.use(
|
||||
const errorCode = payload?.code ?? status ?? 0
|
||||
if (isAuthenticationError(errorCode)) handleUnauthorized(errorCode)
|
||||
|
||||
const requestError = new Error(
|
||||
const message =
|
||||
payload?.message ||
|
||||
(error.code === 'ECONNABORTED' ? '请求超时,请稍后重试' : '网络异常,请检查服务是否正常'),
|
||||
)
|
||||
(error.code === 'ECONNABORTED' ? '请求超时,请稍后重试' : '网络异常,请检查服务是否正常')
|
||||
|
||||
const requestError = new Error(message)
|
||||
requestError.code = payload?.code ?? status ?? -1
|
||||
requestError.data = payload?.data
|
||||
requestError.success = false
|
||||
|
||||
// 服务端不可达(无 status)/ 5xx 属全局性问题,统一弹 Message 提示;
|
||||
// 4xx 等业务错误由调用方自行决定提示方式。
|
||||
// 打 toasted 标记:登录、上传/删除等已有自身 Message 的调用方据此去重,避免重复弹窗
|
||||
if (!status || status >= 500) {
|
||||
Message.error(message)
|
||||
requestError.toasted = true
|
||||
}
|
||||
|
||||
throw requestError
|
||||
},
|
||||
)
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
/* 栅格(grid):默认水平/垂直间距(不随主题变化) */
|
||||
--fms-grid-gutter: 0px;
|
||||
--fms-splitter-bar-size: 2px;
|
||||
--fms-splitter-bar-size: 1px;
|
||||
--fms-splitter-bar-size-hover: 4px;
|
||||
--fms-splitter-handle-size: 20px;
|
||||
|
||||
|
||||
@@ -60,7 +60,8 @@ async function onSubmit(values) {
|
||||
Message.success('登录成功')
|
||||
router.replace(String(route.query.redirect || '/dashboard'))
|
||||
} catch (e) {
|
||||
Message.error(e.message || '登录失败,请稍后重试')
|
||||
// 服务端不可达 / 5xx 已由 http 拦截器统一弹 Message,这里去重避免重复弹窗
|
||||
if (!e?.toasted) Message.error(e.message || '登录失败,请稍后重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user