fix: 修复云端变量记录data为null的问题
- 后端: handleAddRecord增加recordData nil检查,避免存入null
- 后端: 记录读取改用interface{}替代map[string]interface{}解析JSON
- 后端: JSON解析失败时回退返回原始字符串
- 后端: API返回raw_data字段暴露数据库原始数据用于诊断
- 后端: app/cloud.go同步修复同样的JSON解析问题
- 前端: parseRecordData增加raw_data回退解析
- 前端: 添加数据诊断卡片显示原始数据结构
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
@@ -745,11 +745,17 @@ func handleGetCloudVariableRecords(c *gin.Context) {
|
|||||||
|
|
||||||
result := make([]gin.H, len(records))
|
result := make([]gin.H, len(records))
|
||||||
for i, r := range records {
|
for i, r := range records {
|
||||||
var data map[string]interface{}
|
var parsedData interface{}
|
||||||
json.Unmarshal([]byte(r.Data), &data)
|
if r.Data != "" {
|
||||||
|
json.Unmarshal([]byte(r.Data), &parsedData)
|
||||||
|
}
|
||||||
|
if parsedData == nil && r.Data != "" {
|
||||||
|
parsedData = r.Data
|
||||||
|
}
|
||||||
record := gin.H{
|
record := gin.H{
|
||||||
"id": r.ID,
|
"id": r.ID,
|
||||||
"data": data,
|
"data": parsedData,
|
||||||
|
"raw_data": r.Data,
|
||||||
"created_at": r.CreatedAt,
|
"created_at": r.CreatedAt,
|
||||||
}
|
}
|
||||||
if r.AppUserID != nil {
|
if r.AppUserID != nil {
|
||||||
|
|||||||
@@ -818,11 +818,16 @@ func handleAppGetVariableRecords(c *gin.Context) {
|
|||||||
|
|
||||||
result := make([]gin.H, len(records))
|
result := make([]gin.H, len(records))
|
||||||
for i, r := range records {
|
for i, r := range records {
|
||||||
var data map[string]interface{}
|
var parsedData interface{}
|
||||||
json.Unmarshal([]byte(r.Data), &data)
|
if r.Data != "" {
|
||||||
|
json.Unmarshal([]byte(r.Data), &parsedData)
|
||||||
|
}
|
||||||
|
if parsedData == nil && r.Data != "" {
|
||||||
|
parsedData = r.Data
|
||||||
|
}
|
||||||
result[i] = gin.H{
|
result[i] = gin.H{
|
||||||
"id": r.ID,
|
"id": r.ID,
|
||||||
"data": data,
|
"data": parsedData,
|
||||||
"created_at": r.CreatedAt,
|
"created_at": r.CreatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -592,6 +592,9 @@ func handleAddRecord(appID uint, userID *uint, actionMap map[string]interface{})
|
|||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("缺少record_data参数")
|
return fmt.Errorf("缺少record_data参数")
|
||||||
}
|
}
|
||||||
|
if recordData == nil {
|
||||||
|
return fmt.Errorf("record_data不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
var variable model.CloudVariable
|
var variable model.CloudVariable
|
||||||
if err := database.DB.Where("app_id = ? AND key = ?", appID, variableKey).First(&variable).Error; err != nil {
|
if err := database.DB.Where("app_id = ? AND key = ?", appID, variableKey).First(&variable).Error; err != nil {
|
||||||
@@ -684,13 +687,17 @@ func getRecords(appID uint, userID *uint, variableKey string, page, pageSize int
|
|||||||
var recordList []interface{}
|
var recordList []interface{}
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
var data interface{}
|
var data interface{}
|
||||||
if err := json.Unmarshal([]byte(record.Data), &data); err == nil {
|
if record.Data != "" {
|
||||||
recordList = append(recordList, map[string]interface{}{
|
json.Unmarshal([]byte(record.Data), &data)
|
||||||
"id": record.ID,
|
|
||||||
"data": data,
|
|
||||||
"created_at": record.CreatedAt,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
if data == nil && record.Data != "" {
|
||||||
|
data = record.Data
|
||||||
|
}
|
||||||
|
recordList = append(recordList, map[string]interface{}{
|
||||||
|
"id": record.ID,
|
||||||
|
"data": data,
|
||||||
|
"created_at": record.CreatedAt,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
result["success"] = true
|
result["success"] = true
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ interface Variable {
|
|||||||
|
|
||||||
interface VariableRecord {
|
interface VariableRecord {
|
||||||
id: number
|
id: number
|
||||||
data: Record<string, any> | string
|
data: any
|
||||||
|
raw_data?: string
|
||||||
user_id?: number
|
user_id?: number
|
||||||
user?: {
|
user?: {
|
||||||
id: number
|
id: number
|
||||||
@@ -39,16 +40,53 @@ interface VariableRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseRecordData(record: VariableRecord): Record<string, any> {
|
function parseRecordData(record: VariableRecord): Record<string, any> {
|
||||||
if (!record.data) return {}
|
let rawData = record.data
|
||||||
if (typeof record.data === 'string') {
|
|
||||||
|
if ((rawData == null || rawData === '') && record.raw_data) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(record.data)
|
rawData = JSON.parse(record.raw_data)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
rawData = record.raw_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawData == null || rawData === '') return {}
|
||||||
|
|
||||||
|
if (typeof rawData === 'object') {
|
||||||
|
if (Array.isArray(rawData)) {
|
||||||
|
if (rawData.length > 0 && typeof rawData[0] === 'object' && rawData[0] !== null && !Array.isArray(rawData[0])) {
|
||||||
|
return rawData[0] as Record<string, any>
|
||||||
|
}
|
||||||
|
const obj: Record<string, any> = {}
|
||||||
|
rawData.forEach((item, index) => { obj[`item_${index}`] = item })
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
return rawData as Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof rawData === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(rawData)
|
||||||
|
if (parsed != null && typeof parsed === 'object') {
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
if (parsed.length > 0 && typeof parsed[0] === 'object' && parsed[0] !== null && !Array.isArray(parsed[0])) {
|
||||||
|
return parsed[0] as Record<string, any>
|
||||||
|
}
|
||||||
|
const obj: Record<string, any> = {}
|
||||||
|
parsed.forEach((item: any, index: number) => { obj[`item_${index}`] = item })
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
return parsed as Record<string, any>
|
||||||
|
}
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return record.data
|
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -70,21 +108,25 @@ const deleteTargetId = ref<number | null>(null)
|
|||||||
const batchDeleteDialogOpen = ref(false)
|
const batchDeleteDialogOpen = ref(false)
|
||||||
|
|
||||||
const dynamicKeys = computed(() => {
|
const dynamicKeys = computed(() => {
|
||||||
console.log('Computing dynamicKeys, records count:', records.value.length)
|
|
||||||
const keys = new Set<string>()
|
const keys = new Set<string>()
|
||||||
records.value.forEach((record, index) => {
|
records.value.forEach((record) => {
|
||||||
console.log(`Record ${index}:`, record)
|
|
||||||
const data = parseRecordData(record)
|
const data = parseRecordData(record)
|
||||||
console.log(`Record ${index} parsed data:`, data, 'type:', typeof data)
|
|
||||||
if (data && typeof data === 'object') {
|
if (data && typeof data === 'object') {
|
||||||
const recordKeys = Object.keys(data)
|
Object.keys(data).forEach(key => keys.add(key))
|
||||||
console.log(`Record ${index} keys:`, recordKeys)
|
|
||||||
recordKeys.forEach(key => keys.add(key))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const result = Array.from(keys).sort()
|
return Array.from(keys).sort()
|
||||||
console.log('Final dynamicKeys:', result)
|
})
|
||||||
return result
|
|
||||||
|
const debugInfo = computed(() => {
|
||||||
|
if (records.value.length === 0 || dynamicKeys.value.length > 0) return null
|
||||||
|
const first = records.value[0]
|
||||||
|
return {
|
||||||
|
dataType: first.data === null ? 'null' : first.data === undefined ? 'undefined' : typeof first.data,
|
||||||
|
dataValue: first.data === null ? 'null' : first.data === undefined ? 'undefined' : typeof first.data === 'object' ? JSON.stringify(first.data) : String(first.data),
|
||||||
|
rawDataField: first.raw_data === undefined ? '(字段不存在)' : first.raw_data === '' ? '(空字符串)' : first.raw_data || '(falsy)',
|
||||||
|
parsedKeys: (() => { const p = parseRecordData(first); return p && typeof p === 'object' ? Object.keys(p) : [] })(),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = computed<ColumnDef<VariableRecord>[]>(() => {
|
const columns = computed<ColumnDef<VariableRecord>[]>(() => {
|
||||||
@@ -256,13 +298,6 @@ async function fetchRecords() {
|
|||||||
if (searchQuery.value) params.append('search', searchQuery.value)
|
if (searchQuery.value) params.append('search', searchQuery.value)
|
||||||
|
|
||||||
const data = await api.get<{ records: VariableRecord[], total: number }>(`/dev/cloud-variables/${variable.value.id}/records?${params}`)
|
const data = await api.get<{ records: VariableRecord[], total: number }>(`/dev/cloud-variables/${variable.value.id}/records?${params}`)
|
||||||
console.log('API response data:', data)
|
|
||||||
console.log('Records:', data?.records)
|
|
||||||
if (data?.records?.length > 0) {
|
|
||||||
console.log('First record:', data.records[0])
|
|
||||||
console.log('First record data:', data.records[0].data)
|
|
||||||
console.log('First record data type:', typeof data.records[0].data)
|
|
||||||
}
|
|
||||||
records.value = data?.records || []
|
records.value = data?.records || []
|
||||||
total.value = data?.total || 0
|
total.value = data?.total || 0
|
||||||
}
|
}
|
||||||
@@ -405,6 +440,16 @@ onMounted(async () => {
|
|||||||
</UiCard>
|
</UiCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="debugInfo" class="border border-dashed border-yellow-500/50 bg-yellow-500/5 rounded-md p-4 text-sm">
|
||||||
|
<p class="font-medium text-yellow-600 mb-2">数据诊断 (Data Debug)</p>
|
||||||
|
<div class="grid grid-cols-1 gap-2 text-xs font-mono">
|
||||||
|
<div>data 类型: <span class="text-yellow-700">{{ debugInfo.dataType }}</span></div>
|
||||||
|
<div>data 值: <span class="text-yellow-700 break-all">{{ debugInfo.dataValue.substring(0, 500) }}</span></div>
|
||||||
|
<div>raw_data (数据库原始): <span class="text-yellow-700 break-all">{{ debugInfo.rawDataField.substring(0, 500) }}</span></div>
|
||||||
|
<div>解析后 keys: <span class="text-yellow-700">{{ debugInfo.parsedKeys.length > 0 ? debugInfo.parsedKeys.join(', ') : '(空)' }}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<UiCard>
|
<UiCard>
|
||||||
<UiCardContent class="p-6">
|
<UiCardContent class="p-6">
|
||||||
<DataTable :columns="columns" :data="records" :loading="loading" :table="table" :server-pagination="serverPagination" @refresh="fetchRecords">
|
<DataTable :columns="columns" :data="records" :loading="loading" :table="table" :server-pagination="serverPagination" @refresh="fetchRecords">
|
||||||
|
|||||||
Reference in New Issue
Block a user