项目专属AI开发规范手册.md 38 KB

项目专属AI开发规范手册

适用项目:opms_backend/opms_parent

目标:让任意 AI 在无额外教学前提下,直接产出与现有代码风格、分层习惯、业务约束一致的新功能代码。


1. 项目基础信息(技术栈、架构、核心规则)

1.1 技术栈

后端技术栈

  • 语言与版本:Go 1.16(见 go.mod,禁止使用高于 1.16 的语法特性,如泛型)
  • 框架:GoFrame github.com/gogf/gf v1.16.x(禁止使用 v2.x 版本特性,如 gf-cli 新命令、核心 API 变更)
  • RPC:rpcx(服务注册、调用必须遵循项目现有注册格式,见 main.go 示例)
  • 鉴权/租户:opms_libary/micro_srv(必须通过 svc.Init(ctx) 获取租户信息,禁止手动解析 token)
  • 错误体系:opms_libary/myerrors(禁止使用原生 error 直接返回,必须用 myerrors 封装)
  • 工作流与消息:钉钉流程 + 系统消息(service.CreateSystemMessage),消息参数必须包含租户 ID、操作用户 ID
  • 导出:excelize(导出文件命名格式:模块名_操作_时间戳.xlsx,如 work_order_export_202604221530.xlsx

前端技术栈

  • 框架:Vue 2.6.14 + Vuex + Vue Router(hash 模式)
  • UI 组件库:Element UI + VXE Table
  • HTTP 客户端:Axios(配置见 src/utils/request.jssrc/utils/micro_request.js
  • 构建工具:Vue CLI 4.x

1.2 前后端接口调用规范(重要)

本项目采用 微服务 RPCX 架构,前端调用后端接口必须遵循以下规范:

调用方式选择

场景 使用模块 请求方式 说明
业务接口(主流程) micro_request.js RPCX POST 所有业务功能接口必须使用此方式
文件上传/下载 request.js 或原生 axios HTTP 大文件传输等特殊场景
基础/公共接口 request.js REST 登录、字典等公共服务

微服务调用规范(必须使用)

请求 URL 格式:

POST http://localhost:11000/micro-srv-proxy/{servicePath}

请求 Headers:

Content-Type: application/rpcx
X-RPCX-SerializeType: 1
X-RPCX-ServicePath: {ServiceName}      // 如:DeliveryProject、DeliveryProjectEvent
X-RPCX-ServiceMethod: {MethodName}     // 如:GetList、Create、UpdateById
SrvEnv: dev                            // 开发环境标识

前端 API 文件模板:

import micro_request from '@/utils/micro_request'

const basePath = process.env.VUE_APP_ParentPath  // 微服务路径,如:dashoo.opms.parent-0.0.1

export default {
  // 获取列表
  getList(params) {
    return micro_request.postRequest(basePath, 'ServiceName', 'GetList', params)
  },
  
  // 创建
  create(data) {
    return micro_request.postRequest(basePath, 'ServiceName', 'Create', data)
  },
  
  // 更新
  update(data) {
    return micro_request.postRequest(basePath, 'ServiceName', 'UpdateById', data)
  },
  
  // 删除
  deleteByIds(ids) {
    return micro_request.postRequest(basePath, 'ServiceName', 'DeleteByIds', { ids })
  }
}

禁止的调用方式

❌ 错误示例(REST 方式):

// 不要这样写!
import request from '@/utils/request'

const baseUrl = '/api/v1/deliveryProject'  // 错误:使用了 REST 路径

export default {
  getList(params) {
    return request({
      url: `${baseUrl}/list`,  // 错误:生成了 /api/v1/deliveryProject/list
      method: 'get',
      params,
    })
  }
}

✅ 正确示例(RPCX 方式):

// 应该这样写!
import micro_request from '@/utils/micro_request'

const basePath = process.env.VUE_APP_ParentPath

export default {
  getList(params) {
    return micro_request.postRequest(basePath, 'DeliveryProject', 'GetList', params)
  }
}

关键注意点

  1. 必须使用 micro_request:所有业务接口统一使用 micro_request.postRequest 方法
  2. ServiceName 对应 Handler 注册名:必须与 main.gos.RegisterName("Xxx", ...) 的名称一致
  3. MethodName 对应 Handler 方法名:必须与 Handler 结构体中的方法名完全一致
  4. 参数传递:所有参数放在请求 body 中,以 JSON 格式传递
  5. 响应处理:统一返回格式 { code: 200, data: {...}, msg: "" },通过 res.data 获取数据

1.3 分层架构(必须遵守,无例外)

  • 语言与版本:Go 1.16(见 go.mod,禁止使用高于 1.16 的语法特性,如泛型)
  • 框架:GoFrame github.com/gogf/gf v1.16.x(禁止使用 v2.x 版本特性,如 gf-cli 新命令、核心 API 变更)
  • RPC:rpcx(服务注册、调用必须遵循项目现有注册格式,见 main.go 示例)
  • 鉴权/租户:opms_libary/micro_srv(必须通过 svc.Init(ctx) 获取租户信息,禁止手动解析 token)
  • 错误体系:opms_libary/myerrors(禁止使用原生 error 直接返回,必须用 myerrors 封装)
  • 工作流与消息:钉钉流程 + 系统消息(service.CreateSystemMessage),消息参数必须包含租户 ID、操作用户 ID
  • 导出:excelize(导出文件命名格式:模块名_操作_时间戳.xlsx,如 work_order_export_202604221530.xlsx

1.3 分层架构(必须遵守,无例外)

  • app/handler/*:接口层(仅做 3 件事:接收请求参数、简单参数校验、调用 service、封装 rsp.Data,禁止写任何业务逻辑)
  • app/service/*:业务层(核心职责:业务规则校验、事务控制、审批流程调用、系统消息发送、业务动态日志写入)
  • app/dao/*:数据访问层(GF DAO 规范,仅做数据 CRUD,禁止写业务判断;含 internal 生成代码 + 外层封装,外层封装仅做简单查询拼接)
  • app/model/*:模型层(仅定义请求/响应 DTO、数据库实体映射,禁止写方法、禁止引入业务依赖)
  • main.go:RPC 服务注册入口(必须通过 s.RegisterName(...) 注册 handler,注册顺序与现有模块保持一致)

1.4 核心规则(现状抽取,新增代码必须完全对齐)

  • 所有业务 service 初始化必须先拿上下文:svc.Init(ctx),确保拿到 Tenant/CxtUser/DataScope,未初始化禁止操作 DAO
  • DB 写操作(新增/更新)必须补齐审计字段:新增用 service.SetCreatedInfo,更新用 service.SetUpdatedInfo,缺一不可
  • 更新操作必须排除不可改字段:FieldsEx(service.UpdateFieldEx...)service.UpdateFieldEx 为全局统一不可改字段集合(如 idcreated_atcreated_by 等)
  • 用户可见错误优先使用中文提示:myerrors.TipsError("中文提示"),系统内部错误用 myerrors.DbError("") / myerrors.SystemError("")
  • 所有列表接口统一返回格式:g.Map{"list": list, "total": total},禁止返回数组、单个结构体或其他格式
  • 关键业务(创建、转移、审批回调、状态变更)必须写业务动态表,动态日志需包含:操作人、操作类型、操作内容、操作时间、关联业务 ID

2. 代码强制规范(命名、注释、格式、文件结构)

2.1 命名规范(严格匹配,禁止自定义风格)

  • 文件名:全小写 + 下划线,如 work_order.goctr_contract.go;模块目录名全小写,如 workproj
  • 结构体命名:模块语义 + 层级语义,禁止无意义命名(如 Base、Common)
    • Handler:XxxHandler / Xxx(与模块名对应,如工单模块:WorkOrderHandler
    • Service:xxxServiceXxxService(统一前缀为模块名,如 workServiceWorkOrderService
    • 请求体/响应体:XxxReqXxxSearchReqXxxUpdateReqXxxRspReq 对应请求,Rsp 对应响应,SearchReq 对应列表查询请求)
    • 数据库实体:Xxx(与表名对应,首字母大写,如 WorkOrder 对应表 work_order
  • 方法名:动词开头 + 业务语义,禁止模糊命名(如 Do、Handle),常规方法名强制约定(新增代码必须遵守,handler 与 service 保持一致):
    • 分页查询:GetList(入参为 SearchReq,返回 totallisterr
    • 新增:Create(入参为 AddReq / CreateReq,返回 err
    • 编辑:UpdateById(入参为 UpdateReq,必须包含 Id,返回 err
    • 根据 ID 单条删除:DeleteById(入参为 int 类型 Id,返回 err
    • 根据 ID 批量删除:DeleteByIds(入参为 []int64 类型 Ids,返回 err
    • 根据 ID 查询单条:GetById(入参为 int 类型 Id,返回实体、err
  • 状态值:沿用现有字符串枚举(如 "10"/"20"/"30"),禁止随意改成 iota;新增状态需在对应 model 中定义常量,如 const (WorkOrderStatusTodo = "10" WorkOrderStatusDoing = "20")
  • 变量名:小驼峰命名,禁止下划线命名(与文件名区分),如 userNameworkOrderList,禁止单字母命名(除 ctxerrtx 等通用缩写)

2.2 注释规范(简洁实用,禁止冗余)

  • Swagger 注释非必须;涉及对外接口文档或需要自动化文档生成时建议保留,格式统一
  • Swagger 注释推荐格式(如需使用):// Swagger:模块名 中文模块 中文动作,如 // Swagger:work 工单 分页查询
  • 复杂业务段(审批逻辑、权限判断、状态机流转、事务操作)必须写中文块注释,说明业务意图、分支含义,禁止复述代码字面含义
  • 禁止空洞注释(如 // 这里是循环// 删除数据),注释必须解释“为什么这么做”,而非“做了什么”
  • 结构体、方法需添加简要注释(1-2 句话),说明其作用,如 // XxxHandler 工单接口处理类// Create 新增工单,包含事务控制和动态日志

2.3 代码格式与写法(统一范式,禁止创新)

  • 错误处理:必须使用 if err != nil { return err } 早返回风格,禁止嵌套 if-err,禁止忽略 err
  • 变量初始化:常用方式为 data := new(model.Xxx)var t g.Maplist := make([]T, 0),禁止使用 var data model.Xxx 后手动赋值零值
  • 查询链式写法:先构造 db/dao,按请求参数逐项 Where(无参数则不写),最后按“Count + Page + Order + Scan”顺序执行,禁止打乱顺序
  • 事务统一写法:Dao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {...}),事务内所有 DAO 操作必须使用 TX(tx),禁止混合使用非事务 DAO
  • 错误日志与提示:系统错误需打印日志 g.Log().Error(err);用户可见错误必须使用 myerrors.* 封装,禁止直接返回 err 给前端
  • 导入规范:Service 层对 daomodel 引用必须使用别名,别名格式为“模块名+dao/model”,如:
    • xxxdao "dashoo.cn/opms/app/dao/<module>"(如 workdao "dashoo.cn/opms/app/dao/work"
    • xxxmodel "dashoo.cn/opms/app/model/<module>"(如 workmodel "dashoo.cn/opms/app/model/work"
  • DAO 条件字段写法:仅在 DAO 使用了 .As("x")(如 .As("a"))时,后续 Where 条件才可写 x.<字段名>;若未设置 .As("x"),禁止使用 x.<字段名> 前缀
  • 结构体转 Map 写法:将请求结构体转为 g.Map 用于 DAO 写入时,必须使用 gconv.Map(req)禁止使用 gconv.Struct(req, &g.Map{})gconv.Struct(req, data)(其中 data 为 g.Map 类型)。GoFrame 的 gconv.Struct 仅支持将数据转为结构体指针,不支持 g.Map 作为目标,传入 g.Map 会在运行时报错 params should be type of pointer, but got type: map。正确写法:

    // ✅ 正确:使用 gconv.Map 将结构体转为 map
    data := gconv.Map(req)
      
    // ❌ 错误:gconv.Struct 不支持 g.Map 作为目标
    data := g.Map{}
    gconv.Struct(req, &data)  // 运行时报错!
    
  • gconv.Map 的 key 格式与 DAO Columns 的冲突:gconv.Map(req) 产生的 key 是 JSON tag 的小驼峰格式(如 eventNofeedbackDate),而 s.Dao.Columns.Xxx 返回的是数据库列名的蛇形格式(如 event_nofeedback_date)。在 gconv.Map 产生的 data map 中,必须使用小驼峰 key 赋值或判断,禁止使用 s.Dao.Columns.Xxx 作为 key。正确写法:

    data := gconv.Map(req)
    data["eventNo"] = generateEventNo()      // ✅ 正确:使用小驼峰 key
    data[s.Dao.Columns.EventNo] = "xxx"      // ❌ 错误:Columns.EventNo 是 "event_no",与 map 中的 "eventNo" 是不同的 key
    if v, ok := data["feedbackDate"]; ok {}  // ✅ 正确
    if v, ok := data[s.Dao.Columns.FeedbackDate]; ok {} // ❌ 错误:查找 "feedback_date" key,但 map 中只有 "feedbackDate"
    

    注意:直接构造 g.Map{...} 时(非 gconv.Map 产生),key 应使用 s.Dao.Columns.Xxx(蛇形格式),因为手动构造的 map 不经过 JSON tag 转换。 注意:当目标是模型结构体时,gconv.Struct(req, &modelEntity) 是正确的用法,不可混淆。

  • 代码缩进:使用 4 个空格缩进,禁止使用 tab;大括号换行规范:左括号紧跟语句后,右括号单独成行,如:

    if err != nil {
    return err
    }
    

2.4 文件结构规范(新增功能必须补齐,缺一不可)

新增一个业务对象时,最少包含以下文件,文件路径、命名必须严格匹配:

  1. app/model/<module>/<entity>.go:定义请求/响应结构、实体映射(如 app/model/work/work_order.go
  2. app/service/<module>/<entity>.go:业务逻辑实现,包含所有常规方法(GetListCreate 等)(如 app/service/work/work_order.go
  3. app/handler/<module>/<entity>.go:接口入口,实现 handler 方法,调用对应 service(如 app/handler/work/work_order.go
  4. (若涉及新表)app/dao/<module>/... + app/model/<module>/internal/...(通过 GF工具生成,禁止手动修改结构)
  5. main.go:在 RPC 服务注册区,添加该模块 handler 的注册代码(s.RegisterName

3. 业务逻辑规范(核心流程、固定写法、禁忌)

3.1 标准业务流程(CRUD + 动态,所有新增业务必须遵循)

  1. Handler 层:接收请求参数,做简单参数校验(如必填项校验),调用 service 对应的方法,封装 rsp.Data 后返回
  2. Service 层初始化:通过 NewXxxService(ctx) 初始化,获取租户、用户上下文,未初始化成功禁止后续操作
  3. Service 层校验:做数据存在性校验(如更新时查询数据是否存在)、业务前置校验(如状态是否合法、权限是否足够)
  4. 写操作(新增/更新/删除):进入事务控制,确保多表操作原子性
  5. 数据操作:主表变更 + 关联表变更(如有),关联表操作需在主表操作之后,避免外键约束报错
  6. 动态日志:关键业务动作必须写业务动态表,动态信息需完整(操作人、操作类型、操作内容等)
  7. 后续操作:必要时触发审批流(如调用 workflowSrv.StartProcessInstance)、发送系统消息(如 service.CreateSystemMessage

3.2 审批流固定模式(项目、合同、工单通用,禁止修改流程)

  • 提交审批时:先更新业务数据状态为“审批中” -> 调用 workflowSrv.StartProcessInstance 启动审批流程 -> 回写 appro_type(审批类型)、appro_instance_id(审批实例 ID)到业务表
  • 审批回调时:严格校验 bizCode(业务编码)、ProcessType(流程类型)、Result(审批结果),校验失败直接返回错误,不执行后续操作
  • 回调落库:根据审批结果(agree/refuse/terminate)分支设置业务数据状态;必要时回放“最近一次动态”作为最终变更来源,确保动态日志与实际操作一致

3.3 权限与数据范围(按业务需求启用)

  • 数据权限不是默认必选项:仅在需求明确说明“需要数据权限控制”时,才使用以下写法:
    • ctx = context.WithValue(ctx, "contextService", s)
    • dao := s.Dao.DataScope(ctx, "incharge_id").As("a")
  • 未明确要求数据权限控制时,禁止添加上述两行代码,避免引入非需求范围的过滤逻辑
  • 角色特例处理:可在 service 层通过 s.CxtUser.Roles 判断角色,增补数据可见范围(如管理员可见所有数据、普通用户仅可见自己创建的数据)
  • 禁止在 handler 层直接拼接权限 SQL,所有权限逻辑必须放在 service 层,通过 DataScope 或自定义方法实现

3.4 状态机约束(必须先校验再更新,禁止跳过校验)

  • 工单、项目、合同等存在强状态流转的业务,所有状态变更操作前,必须先判断当前状态是否合法(如“已完成”的工单不能再编辑)
  • 典型模式(必须严格沿用):

    // 状态校验
    currentStatus := data.Status
    if currentStatus != "10" {
    return myerrors.TipsError("当前状态不可执行该操作")
    }
    
    // 合法则执行更新
    return s.Dao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
    // 更新状态
    _, err := s.Dao.TX(tx).WherePri(data.Id).Data(g.Map{"status": "20"}).Update()
    if err != nil {
        return err
    }
    // 写动态日志
    // 发送通知(如有)
    return nil
    })
    

3.5 禁忌(绝对禁止,违者需修改)

  • 禁止绕过 service 在 handler 直接操作 DAO(包括查询、新增、更新、删除)
  • 禁止漏写 SetCreatedInfo/SetUpdatedInfo,新增/更新操作必须补齐审计字段
  • 禁止硬编码英文错误给前端(项目现状以中文提示为主),所有用户可见错误必须用 myerrors.TipsError 封装中文提示
  • 禁止改动 app/dao/**/internalapp/model/**/internal 自动生成文件的结构语义,仅可通过 GF 工具重新生成更新
  • 禁止在 model 层写业务逻辑、定义方法,model 层仅允许定义结构体和常量
  • 禁止在 service 层直接返回原生 error,必须用 myerrors 封装后返回
  • 禁止批量更新/删除时不做权限校验,批量操作前必须确认操作用户有对应权限

4. 功能编写模板(新增功能的标准代码结构、复制即用,禁止修改模板格式)

4.1 Model 模板(app/model/<module>/xxx.go

package <module> // 替换为实际模块名,如work、proj

import (
    "dashoo.cn/opms_libary/request"
    "github.com/gogf/gf/frame/g"
)

// XxxSearchReq 列表查询请求
type XxxSearchReq struct {
    request.PageReq            // 必须嵌入分页请求,统一分页参数
    Name   string `json:"name" v:"-"`   // 非必填参数,v:"-"表示不校验
    Status string `json:"status" v:"-"` // 状态参数,沿用字符串枚举
    // 新增其他查询参数,按实际业务补充,字段名小驼峰,注释说明含义
}

// XxxAddReq 新增请求(必填项需加v校验)
type XxxAddReq struct {
    Name   string `json:"name" v:"required#名称不能为空"` // 必填项,中文校验提示
    Status string `json:"status" v:"required#状态不能为空"`
    // 新增其他新增参数,按实际业务补充
}

// XxxUpdateReq 更新请求(必须包含Id,必填)
type XxxUpdateReq struct {
    Id     int    `json:"id" v:"required#ID不能为空"` // 必须包含Id,必填校验
    Name   string `json:"name" v:"-"`                 // 非必填,可选择性更新
    Status string `json:"status" v:"-"`
    // 新增其他更新参数,按实际业务补充
}

// Xxx 数据库实体映射(与表名对应)
type Xxx struct {
    Id          int    `json:"id" orm:"id,pk,autoincr"` // 主键,自增
    Name        string `json:"name" orm:"name"`
    Status      string `json:"status" orm:"status"`
    TenantId    int    `json:"tenantId" orm:"tenant_id"`   // 租户ID,必须包含
    CreatedAt   string `json:"createdAt" orm:"created_at"` // 审计字段,自动填充
    CreatedBy   int    `json:"createdBy" orm:"created_by"`
    CreatedName string `json:"createdName" orm:"created_name"`
    UpdatedAt   string `json:"updatedAt" orm:"updated_at"`
    UpdatedBy   int    `json:"updatedBy" orm:"updated_by"`
    UpdatedName string `json:"updatedName" orm:"updated_name"`
}

// XxxRsp 响应结构体(给前端返回的结构,按需筛选字段)
type XxxRsp struct {
    Id     int    `json:"id"`
    Name   string `json:"name"`
    Status string `json:"status"`
    // 新增其他需要返回的字段,按实际业务补充
}

4.2 Service 模板(app/service/<module>/xxx.go

package <module> // 替换为实际模块名,如work、proj

import (
    "context"

    xxxdao "dashoo.cn/opms/app/dao/<module>"     // 替换为实际dao别名,如workdao
    xxxmodel "dashoo.cn/opms/app/model/<module>" // 替换为实际model别名,如workmodel
    "dashoo.cn/opms/app/service"
    "dashoo.cn/opms_libary/myerrors"
    "github.com/gogf/gf/database/gdb"
    "github.com/gogf/gf/frame/g"
    "github.com/gogf/gf/util/gconv"
)

// xxxService 业务逻辑实现类(替换xxx为模块名,如workService)
type xxxService struct {
    *service.ContextService
    Dao *xxxdao.XxxDao // 替换Xxx为实体名,如WorkOrderDao
}

// NewXxxService 初始化service(替换Xxx为模块名,如NewWorkService)
func NewXxxService(ctx context.Context) (svc *xxxService, err error) {
    svc = new(xxxService)
    // 初始化上下文,获取租户、用户信息
    if svc.ContextService, err = svc.Init(ctx); err != nil {
        return nil, err
    }
    // 初始化DAO,关联租户
    svc.Dao = xxxdao.NewXxxDao(svc.Tenant) // 替换Xxx为实体名,如NewWorkOrderDao
    return svc, nil
}

// GetList 分页查询(固定方法名,与handler保持一致)
func (s *xxxService) GetList(req *xxxmodel.XxxSearchReq) (total int, list []*xxxmodel.XxxRsp, err error) {
    // 构造DAO查询
    db := s.Dao.As("a")
    // 拼接查询条件(按需补充,无条件则删除对应if)
    if req.Name != "" {
        db = db.WhereLike("a."+s.Dao.C.Name, "%"+req.Name+"%")
    }
    if req.Status != "" {
        db = db.Where("a."+s.Dao.C.Status, req.Status)
    }
    // 数据权限过滤(必须添加,无特殊情况不可删除)
    db = service.DataScope(s.Ctx, db, "a.tenant_id")
    // 统计总行数
    total, err = db.Count()
    if err != nil {
        g.Log().Error(err)
        return 0, nil, myerrors.DbError("获取XXX总行数失败") // 替换XXX为业务名称,如工单
    }
    // 分页、排序、查询
    var entityList []*xxxmodel.Xxx
    err = db.Page(req.GetPage()).OrderDesc("a.id").Scan(&entityList)
    if err != nil {
        g.Log().Error(err)
        return 0, nil, myerrors.DbError("查询XXX列表失败")
    }
    // 转换为响应结构体(按需转换,若无需转换可直接返回entityList)
    if err = gconv.Structs(entityList, &list); err != nil {
        g.Log().Error(err)
        return 0, nil, myerrors.SystemError("数据转换失败")
    }
    return
}

// Create 新增(固定方法名,与handler保持一致)
func (s *xxxService) Create(req *xxxmodel.XxxAddReq) error {
    // 转换请求参数为实体
    data := new(xxxmodel.Xxx)
    if err := gconv.Struct(req, data); err != nil {
        g.Log().Error(err)
        return myerrors.SystemError("数据转换失败")
    }
    // 补齐审计字段(必须添加,不可删除)
    service.SetCreatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
    // 关联租户ID(必须添加,不可删除)
    data.TenantId = s.Tenant.Id

    // 事务控制(新增操作必须加事务)
    return s.Dao.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
        // 新增数据
        _, err := s.Dao.TX(tx).Insert(data)
        if err != nil {
            g.Log().Error(err)
            return myerrors.DbError("新增XXX失败")
        }
        // TODO: 写业务动态日志(必须实现,不可删除)
        // 示例:service.CreateDynamic(...)
        // TODO: 触发审批流/发送消息(按需补充)
        return nil
    })
}

// UpdateById 根据ID更新(固定方法名,与handler保持一致)
func (s *xxxService) UpdateById(req *xxxmodel.XxxUpdateReq) error {
    // 校验数据是否存在
    count, err := s.Dao.WherePri(req.Id).Count()
    if err != nil {
        g.Log().Error(err)
        return myerrors.DbError("查询XXX数据失败")
    }
    if count == 0 {
        return myerrors.TipsError("XXX数据不存在")
    }
    // 构造更新数据
    data := g.Map{
        "name":   req.Name,
        "status": req.Status,
        // 补充其他需要更新的字段
    }
    // 补齐审计字段(必须添加,不可删除)
    service.SetUpdatedInfo(data, s.GetCxtUserId(), s.GetCxtUserName())
    // 更新操作,排除不可改字段(必须添加FieldsEx)
    _, err = s.Dao.FieldsEx(service.UpdateFieldEx...).WherePri(req.Id).Data(data).Update()
    if err != nil {
        g.Log().Error(err)
        return myerrors.DbError("更新XXX失败")
    }
    // TODO: 写业务动态日志(必须实现,不可删除)
    // TODO: 触发审批流/发送消息(按需补充)
    return nil
}

// DeleteById 根据ID单条删除(固定方法名,与handler保持一致)
func (s *xxxService) DeleteById(id int) error {
    // 校验数据是否存在(按需补充,可选)
    count, err := s.Dao.WherePri(id).Count()
    if err != nil {
        g.Log().Error(err)
        return myerrors.DbError("查询XXX数据失败")
    }
    if count == 0 {
        return myerrors.TipsError("XXX数据不存在")
    }
    // 删除操作
    _, err = s.Dao.WherePri(id).Delete()
    if err != nil {
        g.Log().Error(err)
        return myerrors.DbError("删除XXX失败")
    }
    // TODO: 写业务动态日志(必须实现,不可删除)
    return nil
}

// DeleteByIds 根据ID批量删除(固定方法名,与handler保持一致)
func (s *xxxService) DeleteByIds(ids []int64) error {
    if len(ids) == 0 {
        return myerrors.TipsError("请选择需要删除的XXX")
    }
    // 删除操作
    _, err := s.Dao.WhereIn(s.Dao.C.Id, ids).Delete()
    if err != nil {
        g.Log().Error(err)
        return myerrors.DbError("批量删除XXX失败")
    }
    // TODO: 写业务动态日志(必须实现,不可删除)
    return nil
}

// GetById 根据ID查询单条(可选,按需补充)
func (s *xxxService) GetById(id int) (*xxxmodel.XxxRsp, error) {
    entity, err := s.Dao.WherePri(id).Get()
    if err != nil {
        g.Log().Error(err)
        return nil, myerrors.DbError("查询XXX数据失败")
    }
    if entity.IsEmpty() {
        return nil, myerrors.TipsError("XXX数据不存在")
    }
    var rsp xxxmodel.XxxRsp
    if err := entity.Struct(&rsp); err != nil {
        g.Log().Error(err)
        return nil, myerrors.SystemError("数据转换失败")
    }
    return &rsp, nil
}

4.3 Handler 模板(app/handler/<module>/xxx.go

package <module> // 替换为实际模块名,如work、proj

import (
    "context"

    "dashoo.cn/common_definition/comm_def"
    xxxmodel "dashoo.cn/opms/app/model/<module>"   // 替换为实际model别名,如workmodel
    services "dashoo.cn/opms/app/service/<module>" // 替换为实际service别名,如workService
    "github.com/gogf/gf/frame/g"
)

// XxxHandler 接口处理类(替换Xxx为模块名,如WorkOrderHandler)
type XxxHandler struct{}

// GetList 分页查询接口(固定方法名,与service保持一致)
func (h *XxxHandler) GetList(ctx context.Context, req *xxxmodel.XxxSearchReq, rsp *comm_def.CommonMsg) error {
    // 初始化service
    s, err := services.NewXxxService(ctx) // 替换Xxx为模块名,如NewWorkService
    if err != nil {
        return err
    }
    // 调用service方法
    total, list, err := s.GetList(req)
    if err != nil {
        return err
    }
    // 统一返回格式(必须按此格式,不可修改)
    rsp.Data = g.Map{"list": list, "total": total}
    return nil
}

// Create 新增接口(固定方法名,与service保持一致)
func (h *XxxHandler) Create(ctx context.Context, req *xxxmodel.XxxAddReq, rsp *comm_def.CommonMsg) error {
    s, err := services.NewXxxService(ctx)
    if err != nil {
        return err
    }
    if err := s.Create(req); err != nil {
        return err
    }
    rsp.Data = g.Map{"msg": "新增成功"}
    return nil
}

// UpdateById 更新接口(固定方法名,与service保持一致)
func (h *XxxHandler) UpdateById(ctx context.Context, req *xxxmodel.XxxUpdateReq, rsp *comm_def.CommonMsg) error {
    s, err := services.NewXxxService(ctx)
    if err != nil {
        return err
    }
    if err := s.UpdateById(req); err != nil {
        return err
    }
    rsp.Data = g.Map{"msg": "更新成功"}
    return nil
}

// DeleteById 单条删除接口(固定方法名,与service保持一致)
func (h *XxxHandler) DeleteById(ctx context.Context, req *xxxmodel.XxxDeleteByIdReq, rsp *comm_def.CommonMsg) error {
    s, err := services.NewXxxService(ctx)
    if err != nil {
        return err
    }
    if err := s.DeleteById(req.Id); err != nil {
        return err
    }
    rsp.Data = g.Map{"msg": "删除成功"}
    return nil
}

// DeleteByIds 批量删除接口(固定方法名,与service保持一致)
func (h *XxxHandler) DeleteByIds(ctx context.Context, req *xxxmodel.XxxDeleteByIdsReq, rsp *comm_def.CommonMsg) error {
    s, err := services.NewXxxService(ctx)
    if err != nil {
        return err
    }
    if err := s.DeleteByIds(req.Ids); err != nil {
        return err
    }
    rsp.Data = g.Map{"msg": "批量删除成功"}
    return nil
}

4.4 注册模板(main.go

// 新增模块注册(添加在现有注册代码之后,按模块顺序排列)
import (
    // 导入新增模块handler
    xxxhandler "dashoo.cn/opms/app/handler/<module>" // 替换为实际handler路径,如workhandler
)

func main() {
    s := rpcx.NewServer()
    // 现有模块注册...

    // 新增模块注册(替换Xxx为模块名,如WorkOrder,handler为新增的handler实例)
    s.RegisterName("Xxx", new(xxxhandler.XxxHandler), "") // 如 s.RegisterName("WorkOrder", new(workhandler.WorkOrderHandler), "")
}

5. 示例对照(我的旧代码 -> 新功能应写成什么样)

示例1:列表查询

  • 旧代码风格(见 app/service/base/base_product.go
    • 逐项 if req.Field != "" { dao = dao.Where... }
    • Count() 后分页 Page(req.GetPage())
  • 新功能必须写成
    • 完全沿用“条件拼接 -> 统计总行数 -> 分页排序 -> Scan查询 -> 转换响应”的顺序
    • 数据权限过滤 service.DataScope 按需求启用:仅在需求明确要求时添加
    • 返回格式固定 list + total,错误必须用 myerrors 封装

示例2:更新操作

  • 旧代码风格(见 app/service/work/work_order.go
    • service.SetUpdatedInfo(...) 补齐审计字段
    • FieldsEx(service.UpdateFieldEx...) 防止误改创建字段
  • 新功能必须写成
    • 先查数据存在性,不存在直接返回 myerrors.TipsError("数据不存在")
    • 构造更新数据 g.Map,调用 SetUpdatedInfo 补齐审计字段
    • 更新时必须加 FieldsEx(service.UpdateFieldEx...),排除不可改字段
    • 更新后必须写业务动态日志

示例3:事务型业务

  • 旧代码风格(见 app/service/proj/business.go
    • 主业务更新 + 关联表更新 + 动态日志 + 审批提交统一放事务
  • 新功能必须写成
    • 多表写入、状态变更 + 动态日志等组合操作,一律放入事务控制
    • 事务内所有 DAO 操作必须使用 TX(tx),确保原子性
    • 任一操作失败立即 return err 回滚,同时打印错误日志

示例4:审批回调

  • 旧代码风格(见 app/service/proj/business.goapp/service/work/work_order.go
    • 严格校验 ProcessType/Result,再落状态
  • 新功能必须写成
    • 不允许直接信任回调数据,必须先校验 bizCode(格式、关联业务是否存在)、ProcessType(是否匹配当前业务)、Result(是否为合法值)
    • 校验失败直接返回错误,不执行后续状态更新操作
    • 状态更新、动态日志写入必须放入事务,确保数据一致性

示例5:Handler 职责边界

  • 旧代码风格(见 app/handler/contract/ctr_contract.go
    • handler 不写业务规则,只做转发和统一返回
  • 新功能必须写成
    • handler 仅做 3 件事:接收请求、简单参数校验、调用 service 并封装返回
    • 复杂规则(状态机、权限、审批、数据校验)全部放 service 层,禁止在 handler 层写任何业务判断
    • 返回格式统一为 rsp.Data = g.Map{...},禁止直接返回其他格式

6. AI 必须遵守的铁律(禁止修改的规则、禁止使用的写法,违反即不合格)

  1. 禁止跨层:不得在 handler 直连 DAO(包括查询、新增、更新、删除),不得在 model 写业务逻辑、定义方法。
  2. 禁止破坏审计字段:新增/更新必须调用 SetCreatedInfo/SetUpdatedInfo,更新时必须添加 FieldsEx(service.UpdateFieldEx...),排除不可改字段。
  3. 禁止跳过状态校验:涉及状态流转的接口(如编辑、删除、审批),必须先判定“当前状态可否执行该操作”,禁止直接更新状态。
  4. 禁止漏动态日志:关键业务动作(创建、修改、转移、关闭、审批回调、批量操作)必须写业务动态表,动态信息需完整。
  5. 禁止随意改枚举语义"10"/"20"/"30"... 这类业务状态码不可擅自重定义,新增状态需在 model 中定义常量并同步到规范。
  6. 禁止改自动生成文件语义app/dao/**/internalapp/model/**/internal 仅可通过 GF 工具生成流程更新,不手改结构、字段、注释。
  7. 禁止风格漂移:错误返回、分页格式、事务写法、命名风格、代码缩进必须与现有代码一致,不引入另一套框架习惯(如 gin 语法、原生 SQL 拼接)。
  8. 禁止无依据“优化重构”:新增功能优先贴合现有实现范式,避免大面积重写旧代码,禁止引入新项目未使用的语法、工具。
  9. 权限控制按需求启用:仅在需求明确要求时才添加 context.WithValue(..., "contextService", s)DataScope(...);未要求时禁止默认添加。
  10. 禁止返回原生 error:所有错误必须用 opms_libary/myerrors 封装,用户可见错误用中文提示,系统错误需打印日志。
  11. 禁止 gconv.Struct 转 g.Map:将结构体转为 g.Map 用于 DAO 写入时,必须使用 gconv.Map(req),禁止使用 gconv.Struct(req, &g.Map{}),后者运行时会报 params should be type of pointer, but got type: map 错误。

AI 执行清单(每次提交前自检,必须全部勾选才能提交)

后端代码检查项

  • 是否遵循 handler -> service -> dao -> model 分层,无跨层操作
  • 是否完成请求结构(model)、service 实现、handler 暴露、main.go 注册,文件齐全
  • 常规方法名是否严格使用:GetListCreateUpdateByIdDeleteByIdDeleteByIds,与 handler 保持一致
  • Service 层对 daomodel 导入是否使用别名(如 xxxDaoxxxModel),格式正确
  • 新增/更新操作是否补齐审计字段(SetCreatedInfo/SetUpdatedInfo),更新是否排除不可改字段
  • 涉及状态流转、权限的接口,是否覆盖状态机校验;数据权限控制是否按需求明确启用(未要求则不加)
  • 关键业务节点(创建、修改、审批等)是否写入动态日志,信息完整
  • 错误返回是否使用 myerrors 封装,用户可见错误为中文提示,列表返回格式为 list+total
  • 是否避免修改自动生成的 internal 文件,未手动改动其结构语义
  • 结构体转 Map 是否使用 gconv.Map(req),而非 gconv.Struct(req, &g.Map{})
  • 代码命名、缩进、格式与现有代码一致,无风格漂移
  • 事务操作(多表写入、状态+动态)是否放入事务控制,确保原子性

前端代码检查项

  • API 调用方式:业务接口是否使用 micro_request.postRequest,禁止使用 request.js 发起 REST 调用
  • ServiceName 正确性:是否与 main.gos.RegisterName 注册的名称完全一致
  • MethodName 正确性:是否与 Handler 中的方法名完全一致(如 GetListCreate
  • basePath 配置:是否正确使用 process.env.VUE_APP_ParentPath,而非硬编码路径
  • 参数格式:是否正确传递参数对象,数字类型字段是否转换为 int(如 parseInt(projectId)

7. AI 开发通用工程原则补充

1. 编码前先思考

不要假设。不要掩饰困惑。明确呈现权衡。

在实现之前:

  • 明确写出你的假设。如果不确定,就提问。
  • 如果存在多种解释,先把它们列出来,不要默默自行选择。
  • 如果有更简单的方法,就直接指出来。在有必要时提出异议。
  • 如果有不清楚的地方,就停下来。说清楚困惑点,并提问。

2. 简单优先

只写解决问题所需的最少代码。不做任何预设性扩展。

  • 不要加入超出需求范围的功能。
  • 不要为一次性代码做抽象。
  • 不要加入未被要求的“灵活性”或“可配置性”。
  • 不要为不可能发生的场景写错误处理。
  • 如果你写了 200 行,但 50 行就够,就重写。

问问自己:“一个资深工程师会认为这太复杂了吗?” 如果答案是会,那就继续简化。

3. 外科手术式修改

只改必须改的内容。只清理你自己造成的问题。

编辑现有代码时:

  • 不要“顺手优化”相邻代码、注释或格式。
  • 不要重构没有坏掉的部分。
  • 保持现有风格,即使你个人会写成别的样子。
  • 如果发现无关的死代码,可以指出,但不要删除。

当你的改动产生遗留项时:

  • 删除那些因你的修改而变成未使用的 import、变量或函数。
  • 不要删除原本就存在的死代码,除非被明确要求。

检验标准:每一行改动都应当能直接追溯到用户请求。

4. 目标驱动执行

先定义成功标准,再循环推进,直到验证通过。

把任务转换成可验证的目标:

  • “添加校验” → “先为非法输入写测试,再让测试通过”
  • “修复这个 bug” → “先写能复现它的测试,再让测试通过”
  • “重构 X” → “确保改动前后测试都通过”

对于多步骤任务,先给出简短计划:

1. [步骤] → 验证:[检查项]
2. [步骤] → 验证:[检查项]
3. [步骤] → 验证:[检查项]

强有力的成功标准能让你独立闭环推进。弱成功标准(“把它弄好”)则会不断需要额外澄清。