mirror of
https://github.com/woodchen-ink/aimodels-prices.git
synced 2025-07-18 21:51:59 +08:00
新增模型类型管理功能
- 后端添加模型类型的增删改查接口,并增加管理员权限控制 - 扩展模型类型模型,新增排序字段 - 前端新增模型类型管理页面入口和路由 - 优化模型类型查询,支持按排序字段排序 - 在创建和更新价格时增加模型重复性检查
This commit is contained in:
parent
da79bf3d6d
commit
0bdadcfef7
@ -2,6 +2,7 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
@ -12,9 +13,9 @@ import (
|
|||||||
func GetModelTypes(c *gin.Context) {
|
func GetModelTypes(c *gin.Context) {
|
||||||
db := c.MustGet("db").(*sql.DB)
|
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 {
|
if err != nil {
|
||||||
c.JSON(500, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
@ -22,14 +23,14 @@ func GetModelTypes(c *gin.Context) {
|
|||||||
var types []models.ModelType
|
var types []models.ModelType
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var t models.ModelType
|
var t models.ModelType
|
||||||
if err := rows.Scan(&t.TypeKey, &t.TypeLabel); err != nil {
|
if err := rows.Scan(&t.TypeKey, &t.TypeLabel, &t.SortOrder); err != nil {
|
||||||
c.JSON(500, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
types = append(types, t)
|
types = append(types, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, types)
|
c.JSON(http.StatusOK, types)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateModelType 添加新的模型类型
|
// CreateModelType 添加新的模型类型
|
||||||
@ -38,20 +39,106 @@ func CreateModelType(c *gin.Context) {
|
|||||||
|
|
||||||
var newType models.ModelType
|
var newType models.ModelType
|
||||||
if err := c.ShouldBindJSON(&newType); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := db.Exec(`
|
_, err := db.Exec(`
|
||||||
INSERT INTO model_type (type_key, type_label)
|
INSERT INTO model_type (type_key, type_label, sort_order)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE type_label = VALUES(type_label)
|
ON DUPLICATE KEY UPDATE type_label = VALUES(type_label), sort_order = VALUES(sort_order)
|
||||||
`, newType.TypeKey, newType.TypeLabel)
|
`, newType.TypeKey, newType.TypeLabel, newType.SortOrder)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
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"})
|
||||||
}
|
}
|
||||||
|
@ -118,6 +118,19 @@ func CreatePrice(c *gin.Context) {
|
|||||||
return
|
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()
|
now := time.Now()
|
||||||
result, err := db.Exec(`
|
result, err := db.Exec(`
|
||||||
INSERT INTO price (model, model_type, billing_type, channel_type, currency, input_price, output_price,
|
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
|
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")
|
user, exists := c.Get("user")
|
||||||
if !exists {
|
if !exists {
|
||||||
|
@ -94,7 +94,9 @@ func main() {
|
|||||||
modelTypes := api.Group("/model-types")
|
modelTypes := api.Group("/model-types")
|
||||||
{
|
{
|
||||||
modelTypes.GET("", handlers.GetModelTypes)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ package models
|
|||||||
type ModelType struct {
|
type ModelType struct {
|
||||||
TypeKey string `json:"key"`
|
TypeKey string `json:"key"`
|
||||||
TypeLabel string `json:"label"`
|
TypeLabel string `json:"label"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateModelTypeTableSQL 返回创建模型类型表的 SQL
|
// CreateModelTypeTableSQL 返回创建模型类型表的 SQL
|
||||||
@ -11,6 +12,7 @@ func CreateModelTypeTableSQL() string {
|
|||||||
return `
|
return `
|
||||||
CREATE TABLE IF NOT EXISTS model_type (
|
CREATE TABLE IF NOT EXISTS model_type (
|
||||||
type_key VARCHAR(50) PRIMARY KEY,
|
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`
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
<div class="nav-buttons">
|
<div class="nav-buttons">
|
||||||
<el-button @click="$router.push('/prices')" :type="$route.path === '/prices' ? 'primary' : ''">价格列表</el-button>
|
<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('/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>
|
</div>
|
||||||
<div class="auth-buttons">
|
<div class="auth-buttons">
|
||||||
@ -35,7 +36,7 @@
|
|||||||
|
|
||||||
<el-footer height="60px">
|
<el-footer height="60px">
|
||||||
<div class="footer-content">
|
<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>
|
</div>
|
||||||
</el-footer>
|
</el-footer>
|
||||||
</el-container>
|
</el-container>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Prices from '../views/Prices.vue'
|
import Prices from '../views/Prices.vue'
|
||||||
import Providers from '../views/Providers.vue'
|
import Providers from '../views/Providers.vue'
|
||||||
|
import ModelTypes from '../views/ModelTypes.vue'
|
||||||
import Login from '../views/Login.vue'
|
import Login from '../views/Login.vue'
|
||||||
import Home from '../views/Home.vue'
|
import Home from '../views/Home.vue'
|
||||||
|
|
||||||
@ -22,6 +23,11 @@ const router = createRouter({
|
|||||||
name: 'providers',
|
name: 'providers',
|
||||||
component: Providers
|
component: Providers
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/model-types',
|
||||||
|
name: 'modelTypes',
|
||||||
|
component: ModelTypes
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
|
181
frontend/src/views/ModelTypes.vue
Normal file
181
frontend/src/views/ModelTypes.vue
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user