# 项目专属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.js` 和 `src/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 文件模板:** ```javascript 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 方式):** ```javascript // 不要这样写! 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 方式):** ```javascript // 应该这样写! 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.go` 中 `s.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` 为全局统一不可改字段集合(如 `id`、`created_at`、`created_by` 等) - 用户可见错误优先使用中文提示:`myerrors.TipsError("中文提示")`,系统内部错误用 `myerrors.DbError("")` / `myerrors.SystemError("")` - 所有列表接口统一返回格式:`g.Map{"list": list, "total": total}`,禁止返回数组、单个结构体或其他格式 - 关键业务(创建、转移、审批回调、状态变更)必须写业务动态表,动态日志需包含:操作人、操作类型、操作内容、操作时间、关联业务 ID --- # 2. 代码强制规范(命名、注释、格式、文件结构) ## 2.1 命名规范(严格匹配,禁止自定义风格) - 文件名:全小写 + 下划线,如 `work_order.go`、`ctr_contract.go`;模块目录名全小写,如 `work`、`proj` - 结构体命名:模块语义 + 层级语义,禁止无意义命名(如 Base、Common) - Handler:`XxxHandler` / `Xxx`(与模块名对应,如工单模块:`WorkOrderHandler`) - Service:`xxxService` 或 `XxxService`(统一前缀为模块名,如 `workService`、`WorkOrderService`) - 请求体/响应体:`XxxReq`、`XxxSearchReq`、`XxxUpdateReq`、`XxxRsp`(`Req` 对应请求,`Rsp` 对应响应,`SearchReq` 对应列表查询请求) - 数据库实体:`Xxx`(与表名对应,首字母大写,如 `WorkOrder` 对应表 `work_order`) - 方法名:动词开头 + 业务语义,禁止模糊命名(如 Do、Handle),常规方法名强制约定(新增代码必须遵守,handler 与 service 保持一致): - 分页查询:`GetList`(入参为 `SearchReq`,返回 `total`、`list`、`err`) - 新增:`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")` - 变量名:小驼峰命名,禁止下划线命名(与文件名区分),如 `userName`、`workOrderList`,禁止单字母命名(除 `ctx`、`err`、`tx` 等通用缩写) ## 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.Map`、`list := 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 层对 `dao` 与 `model` 引用必须使用别名,别名格式为“模块名+dao/model”,如: - `xxxdao "dashoo.cn/opms/app/dao/"`(如 `workdao "dashoo.cn/opms/app/dao/work"`) - `xxxmodel "dashoo.cn/opms/app/model/"`(如 `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`。正确写法: ```go // ✅ 正确:使用 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 的小驼峰格式(如 `eventNo`、`feedbackDate`),而 `s.Dao.Columns.Xxx` 返回的是数据库列名的蛇形格式(如 `event_no`、`feedback_date`)。**在 gconv.Map 产生的 data map 中,必须使用小驼峰 key 赋值或判断,禁止使用 `s.Dao.Columns.Xxx` 作为 key**。正确写法: ```go 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;大括号换行规范:左括号紧跟语句后,右括号单独成行,如: ```go if err != nil { return err } ``` ## 2.4 文件结构规范(新增功能必须补齐,缺一不可) 新增一个业务对象时,最少包含以下文件,文件路径、命名必须严格匹配: 1. `app/model//.go`:定义请求/响应结构、实体映射(如 `app/model/work/work_order.go`) 2. `app/service//.go`:业务逻辑实现,包含所有常规方法(`GetList`、`Create` 等)(如 `app/service/work/work_order.go`) 3. `app/handler//.go`:接口入口,实现 handler 方法,调用对应 service(如 `app/handler/work/work_order.go`) 4. (若涉及新表)`app/dao//...` + `app/model//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 状态机约束(必须先校验再更新,禁止跳过校验) - 工单、项目、合同等存在强状态流转的业务,所有状态变更操作前,必须先判断当前状态是否合法(如“已完成”的工单不能再编辑) - 典型模式(必须严格沿用): ```go // 状态校验 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/**/internal`、`app/model/**/internal` 自动生成文件的结构语义,仅可通过 GF 工具重新生成更新 - 禁止在 model 层写业务逻辑、定义方法,model 层仅允许定义结构体和常量 - 禁止在 service 层直接返回原生 error,必须用 `myerrors` 封装后返回 - 禁止批量更新/删除时不做权限校验,批量操作前必须确认操作用户有对应权限 --- # 4. 功能编写模板(新增功能的标准代码结构、复制即用,禁止修改模板格式) ## 4.1 Model 模板(`app/model//xxx.go`) ```go package // 替换为实际模块名,如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//xxx.go`) ```go package // 替换为实际模块名,如work、proj import ( "context" xxxdao "dashoo.cn/opms/app/dao/" // 替换为实际dao别名,如workdao xxxmodel "dashoo.cn/opms/app/model/" // 替换为实际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//xxx.go`) ```go package // 替换为实际模块名,如work、proj import ( "context" "dashoo.cn/common_definition/comm_def" xxxmodel "dashoo.cn/opms/app/model/" // 替换为实际model别名,如workmodel services "dashoo.cn/opms/app/service/" // 替换为实际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`) ```go // 新增模块注册(添加在现有注册代码之后,按模块顺序排列) import ( // 导入新增模块handler xxxhandler "dashoo.cn/opms/app/handler/" // 替换为实际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.go`、`app/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/**/internal`、`app/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` 注册,文件齐全 - [ ] 常规方法名是否严格使用:`GetList`、`Create`、`UpdateById`、`DeleteById`、`DeleteByIds`,与 handler 保持一致 - [ ] Service 层对 `dao` 与 `model` 导入是否使用别名(如 `xxxDao`、`xxxModel`),格式正确 - [ ] 新增/更新操作是否补齐审计字段(SetCreatedInfo/SetUpdatedInfo),更新是否排除不可改字段 - [ ] 涉及状态流转、权限的接口,是否覆盖状态机校验;数据权限控制是否按需求明确启用(未要求则不加) - [ ] 关键业务节点(创建、修改、审批等)是否写入动态日志,信息完整 - [ ] 错误返回是否使用 `myerrors` 封装,用户可见错误为中文提示,列表返回格式为 `list+total` - [ ] 是否避免修改自动生成的 internal 文件,未手动改动其结构语义 - [ ] 结构体转 Map 是否使用 `gconv.Map(req)`,而非 `gconv.Struct(req, &g.Map{})` - [ ] 代码命名、缩进、格式与现有代码一致,无风格漂移 - [ ] 事务操作(多表写入、状态+动态)是否放入事务控制,确保原子性 ## 前端代码检查项 - [ ] **API 调用方式**:业务接口是否使用 `micro_request.postRequest`,禁止使用 `request.js` 发起 REST 调用 - [ ] **ServiceName 正确性**:是否与 `main.go` 中 `s.RegisterName` 注册的名称完全一致 - [ ] **MethodName 正确性**:是否与 Handler 中的方法名完全一致(如 `GetList`、`Create`) - [ ] **basePath 配置**:是否正确使用 `process.env.VUE_APP_ParentPath`,而非硬编码路径 - [ ] **参数格式**:是否正确传递参数对象,数字类型字段是否转换为 int(如 `parseInt(projectId)`) --- # 7. AI 开发通用工程原则补充 ## 1. 编码前先思考 **不要假设。不要掩饰困惑。明确呈现权衡。** 在实现之前: - 明确写出你的假设。如果不确定,就提问。 - 如果存在多种解释,先把它们列出来,不要默默自行选择。 - 如果有更简单的方法,就直接指出来。在有必要时提出异议。 - 如果有不清楚的地方,就停下来。说清楚困惑点,并提问。 ## 2. 简单优先 **只写解决问题所需的最少代码。不做任何预设性扩展。** - 不要加入超出需求范围的功能。 - 不要为一次性代码做抽象。 - 不要加入未被要求的“灵活性”或“可配置性”。 - 不要为不可能发生的场景写错误处理。 - 如果你写了 200 行,但 50 行就够,就重写。 问问自己:“一个资深工程师会认为这太复杂了吗?” 如果答案是会,那就继续简化。 ## 3. 外科手术式修改 **只改必须改的内容。只清理你自己造成的问题。** 编辑现有代码时: - 不要“顺手优化”相邻代码、注释或格式。 - 不要重构没有坏掉的部分。 - 保持现有风格,即使你个人会写成别的样子。 - 如果发现无关的死代码,可以指出,但不要删除。 当你的改动产生遗留项时: - 删除那些因你的修改而变成未使用的 import、变量或函数。 - 不要删除原本就存在的死代码,除非被明确要求。 检验标准:每一行改动都应当能直接追溯到用户请求。 ## 4. 目标驱动执行 **先定义成功标准,再循环推进,直到验证通过。** 把任务转换成可验证的目标: - “添加校验” → “先为非法输入写测试,再让测试通过” - “修复这个 bug” → “先写能复现它的测试,再让测试通过” - “重构 X” → “确保改动前后测试都通过” 对于多步骤任务,先给出简短计划: ```text 1. [步骤] → 验证:[检查项] 2. [步骤] → 验证:[检查项] 3. [步骤] → 验证:[检查项] ``` 强有力的成功标准能让你独立闭环推进。弱成功标准(“把它弄好”)则会不断需要额外澄清。