Skip to content

Service

go
var ErrXxxNotFound  = errors.New("service: Xxx 不存在")
var ErrXxxKeyExists = errors.New("service: Xxx key 已存在")

type XxxService struct{}
var XxxSvcApp = new(XxxService)
  • 每次调用现取 reporepository.NewXxxRepository(database.DB()),不缓存成字段。
  • 返回 VOvo.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_clientUpdateClientStatus 有意保留 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 取值看 Java CacheNames#ttl 后缀:sys_client#30d30*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 会覆盖)。

基于 MIT 协议开源