mirror of
https://github.com/woodchen-ink/aimodels-prices.git
synced 2025-07-18 05:32:00 +08:00
新增模型类型管理功能
- 后端添加模型类型的增删改查接口,并增加管理员权限控制 - 扩展模型类型模型,新增排序字段 - 前端新增模型类型管理页面入口和路由 - 优化模型类型查询,支持按排序字段排序 - 在创建和更新价格时增加模型重复性检查
This commit is contained in:
parent
da79bf3d6d
commit
0bdadcfef7
@ -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"})
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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`
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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',
|
||||
|
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