453 lines
11 KiB
Go
453 lines
11 KiB
Go
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
|
||
}
|