Files
t-admin/scripts/data-contract-report.js
12914 0a7725f6d8 feat(脏改动归属收尾): 门禁报告去时间戳/pay-order退款空吞补提示/门禁62-0-0
G1: scripts/data-contract-report.js + quality-check.js 删除 Generated 时间戳行
    (时间戳保留在 gitignored 的 *.json generatedAt 备查;reports/*.md
    此后跑门禁不再变脏,归属:门禁产物噪音)
G2: pay-order/list.vue confirmRefund 云端拒绝 errCode 分支 + 校验/网络
    catch 分支均补 toast 用户可见提示(此前静默失败)
门禁: npm test 62 schemas/0 漂移/0 错误全绿;diff --check 按
    core.whitespace=cr-at-eol 通过(仓库既有 CRLF 惯例,autocrlf=false)
2026-09-14 03:24:15 +08:00

258 lines
9.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const fs = require('fs')
const path = require('path')
const root = path.resolve(__dirname, '..')
const reportDir = path.join(root, 'reports')
const excluded = new Set(['.git', 'node_modules', 'unpackage', '.zcode'])
function walk(dir) {
if (!fs.existsSync(dir)) return []
const result = []
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.isDirectory() && excluded.has(entry.name)) continue
const file = path.join(dir, entry.name)
if (entry.isDirectory()) result.push(...walk(file))
else result.push(file)
}
return result
}
function rel(file) {
return path.relative(root, file).replace(/\\/g, '/')
}
function parseJson(file) {
try {
return JSON.parse(stripJsonComments(fs.readFileSync(file, 'utf8')))
} catch (error) {
return { __error: error.message }
}
}
// B1: schema 允许 // 行注释与 /* 块注释头(uniCloud JSONC),比较前剥离
function stripJsonComments(text) {
let out = ''
let i = 0
const n = text.length
let inStr = false
while (i < n) {
const c = text[i]
if (inStr) {
out += c
if (c === '\\') { out += text[i + 1] || ''; i += 2; continue }
if (c === '"') inStr = false
i++
continue
}
if (c === '"') { inStr = true; out += c; i++; continue }
if (c === '/' && text[i + 1] === '/') {
const end = text.indexOf('\n', i)
const stop = end === -1 ? n : end
out += ' '.repeat(stop - i)
i = stop
continue
}
if (c === '/' && text[i + 1] === '*') {
const end = text.indexOf('*/', i + 2)
const stop = end === -1 ? n : end + 2
const skipped = text.slice(i, stop)
out += skipped.replace(/[^\n]/g, ' ')
i = stop
continue
}
out += c
i++
}
return out
}
function schemaFiles() {
const localSchemas = walk(path.join(root, 'uniCloud-alipay', 'database'))
.filter(file => file.endsWith('.schema.json'))
const moduleSchemas = walk(path.join(root, 'uni_modules'))
.filter(file => file.endsWith('.schema.json'))
return [...localSchemas, ...moduleSchemas]
}
function sourceFiles() {
return [
...walk(path.join(root, 'pages')),
...walk(path.join(root, 'components')),
...walk(path.join(root, 'cloudfunctions'))
].filter(file => /\.(vue|js)$/.test(file))
}
function getSchemas() {
const result = new Map()
for (const file of schemaFiles()) {
result.set(path.basename(file, '.schema.json'), { file: rel(file), schema: parseJson(file) })
}
return result
}
function addMap(map, key, value) {
if (!map.has(key)) map.set(key, [])
map.get(key).push(value)
}
function collectCollections(sources) {
const collections = new Map()
const patterns = [
/collectionList\s*:\s*["']([^"']+)["']/g,
/collection\s*:\s*["']([^"']+)["']/g,
/collection\(["']([^"']+)["']\)/g
]
for (const file of sources) {
const text = fs.readFileSync(file, 'utf8')
for (const pattern of patterns) {
let match
while ((match = pattern.exec(text))) addMap(collections, match[1], rel(file))
}
}
return collections
}
function collectFields(sources, collections) {
const fields = new Map()
// B1-1: 前置边界 [\s"] 排除 group-field/groupField;只认属性 field=(不认 .field( JS 调用)
const fieldPattern = /[\s"]field\s*=\s*["']([^"']+)["']/g
// 主集合判定:<unicloud-db ...> 标签内的 collection 绑定
const udbRe = /<unicloud-db\b[^>]*>/g
const udbCollectionRe = /collectionList\s*:\s*["']([^"']+)["']|collection\s*:\s*["']([^"']+)["']/
for (const file of sources.filter(file => file.endsWith('.vue'))) {
const text = fs.readFileSync(file, 'utf8')
// script 区 collectionList: "x" 字面量(:collection="collectionList" 绑定的解析目标)
const scriptCollections = []
const scriptRe = /collectionList\s*:\s*["']([^"']+)["']/g
let sm
while ((sm = scriptRe.exec(text))) scriptCollections.push(sm[1])
const arrayRe = /collectionList\s*:\s*\[([^\]]*)\]/g
let am
while ((am = arrayRe.exec(text))) {
const inner = am[1]
const strRe = /db\.collection\(\s*["']([^"']+)["']\s*\)/g
let im
while ((im = strRe.exec(inner))) scriptCollections.push(im[1])
}
let um
let foundMain = false
const pendingTags = []
while ((um = udbRe.exec(text))) {
const tag = um[0]
const cm = udbCollectionRe.exec(tag)
if (!cm) {
if (/[:\s]collection\s*=/.test(tag)) pendingTags.push(tag)
continue
}
const main = cm[1] || cm[2]
foundMain = true
collectTagFields(tag, main, file, fields)
}
if (pendingTags.length && scriptCollections.length) {
// :collection="collectionList" 绑定的 unicloud-db 标签:只收该标签自身的 field,
// 不收 uni-data-select / uni-data-checkbox 等辅助组件的 field
foundMain = true
const main = scriptCollections[0]
for (const tag of pendingTags) collectTagFields(tag, main, file, fields)
}
}
return fields
}
function collectTagFields(tag, main, file, fields) {
const fieldRe = /[\s"]field\s*=\s*["']([^"']+)["']/g
let fm
while ((fm = fieldRe.exec(tag))) addFieldEntries(main, fm[1], file, fields)
}
function addFieldEntries(main, raw, file, fields) {
for (const field of raw.split(',')) {
// B1-1: 去 as 别名,去表前缀;聚合表达式(含括号)直接丢弃
const name = field.trim().split(/\s+as\s+/i)[0].split('.')[0].trim()
if (!name || name.includes('(') || name.includes(')')) continue
addMap(fields, `${main}:${name}`, rel(file))
}
}
function buildReport() {
const schemas = getSchemas()
const sources = sourceFiles()
const collections = collectCollections(sources)
const fields = collectFields(sources, collections)
const missingSchemas = []
for (const [collection, files] of collections) {
if (!schemas.has(collection)) missingSchemas.push({ collection, files: [...new Set(files)] })
}
const fieldDrift = []
for (const [key, files] of fields) {
const [collection, field] = key.split(':')
const schema = schemas.get(collection)
if (!schema || schema.schema.__error) {
fieldDrift.push({ collection, field, files: [...new Set(files)], schema: schema ? schema.file : null, driftKind: 'missing-in-schema' })
} else if (!schema.schema.properties || !Object.prototype.hasOwnProperty.call(schema.schema.properties, field)) {
fieldDrift.push({ collection, field, files: [...new Set(files)], schema: schema.file, driftKind: 'page-field-typo' })
}
}
const knownMigrations = [
{
source: 'opendb-news-articles',
target: 'uni-cms-articles',
status: 'requires-review',
mapping: { avatar: 'thumbnail[]', content: 'content Delta/object', category_id: 'uni-cms-categories._id' }
},
{
source: 'opendb-news-comments',
target: 'comment',
status: 'missing-target-schema',
mapping: { comment_content: 'content', comment_date: 'create_time', comment_ip: 'ip_location' }
},
{
source: 'opendb-news-favorite',
target: 'comment_like',
status: 'requires-review',
mapping: { create_date: 'create_time', article_id: 'comment_id (semantic mismatch)' }
}
]
const report = {
generatedAt: new Date().toISOString(),
schemas: [...schemas.entries()].map(([name, value]) => ({ name, file: value.file })),
collections: [...collections.entries()].map(([name, files]) => ({ name, files: [...new Set(files)] })),
missingSchemas,
fieldDrift,
knownMigrations,
summary: {
schemaCount: schemas.size,
collectionCount: collections.size,
missingSchemaCount: missingSchemas.length,
fieldDriftCount: fieldDrift.length
}
}
fs.mkdirSync(reportDir, { recursive: true })
fs.writeFileSync(path.join(reportDir, 'data-contract-report.json'), JSON.stringify(report, null, 2) + '\n')
const lines = [
'# Data contract report',
'',
// G1收尾:去掉 Generated 时间戳行 —— 每次跑门禁必变导致 reports/*.md 常脏;
// 时间戳仍保留在 gitignored 的 data-contract-report.json(generatedAt)中备查
`- Schemas: ${report.summary.schemaCount}`,
`- Referenced collections: ${report.summary.collectionCount}`,
`- Missing schemas: ${report.summary.missingSchemaCount}`,
`- Field drift entries: ${report.summary.fieldDriftCount}`,
'',
'## Missing schemas',
''
]
if (!missingSchemas.length) lines.push('None', '')
for (const item of missingSchemas) lines.push(`- **${item.collection}** — ${item.files.join(', ')}`)
lines.push('', '## Field drift', '')
if (!fieldDrift.length) lines.push('None', '')
for (const item of fieldDrift.slice(0, 200)) lines.push(`- **${item.collection}.${item.field}** [\`${item.driftKind}\`] — ${item.files.join(', ')}`)
lines.push('', '## Migration review queue', '')
for (const item of knownMigrations) lines.push(`- **${item.source} → ${item.target}** (${item.status}) — ${JSON.stringify(item.mapping)}`)
fs.writeFileSync(path.join(reportDir, 'data-contract-report.md'), lines.join('\n') + '\n')
return report
}
const report = buildReport()
console.log(JSON.stringify(report.summary))