新增模型类型管理功能

- 后端添加模型类型的增删改查接口,并增加管理员权限控制
- 扩展模型类型模型,新增排序字段
- 前端新增模型类型管理页面入口和路由
- 优化模型类型查询,支持按排序字段排序
- 在创建和更新价格时增加模型重复性检查
This commit is contained in:
wood chen 2025-03-06 23:16:18 +08:00
parent da79bf3d6d
commit 0bdadcfef7
7 changed files with 320 additions and 15 deletions

View File

@ -2,6 +2,7 @@ package handlers
import (
"database/sql"
"net/http"
"github.com/gin-gonic/gin"
@ -12,9 +13,9 @@ import (
func GetModelTypes(c *gin.Context) {
db := c.MustGet("db").(*sql.DB)
rows, err := db.Query("SELECT type_key, type_label FROM model_type")
rows, err := db.Query("SELECT type_key, type_label, sort_order FROM model_type ORDER BY sort_order ASC, type_key ASC")
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer rows.Close()
@ -22,14 +23,14 @@ func GetModelTypes(c *gin.Context) {
var types []models.ModelType
for rows.Next() {
var t models.ModelType
if err := rows.Scan(&t.TypeKey, &t.TypeLabel); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
if err := rows.Scan(&t.TypeKey, &t.TypeLabel, &t.SortOrder); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
types = append(types, t)
}
c.JSON(200, types)
c.JSON(http.StatusOK, types)
}
// CreateModelType 添加新的模型类型
@ -38,20 +39,106 @@ func CreateModelType(c *gin.Context) {
var newType models.ModelType
if err := c.ShouldBindJSON(&newType); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
_, err := db.Exec(`
INSERT INTO model_type (type_key, type_label)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE type_label = VALUES(type_label)
`, newType.TypeKey, newType.TypeLabel)
INSERT INTO model_type (type_key, type_label, sort_order)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE type_label = VALUES(type_label), sort_order = VALUES(sort_order)
`, newType.TypeKey, newType.TypeLabel, newType.SortOrder)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(201, newType)
c.JSON(http.StatusCreated, newType)
}
// UpdateModelType 更新模型类型
func UpdateModelType(c *gin.Context) {
db := c.MustGet("db").(*sql.DB)
typeKey := c.Param("key")
var updateType models.ModelType
if err := c.ShouldBindJSON(&updateType); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 如果key发生变化需要删除旧记录并创建新记录
if typeKey != updateType.TypeKey {
tx, err := db.Begin()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to begin transaction"})
return
}
// 删除旧记录
_, err = tx.Exec("DELETE FROM model_type WHERE type_key = ?", typeKey)
if err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete old model type"})
return
}
// 创建新记录
_, err = tx.Exec(`
INSERT INTO model_type (type_key, type_label, sort_order)
VALUES (?, ?, ?)
`, updateType.TypeKey, updateType.TypeLabel, updateType.SortOrder)
if err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create new model type"})
return
}
if err := tx.Commit(); err != nil {
tx.Rollback()
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to commit transaction"})
return
}
} else {
// 直接更新
_, err := db.Exec(`
UPDATE model_type
SET type_label = ?, sort_order = ?
WHERE type_key = ?
`, updateType.TypeLabel, updateType.SortOrder, typeKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update model type"})
return
}
}
c.JSON(http.StatusOK, updateType)
}
// DeleteModelType 删除模型类型
func DeleteModelType(c *gin.Context) {
db := c.MustGet("db").(*sql.DB)
typeKey := c.Param("key")
// 检查是否有价格记录使用此类型
var count int
err := db.QueryRow("SELECT COUNT(*) FROM price WHERE model_type = ?", typeKey).Scan(&count)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check model type usage"})
return
}
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot delete model type that is in use"})
return
}
_, err = db.Exec("DELETE FROM model_type WHERE type_key = ?", typeKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete model type"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Model type deleted successfully"})
}

View File

@ -118,6 +118,19 @@ func CreatePrice(c *gin.Context) {
return
}
// 检查同一厂商下是否已存在相同名称的模型
var modelExists bool
err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM price WHERE channel_type = ? AND model = ? AND status = 'approved')",
price.ChannelType, price.Model).Scan(&modelExists)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check model existence"})
return
}
if modelExists {
c.JSON(http.StatusBadRequest, gin.H{"error": "Model with the same name already exists for this provider"})
return
}
now := time.Now()
result, err := db.Exec(`
INSERT INTO price (model, model_type, billing_type, channel_type, currency, input_price, output_price,
@ -229,6 +242,19 @@ func UpdatePrice(c *gin.Context) {
return
}
// 检查同一厂商下是否已存在相同名称的模型(排除当前正在编辑的记录)
var modelExists bool
err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM price WHERE channel_type = ? AND model = ? AND id != ? AND status = 'approved')",
price.ChannelType, price.Model, id).Scan(&modelExists)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check model existence"})
return
}
if modelExists {
c.JSON(http.StatusBadRequest, gin.H{"error": "Model with the same name already exists for this provider"})
return
}
// 获取当前用户
user, exists := c.Get("user")
if !exists {

View File

@ -94,7 +94,9 @@ func main() {
modelTypes := api.Group("/model-types")
{
modelTypes.GET("", handlers.GetModelTypes)
modelTypes.POST("", middleware.AuthRequired(), handlers.CreateModelType)
modelTypes.POST("", middleware.AuthRequired(), middleware.AdminRequired(), handlers.CreateModelType)
modelTypes.PUT("/:key", middleware.AuthRequired(), middleware.AdminRequired(), handlers.UpdateModelType)
modelTypes.DELETE("/:key", middleware.AuthRequired(), middleware.AdminRequired(), handlers.DeleteModelType)
}
}

View File

@ -4,6 +4,7 @@ package models
type ModelType struct {
TypeKey string `json:"key"`
TypeLabel string `json:"label"`
SortOrder int `json:"sort_order"`
}
// CreateModelTypeTableSQL 返回创建模型类型表的 SQL
@ -11,6 +12,7 @@ func CreateModelTypeTableSQL() string {
return `
CREATE TABLE IF NOT EXISTS model_type (
type_key VARCHAR(50) PRIMARY KEY,
type_label VARCHAR(255) NOT NULL
type_label VARCHAR(255) NOT NULL,
sort_order INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
}

View File

@ -9,6 +9,7 @@
<div class="nav-buttons">
<el-button @click="$router.push('/prices')" :type="$route.path === '/prices' ? 'primary' : ''">价格列表</el-button>
<el-button @click="$router.push('/providers')" :type="$route.path === '/providers' ? 'primary' : ''">模型厂商</el-button>
<el-button @click="$router.push('/model-types')" :type="$route.path === '/model-types' ? 'primary' : ''">模型类别</el-button>
</div>
</div>
<div class="auth-buttons">
@ -35,7 +36,7 @@
<el-footer height="60px">
<div class="footer-content">
<p>© 2025 Q58 AI模型价格 | <a href="https://q58.club/t/topic/277?u=wood" target="_blank">介绍帖子</a></p>
<p>© 2025 Q58 AI模型价格 | <a href="https://www.q58.club/t/topic/277?u=wood" target="_blank">介绍帖子</a></p>
</div>
</el-footer>
</el-container>

View File

@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import Prices from '../views/Prices.vue'
import Providers from '../views/Providers.vue'
import ModelTypes from '../views/ModelTypes.vue'
import Login from '../views/Login.vue'
import Home from '../views/Home.vue'
@ -22,6 +23,11 @@ const router = createRouter({
name: 'providers',
component: Providers
},
{
path: '/model-types',
name: 'modelTypes',
component: ModelTypes
},
{
path: '/login',
name: 'login',

View File

@ -0,0 +1,181 @@
<template>
<div class="model-types">
<el-card v-loading="loading" element-loading-text="加载中...">
<template #header>
<div class="card-header">
<span>模型类别</span>
<el-button v-if="isAdmin" type="primary" @click="handleAdd">添加模型类别</el-button>
</div>
</template>
<el-table
:data="modelTypes"
style="width: 100%"
v-loading="tableLoading"
element-loading-text="加载中..."
row-key="key"
>
<el-table-column prop="key" label="类别键值" width="180" />
<el-table-column prop="label" label="类别名称" width="180" />
<el-table-column prop="sort_order" label="排序" width="100" />
<el-table-column v-if="isAdmin" label="操作" width="200">
<template #default="{ row }">
<el-button-group>
<el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</el-button-group>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" :title="editingType ? '编辑模型类别' : '添加模型类别'" width="500px">
<el-form :model="form" label-width="100px">
<el-form-item label="类别键值">
<el-input v-model="form.key" placeholder="请输入类别键值" :disabled="!!editingType" />
</el-form-item>
<el-form-item label="类别名称">
<el-input v-model="form.label" placeholder="请输入类别名称" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sort_order" :min="0" :step="1" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import axios from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
const props = defineProps({
user: Object
})
const modelTypes = ref([])
const dialogVisible = ref(false)
const editingType = ref(null)
const form = ref({
key: '',
label: '',
sort_order: 0
})
const loading = ref(true)
const tableLoading = ref(false)
const isAdmin = computed(() => props.user?.role === 'admin')
//
const fetchModelTypes = async () => {
try {
tableLoading.value = true
const response = await axios.get('/api/model-types')
modelTypes.value = response.data
} catch (error) {
ElMessage.error('获取模型类别失败')
console.error(error)
} finally {
loading.value = false
tableLoading.value = false
}
}
//
const handleAdd = () => {
editingType.value = null
form.value = {
key: '',
label: '',
sort_order: 0
}
dialogVisible.value = true
}
//
const handleEdit = (row) => {
editingType.value = row
form.value = {
key: row.key,
label: row.label,
sort_order: row.sort_order
}
dialogVisible.value = true
}
//
const handleDelete = (row) => {
ElMessageBox.confirm(
'确定要删除此模型类别吗?如果有价格记录使用此类别,将无法删除。',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
try {
await axios.delete(`/api/model-types/${row.key}`)
ElMessage.success('删除成功')
fetchModelTypes()
} catch (error) {
ElMessage.error(error.response?.data?.error || '删除失败')
}
})
.catch(() => {
//
})
}
//
const submitForm = async () => {
try {
if (editingType.value) {
//
await axios.put(`/api/model-types/${editingType.value.key}`, {
key: form.value.key,
label: form.value.label,
sort_order: form.value.sort_order
})
ElMessage.success('更新成功')
} else {
//
await axios.post('/api/model-types', {
key: form.value.key,
label: form.value.label,
sort_order: form.value.sort_order
})
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchModelTypes()
} catch (error) {
ElMessage.error(error.response?.data?.error || '操作失败')
}
}
onMounted(() => {
fetchModelTypes()
})
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
</style>