package controllers import ( "encoding/json" "io" "strconv" "strings" "server/services" ) // 移动端 OA 报销管理接口,挂在 AppOaController 上。 // // 与 Web 管理端 /backend/reimbursements* 共用同一套数据 // (yz_backend_reimbursements / _items / _expense_types,按 JWT 中 tid + user_id // 双重隔离,仅本人单据可见可操作)与核心逻辑(reimburse*Core 共享函数)。 // 小程序「报销管理」页面走这里,不直接调用 /backend/* 接口。 // expenseReadPayload 读取并校验新建/编辑报销单请求体;失败时直接输出错误响应。 func (c *AppOaController) expenseReadPayload() (reimbursementPayload, bool) { var payload reimbursementPayload raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil || json.Unmarshal(raw, &payload) != nil { c.oaErr(400, 400, "参数错误") return payload, false } return payload, true } // expenseReadItemPayload 读取并校验费用明细请求体;失败时直接输出错误响应。 func (c *AppOaController) expenseReadItemPayload() (reimbursementItemPayload, bool) { var payload reimbursementItemPayload raw, err := io.ReadAll(c.Ctx.Request.Body) if err != nil || json.Unmarshal(raw, &payload) != nil { c.oaErr(400, 400, "参数错误") return payload, false } return payload, true } // ExpenseList GET /api/oa/expense/list // 移动端报销单列表:参数与返回结构与 /backend/reimbursements 一致 // (page/pageSize/keyword/status,返回 list/total)。 func (c *AppOaController) ExpenseList() { claims, err := c.oaClaims() if err != nil { c.oaErr(401, 401, "未登录或无权限") return } page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 10) status, statusErr := c.GetInt("status") data, code, msg := reimburseListData(claims, page, pageSize, strings.TrimSpace(c.GetString("keyword")), statusErr == nil, status) if msg != "" { c.oaErr(code, code, msg) return } c.oaOk(data) } // ExpenseDetail GET /api/oa/expense/detail/:id // 报销单详情(主单 + 费用明细 + 发票文件)。 func (c *AppOaController) ExpenseDetail() { claims, err := c.oaClaims() if err != nil { c.oaErr(401, 401, "未登录或无权限") return } id := c.oaScheduleIDParam() if id == 0 { c.oaErr(400, 400, "无效ID") return } scheme, host := services.PublicRequestBaseURL(&c.Controller) data, code, msg := reimburseDetailData(claims, id, scheme, host) if msg != "" { c.oaErr(code, code, msg) return } c.oaOk(data) } // ExpenseCreate POST /api/oa/expense/create 新建报销单(草稿)。 func (c *AppOaController) ExpenseCreate() { claims, err := c.oaClaims() if err != nil { c.oaErr(401, 401, "未登录或无权限") return } payload, ok := c.expenseReadPayload() if !ok { return } id, code, msg := reimburseCreateCore(claims, payload) if msg != "" { c.oaErr(code, code, msg) return } c.oaOk(map[string]interface{}{"id": id}) } // ExpenseUpdate POST /api/oa/expense/update/:id 编辑报销单(仅草稿)。 func (c *AppOaController) ExpenseUpdate() { claims, err := c.oaClaims() if err != nil { c.oaErr(401, 401, "未登录或无权限") return } id := c.oaScheduleIDParam() if id == 0 { c.oaErr(400, 400, "无效ID") return } payload, ok := c.expenseReadPayload() if !ok { return } code, msg := reimburseUpdateCore(claims, id, payload) if msg != "" { c.oaErr(code, code, msg) return } c.oaOk(map[string]interface{}{"id": id}) } // ExpenseDelete DELETE /api/oa/expense/delete/:id 删除报销单(仅草稿)。 func (c *AppOaController) ExpenseDelete() { claims, err := c.oaClaims() if err != nil { c.oaErr(401, 401, "未登录或无权限") return } id := c.oaScheduleIDParam() if id == 0 { c.oaErr(400, 400, "无效ID") return } code, msg := reimburseDeleteCore(claims, id) if msg != "" { c.oaErr(code, code, msg) return } c.oaOk(nil) } // ExpenseAddItem POST /api/oa/expense/item/add/:id 新增费用明细(仅草稿)。 func (c *AppOaController) ExpenseAddItem() { claims, err := c.oaClaims() if err != nil { c.oaErr(401, 401, "未登录或无权限") return } id := c.oaScheduleIDParam() if id == 0 { c.oaErr(400, 400, "无效报销单ID") return } payload, ok := c.expenseReadItemPayload() if !ok { return } scheme, host := services.PublicRequestBaseURL(&c.Controller) item, code, msg := reimburseAddItemCore(claims, id, payload, scheme, host) if msg != "" { c.oaErr(code, code, msg) return } c.oaOk(item) } // ExpenseUpdateItem POST /api/oa/expense/item/update/:id/:item_id 更新费用明细(仅草稿)。 func (c *AppOaController) ExpenseUpdateItem() { claims, err := c.oaClaims() if err != nil { c.oaErr(401, 401, "未登录或无权限") return } id := c.oaScheduleIDParam() itemID := c.expenseItemIDParam() if id == 0 || itemID == 0 { c.oaErr(400, 400, "无效ID") return } payload, ok := c.expenseReadItemPayload() if !ok { return } scheme, host := services.PublicRequestBaseURL(&c.Controller) item, code, msg := reimburseUpdateItemCore(claims, id, itemID, payload, scheme, host) if msg != "" { c.oaErr(code, code, msg) return } c.oaOk(item) } // ExpenseDeleteItem DELETE /api/oa/expense/item/delete/:id/:item_id 删除费用明细(仅草稿)。 func (c *AppOaController) ExpenseDeleteItem() { claims, err := c.oaClaims() if err != nil { c.oaErr(401, 401, "未登录或无权限") return } id := c.oaScheduleIDParam() itemID := c.expenseItemIDParam() if id == 0 || itemID == 0 { c.oaErr(400, 400, "无效ID") return } code, msg := reimburseDeleteItemCore(claims, id, itemID) if msg != "" { c.oaErr(code, code, msg) return } c.oaOk(nil) } // ExpenseSubmit POST /api/oa/expense/submit/:id 提交审批(仅草稿,需已配置审批流)。 func (c *AppOaController) ExpenseSubmit() { claims, err := c.oaClaims() if err != nil { c.oaErr(401, 401, "未登录或无权限") return } id := c.oaScheduleIDParam() if id == 0 { c.oaErr(400, 400, "无效ID") return } code, msg := reimburseSubmitCore(claims, id) if msg != "" { c.oaErr(code, code, msg) return } c.oaOk(nil) } // ExpenseStatus POST /api/oa/expense/status/:id 手动确认更改单据状态(body: {status})。 func (c *AppOaController) ExpenseStatus() { claims, err := c.oaClaims() if err != nil { c.oaErr(401, 401, "未登录或无权限") return } id := c.oaScheduleIDParam() if id == 0 { c.oaErr(400, 400, "无效ID") return } var payload reimbursementStatusPayload raw, err := io.ReadAll(c.Ctx.Request.Body) if len(raw) > 0 && (err != nil || json.Unmarshal(raw, &payload) != nil) { c.oaErr(400, 400, "参数错误") return } newStatus, code, msg := reimburseUpdateStatusCore(claims, id, payload.Status) if msg != "" { c.oaErr(code, code, msg) return } c.oaOk(map[string]interface{}{"status": newStatus}) } // ExpenseRecords GET /api/oa/expense/records/:id 审批记录时间线。 func (c *AppOaController) ExpenseRecords() { claims, err := c.oaClaims() if err != nil { c.oaErr(401, 401, "未登录或无权限") return } id := c.oaScheduleIDParam() if id == 0 { c.oaErr(400, 400, "无效ID") return } rows, code, msg := reimburseRecordsData(claims, id) if msg != "" { c.oaErr(code, code, msg) return } c.oaOk(rows) } // ExpenseTypes GET /api/oa/expense/types 费用类型下拉(全局通用 + 本租户专有,仅启用)。 func (c *AppOaController) ExpenseTypes() { claims, err := c.oaClaims() if err != nil { c.oaErr(401, 401, "未登录或无权限") return } rows, err := queryReimburseExpenseTypes(claims.TenantId, true) if err != nil { c.oaErr(500, 500, "查询费用类型失败") return } list := make([]map[string]interface{}, 0, len(rows)) for _, row := range rows { list = append(list, expenseTypeDTO(row, claims.TenantId)) } c.oaOk(list) } // ExpenseUploadImage POST /api/oa/expense/upload(multipart,字段名 file) // 上传报销发票图片,共享 uploadTenantImage,返回 {id,url,name}; // 随后由费用明细接口把文件 ID 写进 receipt_file_ids。 func (c *AppOaController) ExpenseUploadImage() { c.uploadTenantImage() } // expenseItemIDParam 解析 :item_id 路由参数,非法返回 0。 func (c *AppOaController) expenseItemIDParam() uint64 { id, err := strconv.ParseUint(c.Ctx.Input.Param(":item_id"), 10, 64) if err != nil { return 0 } return id }