♻️ refactor(model): replace gorm.io/datatypes with JSONValue for PrefillGroup.Items; fix JSON scan across drivers
- Why: - Avoid introducing `gorm.io/datatypes` for a single field. - Align with existing pattern (`ChannelInfo`, `Properties`) using `Scanner`/`Valuer`. - Fix runtime error when drivers return JSON as string. - What: - Introduced `JSONValue` (based on `json.RawMessage`) implementing `sql.Scanner` and `driver.Valuer`, with `MarshalJSON`/`UnmarshalJSON` to preserve raw JSON in API. - Updated `PrefillGroup.Items` to use `JSONValue` with `gorm:"type:json"`. - Localized comments in `model/prefill_group.go` to Chinese. - Impact: - Resolves “unsupported Scan, storing driver.Value type string into type *json.RawMessage”. - Works with MySQL/Postgres/SQLite whether JSON is returned as `[]byte` or `string`. - API and DB schema remain unchanged; no `go.mod` changes; lints pass. Files changed: - model/prefill_group.go
This commit is contained in:
@@ -2,6 +2,7 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"database/sql/driver"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -14,11 +15,68 @@ import (
|
|||||||
// ["gpt-4o", "gpt-3.5-turbo"]
|
// ["gpt-4o", "gpt-3.5-turbo"]
|
||||||
// 设计遵循 3NF,避免冗余,提供灵活扩展能力。
|
// 设计遵循 3NF,避免冗余,提供灵活扩展能力。
|
||||||
|
|
||||||
|
// JSONValue 基于 json.RawMessage 实现,支持从数据库的 []byte 和 string 两种类型读取
|
||||||
|
type JSONValue json.RawMessage
|
||||||
|
|
||||||
|
// Value 实现 driver.Valuer 接口,用于数据库写入
|
||||||
|
func (j JSONValue) Value() (driver.Value, error) {
|
||||||
|
if j == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return []byte(j), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan 实现 sql.Scanner 接口,兼容不同驱动返回的类型
|
||||||
|
func (j *JSONValue) Scan(value interface{}) error {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case nil:
|
||||||
|
*j = nil
|
||||||
|
return nil
|
||||||
|
case []byte:
|
||||||
|
// 拷贝底层字节,避免保留底层缓冲区
|
||||||
|
b := make([]byte, len(v))
|
||||||
|
copy(b, v)
|
||||||
|
*j = JSONValue(b)
|
||||||
|
return nil
|
||||||
|
case string:
|
||||||
|
*j = JSONValue([]byte(v))
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
// 其他类型尝试序列化为 JSON
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*j = JSONValue(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON 确保在对外编码时与 json.RawMessage 行为一致
|
||||||
|
func (j JSONValue) MarshalJSON() ([]byte, error) {
|
||||||
|
if j == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
return j, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON 确保在对外解码时与 json.RawMessage 行为一致
|
||||||
|
func (j *JSONValue) UnmarshalJSON(data []byte) error {
|
||||||
|
if data == nil {
|
||||||
|
*j = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
b := make([]byte, len(data))
|
||||||
|
copy(b, data)
|
||||||
|
*j = JSONValue(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type PrefillGroup struct {
|
type PrefillGroup struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Name string `json:"name" gorm:"size:64;not null;uniqueIndex:uk_prefill_name,where:deleted_at IS NULL"`
|
Name string `json:"name" gorm:"size:64;not null;uniqueIndex:uk_prefill_name,where:deleted_at IS NULL"`
|
||||||
Type string `json:"type" gorm:"size:32;index;not null"`
|
Type string `json:"type" gorm:"size:32;index;not null"`
|
||||||
Items json.RawMessage `json:"items" gorm:"type:json"`
|
Items JSONValue `json:"items" gorm:"type:json"`
|
||||||
Description string `json:"description,omitempty" gorm:"type:varchar(255)"`
|
Description string `json:"description,omitempty" gorm:"type:varchar(255)"`
|
||||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||||
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
|
UpdatedTime int64 `json:"updated_time" gorm:"bigint"`
|
||||||
|
|||||||
@@ -637,7 +637,7 @@ const JSONEditor = ({
|
|||||||
{/* 额外文本显示在卡片底部 */}
|
{/* 额外文本显示在卡片底部 */}
|
||||||
{extraText && (
|
{extraText && (
|
||||||
<Divider margin='12px' align='center'>
|
<Divider margin='12px' align='center'>
|
||||||
{extraText}
|
<Text type="tertiary" size="small">{extraText}</Text>
|
||||||
</Divider>
|
</Divider>
|
||||||
)}
|
)}
|
||||||
{extraFooter && (
|
{extraFooter && (
|
||||||
|
|||||||
@@ -1247,11 +1247,7 @@ const EditChannelModal = (props) => {
|
|||||||
templateLabel={t('填入模板')}
|
templateLabel={t('填入模板')}
|
||||||
editorType="region"
|
editorType="region"
|
||||||
formApi={formApiRef.current}
|
formApi={formApiRef.current}
|
||||||
extraText={
|
extraText={t('设置默认地区和特定模型的专用地区')}
|
||||||
<Text type="tertiary" size="small">
|
|
||||||
{t('设置默认地区和特定模型的专用地区')}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1520,11 +1516,7 @@ const EditChannelModal = (props) => {
|
|||||||
templateLabel={t('填入模板')}
|
templateLabel={t('填入模板')}
|
||||||
editorType="keyValue"
|
editorType="keyValue"
|
||||||
formApi={formApiRef.current}
|
formApi={formApiRef.current}
|
||||||
extraText={
|
extraText={t('键为请求中的模型名称,值为要替换的模型名称')}
|
||||||
<Text type="tertiary" size="small">
|
|
||||||
{t('键为请求中的模型名称,值为要替换的模型名称')}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -1628,11 +1620,7 @@ const EditChannelModal = (props) => {
|
|||||||
templateLabel={t('填入模板')}
|
templateLabel={t('填入模板')}
|
||||||
editorType="keyValue"
|
editorType="keyValue"
|
||||||
formApi={formApiRef.current}
|
formApi={formApiRef.current}
|
||||||
extraText={
|
extraText={t('键为原状态码,值为要复写的状态码,仅影响本地判断')}
|
||||||
<Text type="tertiary" size="small">
|
|
||||||
{t('键为原状态码,值为要复写的状态码,仅影响本地判断')}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -390,7 +390,7 @@ const EditModelModal = (props) => {
|
|||||||
editorType='object'
|
editorType='object'
|
||||||
template={ENDPOINT_TEMPLATE}
|
template={ENDPOINT_TEMPLATE}
|
||||||
templateLabel={t('填入模板')}
|
templateLabel={t('填入模板')}
|
||||||
extraText={(<Text type="tertiary" size="small">{t('留空则使用默认端点;支持 {path, method}')}</Text>)}
|
extraText={t('留空则使用默认端点;支持 {path, method}')}
|
||||||
extraFooter={endpointGroups.length > 0 && (
|
extraFooter={endpointGroups.length > 0 && (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{endpointGroups.map(group => (
|
{endpointGroups.map(group => (
|
||||||
|
|||||||
Reference in New Issue
Block a user