Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
5fa93ebd
Unverified
Commit
5fa93ebd
authored
Feb 08, 2026
by
Wesley Liddick
Committed by
GitHub
Feb 08, 2026
Browse files
Merge pull request #519 from bayma888/feature/group-sort-order
feat(admin): 新增-分组管理自由拖拽排序功能
parents
e4d74ae1
8aa0aed5
Changes
35
Hide whitespace changes
Inline
Side-by-side
DEV_GUIDE.md
0 → 100644
View file @
5fa93ebd
# sub2api 项目开发指南
> 本文档记录项目环境配置、常见坑点和注意事项,供 Claude Code 和团队成员参考。
## 一、项目基本信息
| 项目 | 说明 |
|------|------|
|
**上游仓库**
| Wei-Shaw/sub2api |
|
**Fork 仓库**
| bayma888/sub2api-bmai |
|
**技术栈**
| Go 后端 (Ent ORM + Gin) + Vue3 前端 (pnpm) |
|
**数据库**
| PostgreSQL 16 + Redis |
|
**包管理**
| 后端: go modules, 前端:
**pnpm**
(不是 npm) |
## 二、本地环境配置
### PostgreSQL 16 (Windows 服务)
| 配置项 | 值 |
|--------|-----|
| 端口 | 5432 |
| psql 路径 |
`C:\Program Files\PostgreSQL\16\bin\psql.exe`
|
| pg_hba.conf |
`C:\Program Files\PostgreSQL\16\data\pg_hba.conf`
|
| 数据库凭据 | user=
`sub2api`
, password=
`sub2api`
, dbname=
`sub2api`
|
| 超级用户 | user=
`postgres`
, password=
`postgres`
|
### Redis
| 配置项 | 值 |
|--------|-----|
| 端口 | 6379 |
| 密码 | 无 |
### 开发工具
```
bash
# golangci-lint v2.7
go
install
github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7
# pnpm (前端包管理)
npm
install
-g
pnpm
```
## 三、CI/CD 流水线
### GitHub Actions Workflows
| Workflow | 触发条件 | 检查内容 |
|----------|----------|----------|
|
**backend-ci.yml**
| push, pull_request | 单元测试 + 集成测试 + golangci-lint v2.7 |
|
**security-scan.yml**
| push, pull_request, 每周一 | govulncheck + gosec + pnpm audit |
|
**release.yml**
| tag
`v*`
| 构建发布(PR 不触发) |
### CI 要求
-
Go 版本必须是
**1.25.7**
-
前端使用
`pnpm install --frozen-lockfile`
,必须提交
`pnpm-lock.yaml`
### 本地测试命令
```
bash
# 后端单元测试
cd
backend
&&
go
test
-tags
=
unit ./...
# 后端集成测试
cd
backend
&&
go
test
-tags
=
integration ./...
# 代码质量检查
cd
backend
&&
golangci-lint run ./...
# 前端依赖安装(必须用 pnpm)
cd
frontend
&&
pnpm
install
```
## 四、常见坑点 & 解决方案
### 坑 1:pnpm-lock.yaml 必须同步提交
**问题**
:
`package.json`
新增依赖后,CI 的
`pnpm install --frozen-lockfile`
失败。
**原因**
:上游 CI 使用 pnpm,lock 文件不同步会报错。
**解决**
:
```
bash
cd
frontend
pnpm
install
# 更新 pnpm-lock.yaml
git add pnpm-lock.yaml
git commit
-m
"chore: update pnpm-lock.yaml"
```
---
### 坑 2:npm 和 pnpm 的 node_modules 冲突
**问题**
:之前用 npm 装过
`node_modules`
,pnpm install 报
`EPERM`
错误。
**解决**
:
```
bash
cd
frontend
rm
-rf
node_modules
# 或 PowerShell: Remove-Item -Recurse -Force node_modules
pnpm
install
```
---
### 坑 3:PowerShell 中 bcrypt hash 的 `$` 被转义
**问题**
:bcrypt hash 格式如
`$2a$10$xxx...`
,PowerShell 把
`$2a`
当变量解析,导致数据丢失。
**解决**
:将 SQL 写入文件,用
`psql -f`
执行:
```
bash
# 错误示范(PowerShell 会吃掉 $)
psql
-c
"INSERT INTO users ... VALUES ('
$2a$10$.
..')"
# 正确做法
echo
"INSERT INTO users ... VALUES ('
\$
2a
\$
10
\$
...')"
>
temp.sql
psql
-U
sub2api
-h
127.0.0.1
-d
sub2api
-f
temp.sql
```
---
### 坑 4:psql 不支持中文路径
**问题**
:
`psql -f "D:\中文路径\file.sql"`
报错找不到文件。
**解决**
:复制到纯英文路径再执行:
```
bash
cp
"D:
\中
文路径
\f
ile.sql"
"C:
\t
emp.sql"
psql
-f
"C:
\t
emp.sql"
```
---
### 坑 5:PostgreSQL 密码重置流程
**场景**
:忘记 PostgreSQL 密码。
**步骤**
:
1.
修改
`C:\Program Files\PostgreSQL\16\data\pg_hba.conf`
```
# 将 scram-sha-256 改为 trust
host all all 127.0.0.1/32 trust
```
2.
重启 PostgreSQL 服务
```
powershell
Restart-Service
postgresql-x64-16
```
3.
无密码登录并重置
```
bash
psql
-U
postgres
-h
127.0.0.1
ALTER USER sub2api WITH PASSWORD
'sub2api'
;
ALTER USER postgres WITH PASSWORD
'postgres'
;
```
4.
改回
`scram-sha-256`
并重启
---
### 坑 6:Go interface 新增方法后 test stub 必须补全
**问题**
:给 interface 新增方法后,编译报错
`does not implement interface (missing method XXX)`
。
**原因**
:所有测试文件中实现该 interface 的 stub/mock 都必须补上新方法。
**解决**
:
```
bash
# 搜索所有实现该 interface 的 struct
cd
backend
grep
-r
"type.*Stub.*struct"
internal/
grep
-r
"type.*Mock.*struct"
internal/
# 逐一补全新方法
```
---
### 坑 7:Windows 上 psql 连 localhost 的 IPv6 问题
**问题**
:psql 连
`localhost`
先尝试 IPv6 (::1),可能报错后再回退 IPv4。
**建议**
:直接用
`127.0.0.1`
代替
`localhost`
。
---
### 坑 8:Windows 没有 make 命令
**问题**
:CI 里用
`make test-unit`
,本地 Windows 没有 make。
**解决**
:直接用 Makefile 里的原始命令:
```
bash
# 代替 make test-unit
go
test
-tags
=
unit ./...
# 代替 make test-integration
go
test
-tags
=
integration ./...
```
---
### 坑 9:Ent Schema 修改后必须重新生成
**问题**
:修改
`ent/schema/*.go`
后,代码不生效。
**解决**
:
```
bash
cd
backend
go generate ./ent
# 重新生成 ent 代码
git add ent/
# 生成的文件也要提交
```
---
### 坑 10:PR 提交前检查清单
提交 PR 前务必本地验证:
-
[ ]
`go test -tags=unit ./...`
通过
-
[ ]
`go test -tags=integration ./...`
通过
-
[ ]
`golangci-lint run ./...`
无新增问题
-
[ ]
`pnpm-lock.yaml`
已同步(如果改了 package.json)
-
[ ] 所有 test stub 补全新接口方法(如果改了 interface)
-
[ ] Ent 生成的代码已提交(如果改了 schema)
## 五、常用命令速查
### 数据库操作
```
bash
# 连接数据库
psql
-U
sub2api
-h
127.0.0.1
-d
sub2api
# 查看所有用户
psql
-U
postgres
-h
127.0.0.1
-c
"
\d
u"
# 查看所有数据库
psql
-U
postgres
-h
127.0.0.1
-c
"
\l
"
# 执行 SQL 文件
psql
-U
sub2api
-h
127.0.0.1
-d
sub2api
-f
migration.sql
```
### Git 操作
```
bash
# 同步上游
git fetch upstream
git checkout main
git merge upstream/main
git push origin main
# 创建功能分支
git checkout
-b
feature/xxx
# Rebase 到最新 main
git fetch upstream
git rebase upstream/main
```
### 前端操作
```
bash
# 安装依赖(必须用 pnpm)
cd
frontend
pnpm
install
# 开发服务器
pnpm dev
# 构建
pnpm build
```
### 后端操作
```
bash
# 运行服务器
cd
backend
go run ./cmd/server/
# 生成 Ent 代码
go generate ./ent
# 运行测试
go
test
-tags
=
unit ./...
go
test
-tags
=
integration ./...
# Lint 检查
golangci-lint run ./...
```
## 六、项目结构速览
```
sub2api-bmai/
├── backend/
│ ├── cmd/server/ # 主程序入口
│ ├── ent/ # Ent ORM 生成代码
│ │ └── schema/ # 数据库 Schema 定义
│ ├── internal/
│ │ ├── handler/ # HTTP 处理器
│ │ ├── service/ # 业务逻辑
│ │ ├── repository/ # 数据访问层
│ │ └── server/ # 服务器配置
│ ├── migrations/ # 数据库迁移脚本
│ └── config.yaml # 配置文件
├── frontend/
│ ├── src/
│ │ ├── api/ # API 调用
│ │ ├── components/ # Vue 组件
│ │ ├── views/ # 页面视图
│ │ ├── types/ # TypeScript 类型
│ │ └── i18n/ # 国际化
│ ├── package.json # 依赖配置
│ └── pnpm-lock.yaml # pnpm 锁文件(必须提交)
└── .claude/
└── CLAUDE.md # 本文档
```
## 七、参考资源
-
[
上游仓库
](
https://github.com/Wei-Shaw/sub2api
)
-
[
Ent 文档
](
https://entgo.io/docs/getting-started
)
-
[
Vue3 文档
](
https://vuejs.org/
)
-
[
pnpm 文档
](
https://pnpm.io/
)
backend/ent/group.go
View file @
5fa93ebd
...
...
@@ -66,6 +66,8 @@ type Group struct {
McpXMLInject
bool
`json:"mcp_xml_inject,omitempty"`
// 支持的模型系列:claude, gemini_text, gemini_image
SupportedModelScopes
[]
string
`json:"supported_model_scopes,omitempty"`
// 分组显示排序,数值越小越靠前
SortOrder
int
`json:"sort_order,omitempty"`
// Edges holds the relations/edges for other nodes in the graph.
// The values are being populated by the GroupQuery when eager-loading is set.
Edges
GroupEdges
`json:"edges"`
...
...
@@ -178,7 +180,7 @@ func (*Group) scanValues(columns []string) ([]any, error) {
values
[
i
]
=
new
(
sql
.
NullBool
)
case
group
.
FieldRateMultiplier
,
group
.
FieldDailyLimitUsd
,
group
.
FieldWeeklyLimitUsd
,
group
.
FieldMonthlyLimitUsd
,
group
.
FieldImagePrice1k
,
group
.
FieldImagePrice2k
,
group
.
FieldImagePrice4k
:
values
[
i
]
=
new
(
sql
.
NullFloat64
)
case
group
.
FieldID
,
group
.
FieldDefaultValidityDays
,
group
.
FieldFallbackGroupID
,
group
.
FieldFallbackGroupIDOnInvalidRequest
:
case
group
.
FieldID
,
group
.
FieldDefaultValidityDays
,
group
.
FieldFallbackGroupID
,
group
.
FieldFallbackGroupIDOnInvalidRequest
,
group
.
FieldSortOrder
:
values
[
i
]
=
new
(
sql
.
NullInt64
)
case
group
.
FieldName
,
group
.
FieldDescription
,
group
.
FieldStatus
,
group
.
FieldPlatform
,
group
.
FieldSubscriptionType
:
values
[
i
]
=
new
(
sql
.
NullString
)
...
...
@@ -363,6 +365,12 @@ func (_m *Group) assignValues(columns []string, values []any) error {
return
fmt
.
Errorf
(
"unmarshal field supported_model_scopes: %w"
,
err
)
}
}
case
group
.
FieldSortOrder
:
if
value
,
ok
:=
values
[
i
]
.
(
*
sql
.
NullInt64
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field sort_order"
,
values
[
i
])
}
else
if
value
.
Valid
{
_m
.
SortOrder
=
int
(
value
.
Int64
)
}
default
:
_m
.
selectValues
.
Set
(
columns
[
i
],
values
[
i
])
}
...
...
@@ -530,6 +538,9 @@ func (_m *Group) String() string {
builder
.
WriteString
(
", "
)
builder
.
WriteString
(
"supported_model_scopes="
)
builder
.
WriteString
(
fmt
.
Sprintf
(
"%v"
,
_m
.
SupportedModelScopes
))
builder
.
WriteString
(
", "
)
builder
.
WriteString
(
"sort_order="
)
builder
.
WriteString
(
fmt
.
Sprintf
(
"%v"
,
_m
.
SortOrder
))
builder
.
WriteByte
(
')'
)
return
builder
.
String
()
}
...
...
backend/ent/group/group.go
View file @
5fa93ebd
...
...
@@ -63,6 +63,8 @@ const (
FieldMcpXMLInject
=
"mcp_xml_inject"
// FieldSupportedModelScopes holds the string denoting the supported_model_scopes field in the database.
FieldSupportedModelScopes
=
"supported_model_scopes"
// FieldSortOrder holds the string denoting the sort_order field in the database.
FieldSortOrder
=
"sort_order"
// EdgeAPIKeys holds the string denoting the api_keys edge name in mutations.
EdgeAPIKeys
=
"api_keys"
// EdgeRedeemCodes holds the string denoting the redeem_codes edge name in mutations.
...
...
@@ -162,6 +164,7 @@ var Columns = []string{
FieldModelRoutingEnabled
,
FieldMcpXMLInject
,
FieldSupportedModelScopes
,
FieldSortOrder
,
}
var
(
...
...
@@ -225,6 +228,8 @@ var (
DefaultMcpXMLInject
bool
// DefaultSupportedModelScopes holds the default value on creation for the "supported_model_scopes" field.
DefaultSupportedModelScopes
[]
string
// DefaultSortOrder holds the default value on creation for the "sort_order" field.
DefaultSortOrder
int
)
// OrderOption defines the ordering options for the Group queries.
...
...
@@ -345,6 +350,11 @@ func ByMcpXMLInject(opts ...sql.OrderTermOption) OrderOption {
return
sql
.
OrderByField
(
FieldMcpXMLInject
,
opts
...
)
.
ToFunc
()
}
// BySortOrder orders the results by the sort_order field.
func
BySortOrder
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldSortOrder
,
opts
...
)
.
ToFunc
()
}
// ByAPIKeysCount orders the results by api_keys count.
func
ByAPIKeysCount
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
func
(
s
*
sql
.
Selector
)
{
...
...
backend/ent/group/where.go
View file @
5fa93ebd
...
...
@@ -165,6 +165,11 @@ func McpXMLInject(v bool) predicate.Group {
return
predicate
.
Group
(
sql
.
FieldEQ
(
FieldMcpXMLInject
,
v
))
}
// SortOrder applies equality check predicate on the "sort_order" field. It's identical to SortOrderEQ.
func
SortOrder
(
v
int
)
predicate
.
Group
{
return
predicate
.
Group
(
sql
.
FieldEQ
(
FieldSortOrder
,
v
))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
func
CreatedAtEQ
(
v
time
.
Time
)
predicate
.
Group
{
return
predicate
.
Group
(
sql
.
FieldEQ
(
FieldCreatedAt
,
v
))
...
...
@@ -1160,6 +1165,46 @@ func McpXMLInjectNEQ(v bool) predicate.Group {
return
predicate
.
Group
(
sql
.
FieldNEQ
(
FieldMcpXMLInject
,
v
))
}
// SortOrderEQ applies the EQ predicate on the "sort_order" field.
func
SortOrderEQ
(
v
int
)
predicate
.
Group
{
return
predicate
.
Group
(
sql
.
FieldEQ
(
FieldSortOrder
,
v
))
}
// SortOrderNEQ applies the NEQ predicate on the "sort_order" field.
func
SortOrderNEQ
(
v
int
)
predicate
.
Group
{
return
predicate
.
Group
(
sql
.
FieldNEQ
(
FieldSortOrder
,
v
))
}
// SortOrderIn applies the In predicate on the "sort_order" field.
func
SortOrderIn
(
vs
...
int
)
predicate
.
Group
{
return
predicate
.
Group
(
sql
.
FieldIn
(
FieldSortOrder
,
vs
...
))
}
// SortOrderNotIn applies the NotIn predicate on the "sort_order" field.
func
SortOrderNotIn
(
vs
...
int
)
predicate
.
Group
{
return
predicate
.
Group
(
sql
.
FieldNotIn
(
FieldSortOrder
,
vs
...
))
}
// SortOrderGT applies the GT predicate on the "sort_order" field.
func
SortOrderGT
(
v
int
)
predicate
.
Group
{
return
predicate
.
Group
(
sql
.
FieldGT
(
FieldSortOrder
,
v
))
}
// SortOrderGTE applies the GTE predicate on the "sort_order" field.
func
SortOrderGTE
(
v
int
)
predicate
.
Group
{
return
predicate
.
Group
(
sql
.
FieldGTE
(
FieldSortOrder
,
v
))
}
// SortOrderLT applies the LT predicate on the "sort_order" field.
func
SortOrderLT
(
v
int
)
predicate
.
Group
{
return
predicate
.
Group
(
sql
.
FieldLT
(
FieldSortOrder
,
v
))
}
// SortOrderLTE applies the LTE predicate on the "sort_order" field.
func
SortOrderLTE
(
v
int
)
predicate
.
Group
{
return
predicate
.
Group
(
sql
.
FieldLTE
(
FieldSortOrder
,
v
))
}
// HasAPIKeys applies the HasEdge predicate on the "api_keys" edge.
func
HasAPIKeys
()
predicate
.
Group
{
return
predicate
.
Group
(
func
(
s
*
sql
.
Selector
)
{
...
...
backend/ent/group_create.go
View file @
5fa93ebd
...
...
@@ -340,6 +340,20 @@ func (_c *GroupCreate) SetSupportedModelScopes(v []string) *GroupCreate {
return
_c
}
// SetSortOrder sets the "sort_order" field.
func
(
_c
*
GroupCreate
)
SetSortOrder
(
v
int
)
*
GroupCreate
{
_c
.
mutation
.
SetSortOrder
(
v
)
return
_c
}
// SetNillableSortOrder sets the "sort_order" field if the given value is not nil.
func
(
_c
*
GroupCreate
)
SetNillableSortOrder
(
v
*
int
)
*
GroupCreate
{
if
v
!=
nil
{
_c
.
SetSortOrder
(
*
v
)
}
return
_c
}
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func
(
_c
*
GroupCreate
)
AddAPIKeyIDs
(
ids
...
int64
)
*
GroupCreate
{
_c
.
mutation
.
AddAPIKeyIDs
(
ids
...
)
...
...
@@ -521,6 +535,10 @@ func (_c *GroupCreate) defaults() error {
v
:=
group
.
DefaultSupportedModelScopes
_c
.
mutation
.
SetSupportedModelScopes
(
v
)
}
if
_
,
ok
:=
_c
.
mutation
.
SortOrder
();
!
ok
{
v
:=
group
.
DefaultSortOrder
_c
.
mutation
.
SetSortOrder
(
v
)
}
return
nil
}
...
...
@@ -585,6 +603,9 @@ func (_c *GroupCreate) check() error {
if
_
,
ok
:=
_c
.
mutation
.
SupportedModelScopes
();
!
ok
{
return
&
ValidationError
{
Name
:
"supported_model_scopes"
,
err
:
errors
.
New
(
`ent: missing required field "Group.supported_model_scopes"`
)}
}
if
_
,
ok
:=
_c
.
mutation
.
SortOrder
();
!
ok
{
return
&
ValidationError
{
Name
:
"sort_order"
,
err
:
errors
.
New
(
`ent: missing required field "Group.sort_order"`
)}
}
return
nil
}
...
...
@@ -708,6 +729,10 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
_spec
.
SetField
(
group
.
FieldSupportedModelScopes
,
field
.
TypeJSON
,
value
)
_node
.
SupportedModelScopes
=
value
}
if
value
,
ok
:=
_c
.
mutation
.
SortOrder
();
ok
{
_spec
.
SetField
(
group
.
FieldSortOrder
,
field
.
TypeInt
,
value
)
_node
.
SortOrder
=
value
}
if
nodes
:=
_c
.
mutation
.
APIKeysIDs
();
len
(
nodes
)
>
0
{
edge
:=
&
sqlgraph
.
EdgeSpec
{
Rel
:
sqlgraph
.
O2M
,
...
...
@@ -1266,6 +1291,24 @@ func (u *GroupUpsert) UpdateSupportedModelScopes() *GroupUpsert {
return
u
}
// SetSortOrder sets the "sort_order" field.
func
(
u
*
GroupUpsert
)
SetSortOrder
(
v
int
)
*
GroupUpsert
{
u
.
Set
(
group
.
FieldSortOrder
,
v
)
return
u
}
// UpdateSortOrder sets the "sort_order" field to the value that was provided on create.
func
(
u
*
GroupUpsert
)
UpdateSortOrder
()
*
GroupUpsert
{
u
.
SetExcluded
(
group
.
FieldSortOrder
)
return
u
}
// AddSortOrder adds v to the "sort_order" field.
func
(
u
*
GroupUpsert
)
AddSortOrder
(
v
int
)
*
GroupUpsert
{
u
.
Add
(
group
.
FieldSortOrder
,
v
)
return
u
}
// UpdateNewValues updates the mutable fields using the new values that were set on create.
// Using this option is equivalent to using:
//
...
...
@@ -1780,6 +1823,27 @@ func (u *GroupUpsertOne) UpdateSupportedModelScopes() *GroupUpsertOne {
})
}
// SetSortOrder sets the "sort_order" field.
func
(
u
*
GroupUpsertOne
)
SetSortOrder
(
v
int
)
*
GroupUpsertOne
{
return
u
.
Update
(
func
(
s
*
GroupUpsert
)
{
s
.
SetSortOrder
(
v
)
})
}
// AddSortOrder adds v to the "sort_order" field.
func
(
u
*
GroupUpsertOne
)
AddSortOrder
(
v
int
)
*
GroupUpsertOne
{
return
u
.
Update
(
func
(
s
*
GroupUpsert
)
{
s
.
AddSortOrder
(
v
)
})
}
// UpdateSortOrder sets the "sort_order" field to the value that was provided on create.
func
(
u
*
GroupUpsertOne
)
UpdateSortOrder
()
*
GroupUpsertOne
{
return
u
.
Update
(
func
(
s
*
GroupUpsert
)
{
s
.
UpdateSortOrder
()
})
}
// Exec executes the query.
func
(
u
*
GroupUpsertOne
)
Exec
(
ctx
context
.
Context
)
error
{
if
len
(
u
.
create
.
conflict
)
==
0
{
...
...
@@ -2460,6 +2524,27 @@ func (u *GroupUpsertBulk) UpdateSupportedModelScopes() *GroupUpsertBulk {
})
}
// SetSortOrder sets the "sort_order" field.
func
(
u
*
GroupUpsertBulk
)
SetSortOrder
(
v
int
)
*
GroupUpsertBulk
{
return
u
.
Update
(
func
(
s
*
GroupUpsert
)
{
s
.
SetSortOrder
(
v
)
})
}
// AddSortOrder adds v to the "sort_order" field.
func
(
u
*
GroupUpsertBulk
)
AddSortOrder
(
v
int
)
*
GroupUpsertBulk
{
return
u
.
Update
(
func
(
s
*
GroupUpsert
)
{
s
.
AddSortOrder
(
v
)
})
}
// UpdateSortOrder sets the "sort_order" field to the value that was provided on create.
func
(
u
*
GroupUpsertBulk
)
UpdateSortOrder
()
*
GroupUpsertBulk
{
return
u
.
Update
(
func
(
s
*
GroupUpsert
)
{
s
.
UpdateSortOrder
()
})
}
// Exec executes the query.
func
(
u
*
GroupUpsertBulk
)
Exec
(
ctx
context
.
Context
)
error
{
if
u
.
create
.
err
!=
nil
{
...
...
backend/ent/group_update.go
View file @
5fa93ebd
...
...
@@ -475,6 +475,27 @@ func (_u *GroupUpdate) AppendSupportedModelScopes(v []string) *GroupUpdate {
return
_u
}
// SetSortOrder sets the "sort_order" field.
func
(
_u
*
GroupUpdate
)
SetSortOrder
(
v
int
)
*
GroupUpdate
{
_u
.
mutation
.
ResetSortOrder
()
_u
.
mutation
.
SetSortOrder
(
v
)
return
_u
}
// SetNillableSortOrder sets the "sort_order" field if the given value is not nil.
func
(
_u
*
GroupUpdate
)
SetNillableSortOrder
(
v
*
int
)
*
GroupUpdate
{
if
v
!=
nil
{
_u
.
SetSortOrder
(
*
v
)
}
return
_u
}
// AddSortOrder adds value to the "sort_order" field.
func
(
_u
*
GroupUpdate
)
AddSortOrder
(
v
int
)
*
GroupUpdate
{
_u
.
mutation
.
AddSortOrder
(
v
)
return
_u
}
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func
(
_u
*
GroupUpdate
)
AddAPIKeyIDs
(
ids
...
int64
)
*
GroupUpdate
{
_u
.
mutation
.
AddAPIKeyIDs
(
ids
...
)
...
...
@@ -912,6 +933,12 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
sqljson
.
Append
(
u
,
group
.
FieldSupportedModelScopes
,
value
)
})
}
if
value
,
ok
:=
_u
.
mutation
.
SortOrder
();
ok
{
_spec
.
SetField
(
group
.
FieldSortOrder
,
field
.
TypeInt
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
AddedSortOrder
();
ok
{
_spec
.
AddField
(
group
.
FieldSortOrder
,
field
.
TypeInt
,
value
)
}
if
_u
.
mutation
.
APIKeysCleared
()
{
edge
:=
&
sqlgraph
.
EdgeSpec
{
Rel
:
sqlgraph
.
O2M
,
...
...
@@ -1666,6 +1693,27 @@ func (_u *GroupUpdateOne) AppendSupportedModelScopes(v []string) *GroupUpdateOne
return
_u
}
// SetSortOrder sets the "sort_order" field.
func
(
_u
*
GroupUpdateOne
)
SetSortOrder
(
v
int
)
*
GroupUpdateOne
{
_u
.
mutation
.
ResetSortOrder
()
_u
.
mutation
.
SetSortOrder
(
v
)
return
_u
}
// SetNillableSortOrder sets the "sort_order" field if the given value is not nil.
func
(
_u
*
GroupUpdateOne
)
SetNillableSortOrder
(
v
*
int
)
*
GroupUpdateOne
{
if
v
!=
nil
{
_u
.
SetSortOrder
(
*
v
)
}
return
_u
}
// AddSortOrder adds value to the "sort_order" field.
func
(
_u
*
GroupUpdateOne
)
AddSortOrder
(
v
int
)
*
GroupUpdateOne
{
_u
.
mutation
.
AddSortOrder
(
v
)
return
_u
}
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func
(
_u
*
GroupUpdateOne
)
AddAPIKeyIDs
(
ids
...
int64
)
*
GroupUpdateOne
{
_u
.
mutation
.
AddAPIKeyIDs
(
ids
...
)
...
...
@@ -2133,6 +2181,12 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
sqljson
.
Append
(
u
,
group
.
FieldSupportedModelScopes
,
value
)
})
}
if
value
,
ok
:=
_u
.
mutation
.
SortOrder
();
ok
{
_spec
.
SetField
(
group
.
FieldSortOrder
,
field
.
TypeInt
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
AddedSortOrder
();
ok
{
_spec
.
AddField
(
group
.
FieldSortOrder
,
field
.
TypeInt
,
value
)
}
if
_u
.
mutation
.
APIKeysCleared
()
{
edge
:=
&
sqlgraph
.
EdgeSpec
{
Rel
:
sqlgraph
.
O2M
,
...
...
backend/ent/migrate/schema.go
View file @
5fa93ebd
...
...
@@ -372,6 +372,7 @@ var (
{
Name
:
"model_routing_enabled"
,
Type
:
field
.
TypeBool
,
Default
:
false
},
{
Name
:
"mcp_xml_inject"
,
Type
:
field
.
TypeBool
,
Default
:
true
},
{
Name
:
"supported_model_scopes"
,
Type
:
field
.
TypeJSON
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"jsonb"
}},
{
Name
:
"sort_order"
,
Type
:
field
.
TypeInt
,
Default
:
0
},
}
// GroupsTable holds the schema information for the "groups" table.
GroupsTable
=
&
schema
.
Table
{
...
...
@@ -404,6 +405,11 @@ var (
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
GroupsColumns
[
3
]},
},
{
Name
:
"group_sort_order"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
GroupsColumns
[
25
]},
},
},
}
// PromoCodesColumns holds the columns for the "promo_codes" table.
...
...
backend/ent/mutation.go
View file @
5fa93ebd
...
...
@@ -7059,6 +7059,8 @@ type GroupMutation struct {
mcp_xml_inject *bool
supported_model_scopes *[]string
appendsupported_model_scopes []string
sort_order *int
addsort_order *int
clearedFields map[string]struct{}
api_keys map[int64]struct{}
removedapi_keys map[int64]struct{}
...
...
@@ -8411,6 +8413,62 @@ func (m *GroupMutation) ResetSupportedModelScopes() {
m.appendsupported_model_scopes = nil
}
// SetSortOrder sets the "sort_order" field.
func (m *GroupMutation) SetSortOrder(i int) {
m.sort_order = &i
m.addsort_order = nil
}
// SortOrder returns the value of the "sort_order" field in the mutation.
func (m *GroupMutation) SortOrder() (r int, exists bool) {
v := m.sort_order
if v == nil {
return
}
return *v, true
}
// OldSortOrder returns the old "sort_order" field's value of the Group entity.
// If the Group object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *GroupMutation) OldSortOrder(ctx context.Context) (v int, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldSortOrder is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldSortOrder requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldSortOrder: %w", err)
}
return oldValue.SortOrder, nil
}
// AddSortOrder adds i to the "sort_order" field.
func (m *GroupMutation) AddSortOrder(i int) {
if m.addsort_order != nil {
*m.addsort_order += i
} else {
m.addsort_order = &i
}
}
// AddedSortOrder returns the value that was added to the "sort_order" field in this mutation.
func (m *GroupMutation) AddedSortOrder() (r int, exists bool) {
v := m.addsort_order
if v == nil {
return
}
return *v, true
}
// ResetSortOrder resets all changes to the "sort_order" field.
func (m *GroupMutation) ResetSortOrder() {
m.sort_order = nil
m.addsort_order = nil
}
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by ids.
func (m *GroupMutation) AddAPIKeyIDs(ids ...int64) {
if m.api_keys == nil {
...
...
@@ -8769,7 +8827,7 @@ func (m *GroupMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *GroupMutation) Fields() []string {
fields := make([]string, 0, 2
4
)
fields := make([]string, 0, 2
5
)
if m.created_at != nil {
fields = append(fields, group.FieldCreatedAt)
}
...
...
@@ -8842,6 +8900,9 @@ func (m *GroupMutation) Fields() []string {
if m.supported_model_scopes != nil {
fields = append(fields, group.FieldSupportedModelScopes)
}
if m.sort_order != nil {
fields = append(fields, group.FieldSortOrder)
}
return fields
}
...
...
@@ -8898,6 +8959,8 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) {
return m.McpXMLInject()
case group.FieldSupportedModelScopes:
return m.SupportedModelScopes()
case group.FieldSortOrder:
return m.SortOrder()
}
return nil, false
}
...
...
@@ -8955,6 +9018,8 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e
return m.OldMcpXMLInject(ctx)
case group.FieldSupportedModelScopes:
return m.OldSupportedModelScopes(ctx)
case group.FieldSortOrder:
return m.OldSortOrder(ctx)
}
return nil, fmt.Errorf("unknown Group field %s", name)
}
...
...
@@ -9132,6 +9197,13 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error {
}
m.SetSupportedModelScopes(v)
return nil
case group.FieldSortOrder:
v, ok := value.(int)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetSortOrder(v)
return nil
}
return fmt.Errorf("unknown Group field %s", name)
}
...
...
@@ -9170,6 +9242,9 @@ func (m *GroupMutation) AddedFields() []string {
if m.addfallback_group_id_on_invalid_request != nil {
fields = append(fields, group.FieldFallbackGroupIDOnInvalidRequest)
}
if m.addsort_order != nil {
fields = append(fields, group.FieldSortOrder)
}
return fields
}
...
...
@@ -9198,6 +9273,8 @@ func (m *GroupMutation) AddedField(name string) (ent.Value, bool) {
return m.AddedFallbackGroupID()
case group.FieldFallbackGroupIDOnInvalidRequest:
return m.AddedFallbackGroupIDOnInvalidRequest()
case group.FieldSortOrder:
return m.AddedSortOrder()
}
return nil, false
}
...
...
@@ -9277,6 +9354,13 @@ func (m *GroupMutation) AddField(name string, value ent.Value) error {
}
m.AddFallbackGroupIDOnInvalidRequest(v)
return nil
case group.FieldSortOrder:
v, ok := value.(int)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.AddSortOrder(v)
return nil
}
return fmt.Errorf("unknown Group numeric field %s", name)
}
...
...
@@ -9445,6 +9529,9 @@ func (m *GroupMutation) ResetField(name string) error {
case group.FieldSupportedModelScopes:
m.ResetSupportedModelScopes()
return nil
case group.FieldSortOrder:
m.ResetSortOrder()
return nil
}
return fmt.Errorf("unknown Group field %s", name)
}
...
...
backend/ent/runtime/runtime.go
View file @
5fa93ebd
...
...
@@ -409,6 +409,10 @@ func init() {
groupDescSupportedModelScopes
:=
groupFields
[
20
]
.
Descriptor
()
// group.DefaultSupportedModelScopes holds the default value on creation for the supported_model_scopes field.
group
.
DefaultSupportedModelScopes
=
groupDescSupportedModelScopes
.
Default
.
([]
string
)
// groupDescSortOrder is the schema descriptor for sort_order field.
groupDescSortOrder
:=
groupFields
[
21
]
.
Descriptor
()
// group.DefaultSortOrder holds the default value on creation for the sort_order field.
group
.
DefaultSortOrder
=
groupDescSortOrder
.
Default
.
(
int
)
promocodeFields
:=
schema
.
PromoCode
{}
.
Fields
()
_
=
promocodeFields
// promocodeDescCode is the schema descriptor for code field.
...
...
backend/ent/schema/group.go
View file @
5fa93ebd
...
...
@@ -121,6 +121,11 @@ func (Group) Fields() []ent.Field {
Default
([]
string
{
"claude"
,
"gemini_text"
,
"gemini_image"
})
.
SchemaType
(
map
[
string
]
string
{
dialect
.
Postgres
:
"jsonb"
})
.
Comment
(
"支持的模型系列:claude, gemini_text, gemini_image"
),
// 分组排序 (added by migration 052)
field
.
Int
(
"sort_order"
)
.
Default
(
0
)
.
Comment
(
"分组显示排序,数值越小越靠前"
),
}
}
...
...
@@ -149,5 +154,6 @@ func (Group) Indexes() []ent.Index {
index
.
Fields
(
"subscription_type"
),
index
.
Fields
(
"is_exclusive"
),
index
.
Fields
(
"deleted_at"
),
index
.
Fields
(
"sort_order"
),
}
}
backend/go.sum
View file @
5fa93ebd
...
...
@@ -135,6 +135,8 @@ github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/imroc/req/v3 v3.57.0 h1:LMTUjNRUybUkTPn8oJDq8Kg3JRBOBTcnDhKu7mzupKI=
github.com/imroc/req/v3 v3.57.0/go.mod h1:JL62ey1nvSLq81HORNcosvlf7SxZStONNqOprg0Pz00=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
...
...
@@ -170,6 +172,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
...
...
@@ -203,6 +207,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
...
...
@@ -230,6 +236,8 @@ github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkr
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
...
...
@@ -252,6 +260,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
...
...
backend/internal/handler/admin/admin_service_stub_test.go
View file @
5fa93ebd
...
...
@@ -357,5 +357,9 @@ func (s *stubAdminService) GetUserBalanceHistory(ctx context.Context, userID int
return
s
.
redeems
,
int64
(
len
(
s
.
redeems
)),
100.0
,
nil
}
func
(
s
*
stubAdminService
)
UpdateGroupSortOrders
(
ctx
context
.
Context
,
updates
[]
service
.
GroupSortOrderUpdate
)
error
{
return
nil
}
// Ensure stub implements interface.
var
_
service
.
AdminService
=
(
*
stubAdminService
)(
nil
)
backend/internal/handler/admin/group_handler.go
View file @
5fa93ebd
...
...
@@ -302,3 +302,36 @@ func (h *GroupHandler) GetGroupAPIKeys(c *gin.Context) {
}
response
.
Paginated
(
c
,
outKeys
,
total
,
page
,
pageSize
)
}
// UpdateSortOrderRequest represents the request to update group sort orders
type
UpdateSortOrderRequest
struct
{
Updates
[]
struct
{
ID
int64
`json:"id" binding:"required"`
SortOrder
int
`json:"sort_order"`
}
`json:"updates" binding:"required,min=1"`
}
// UpdateSortOrder handles updating group sort orders
// PUT /api/v1/admin/groups/sort-order
func
(
h
*
GroupHandler
)
UpdateSortOrder
(
c
*
gin
.
Context
)
{
var
req
UpdateSortOrderRequest
if
err
:=
c
.
ShouldBindJSON
(
&
req
);
err
!=
nil
{
response
.
BadRequest
(
c
,
"Invalid request: "
+
err
.
Error
())
return
}
updates
:=
make
([]
service
.
GroupSortOrderUpdate
,
0
,
len
(
req
.
Updates
))
for
_
,
u
:=
range
req
.
Updates
{
updates
=
append
(
updates
,
service
.
GroupSortOrderUpdate
{
ID
:
u
.
ID
,
SortOrder
:
u
.
SortOrder
,
})
}
if
err
:=
h
.
adminService
.
UpdateGroupSortOrders
(
c
.
Request
.
Context
(),
updates
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
response
.
Success
(
c
,
gin
.
H
{
"message"
:
"Sort order updated successfully"
})
}
backend/internal/handler/dto/mappers.go
View file @
5fa93ebd
...
...
@@ -115,6 +115,7 @@ func GroupFromServiceAdmin(g *service.Group) *AdminGroup {
MCPXMLInject
:
g
.
MCPXMLInject
,
SupportedModelScopes
:
g
.
SupportedModelScopes
,
AccountCount
:
g
.
AccountCount
,
SortOrder
:
g
.
SortOrder
,
}
if
len
(
g
.
AccountGroups
)
>
0
{
out
.
AccountGroups
=
make
([]
AccountGroup
,
0
,
len
(
g
.
AccountGroups
))
...
...
backend/internal/handler/dto/types.go
View file @
5fa93ebd
...
...
@@ -98,6 +98,9 @@ type AdminGroup struct {
SupportedModelScopes
[]
string
`json:"supported_model_scopes"`
AccountGroups
[]
AccountGroup
`json:"account_groups,omitempty"`
AccountCount
int64
`json:"account_count,omitempty"`
// 分组排序
SortOrder
int
`json:"sort_order"`
}
type
Account
struct
{
...
...
backend/internal/repository/api_key_repo.go
View file @
5fa93ebd
...
...
@@ -485,6 +485,7 @@ func groupEntityToService(g *dbent.Group) *service.Group {
ModelRoutingEnabled
:
g
.
ModelRoutingEnabled
,
MCPXMLInject
:
g
.
McpXMLInject
,
SupportedModelScopes
:
g
.
SupportedModelScopes
,
SortOrder
:
g
.
SortOrder
,
CreatedAt
:
g
.
CreatedAt
,
UpdatedAt
:
g
.
UpdatedAt
,
}
...
...
backend/internal/repository/group_repo.go
View file @
5fa93ebd
...
...
@@ -191,7 +191,7 @@ func (r *groupRepository) ListWithFilters(ctx context.Context, params pagination
groups
,
err
:=
q
.
Offset
(
params
.
Offset
())
.
Limit
(
params
.
Limit
())
.
Order
(
dbent
.
Asc
(
group
.
FieldID
))
.
Order
(
dbent
.
Asc
(
group
.
FieldSortOrder
),
dbent
.
Asc
(
group
.
FieldID
))
.
All
(
ctx
)
if
err
!=
nil
{
return
nil
,
nil
,
err
...
...
@@ -218,7 +218,7 @@ func (r *groupRepository) ListWithFilters(ctx context.Context, params pagination
func
(
r
*
groupRepository
)
ListActive
(
ctx
context
.
Context
)
([]
service
.
Group
,
error
)
{
groups
,
err
:=
r
.
client
.
Group
.
Query
()
.
Where
(
group
.
StatusEQ
(
service
.
StatusActive
))
.
Order
(
dbent
.
Asc
(
group
.
FieldID
))
.
Order
(
dbent
.
Asc
(
group
.
FieldSortOrder
),
dbent
.
Asc
(
group
.
FieldID
))
.
All
(
ctx
)
if
err
!=
nil
{
return
nil
,
err
...
...
@@ -245,7 +245,7 @@ func (r *groupRepository) ListActive(ctx context.Context) ([]service.Group, erro
func
(
r
*
groupRepository
)
ListActiveByPlatform
(
ctx
context
.
Context
,
platform
string
)
([]
service
.
Group
,
error
)
{
groups
,
err
:=
r
.
client
.
Group
.
Query
()
.
Where
(
group
.
StatusEQ
(
service
.
StatusActive
),
group
.
PlatformEQ
(
platform
))
.
Order
(
dbent
.
Asc
(
group
.
FieldID
))
.
Order
(
dbent
.
Asc
(
group
.
FieldSortOrder
),
dbent
.
Asc
(
group
.
FieldID
))
.
All
(
ctx
)
if
err
!=
nil
{
return
nil
,
err
...
...
@@ -497,3 +497,29 @@ func (r *groupRepository) BindAccountsToGroup(ctx context.Context, groupID int64
return
nil
}
// UpdateSortOrders 批量更新分组排序
func
(
r
*
groupRepository
)
UpdateSortOrders
(
ctx
context
.
Context
,
updates
[]
service
.
GroupSortOrderUpdate
)
error
{
if
len
(
updates
)
==
0
{
return
nil
}
// 使用事务批量更新
tx
,
err
:=
r
.
client
.
Tx
(
ctx
)
if
err
!=
nil
{
return
err
}
defer
func
()
{
_
=
tx
.
Rollback
()
}()
for
_
,
u
:=
range
updates
{
if
_
,
err
:=
tx
.
Group
.
UpdateOneID
(
u
.
ID
)
.
SetSortOrder
(
u
.
SortOrder
)
.
Save
(
ctx
);
err
!=
nil
{
return
translatePersistenceError
(
err
,
service
.
ErrGroupNotFound
,
nil
)
}
}
if
err
:=
tx
.
Commit
();
err
!=
nil
{
return
err
}
return
nil
}
backend/internal/server/api_contract_test.go
View file @
5fa93ebd
...
...
@@ -896,6 +896,10 @@ func (stubGroupRepo) GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int
return
nil
,
errors
.
New
(
"not implemented"
)
}
func
(
stubGroupRepo
)
UpdateSortOrders
(
ctx
context
.
Context
,
updates
[]
service
.
GroupSortOrderUpdate
)
error
{
return
nil
}
type
stubAccountRepo
struct
{
bulkUpdateIDs
[]
int64
}
...
...
backend/internal/server/routes/admin.go
View file @
5fa93ebd
...
...
@@ -192,6 +192,7 @@ func registerGroupRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
{
groups
.
GET
(
""
,
h
.
Admin
.
Group
.
List
)
groups
.
GET
(
"/all"
,
h
.
Admin
.
Group
.
GetAll
)
groups
.
PUT
(
"/sort-order"
,
h
.
Admin
.
Group
.
UpdateSortOrder
)
groups
.
GET
(
"/:id"
,
h
.
Admin
.
Group
.
GetByID
)
groups
.
POST
(
""
,
h
.
Admin
.
Group
.
Create
)
groups
.
PUT
(
"/:id"
,
h
.
Admin
.
Group
.
Update
)
...
...
backend/internal/service/admin_service.go
View file @
5fa93ebd
...
...
@@ -36,6 +36,7 @@ type AdminService interface {
UpdateGroup
(
ctx
context
.
Context
,
id
int64
,
input
*
UpdateGroupInput
)
(
*
Group
,
error
)
DeleteGroup
(
ctx
context
.
Context
,
id
int64
)
error
GetGroupAPIKeys
(
ctx
context
.
Context
,
groupID
int64
,
page
,
pageSize
int
)
([]
APIKey
,
int64
,
error
)
UpdateGroupSortOrders
(
ctx
context
.
Context
,
updates
[]
GroupSortOrderUpdate
)
error
// Account management
ListAccounts
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
accountType
,
status
,
search
string
)
([]
Account
,
int64
,
error
)
...
...
@@ -1015,6 +1016,10 @@ func (s *adminServiceImpl) GetGroupAPIKeys(ctx context.Context, groupID int64, p
return
keys
,
result
.
Total
,
nil
}
func
(
s
*
adminServiceImpl
)
UpdateGroupSortOrders
(
ctx
context
.
Context
,
updates
[]
GroupSortOrderUpdate
)
error
{
return
s
.
groupRepo
.
UpdateSortOrders
(
ctx
,
updates
)
}
// Account management implementations
func
(
s
*
adminServiceImpl
)
ListAccounts
(
ctx
context
.
Context
,
page
,
pageSize
int
,
platform
,
accountType
,
status
,
search
string
)
([]
Account
,
int64
,
error
)
{
params
:=
pagination
.
PaginationParams
{
Page
:
page
,
PageSize
:
pageSize
}
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment