从 PHP 到 AI + Golang,程序员自救转型手记(四十七):列表通用排序接口实现(增量重排法)
这是一个系列 Blog,作者将以一个 PHP 全栈工程师的身份,利用 AI 工具(claude code、codex、deepseek、豆包等):从零开始学习 golang 语言,并最终完成 ai-go-admin(github | gitee)开源项目的制作,全程记录分享。
在上一期,我们进行了 “再次优化基类”,本期将完成:列表通用排序接口实现(增量重排法)
列表通用排序接口实现(增量重排法)
实现初版的提示词
继续啃BuildAdmin的老本:
参考@../badmin-v2.3.7-full/app/admin/library/traits/Backend.php中的sortable方法,在当前项目的基类(@internal/handler/base.go和@internal/repository/base.go和@internal/service/base.go)中,实现sort方法,并注册路由,前端传递的参数有:
move: moveRow[table.pk!], // 移动行 target: targetRow[table.pk!], // 目标行 sort: table.filter?.sort, // 排序字段(权重字段) order: table.filter?.order, // 排序方式 direction: evt.newIndex>evt.oldIndex ?'down':'up', // 拖拽方向- 忽略参考代码中的
$dataLimitAdminIds - 忽略参考代码中的 “当前是否以权重字段排序” 检查(即:只检查当前排序和默认排序字段,不检查有序保证字段)
功能需求分析
前端拖动排序,如:将权重为1的行(排序行),拖动至权重为5的行(目标权重行或目标行)
所谓增量重排法,也可以称做区间位移法:实现的是将涉及到的行,权重值全部按算法重设,以达到视觉上的排序调整
比起全量重排,增量重排法影响范围更小,比起交换重排,重排后的顺序更加合理。
难点/注意点如下:
- 复用
List列表方法的排序和过滤规则,确保执行重新排序时,查得的数据列表和前端的一样。 - 只支持以
weigh(权重)字段排序时,进行重排操作(字段名是可以自定义的;只使用权重字段,是因为重排是修改权重字段值实现的,假设以创建时间排序,重排功能不可能去修改创建时间)。 - 用户当前可能以
weigh asc和weigh desc两种方式进行排序,并且可能将排序行向上拖或者向下拖,需要全部考虑到位。 - 目标行的权重值,不只一行:比如目标行权重为
5,但数据表中权重值为5的行有很多。 - 不要一个 SQL 修改一行,而是尽量一个 SQL 完成一类权重值的全部修改:比如
权重值 > 目标权重的行全部+1,不能一条一条的遍历去改。
AI 实现后
控制器层的代码很简单,主要是新增了Sort方法及其请求体结构体定义,并注册了sort路由。
主要功能实现是在服务层,所以服务层问题也最多:
weigh 字段名不固定
实现排序功能期间,需要读取目标行的权重值,但字段名是不固定的,go 里边没有$row[$weigh]这种写法,所以 AI 使用了反射的方式去读取权重值,反射首先是实现复杂,二是性能一般(拖拽排序其实无需考虑性能),但我们还是直接改为从前端传递目标行权重值即可,服务端直接读取并使用,去掉反射相关代码。
AI 根据需求实现后,接受的前端变量名为target_weigh,由于我们服务端不需要使用排序行的权重值,只需要目标行的权重值,这里将target_weigh改名为更简洁的weigh即可。
并且,AI 将weigh的类型定义为any,这里直接固定为int64。
改为 Generics API
在这种需要比较复杂的 update 语句时,AI 又开始忘记AGENTS.md中的规则了,这里强行要求它改为Generics API:
// 原来的iferr:=tx.Model(new(T)).Where(weighField+" "+bulkOp+" ?",weigh).Where(pkField+" <> ?",move).UpdateColumn(weighField,gorm.Expr(weighField+" "+updateOp+" ?",weighRowsCount)).Error;err!=nil{returnerr}// 改为gtx:=gorm.G[T](tx)iferr:=gtx.Where(weighField+" "+bulkOp+" ?",weigh).Where(pkField+" <> ?",move).UpdateColumn(weighField,gorm.Expr(weighField+" "+updateOp+" ?",weighRowsCount));err!=nil{returnerr}// 且 gtx 后续复用另外,重排实现内,必需使用一下传统 API 的Pluck方法,当然也可以使用Generics API的Select方法去选择字段,只是这样又需要反射读取结构体中数据库列名对应的字段值了,没必要。
反转切片代码优化
实现重排时,有一段反转数组的代码:
// 向下拖动时反转,保证等权重区间内相对顺序不变ifdirection=="down"{fori,j:=0,len(weighIDs)-1;i<j;i,j=i+1,j-1{weighIDs[i],weighIDs[j]=weighIDs[j],weighIDs[i]}}在Go 1.25 +提供了slices.Reverse方法,可以直接使用,简化为:
// 向下拖动时反转,保证等权重区间内相对顺序不变ifdirection=="down"{slices.Reverse(weighIDs)}多余的封装
AI 帮忙封装了一个exprOffset函数,用于组装一些排序用的查询表达式,但其对整体无益,且不被Sort方法以外的地方复用,这里让它直接改为:在Sort方法内封装为闭包函数即可,不要放在Sort方法外边。
BUG
我在测试前,首先让 AI 直接再写一点调试代码,打印出Sort方法内执行的所有SQL,然后前端进行操作,对比我的需求与执行的SQL,发现一处排序错误;
代码我们已经review过了,没有发现明显问题,应该是某处细节上的逻辑错误,这里直接把日志给 AI,并加以描述:
提示词:好像有 BUG,排序后有权重值重复的行,我有权重值为1,2,3的行,入参move=1 target=3时,执行的SQL有以下:
2026/07/25 00:19:34 C:/Users/yang/Desktop/admin/internal/service/base.go:148[0.709ms][rows:1]SELECT"id"FROM"admin_rules"WHERE weigh=3ORDER BY weigh DESC,id DESC2026/07/25 00:19:34 C:/Users/yang/Desktop/admin/internal/service/base.go:171[0.525ms][rows:0]UPDATE"admin_rules"SET"weigh"=weigh -1,"updated_at"='2026-07-25 00:19:34.578'WHERE weigh>3ANDid<>'1'2026/07/25 00:19:34 C:/Users/yang/Desktop/admin/internal/service/base.go:201[0.628ms][rows:1]UPDATE"admin_rules"SET"weigh"=3,"updated_at"='2026-07-25 00:19:34.578'WHEREid='1'2026/07/25 00:19:34 C:/Users/yang/Desktop/admin/internal/service/base.go:213[0.541ms][rows:1]UPDATE"admin_rules"SET"weigh"=2,"updated_at"='2026-07-25 00:19:34.579'WHEREid='3'最终结果是:有两行的权重值均为2,这与目标不符,应该不出现重复的权重才对,对比参考代码:@../badmin-v2.3.7-full/app/admin/library/traits/Backend.php的sortable方法,找出原因:
找到原因了。对比PHP代码的bulk update逻辑:
PHP(正确):
// updateMethod == 'dec' → WHERE weigh < target// updateMethod == 'inc' → WHERE weigh > target->where(weigh,$updateMethod=='dec'?'<':'>',$weigh)Go(错误):
// updateOp == "-" → bulkOp = ">" ← 反了!// updateOp == "+" → bulkOp = "<"bulkOp:=">"ifupdateOp=="+"{bulkOp="<"}dec(权重 −1)应该影响权重比目标小的行(<),让它们再减,给拖动行腾位置。我把 > 和 < 写反了。
嚯,AI 居然还会犯这种逻辑错误。
