aiDoc/examples/backend/service-example.md
Service 层负责业务逻辑、数据库查询、事务控制和数据拼装,不负责 HTTP 参数绑定和响应输出。
package system
import (
"context"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
)
type OrderService struct{}
func (s *OrderService) GetOrderList(ctx context.Context, info systemReq.OrderSearch) (list []system.Order, total int64, err error) {
limit, offset := info.LimitOffset()
db := global.GVA_DB.WithContext(ctx).Model(&system.Order{})
if info.Name != "" {
db = db.Where("name LIKE ?", "%"+info.Name+"%")
}
if info.Status != nil {
db = db.Where("status = ?", *info.Status)
}
err = db.Count(&total).Error
if err != nil {
return nil, 0, err
}
err = db.Limit(limit).Offset(offset).Order("id desc").Find(&list).Error
return list, total, err
}
数据权限(行级过滤)由统一引擎的 GORM 全局回调实现(server/utils/datascope/),
Service 层不写任何范围过滤条件。以下示例均摘自真实代码,直接照此写。
查询——和普通表完全一样(server/service/example/exa_customer.go 的 GetCustomerInfoList):
func (exa *CustomerService) GetCustomerInfoList(ctx context.Context, sysUserAuthorityID uint, info request.PageInfo) (list interface{}, total int64, err error) {
_ = sysUserAuthorityID // 数据范围已由数据权限引擎接管, 旧的按角色 DataAuthorityId 手写过滤已移除
limit, offset := info.LimitOffset()
// 数据范围(本部门及子级/仅本人)由数据权限引擎的 GORM 回调按当前角色 data_scope
// 自动追加 dept_id/created_by 过滤, 此处只写常规分页查询即可。
db := global.GVA_DB.WithContext(ctx).Model(&example.ExaCustomer{})
var CustomerList []example.ExaCustomer
if err = db.Count(&total).Error; err != nil {
return CustomerList, total, err
}
err = db.Limit(limit).Offset(offset).Preload("SysUser").Find(&CustomerList).Error
return CustomerList, total, err
}
创建同样零特殊代码(CreateExaCustomer 就是一句普通 Create),created_by/dept_id
由 create 回调自动盖章;更新时用 Omit 保护归属列(UpdateExaCustomer):
func (exa *CustomerService) UpdateExaCustomer(ctx context.Context, e *example.ExaCustomer) (err error) {
// 归属列(dept_id/created_by)创建时盖章后即不可变, 更新时忽略, 防止被表单未回传的零值覆盖
err = global.GVA_DB.WithContext(ctx).Omit("dept_id", "created_by").Save(e).Error
return err
}
两种合法旁路(均摘自真实代码):
// 1) 显式旁路:引擎自身或确需跨范围的查询
// (server/service/system/data_scope.go 的 BuildIdentity,旁路以避免回调递归)
err := global.GVA_DB.WithContext(ctx).Set("data_scope:skip", true).
Select("authority_id", "data_scope").
Where("authority_id = ?", authorityID).First(&auth).Error
// 2) 系统上下文:定时任务/CLI/初始化等无请求身份的场景(server/initialize/timer.go)
sysDB := global.GVA_DB.WithContext(datascope.WithSystem(context.Background()))
必须遵守的规则:
WithContext(ctx) 不可省,且 ctx 必须一路来自 c.Request.Context():身份由 DataScope 中间件注入 request context,引擎回调从中读取;漏传 ctx 等于旁路数据权限(现阶段会放行,但响亮告警并落审计表 sys_data_access_logs)dept_id / created_by 的权限过滤 WHERE:引擎会按角色档位自动追加,手写会重复甚至冲突CreatedBy / DeptId:引擎在 create 回调里按当前身份自动盖章UpdatedBy / DeletedBy 同样不手动赋值:表有 updated_by 列则每次更新自动盖;表同时有 deleted_by 列与 gorm.DeletedAt 时,软删除的那条 UPDATE 会同时 SET deleted_at + deleted_by(硬删除 / Unscoped 不盖)。盖章只在有请求身份时发生:无身份、WithSystem 系统上下文、UpdateColumn(SkipHooks)等场景静默不盖Save 等全量写时 Omit("dept_id", "created_by"):归属列创建盖章后不可变,防止被表单未回传的零值覆盖;updated_by 不要放进 Omit,它靠引擎每次更新自动刷新db.Set("data_scope:skip", true) 显式旁路datascope.WithSystem(ctx) 标记系统上下文放行,不要裸用 context.Background()error,便于 API 层统一处理ctx context.Context 为首参并用 global.GVA_DB.WithContext(ctx) 查询,串联请求链路(API 层传 c.Request.Context())info.LimitOffset()(request.PageInfo 提供),pageSize 超过 MaxPageSize(100) 会自动截断,不要手写换算global.GVA_DB 或事务对象,保持项目一致性gin.Contextlimit := info.PageSize; offset := info.PageSize * (info.Page - 1) 换算,绕过 LimitOffset() 的上限保护dept_id/created_by 过滤条件,或漏传 ctx 导致引擎拿不到身份(旁路数据权限)response.OkWith...server/service/system/sys_user.goserver/service/example/exa_customer.go(GetCustomerInfoList 是数据权限表的标准查询范例)server/utils/datascope/datascope.go(数据权限引擎:档位、护栏与旁路机制)