Files

453 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package controllers
import (
"encoding/json"
"io"
"strconv"
"strings"
"server/pkg/identity"
"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 {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
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 {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
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 {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
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 {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
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 {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
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 {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
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 {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
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 {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
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 {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
return
}
id := c.oaScheduleIDParam()
if id == 0 {
c.oaErr(400, 400, "无效ID")
return
}
code, msg := reimburseSubmitCore(claims, id, readReimburseComment(&c.Controller))
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(nil)
}
// ExpenseStatus POST /api/oa/expense/status/:id 报销人确认款项到账(body: {status:5})。
// 仅限「已通过」的单据确认收款,其它状态由审批引擎推进。
func (c *AppOaController) ExpenseStatus() {
claims, err := c.oaClaims()
if err != nil {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
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 {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
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)
}
// expenseActPayload 审批操作请求体。
type expenseActPayload struct {
Comment string `json:"comment"`
}
// expenseReadActPayload 读取审批意见(可空)。
func (c *AppOaController) expenseReadActPayload() string {
var payload expenseActPayload
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err == nil && len(raw) > 0 {
_ = json.Unmarshal(raw, &payload)
}
return strings.TrimSpace(payload.Comment)
}
// ExpenseApprove POST /api/oa/expense/approve/:id 通过当前审批节点(body: {comment})。
// 审批人视角:无需是单据创建人,只要是当前节点待办审批人即可操作。
func (c *AppOaController) ExpenseApprove() {
claims, err := c.oaClaims()
if err != nil {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
return
}
id := c.oaScheduleIDParam()
if id == 0 {
c.oaErr(400, 400, "无效ID")
return
}
code, msg, hint := reimburseActCore(claims, id, true, c.expenseReadActPayload())
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(map[string]interface{}{"message": hint})
}
// ExpenseReject POST /api/oa/expense/reject/:id 驳回报销单(body: {comment})。
func (c *AppOaController) ExpenseReject() {
claims, err := c.oaClaims()
if err != nil {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
return
}
id := c.oaScheduleIDParam()
if id == 0 {
c.oaErr(400, 400, "无效ID")
return
}
code, msg, hint := reimburseActCore(claims, id, false, c.expenseReadActPayload())
if msg != "" {
c.oaErr(code, code, msg)
return
}
c.oaOk(map[string]interface{}{"message": hint})
}
// ExpenseMyApproveTask GET /api/oa/expense/my-approve-task/:id
// 查当前用户在该单据上的待审批任务:返回 {task_id,node_name},无待办返回 data=null。
// 小程序详情页据此决定是否展示「通过/驳回」按钮。
func (c *AppOaController) ExpenseMyApproveTask() {
claims, err := c.oaClaims()
if err != nil {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
return
}
id := c.oaScheduleIDParam()
if id == 0 {
c.oaErr(400, 400, "无效ID")
return
}
task, err := reimburseMyTaskCore(claims, id)
if err != nil {
c.oaErr(500, 500, "查询审批任务失败")
return
}
if task == nil {
c.oaOk(nil)
return
}
c.oaOk(map[string]interface{}{
"task_id": task.ID,
"node_name": task.NodeName,
})
}
// ExpenseTypes GET /api/oa/expense/types 费用类型下拉(全局通用 + 本租户专有,仅启用)。
func (c *AppOaController) ExpenseTypes() {
claims, err := c.oaClaims()
if err != nil {
code := identity.Status(err)
c.oaErr(code, code, err.Error())
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
}