Service
go
var ErrXxxNotFound = errors.New("service: Xxx 不存在")
var ErrXxxKeyExists = errors.New("service: Xxx key 已存在")
type XxxService struct{}
var XxxSvcApp = new(XxxService)- 每次调用现取 repo:
repository.NewXxxRepository(database.DB()),不缓存成字段。 - 返回 VO(
vo.Conv.ConvertToSysXxx),不把 entity 漏给 handler。 - 分页要 重建
PageResult:两个泛型实例是不同类型。pkgrepo.Page(vo.Conv.ConvertToSysXxxVoList(res.Rows), res.Total) - 错误一律
return err, 由 handler 转 response。需要自定义文案时用errs.New(0, "文案", "")(code 传 0 → 渲染成 500 + 该文案)。
插入:主键必须自己发号
go
add := bo.Conv.ConvertToSysXxx(b)
add.XxxID = snowflake.Next() // 各业务表主键均无 auto_increment
// 审计字段不在此处填:由 pkg/repository 的插入回调统一注入。唯一性校验:excludeID 区分新增与修改
go
func (s *XxxService) CheckXxxKeyUnique(ctx, key string, excludeID int64) (bool, error)
s.CheckXxxKeyUnique(ctx, b.Key, 0) // 新增:无自身可排除
s.CheckXxxKeyUnique(ctx, b.Key, b.ID) // 修改:排除自身,改回原 key 不算冲突⚠️ 存在性判定不要用受影响行数
go
// 先取原记录定存在性,不靠 UpdateByID 的受影响行数:值与库中完全相同时
// MySQL 报 0 行(update_time 是秒精度,同秒内重复提交连它都不变),
// 那会把一次幂等的重复保存误报成"配置不存在"。
old, err := repo.SelectByID(ctx, b.ID)
if err != nil {
if errors.Is(err, repository.ErrXxxNotFound) {
return ErrXxxNotFound
}
return err
}
if _, err := repo.UpdateByID(ctx, b.ID, buildXxxUpdateColumns(b)); err != nil {
return err
}例外:sys_client 的 UpdateClientStatus 有意保留 affected == 0 → ErrNotFound,改状态这类接口按原样即可。
更新列的取舍:可编辑字段一律写,控制字段缺省不改
go
func buildXxxUpdateColumns(b *bo.SysXxxBo) map[string]any {
columns := map[string]any{
"name": b.Name,
// 一律写入,让前端能把备注清空——这正是编辑表单的本意。
"remark": b.Remark,
}
// 缺省即视为不改:漏传字段不该把线上的 'Y' 刷成空串,
// 那会让内置数据失去删除保护。
if b.ConfigType != "" {
columns["config_type"] = b.ConfigType
}
return columns
}判断依据: 该字段被清空是用户的合法意图吗? 是 → 一律写(remark、白名单);否 → 空值跳过(status、timeout、内置标记)。
删除:先整批校验,再删
go
rows, err := repo.SelectByIDs(ctx, ids)
// 先整批校验再删,不边删边校验:一旦先删了几行再撞上内置参数就会留下删一半的状态。
for _, e := range rows {
if e.ConfigType == constant.Yes {
return errs.New(0, fmt.Sprintf("内置参数【%s】不能删除", e.ConfigKey), "")
}
}
if _, err := repo.DeleteByIDs(ctx, ids); err != nil { return err }
// 删除后再失效:提前清缓存会让删除失败时白丢一批热数据。
for _, e := range rows { _ = cache.Evict(ctx, constant.CacheSysXxx, e.Key) }缓存:Java 注解 → Go 调用
| Java 注解 | Go 写法 |
|---|---|
@Cacheable(key="#k") | 读头 cache.Get 命中即返回;miss 查库后 cache.Put |
@CachePut(key="#k") | 写库成功后 cache.Put(ctx, group, k, val, ttl) |
@CacheEvict(key="#k") | cache.Evict(ctx, group, k) |
@CacheEvict(allEntries=true) | cache.EvictGroup(ctx, group) |
| 防击穿的读穿 | cache.GetOrSet(...)(内含 singleflight) |
- 组名与 TTL 加在
pkg/constant/cache_names.go。TTL 取值看 JavaCacheNames的#ttl后缀:sys_client#30d→30*24*time.Hour, 无后缀 →0(不过期)。 - 缓存写 / 删(
Put/Evict/EvictGroup) 一律_ =忽略错误(fail-open,Redis 挂了要能继续走 DB)。但Get返回两个值, 不能_ = cache.Get(...)——用hit, _ := cache.Get(ctx, group, key, &dest)。 - 改了 key 的更新, 旧 key 要单独 Evict——
@CachePut只写新 key,旧的会成孤儿。 - Java
@Cacheable缓存的是返回值, 「查不到」的空串也会入缓存,照搬即可(写路径的@CachePut会覆盖)。