diff --git a/backend/components.d.ts b/backend/components.d.ts index fe69c43..9017497 100644 --- a/backend/components.d.ts +++ b/backend/components.d.ts @@ -65,6 +65,8 @@ declare module 'vue' { ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabs: typeof import('element-plus/es')['ElTabs'] ElTag: typeof import('element-plus/es')['ElTag'] + ElTimeline: typeof import('element-plus/es')['ElTimeline'] + ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTree: typeof import('element-plus/es')['ElTree'] ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] diff --git a/backend/src/api/reimburse.js b/backend/src/api/reimburse.js new file mode 100644 index 0000000..6c65bd3 --- /dev/null +++ b/backend/src/api/reimburse.js @@ -0,0 +1,90 @@ +import request from '@/utils/request' + +export function getReimbursementList(params) { + return request({ + url: '/backend/reimbursements', + method: 'get', + params + }) +} + +export function getReimbursementDetail(id) { + return request({ + url: `/backend/reimbursements/${id}`, + method: 'get' + }) +} + +export function createReimbursement(data) { + return request({ + url: '/backend/reimbursements', + method: 'post', + data + }) +} + +export function updateReimbursement(id, data) { + return request({ + url: `/backend/reimbursements/${id}`, + method: 'post', + data + }) +} + +export function deleteReimbursement(id) { + return request({ + url: `/backend/reimbursements/${id}`, + method: 'delete' + }) +} + +export function submitReimbursement(id) { + return request({ + url: `/backend/reimbursements/${id}/submit`, + method: 'post' + }) +} + +export function getReimbursementRecords(id) { + return request({ + url: `/backend/reimbursements/${id}/records`, + method: 'get' + }) +} + +export function addReimbursementItem(id, data) { + return request({ + url: `/backend/reimbursements/${id}/items`, + method: 'post', + data + }) +} + +export function updateReimbursementItem(id, itemId, data) { + return request({ + url: `/backend/reimbursements/${id}/items/${itemId}`, + method: 'post', + data + }) +} + +export function deleteReimbursementItem(id, itemId) { + return request({ + url: `/backend/reimbursements/${id}/items/${itemId}`, + method: 'delete' + }) +} + +export function getApprovalFlows() { + return request({ + url: '/backend/reimbursements/flows', + method: 'get' + }) +} + +export function getReimbursementExpenseTypes() { + return request({ + url: '/backend/reimbursements/types', + method: 'get' + }) +} diff --git a/backend/src/views/apps/oa/dashboard/index.vue b/backend/src/views/apps/oa/dashboard/index.vue new file mode 100644 index 0000000..b29aa5c --- /dev/null +++ b/backend/src/views/apps/oa/dashboard/index.vue @@ -0,0 +1,329 @@ + + + + + diff --git a/backend/src/views/apps/oa/index.vue b/backend/src/views/apps/oa/index.vue new file mode 100644 index 0000000..3ea5af6 --- /dev/null +++ b/backend/src/views/apps/oa/index.vue @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/backend/src/views/apps/oa/reimburse/components/detail.vue b/backend/src/views/apps/oa/reimburse/components/detail.vue new file mode 100644 index 0000000..a40b59b --- /dev/null +++ b/backend/src/views/apps/oa/reimburse/components/detail.vue @@ -0,0 +1,398 @@ + + + + + diff --git a/backend/src/views/apps/oa/reimburse/components/edit.vue b/backend/src/views/apps/oa/reimburse/components/edit.vue new file mode 100644 index 0000000..e1a73c0 --- /dev/null +++ b/backend/src/views/apps/oa/reimburse/components/edit.vue @@ -0,0 +1,73 @@ + + + diff --git a/backend/src/views/apps/oa/reimburse/components/itemForm.vue b/backend/src/views/apps/oa/reimburse/components/itemForm.vue new file mode 100644 index 0000000..14b0ecb --- /dev/null +++ b/backend/src/views/apps/oa/reimburse/components/itemForm.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/backend/src/views/apps/oa/reimburse/index.vue b/backend/src/views/apps/oa/reimburse/index.vue new file mode 100644 index 0000000..b8b81be --- /dev/null +++ b/backend/src/views/apps/oa/reimburse/index.vue @@ -0,0 +1,330 @@ + + + + + diff --git a/go/controllers/backend_reimburse.go b/go/controllers/backend_reimburse.go new file mode 100644 index 0000000..652b179 --- /dev/null +++ b/go/controllers/backend_reimburse.go @@ -0,0 +1,701 @@ +package controllers + +import ( + "encoding/json" + "fmt" + "io" + "strconv" + "strings" + "time" + + "server/models" + "server/pkg/jwtutil" + + "github.com/beego/beego/v2/client/orm" + beego "github.com/beego/beego/v2/server/web" +) + +// BackendReimburseController 报销及审批接口。 +type BackendReimburseController struct { + beego.Controller +} + +type reimbursementItemPayload struct { + ID uint64 `json:"id"` + ExpenseType string `json:"expense_type"` + ExpenseDate string `json:"expense_date"` + Amount float64 `json:"amount"` + Quantity int `json:"quantity"` + UnitPrice float64 `json:"unit_price"` + Description string `json:"description"` + ReceiptFlag int8 `json:"receipt_flag"` + ReceiptURL string `json:"receipt_url"` +} + +type reimbursementPayload struct { + Title string `json:"title"` + DepartmentID *uint64 `json:"department_id"` + ExpenseType string `json:"expense_type"` + ApplyDate string `json:"apply_date"` + Description string `json:"description"` + Items []reimbursementItemPayload `json:"items"` +} + +func (c *BackendReimburseController) claims() (*jwtutil.Claims, bool) { + auth := strings.TrimSpace(c.Ctx.Request.Header.Get("Authorization")) + parts := strings.SplitN(auth, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + return nil, false + } + claims, err := jwtutil.ParseToken(parts[1]) + if err != nil || claims.UserType != "backend" || claims.TenantId <= 0 || claims.UserID <= 0 { + return nil, false + } + return claims, true +} + +func (c *BackendReimburseController) reply(code int, msg string, data interface{}) { + if code != 200 { + c.Ctx.Output.SetStatus(code) + } + result := map[string]interface{}{"code": code, "msg": msg} + if data != nil { + result["data"] = data + } + c.Data["json"] = result + _ = c.ServeJSON() +} + +func (c *BackendReimburseController) parsePayload() (reimbursementPayload, error) { + var payload reimbursementPayload + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil { + return payload, err + } + err = json.Unmarshal(raw, &payload) + return payload, err +} + +func parseReimburseDate(value string) (time.Time, error) { + return time.Parse("2006-01-02", strings.TrimSpace(value)) +} + +func (c *BackendReimburseController) getID() (uint64, bool) { + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + return id, err == nil && id > 0 +} + +func (c *BackendReimburseController) findOwned(id uint64, claims *jwtutil.Claims) (*models.BackendReimbursement, error) { + row := &models.BackendReimbursement{} + err := models.Orm.QueryTable(new(models.BackendReimbursement)). + Filter("id", id). + Filter("tid", claims.TenantId). + Filter("user_id", claims.UserID). + Filter("is_deleted", 0). + One(row) + return row, err +} + +func reimbursementItemDTO(item models.BackendReimbursementItem) map[string]interface{} { + return map[string]interface{}{ + "id": item.ID, "reimbursement_id": item.ReimbursementID, + "expense_type": item.ExpenseType, "expense_date": item.ExpenseDate.Format("2006-01-02"), + "amount": item.Amount, "quantity": item.Quantity, "unit_price": item.UnitPrice, + "description": item.Description, "receipt_flag": item.ReceiptFlag, "receipt_url": item.ReceiptURL, + } +} + +func reimbursementDTO(row models.BackendReimbursement, items []models.BackendReimbursementItem) map[string]interface{} { + result := map[string]interface{}{ + "id": row.ID, "tid": row.Tid, "uid": row.UID, "user_id": row.UserID, + "department_id": row.DepartmentID, "title": row.Title, "total_amount": row.TotalAmount, + "status": row.Status, "expense_type": row.ExpenseType, "apply_date": row.ApplyDate.Format("2006-01-02"), + "description": row.Description, "current_approver_id": row.CurrentApproverID, + "current_step": row.CurrentStep, "create_time": row.CreateTime, "update_time": row.UpdateTime, + } + list := make([]map[string]interface{}, 0, len(items)) + for _, item := range items { + list = append(list, reimbursementItemDTO(item)) + } + result["items"] = list + return result +} + +// List GET /backend/reimbursements +func (c *BackendReimburseController) List() { + claims, ok := c.claims() + if !ok { + c.reply(401, "未登录或无权限", nil) + return + } + page, _ := c.GetInt("page", 1) + pageSize, _ := c.GetInt("pageSize", 10) + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 10 + } + keyword := strings.TrimSpace(c.GetString("keyword")) + status, statusErr := c.GetInt("status") + hasStatus := statusErr == nil + + qs := models.Orm.QueryTable(new(models.BackendReimbursement)). + Filter("tid", claims.TenantId).Filter("user_id", claims.UserID).Filter("is_deleted", 0) + if keyword != "" { + qs = qs.Filter("description__icontains", keyword) + } + if hasStatus { + qs = qs.Filter("status", status) + } + total, err := qs.Count() + if err != nil { + c.reply(500, "查询失败", nil) + return + } + var rows []models.BackendReimbursement + _, err = qs.OrderBy("-id").Limit(pageSize, (page-1)*pageSize).All(&rows) + if err != nil && err != orm.ErrNoRows { + c.reply(500, "查询失败", nil) + return + } + if rows == nil { + rows = []models.BackendReimbursement{} + } + list := make([]map[string]interface{}, 0, len(rows)) + for _, row := range rows { + list = append(list, map[string]interface{}{ + "id": row.ID, + "tid": row.Tid, + "uid": row.UID, + "user_id": row.UserID, + "department_id": row.DepartmentID, + "title": row.Title, + "total_amount": row.TotalAmount, + "status": row.Status, + "expense_type": row.ExpenseType, + "apply_date": row.ApplyDate.Format("2006-01-02"), + "description": row.Description, + "current_approver_id": row.CurrentApproverID, + "current_step": row.CurrentStep, + "create_time": row.CreateTime, + "update_time": row.UpdateTime, + }) + } + c.reply(200, "success", map[string]interface{}{"list": list, "total": total}) +} + +// Detail GET /backend/reimbursements/:id +func (c *BackendReimburseController) Detail() { + claims, ok := c.claims() + if !ok { + c.reply(401, "未登录或无权限", nil) + return + } + id, ok := c.getID() + if !ok { + c.reply(400, "无效ID", nil) + return + } + row, err := c.findOwned(id, claims) + if err != nil { + c.reply(404, "报销单不存在", nil) + return + } + var items []models.BackendReimbursementItem + _, err = models.Orm.QueryTable(new(models.BackendReimbursementItem)). + Filter("reimbursement_id", id).Filter("tid", claims.TenantId).Filter("is_deleted", 0). + OrderBy("id").All(&items) + if err != nil && err != orm.ErrNoRows { + c.reply(500, "查询明细失败", nil) + return + } + c.reply(200, "success", reimbursementDTO(*row, items)) +} + +// Create POST /backend/reimbursements +func (c *BackendReimburseController) Create() { + claims, ok := c.claims() + if !ok { + c.reply(401, "未登录或无权限", nil) + return + } + payload, err := c.parsePayload() + if err != nil { + c.reply(400, "参数错误", nil) + return + } + applyDate, err := parseReimburseDate(payload.ApplyDate) + if err != nil { + c.reply(400, "申请日期格式错误", nil) + return + } + uid := uint64(claims.UserID) + row := &models.BackendReimbursement{ + Tid: uint64(claims.TenantId), UID: &uid, UserID: uid, DepartmentID: payload.DepartmentID, + Title: "", TotalAmount: 0, Status: 0, + ExpenseType: strings.TrimSpace(payload.ExpenseType), ApplyDate: applyDate, + Description: nullableReimburseString(payload.Description), + } + if err := saveReimbursement(row); err != nil { + c.reply(500, "创建报销单失败", nil) + return + } + c.reply(200, "success", map[string]interface{}{"id": row.ID}) +} + +// Update POST /backend/reimbursements/:id +func (c *BackendReimburseController) Update() { + claims, ok := c.claims() + if !ok { + c.reply(401, "未登录或无权限", nil) + return + } + id, ok := c.getID() + if !ok { + c.reply(400, "无效ID", nil) + return + } + row, err := c.findOwned(id, claims) + if err != nil { + c.reply(404, "报销单不存在", nil) + return + } + if row.Status != 0 { + c.reply(400, "只有草稿可以编辑", nil) + return + } + payload, err := c.parsePayload() + if err != nil { + c.reply(400, "参数错误", nil) + return + } + applyDate, err := parseReimburseDate(payload.ApplyDate) + if err != nil { + c.reply(400, "申请日期格式错误", nil) + return + } + row.ApplyDate, row.Description = applyDate, nullableReimburseString(payload.Description) + if err := saveReimbursement(row); err != nil { + c.reply(500, "更新报销单失败", nil) + return + } + c.reply(200, "success", map[string]interface{}{"id": row.ID}) +} + +// Delete DELETE /backend/reimbursements/:id +func (c *BackendReimburseController) Delete() { + claims, ok := c.claims() + if !ok { + c.reply(401, "未登录或无权限", nil) + return + } + id, ok := c.getID() + if !ok { + c.reply(400, "无效ID", nil) + return + } + row, err := c.findOwned(id, claims) + if err != nil { + c.reply(404, "报销单不存在", nil) + return + } + if row.Status != 0 { + c.reply(400, "只有草稿可以删除", nil) + return + } + now := time.Now() + _, err = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id). + Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now}) + if err != nil { + c.reply(500, "删除失败", nil) + return + } + _, _ = models.Orm.QueryTable(new(models.BackendReimbursementItem)). + Filter("reimbursement_id", id).Update(orm.Params{"is_deleted": 1, "delete_time": now}) + c.reply(200, "success", nil) +} + +func (c *BackendReimburseController) parseItemPayload() (reimbursementItemPayload, error) { + var payload reimbursementItemPayload + raw, err := io.ReadAll(c.Ctx.Request.Body) + if err != nil { + return payload, err + } + err = json.Unmarshal(raw, &payload) + return payload, err +} + +func (c *BackendReimburseController) getItemID() (uint64, bool) { + id, err := strconv.ParseUint(c.Ctx.Input.Param(":item_id"), 10, 64) + return id, err == nil && id > 0 +} + +func (c *BackendReimburseController) findOwnedItem(id, reimbursementID uint64, claims *jwtutil.Claims) (*models.BackendReimbursementItem, error) { + item := &models.BackendReimbursementItem{} + err := models.Orm.QueryTable(new(models.BackendReimbursementItem)). + Filter("id", id). + Filter("reimbursement_id", reimbursementID). + Filter("tid", claims.TenantId). + Filter("uid", claims.UserID). + Filter("is_deleted", 0). + One(item) + return item, err +} + +func validateReimbursementItem(item reimbursementItemPayload) (time.Time, error) { + if strings.TrimSpace(item.ExpenseType) == "" { + return time.Time{}, fmt.Errorf("费用类型不能为空") + } + date, err := parseReimburseDate(item.ExpenseDate) + if err != nil { + return time.Time{}, fmt.Errorf("费用日期格式错误") + } + if item.Amount <= 0 { + return time.Time{}, fmt.Errorf("费用金额必须大于0") + } + return date, nil +} + +func (c *BackendReimburseController) ensureDraftReimbursement(id uint64, claims *jwtutil.Claims) (*models.BackendReimbursement, bool) { + row, err := c.findOwned(id, claims) + if err != nil { + c.reply(404, "报销单不存在", nil) + return nil, false + } + if row.Status != 0 { + c.reply(400, "只有草稿可以维护费用明细", nil) + return nil, false + } + return row, true +} + +func recalculateReimbursementTotal(id uint64) error { + var items []models.BackendReimbursementItem + _, err := models.Orm.QueryTable(new(models.BackendReimbursementItem)). + Filter("reimbursement_id", id).Filter("is_deleted", 0).All(&items) + if err != nil && err != orm.ErrNoRows { + return err + } + var total float64 + for _, item := range items { + total += item.Amount + } + _, err = models.Orm.QueryTable(new(models.BackendReimbursement)). + Filter("id", id).Update(orm.Params{ + "total_amount": total, + "update_time": time.Now(), + }) + return err +} + +// AddItem POST /backend/reimbursements/:id/items +func (c *BackendReimburseController) AddItem() { + claims, ok := c.claims() + if !ok { + c.reply(401, "未登录或无权限", nil) + return + } + reimbursementID, ok := c.getID() + if !ok { + c.reply(400, "无效报销单ID", nil) + return + } + if _, ok := c.ensureDraftReimbursement(reimbursementID, claims); !ok { + return + } + payload, err := c.parseItemPayload() + if err != nil { + c.reply(400, "参数错误", nil) + return + } + date, err := validateReimbursementItem(payload) + if err != nil { + c.reply(400, err.Error(), nil) + return + } + uid := uint64(claims.UserID) + quantity := payload.Quantity + if quantity <= 0 { + quantity = 1 + } + item := &models.BackendReimbursementItem{ + Tid: uint64(claims.TenantId), UID: &uid, ReimbursementID: reimbursementID, + ExpenseType: strings.TrimSpace(payload.ExpenseType), ExpenseDate: date, + Amount: payload.Amount, Quantity: quantity, UnitPrice: payload.UnitPrice, + Description: strings.TrimSpace(payload.Description), ReceiptFlag: payload.ReceiptFlag, + ReceiptURL: strings.TrimSpace(payload.ReceiptURL), + } + if _, err := models.Orm.Insert(item); err != nil { + c.reply(500, "新增费用明细失败", nil) + return + } + if err := recalculateReimbursementTotal(reimbursementID); err != nil { + c.reply(500, "更新报销总额失败", nil) + return + } + c.reply(200, "success", reimbursementItemDTO(*item)) +} + +// UpdateItem POST /backend/reimbursements/:id/items/:item_id +func (c *BackendReimburseController) UpdateItem() { + claims, ok := c.claims() + if !ok { + c.reply(401, "未登录或无权限", nil) + return + } + reimbursementID, ok := c.getID() + if !ok { + c.reply(400, "无效报销单ID", nil) + return + } + itemID, ok := c.getItemID() + if !ok { + c.reply(400, "无效费用明细ID", nil) + return + } + if _, ok := c.ensureDraftReimbursement(reimbursementID, claims); !ok { + return + } + item, err := c.findOwnedItem(itemID, reimbursementID, claims) + if err != nil { + c.reply(404, "费用明细不存在", nil) + return + } + payload, err := c.parseItemPayload() + if err != nil { + c.reply(400, "参数错误", nil) + return + } + date, err := validateReimbursementItem(payload) + if err != nil { + c.reply(400, err.Error(), nil) + return + } + quantity := payload.Quantity + if quantity <= 0 { + quantity = 1 + } + item.ExpenseType = strings.TrimSpace(payload.ExpenseType) + item.ExpenseDate = date + item.Amount = payload.Amount + item.Quantity = quantity + item.UnitPrice = payload.UnitPrice + item.Description = strings.TrimSpace(payload.Description) + item.ReceiptFlag = payload.ReceiptFlag + item.ReceiptURL = strings.TrimSpace(payload.ReceiptURL) + if _, err := models.Orm.Update(item); err != nil { + c.reply(500, "更新费用明细失败", nil) + return + } + if err := recalculateReimbursementTotal(reimbursementID); err != nil { + c.reply(500, "更新报销总额失败", nil) + return + } + c.reply(200, "success", reimbursementItemDTO(*item)) +} + +// DeleteItem DELETE /backend/reimbursements/:id/items/:item_id +func (c *BackendReimburseController) DeleteItem() { + claims, ok := c.claims() + if !ok { + c.reply(401, "未登录或无权限", nil) + return + } + reimbursementID, ok := c.getID() + if !ok { + c.reply(400, "无效报销单ID", nil) + return + } + itemID, ok := c.getItemID() + if !ok { + c.reply(400, "无效费用明细ID", nil) + return + } + if _, ok := c.ensureDraftReimbursement(reimbursementID, claims); !ok { + return + } + item, err := c.findOwnedItem(itemID, reimbursementID, claims) + if err != nil { + c.reply(404, "费用明细不存在", nil) + return + } + now := time.Now() + _, err = models.Orm.QueryTable(new(models.BackendReimbursementItem)). + Filter("id", item.ID).Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now}) + if err != nil { + c.reply(500, "删除费用明细失败", nil) + return + } + if err := recalculateReimbursementTotal(reimbursementID); err != nil { + c.reply(500, "更新报销总额失败", nil) + return + } + c.reply(200, "success", nil) +} + +// Submit POST /backend/reimbursements/:id/submit +func (c *BackendReimburseController) Submit() { + claims, ok := c.claims() + if !ok { + c.reply(401, "未登录或无权限", nil) + return + } + id, ok := c.getID() + if !ok { + c.reply(400, "无效ID", nil) + return + } + row, err := c.findOwned(id, claims) + if err != nil { + c.reply(404, "报销单不存在", nil) + return + } + if row.Status != 0 { + c.reply(400, "当前状态不可提交", nil) + return + } + var flow models.BackendApprovalFlow + err = models.Orm.QueryTable(new(models.BackendApprovalFlow)). + Filter("tid", claims.TenantId).Filter("is_active", 1).Filter("is_deleted", 0). + OrderBy("step_order").One(&flow) + if err != nil { + c.reply(400, "尚未配置审批流程", nil) + return + } + now := time.Now() + _, err = models.Orm.QueryTable(new(models.BackendReimbursement)).Filter("id", id).Update(orm.Params{ + "status": 1, "current_step": flow.StepOrder, "current_approver_id": 0, "update_time": now, + }) + if err != nil { + c.reply(500, "提交失败", nil) + return + } + _, err = models.Orm.Insert(&models.BackendApprovalRecord{ + Tid: uint64(claims.TenantId), UID: uint64Ptr(uint64(claims.UserID)), ReimbursementID: id, + ApproverID: uint64(claims.UserID), ApproverName: claims.Username, Action: 1, + Step: flow.StepOrder, PrevStep: 0, + }) + if err != nil { + c.reply(500, "提交审批记录失败", nil) + return + } + c.reply(200, "success", nil) +} + +// Records GET /backend/reimbursements/:id/records +func (c *BackendReimburseController) Records() { + claims, ok := c.claims() + if !ok { + c.reply(401, "未登录或无权限", nil) + return + } + id, ok := c.getID() + if !ok { + c.reply(400, "无效ID", nil) + return + } + if _, err := c.findOwned(id, claims); err != nil { + c.reply(404, "报销单不存在", nil) + return + } + var rows []models.BackendApprovalRecord + _, err := models.Orm.QueryTable(new(models.BackendApprovalRecord)). + Filter("tid", claims.TenantId).Filter("reimbursement_id", id).Filter("is_deleted", 0). + OrderBy("step", "id").All(&rows) + if err != nil && err != orm.ErrNoRows { + c.reply(500, "查询审批记录失败", nil) + return + } + if rows == nil { + rows = []models.BackendApprovalRecord{} + } + c.reply(200, "success", rows) +} + +// Types GET /backend/reimbursements/types +func (c *BackendReimburseController) Types() { + claims, ok := c.claims() + if !ok { + c.reply(401, "未登录或无权限", nil) + return + } + var rows []models.BackendReimbursementExpenseType + _, err := models.Orm.QueryTable(new(models.BackendReimbursementExpenseType)). + Filter("tid", claims.TenantId).Filter("is_active", 1).Filter("is_deleted", 0). + OrderBy("sort_order", "id").All(&rows) + if err != nil && err != orm.ErrNoRows { + c.reply(500, "查询费用类型失败", nil) + return + } + if rows == nil { + rows = []models.BackendReimbursementExpenseType{} + } + c.reply(200, "success", rows) +} + +// Flows GET /backend/reimbursements/flows +func (c *BackendReimburseController) Flows() { + claims, ok := c.claims() + if !ok { + c.reply(401, "未登录或无权限", nil) + return + } + var rows []models.BackendApprovalFlow + _, err := models.Orm.QueryTable(new(models.BackendApprovalFlow)). + Filter("tid", claims.TenantId).Filter("is_active", 1).Filter("is_deleted", 0). + OrderBy("step_order").All(&rows) + if err != nil && err != orm.ErrNoRows { + c.reply(500, "查询审批流程失败", nil) + return + } + if rows == nil { + rows = []models.BackendApprovalFlow{} + } + c.reply(200, "success", rows) +} + +func validateItems(items []reimbursementItemPayload) (float64, error) { + var total float64 + for i, item := range items { + if strings.TrimSpace(item.ExpenseType) == "" { + return 0, fmt.Errorf("第%d条费用类型不能为空", i+1) + } + if _, err := parseReimburseDate(item.ExpenseDate); err != nil { + return 0, fmt.Errorf("第%d条费用日期格式错误", i+1) + } + if item.Amount <= 0 { + return 0, fmt.Errorf("第%d条费用金额必须大于0", i+1) + } + if item.Quantity <= 0 { + item.Quantity = 1 + } + total += item.Amount + } + return total, nil +} + +func saveReimbursement(row *models.BackendReimbursement) error { + if row.ID == 0 { + id, err := models.Orm.Insert(row) + if err != nil { + return err + } + row.ID = uint64(id) + return nil + } + _, err := models.Orm.Update(row) + return err +} + +func nullableReimburseString(value string) *string { + value = strings.TrimSpace(value) + if value == "" { + return nil + } + return &value +} + +func uint64Ptr(value uint64) *uint64 { + return &value +} diff --git a/go/models/init.go b/go/models/init.go index 25cfaef..3df4a55 100644 --- a/go/models/init.go +++ b/go/models/init.go @@ -1,86 +1,91 @@ -package models - -import ( - "fmt" - - "github.com/beego/beego/v2/client/orm" - beego "github.com/beego/beego/v2/server/web" - _ "github.com/go-sql-driver/mysql" -) - -// Orm 全局 ORM 对象,供业务层和控制器使用 -var Orm orm.Ormer - -// Init 初始化模型层资源(数据库连接、模型注册等)。 -func Init(_ string) { - // 从配置读取数据库连接信息 - user, _ := beego.AppConfig.String("mysqluser") - pass, _ := beego.AppConfig.String("mysqlpass") - urls, _ := beego.AppConfig.String("mysqlurls") - dbname, _ := beego.AppConfig.String("mysqldb") - - if user == "" || urls == "" || dbname == "" { - panic("数据库配置(mysqluser/mysqlurls/mysqldb) 未正确设置") - } - - // 组装 DSN:user:pass@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local - dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, pass, urls, dbname) - - // 注册默认数据库 - if err := orm.RegisterDataBase("default", "mysql", dsn); err != nil { - panic("注册数据库失败: " + err.Error()) - } - - // 注册模型 - orm.RegisterModel( - new(SystemTenant), - new(SystemTenantUser), - new(BackendErpOrganization), - new(BackendErpEmployee), - new(BackendErpPosition), - new(SystemMenu), - new(AdminUser), - new(AdminRole), - new(SystemFile), - new(SystemFilesCategory), - new(SystemSMSTask), - new(SystemOperationLog), - new(SystemDomainPool), - new(SystemTenantDomain), - new(SystemModules), - new(StorageConfig), - new(TenantSiteSetting), - new(ComplaintCategory), - new(PlatformComplaint), - new(SystemSoftwareUpgrade), - new(PlatformCursorEquipment), - new(PlatformCursorActivationCode), - new(PlatformCursorEquipmentIpLog), - new(PlatformAccountPoolKiro), - new(PlatformAccountPoolWindsurf), - new(PlatformAccountPoolCursor), - new(PlatformAccountPoolCodex), - new(PlatformNotebook), - - new(CmsArticleCategory), - new(CmsArticle), - - new(SystemReminderList), - - new(SystemNormalSetting), - new(PlatformNormalSetting), - new(BackendNormalSetting), - - new(PlatformSchedule), - new(PlatformScheduleReminder), - new(PlatformScheduleReminderSendLog), - - new(BackendNotebook), - new(BackendSchedule), - new(BackendScheduleReminder), - new(BackendScheduleReminderSendLog), - ) - - // 创建全局 Ormer - Orm = orm.NewOrm() -} +package models + +import ( + "fmt" + + "github.com/beego/beego/v2/client/orm" + beego "github.com/beego/beego/v2/server/web" + _ "github.com/go-sql-driver/mysql" +) + +// Orm 全局 ORM 对象,供业务层和控制器使用 +var Orm orm.Ormer + +// Init 初始化模型层资源(数据库连接、模型注册等)。 +func Init(_ string) { + // 从配置读取数据库连接信息 + user, _ := beego.AppConfig.String("mysqluser") + pass, _ := beego.AppConfig.String("mysqlpass") + urls, _ := beego.AppConfig.String("mysqlurls") + dbname, _ := beego.AppConfig.String("mysqldb") + + if user == "" || urls == "" || dbname == "" { + panic("数据库配置(mysqluser/mysqlurls/mysqldb) 未正确设置") + } + + // 组装 DSN:user:pass@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local + dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, pass, urls, dbname) + + // 注册默认数据库 + if err := orm.RegisterDataBase("default", "mysql", dsn); err != nil { + panic("注册数据库失败: " + err.Error()) + } + + // 注册模型 + orm.RegisterModel( + new(SystemTenant), + new(SystemTenantUser), + new(BackendErpOrganization), + new(BackendErpEmployee), + new(BackendErpPosition), + new(BackendApprovalFlow), + new(BackendApprovalRecord), + new(BackendReimbursement), + new(BackendReimbursementItem), + new(BackendReimbursementExpenseType), + new(SystemMenu), + new(AdminUser), + new(AdminRole), + new(SystemFile), + new(SystemFilesCategory), + new(SystemSMSTask), + new(SystemOperationLog), + new(SystemDomainPool), + new(SystemTenantDomain), + new(SystemModules), + new(StorageConfig), + new(TenantSiteSetting), + new(ComplaintCategory), + new(PlatformComplaint), + new(SystemSoftwareUpgrade), + new(PlatformCursorEquipment), + new(PlatformCursorActivationCode), + new(PlatformCursorEquipmentIpLog), + new(PlatformAccountPoolKiro), + new(PlatformAccountPoolWindsurf), + new(PlatformAccountPoolCursor), + new(PlatformAccountPoolCodex), + new(PlatformNotebook), + + new(CmsArticleCategory), + new(CmsArticle), + + new(SystemReminderList), + + new(SystemNormalSetting), + new(PlatformNormalSetting), + new(BackendNormalSetting), + + new(PlatformSchedule), + new(PlatformScheduleReminder), + new(PlatformScheduleReminderSendLog), + + new(BackendNotebook), + new(BackendSchedule), + new(BackendScheduleReminder), + new(BackendScheduleReminderSendLog), + ) + + // 创建全局 Ormer + Orm = orm.NewOrm() +} diff --git a/go/models/reimburse.go b/go/models/reimburse.go new file mode 100644 index 0000000..17e5b3e --- /dev/null +++ b/go/models/reimburse.go @@ -0,0 +1,113 @@ +package models + +import "time" + +// BackendApprovalFlow 审批流程配置表 yz_backend_approval_flows +type BackendApprovalFlow struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid uint64 `orm:"column(tid)" json:"tid"` + UID *uint64 `orm:"column(uid);null" json:"uid"` + FlowName string `orm:"column(flow_name);size(100)" json:"flow_name"` + StepOrder int8 `orm:"column(step_order);default(0)" json:"step_order"` + ApproverRole string `orm:"column(approver_role);size(50)" json:"approver_role"` + ApproverRule string `orm:"column(approver_rule);size(200)" json:"approver_rule"` + TimeoutHours int `orm:"column(timeout_hours);default(48)" json:"timeout_hours"` + IsActive int8 `orm:"column(is_active);default(1)" json:"is_active"` + IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"` +} + +func (m *BackendApprovalFlow) TableName() string { + return "yz_backend_approval_flows" +} + +// BackendApprovalRecord 审批记录表 yz_backend_approval_records +type BackendApprovalRecord struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid uint64 `orm:"column(tid)" json:"tid"` + UID *uint64 `orm:"column(uid);null" json:"uid"` + ReimbursementID uint64 `orm:"column(reimbursement_id)" json:"reimbursement_id"` + ApproverID uint64 `orm:"column(approver_id)" json:"approver_id"` + ApproverName string `orm:"column(approver_name);size(100)" json:"approver_name"` + Action int8 `orm:"column(action);default(0)" json:"action"` + Comment *string `orm:"column(comment);null;type(text)" json:"comment"` + Step int8 `orm:"column(step);default(0)" json:"step"` + PrevStep int8 `orm:"column(prev_step);default(0)" json:"prev_step"` + IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"` +} + +func (m *BackendApprovalRecord) TableName() string { + return "yz_backend_approval_records" +} + +// BackendReimbursement 报销单主表 yz_backend_reimbursements +type BackendReimbursement struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid uint64 `orm:"column(tid)" json:"tid"` + UID *uint64 `orm:"column(uid);null" json:"uid"` + UserID uint64 `orm:"column(user_id)" json:"user_id"` + DepartmentID *uint64 `orm:"column(department_id);null" json:"department_id"` + Title string `orm:"column(title);size(200)" json:"title"` + TotalAmount float64 `orm:"column(total_amount);type(decimal);digits(10);decimals(2)" json:"total_amount"` + Status int8 `orm:"column(status);default(0)" json:"status"` + ExpenseType string `orm:"column(expense_type);size(50)" json:"expense_type"` + ApplyDate time.Time `orm:"column(apply_date);type(date)" json:"apply_date"` + Description *string `orm:"column(description);null;type(text)" json:"description"` + CurrentApproverID uint64 `orm:"column(current_approver_id);default(0)" json:"current_approver_id"` + CurrentStep int8 `orm:"column(current_step);default(0)" json:"current_step"` + IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"` +} + +func (m *BackendReimbursement) TableName() string { + return "yz_backend_reimbursements" +} + +// BackendReimbursementItem 报销费用明细表 yz_backend_reimbursement_items +type BackendReimbursementItem struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid uint64 `orm:"column(tid)" json:"tid"` + UID *uint64 `orm:"column(uid);null" json:"uid"` + ReimbursementID uint64 `orm:"column(reimbursement_id)" json:"reimbursement_id"` + ExpenseType string `orm:"column(expense_type);size(50)" json:"expense_type"` + ExpenseDate time.Time `orm:"column(expense_date);type(date)" json:"expense_date"` + Amount float64 `orm:"column(amount);type(decimal);digits(10);decimals(2)" json:"amount"` + Quantity int `orm:"column(quantity);default(1)" json:"quantity"` + UnitPrice float64 `orm:"column(unit_price);type(decimal);digits(10);decimals(2)" json:"unit_price"` + Description string `orm:"column(description);size(200)" json:"description"` + ReceiptFlag int8 `orm:"column(receipt_flag);default(0)" json:"receipt_flag"` + ReceiptURL string `orm:"column(receipt_url);size(500)" json:"receipt_url"` + IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"` +} + +func (m *BackendReimbursementItem) TableName() string { + return "yz_backend_reimbursement_items" +} + +// BackendReimbursementExpenseType 报销费用类型表 yz_backend_reimbursement_expense_types +type BackendReimbursementExpenseType struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Tid uint64 `orm:"column(tid)" json:"tid"` + UID *uint64 `orm:"column(uid);null" json:"uid"` + Name string `orm:"column(name);size(50)" json:"name"` + SortOrder int `orm:"column(sort_order);default(0)" json:"sort_order"` + IsActive int8 `orm:"column(is_active);default(1)" json:"is_active"` + IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"` + CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);null;type(datetime)" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"` +} + +func (m *BackendReimbursementExpenseType) TableName() string { + return "yz_backend_reimbursement_expense_types" +} diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go index 6745ca8..efa2c79 100644 --- a/go/routers/backend/backend.go +++ b/go/routers/backend/backend.go @@ -1,162 +1,172 @@ -package backend - -import ( - "server/controllers" - - beego "github.com/beego/beego/v2/server/web" -) - -// Register 注册租户端(backend)路由。 -// 该端不包含平台菜单配置接口。 -func Register() { - RegisterAuthRoutes() -} - -// RegisterAuthRoutes 注册 backend 认证相关路由。 -func RegisterAuthRoutes() { - // 登录、注册与找回密码相关 - beego.Router("/backend/login", &controllers.BackendAuthController{}, "post:LoginBackend") - beego.Router("/backend/sendLoginCode", &controllers.BackendAuthController{}, "post:SendLoginCode") - beego.Router("/backend/loginBySms", &controllers.BackendAuthController{}, "post:LoginBySms") - beego.Router("/backend/logout", &controllers.BackendAuthController{}, "post:Logout") - beego.Router("/backend/register", &controllers.BackendAuthController{}, "post:Register") - beego.Router("/backend/sendRegisterCode", &controllers.BackendAuthController{}, "post:SendRegisterCode") - beego.Router("/backend/resetPassword", &controllers.BackendAuthController{}, "post:ResetPassword") - beego.Router("/backend/sendResetCode", &controllers.BackendAuthController{}, "post:SendResetCode") - - // 极验与登录验证配置 - beego.Router("/backend/login/getGeetest3Infos", &controllers.BackendAuthController{}, "get:GetGeetest3Infos") - beego.Router("/backend/login/getGeetest4Infos", &controllers.BackendAuthController{}, "get:GetGeetest4Infos") - beego.Router("/backend/login/getOpenVerify", &controllers.BackendAuthController{}, "get:GetOpenVerify") - - // 菜单接口 - beego.Router("/backend/menu/:id", &controllers.BackendMenuController{}, "get:GetBackendMenu") - beego.Router("/backend/allmenu", &controllers.BackendMenuController{}, "get:GetAllBackendMenus") - - // 操作日志(yz_system_operation_log) - beego.Router("/backend/operationLogs", &controllers.BackendOperationLogController{}, "get:List") - beego.Router("/backend/operationLogs/statistics", &controllers.BackendOperationLogController{}, "get:Statistics") - beego.Router("/backend/operationLogs/:id", &controllers.BackendOperationLogController{}, "get:Detail;delete:Delete") - beego.Router("/backend/operationLogs/batchDelete", &controllers.BackendOperationLogController{}, "post:BatchDelete") - - // 租户站点设置 - beego.Router("/backend/normalInfos", &controllers.BackendSiteSettingsController{}, "get:GetNormalInfos") - beego.Router("/backend/saveNormalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveNormalInfos") - beego.Router("/backend/legalInfos", &controllers.BackendSiteSettingsController{}, "get:GetLegalInfos") - beego.Router("/backend/saveLegalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveLegalInfos") - beego.Router("/backend/companyInfos", &controllers.BackendSiteSettingsController{}, "get:GetCompanyInfos") - beego.Router("/backend/saveCompanyInfos", &controllers.BackendSiteSettingsController{}, "post:SaveCompanyInfos") - beego.Router("/backend/companySeo", &controllers.BackendSiteSettingsController{}, "get:GetCompanySeo") - beego.Router("/backend/saveCompanySeo", &controllers.BackendSiteSettingsController{}, "post:SaveCompanySeo") - beego.Router("/backend/loginVerifyInfos", &controllers.BackendLoginVerifyController{}, "get:GetLoginVerifyInfos") - beego.Router("/backend/saveloginVerifyInfos", &controllers.BackendLoginVerifyController{}, "post:SaveLoginVerifyInfos") - - // 站内信(yz_system_reminderlist) - beego.Router("/backend/sitereminder/myList", &controllers.BackendSiteReminderController{}, "get:GetMyList") - beego.Router("/backend/sitereminder/read", &controllers.BackendSiteReminderController{}, "post:MarkRead") - beego.Router("/backend/sitereminder/readall", &controllers.BackendSiteReminderController{}, "post:MarkAllRead") - beego.Router("/backend/sitereminder/delete", &controllers.BackendSiteReminderController{}, "post:Delete") - - // 文件管理(yz_system_files / yz_system_files_category) - beego.Router("/backend/usercate", &controllers.BackendFileController{}, "get:GetUserCate") - beego.Router("/backend/allfiles", &controllers.BackendFileController{}, "get:GetAllFiles") - beego.Router("/backend/catefiles/:id", &controllers.BackendFileController{}, "get:GetCateFiles") - beego.Router("/backend/file/:id", &controllers.BackendFileController{}, "get:GetFileByID") - beego.Router("/backend/deletefilepermanently/:id", &controllers.BackendFileController{}, "delete:DeleteFilePermanently") - beego.Router("/backend/uploadfile", &controllers.BackendFileController{}, "post:UploadFile") - beego.Router("/backend/uploadfiles", &controllers.BackendFileController{}, "post:UploadFile") - beego.Router("/backend/updatefile/:id", &controllers.BackendFileController{}, "post:UpdateFile") - beego.Router("/backend/deletefile/:id", &controllers.BackendFileController{}, "delete:DeleteFile") - beego.Router("/backend/movefile/:id", &controllers.BackendFileController{}, "get:MoveFile") - beego.Router("/backend/createfilecate", &controllers.BackendFileController{}, "post:CreateFileCate") - beego.Router("/backend/renamefilecate/:id", &controllers.BackendFileController{}, "post:RenameFileCate") - beego.Router("/backend/deletefilecate/:id", &controllers.BackendFileController{}, "delete:DeleteFileCate") - beego.Router("/backend/uploadavatar", &controllers.BackendFileController{}, "post:UploadAvatar") - beego.Router("/backend/uploadavatar/:id", &controllers.BackendFileController{}, "post:UpdateAvatar") - beego.Router("/backend/batchdeletefiles", &controllers.BackendFileController{}, "post:BatchDeleteFiles") - beego.Router("/backend/batchDeleteFilesPermanently", &controllers.BackendFileController{}, "post:BatchDeleteFilesPermanently") - beego.Router("/backend/batchMoveFiles", &controllers.BackendFileController{}, "post:BatchMoveFiles") - - // 模块接口 - beego.Router("/backend/modules/getTenantList", &controllers.BackendModulesController{}, "get:GetTenantList") - - // 用户接口 - beego.Router("/backend/getTenantUsers/:tid", &controllers.BackendAdminUserController{}, "get:GetTenantUsers") - beego.Router("/backend/getAllUsers", &controllers.BackendAdminUserController{}, "get:GetAllUsers") - beego.Router("/backend/getUserInfo/:id", &controllers.BackendAdminUserController{}, "get:GetUserInfo") - beego.Router("/backend/addUser", &controllers.BackendAdminUserController{}, "post:AddUser") - beego.Router("/backend/editUser/:id", &controllers.BackendAdminUserController{}, "post:EditUser") - beego.Router("/backend/deleteUser/:id", &controllers.BackendAdminUserController{}, "delete:DeleteUser") - beego.Router("/backend/changePassword", &controllers.BackendAdminUserController{}, "post:ChangePassword") - - // ERP 接口 - beego.Router("/backend/erp/getOrganization", &controllers.BackendErpController{}, "get:GetOrganization") - beego.Router("/backend/erp/getOrganizationDetail/:id", &controllers.BackendErpController{}, "get:GetOrganizationDetail") - beego.Router("/backend/erp/createOrganization", &controllers.BackendErpController{}, "post:CreateOrganization") - beego.Router("/backend/erp/editOrganization/:id", &controllers.BackendErpController{}, "post:EditOrganization") - beego.Router("/backend/erp/deleteOrganization/:id", &controllers.BackendErpController{}, "delete:DeleteOrganization") - beego.Router("/backend/erp/getCompanys", &controllers.BackendErpController{}, "get:GetCompanys") - beego.Router("/backend/erp/getDepartments", &controllers.BackendErpController{}, "get:GetDepartments") - beego.Router("/backend/erp/getEmployee", &controllers.BackendErpController{}, "get:GetEmployee") - beego.Router("/backend/erp/getEmployeeDetail/:id", &controllers.BackendErpController{}, "get:GetEmployeeDetail") - beego.Router("/backend/erp/createEmployee", &controllers.BackendErpController{}, "post:CreateEmployee") - beego.Router("/backend/erp/editEmployee/:id", &controllers.BackendErpController{}, "post:EditEmployee") - beego.Router("/backend/erp/deleteEmployee/:id", &controllers.BackendErpController{}, "delete:DeleteEmployee") - beego.Router("/backend/erp/getPosition", &controllers.BackendErpController{}, "get:GetPosition") - beego.Router("/backend/erp/getPositionDetail/:id", &controllers.BackendErpController{}, "get:GetPositionDetail") - beego.Router("/backend/erp/createPosition", &controllers.BackendErpController{}, "post:CreatePosition") - beego.Router("/backend/erp/editPosition/:id", &controllers.BackendErpController{}, "post:EditPosition") - beego.Router("/backend/erp/deletePosition/:id", &controllers.BackendErpController{}, "delete:DeletePosition") - - // 文章管理 - beego.Router("/backend/articlesList", &controllers.BackendArticleController{}, "get:List") - beego.Router("/backend/contentstats", &controllers.BackendArticleController{}, "get:ContentStats") - beego.Router("/backend/allarticles", &controllers.BackendArticleController{}, "get:ListAll") - beego.Router("/backend/articles/:id", &controllers.BackendArticleController{}, "get:Detail") - beego.Router("/backend/createarticle", &controllers.BackendArticleController{}, "post:Create") - beego.Router("/backend/editarticle/:id", &controllers.BackendArticleController{}, "post:Update") - beego.Router("/backend/deletearticle/:id", &controllers.BackendArticleController{}, "delete:Delete") - beego.Router("/backend/publisharticle/:id", &controllers.BackendArticleController{}, "post:Publish") - beego.Router("/backend/unPublisharticle/:id", &controllers.BackendArticleController{}, "post:Unpublish") - beego.Router("/backend/articleRecommend/:id", &controllers.BackendArticleController{}, "post:Recommend") - beego.Router("/backend/unArticleRecommend/:id", &controllers.BackendArticleController{}, "post:Unrecommend") - beego.Router("/backend/articleTop/:id", &controllers.BackendArticleController{}, "post:Top") - beego.Router("/backend/unArticleTop/:id", &controllers.BackendArticleController{}, "post:Untop") - - beego.Router("/backend/categories", &controllers.BackendArticleCategoryController{}, "get:List") - beego.Router("/backend/allcategories", &controllers.BackendArticleCategoryController{}, "get:ListAll") - beego.Router("/backend/categories/:id", &controllers.BackendArticleCategoryController{}, "get:Detail;delete:Delete") - beego.Router("/backend/createCategory", &controllers.BackendArticleCategoryController{}, "post:Create") - beego.Router("/backend/editCategory/:id", &controllers.BackendArticleCategoryController{}, "post:Update") - beego.Router("/backend/categories/:id/status", &controllers.BackendArticleCategoryController{}, "patch:UpdateStatus") - - // 域名管理(主域名池 / 租户域名) - beego.Router("/backend/domain/pool/index", &controllers.BackendDomainPoolController{}, "get:Index") - beego.Router("/backend/domain/pool/getEnabledDomains", &controllers.BackendDomainPoolController{}, "get:GetEnabledDomains") - beego.Router("/backend/domain/pool/create", &controllers.BackendDomainPoolController{}, "post:Create") - beego.Router("/backend/domain/pool/update", &controllers.BackendDomainPoolController{}, "post:Update") - beego.Router("/backend/domain/pool/delete/:id", &controllers.BackendDomainPoolController{}, "delete:Delete") - beego.Router("/backend/domain/pool/toggleStatus", &controllers.BackendDomainPoolController{}, "post:ToggleStatus") - - beego.Router("/backend/domain/tenant/index", &controllers.BackendTenantDomainController{}, "get:Index") - beego.Router("/backend/domain/tenant/myDomains", &controllers.BackendTenantDomainController{}, "get:MyDomains") - beego.Router("/backend/domain/tenant/apply", &controllers.BackendTenantDomainController{}, "post:Apply") - beego.Router("/backend/domain/tenant/audit", &controllers.BackendTenantDomainController{}, "post:Audit") - beego.Router("/backend/domain/tenant/toggleStatus", &controllers.BackendTenantDomainController{}, "post:ToggleStatus") - beego.Router("/backend/domain/tenant/delete/:id", &controllers.BackendTenantDomainController{}, "delete:Delete") - - // 记事本管理 - beego.Router("/backend/notebook/list", &controllers.BackendNotebookController{}, "get:List") - beego.Router("/backend/notebook/detail/:id", &controllers.BackendNotebookController{}, "get:Detail") - beego.Router("/backend/notebook/create", &controllers.BackendNotebookController{}, "post:Create") - beego.Router("/backend/notebook/update/:id", &controllers.BackendNotebookController{}, "post:Update") - beego.Router("/backend/notebook/delete/:id", &controllers.BackendNotebookController{}, "delete:Delete") - - // 日程提醒管理 - beego.Router("/backend/reminder/list", &controllers.BackendReminderController{}, "get:GetReminderList") - beego.Router("/backend/reminder/test", &controllers.BackendReminderController{}, "post:TestReminder") - beego.Router("/backend/reminder/:id", &controllers.BackendReminderController{}, "get:GetReminderDetail;put:UpdateReminder;delete:DeleteReminder") - beego.Router("/backend/reminder", &controllers.BackendReminderController{}, "post:CreateReminder") - beego.Router("/backend/reminder/batchDelete", &controllers.BackendReminderController{}, "post:BatchDeleteReminder") - beego.Router("/backend/reminder/finish/:id", &controllers.BackendReminderController{}, "post:FinishReminder") -} +package backend + +import ( + "server/controllers" + + beego "github.com/beego/beego/v2/server/web" +) + +// Register 注册租户端(backend)路由。 +// 该端不包含平台菜单配置接口。 +func Register() { + RegisterAuthRoutes() +} + +// RegisterAuthRoutes 注册 backend 认证相关路由。 +func RegisterAuthRoutes() { + // 登录、注册与找回密码相关 + beego.Router("/backend/login", &controllers.BackendAuthController{}, "post:LoginBackend") + beego.Router("/backend/sendLoginCode", &controllers.BackendAuthController{}, "post:SendLoginCode") + beego.Router("/backend/loginBySms", &controllers.BackendAuthController{}, "post:LoginBySms") + beego.Router("/backend/logout", &controllers.BackendAuthController{}, "post:Logout") + beego.Router("/backend/register", &controllers.BackendAuthController{}, "post:Register") + beego.Router("/backend/sendRegisterCode", &controllers.BackendAuthController{}, "post:SendRegisterCode") + beego.Router("/backend/resetPassword", &controllers.BackendAuthController{}, "post:ResetPassword") + beego.Router("/backend/sendResetCode", &controllers.BackendAuthController{}, "post:SendResetCode") + + // 极验与登录验证配置 + beego.Router("/backend/login/getGeetest3Infos", &controllers.BackendAuthController{}, "get:GetGeetest3Infos") + beego.Router("/backend/login/getGeetest4Infos", &controllers.BackendAuthController{}, "get:GetGeetest4Infos") + beego.Router("/backend/login/getOpenVerify", &controllers.BackendAuthController{}, "get:GetOpenVerify") + + // 菜单接口 + beego.Router("/backend/menu/:id", &controllers.BackendMenuController{}, "get:GetBackendMenu") + beego.Router("/backend/allmenu", &controllers.BackendMenuController{}, "get:GetAllBackendMenus") + + // 操作日志(yz_system_operation_log) + beego.Router("/backend/operationLogs", &controllers.BackendOperationLogController{}, "get:List") + beego.Router("/backend/operationLogs/statistics", &controllers.BackendOperationLogController{}, "get:Statistics") + beego.Router("/backend/operationLogs/:id", &controllers.BackendOperationLogController{}, "get:Detail;delete:Delete") + beego.Router("/backend/operationLogs/batchDelete", &controllers.BackendOperationLogController{}, "post:BatchDelete") + + // 租户站点设置 + beego.Router("/backend/normalInfos", &controllers.BackendSiteSettingsController{}, "get:GetNormalInfos") + beego.Router("/backend/saveNormalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveNormalInfos") + beego.Router("/backend/legalInfos", &controllers.BackendSiteSettingsController{}, "get:GetLegalInfos") + beego.Router("/backend/saveLegalInfos", &controllers.BackendSiteSettingsController{}, "post:SaveLegalInfos") + beego.Router("/backend/companyInfos", &controllers.BackendSiteSettingsController{}, "get:GetCompanyInfos") + beego.Router("/backend/saveCompanyInfos", &controllers.BackendSiteSettingsController{}, "post:SaveCompanyInfos") + beego.Router("/backend/companySeo", &controllers.BackendSiteSettingsController{}, "get:GetCompanySeo") + beego.Router("/backend/saveCompanySeo", &controllers.BackendSiteSettingsController{}, "post:SaveCompanySeo") + beego.Router("/backend/loginVerifyInfos", &controllers.BackendLoginVerifyController{}, "get:GetLoginVerifyInfos") + beego.Router("/backend/saveloginVerifyInfos", &controllers.BackendLoginVerifyController{}, "post:SaveLoginVerifyInfos") + + // 站内信(yz_system_reminderlist) + beego.Router("/backend/sitereminder/myList", &controllers.BackendSiteReminderController{}, "get:GetMyList") + beego.Router("/backend/sitereminder/read", &controllers.BackendSiteReminderController{}, "post:MarkRead") + beego.Router("/backend/sitereminder/readall", &controllers.BackendSiteReminderController{}, "post:MarkAllRead") + beego.Router("/backend/sitereminder/delete", &controllers.BackendSiteReminderController{}, "post:Delete") + + // 文件管理(yz_system_files / yz_system_files_category) + beego.Router("/backend/usercate", &controllers.BackendFileController{}, "get:GetUserCate") + beego.Router("/backend/allfiles", &controllers.BackendFileController{}, "get:GetAllFiles") + beego.Router("/backend/catefiles/:id", &controllers.BackendFileController{}, "get:GetCateFiles") + beego.Router("/backend/file/:id", &controllers.BackendFileController{}, "get:GetFileByID") + beego.Router("/backend/deletefilepermanently/:id", &controllers.BackendFileController{}, "delete:DeleteFilePermanently") + beego.Router("/backend/uploadfile", &controllers.BackendFileController{}, "post:UploadFile") + beego.Router("/backend/uploadfiles", &controllers.BackendFileController{}, "post:UploadFile") + beego.Router("/backend/updatefile/:id", &controllers.BackendFileController{}, "post:UpdateFile") + beego.Router("/backend/deletefile/:id", &controllers.BackendFileController{}, "delete:DeleteFile") + beego.Router("/backend/movefile/:id", &controllers.BackendFileController{}, "get:MoveFile") + beego.Router("/backend/createfilecate", &controllers.BackendFileController{}, "post:CreateFileCate") + beego.Router("/backend/renamefilecate/:id", &controllers.BackendFileController{}, "post:RenameFileCate") + beego.Router("/backend/deletefilecate/:id", &controllers.BackendFileController{}, "delete:DeleteFileCate") + beego.Router("/backend/uploadavatar", &controllers.BackendFileController{}, "post:UploadAvatar") + beego.Router("/backend/uploadavatar/:id", &controllers.BackendFileController{}, "post:UpdateAvatar") + beego.Router("/backend/batchdeletefiles", &controllers.BackendFileController{}, "post:BatchDeleteFiles") + beego.Router("/backend/batchDeleteFilesPermanently", &controllers.BackendFileController{}, "post:BatchDeleteFilesPermanently") + beego.Router("/backend/batchMoveFiles", &controllers.BackendFileController{}, "post:BatchMoveFiles") + + // 模块接口 + beego.Router("/backend/modules/getTenantList", &controllers.BackendModulesController{}, "get:GetTenantList") + + // 用户接口 + beego.Router("/backend/getTenantUsers/:tid", &controllers.BackendAdminUserController{}, "get:GetTenantUsers") + beego.Router("/backend/getAllUsers", &controllers.BackendAdminUserController{}, "get:GetAllUsers") + beego.Router("/backend/getUserInfo/:id", &controllers.BackendAdminUserController{}, "get:GetUserInfo") + beego.Router("/backend/addUser", &controllers.BackendAdminUserController{}, "post:AddUser") + beego.Router("/backend/editUser/:id", &controllers.BackendAdminUserController{}, "post:EditUser") + beego.Router("/backend/deleteUser/:id", &controllers.BackendAdminUserController{}, "delete:DeleteUser") + beego.Router("/backend/changePassword", &controllers.BackendAdminUserController{}, "post:ChangePassword") + + // ERP 接口 + beego.Router("/backend/erp/getOrganization", &controllers.BackendErpController{}, "get:GetOrganization") + beego.Router("/backend/erp/getOrganizationDetail/:id", &controllers.BackendErpController{}, "get:GetOrganizationDetail") + beego.Router("/backend/erp/createOrganization", &controllers.BackendErpController{}, "post:CreateOrganization") + beego.Router("/backend/erp/editOrganization/:id", &controllers.BackendErpController{}, "post:EditOrganization") + beego.Router("/backend/erp/deleteOrganization/:id", &controllers.BackendErpController{}, "delete:DeleteOrganization") + beego.Router("/backend/erp/getCompanys", &controllers.BackendErpController{}, "get:GetCompanys") + beego.Router("/backend/erp/getDepartments", &controllers.BackendErpController{}, "get:GetDepartments") + beego.Router("/backend/erp/getEmployee", &controllers.BackendErpController{}, "get:GetEmployee") + beego.Router("/backend/erp/getEmployeeDetail/:id", &controllers.BackendErpController{}, "get:GetEmployeeDetail") + beego.Router("/backend/erp/createEmployee", &controllers.BackendErpController{}, "post:CreateEmployee") + beego.Router("/backend/erp/editEmployee/:id", &controllers.BackendErpController{}, "post:EditEmployee") + beego.Router("/backend/erp/deleteEmployee/:id", &controllers.BackendErpController{}, "delete:DeleteEmployee") + beego.Router("/backend/erp/getPosition", &controllers.BackendErpController{}, "get:GetPosition") + beego.Router("/backend/erp/getPositionDetail/:id", &controllers.BackendErpController{}, "get:GetPositionDetail") + beego.Router("/backend/erp/createPosition", &controllers.BackendErpController{}, "post:CreatePosition") + beego.Router("/backend/erp/editPosition/:id", &controllers.BackendErpController{}, "post:EditPosition") + beego.Router("/backend/erp/deletePosition/:id", &controllers.BackendErpController{}, "delete:DeletePosition") + + // 文章管理 + beego.Router("/backend/articlesList", &controllers.BackendArticleController{}, "get:List") + beego.Router("/backend/contentstats", &controllers.BackendArticleController{}, "get:ContentStats") + beego.Router("/backend/allarticles", &controllers.BackendArticleController{}, "get:ListAll") + beego.Router("/backend/articles/:id", &controllers.BackendArticleController{}, "get:Detail") + beego.Router("/backend/createarticle", &controllers.BackendArticleController{}, "post:Create") + beego.Router("/backend/editarticle/:id", &controllers.BackendArticleController{}, "post:Update") + beego.Router("/backend/deletearticle/:id", &controllers.BackendArticleController{}, "delete:Delete") + beego.Router("/backend/publisharticle/:id", &controllers.BackendArticleController{}, "post:Publish") + beego.Router("/backend/unPublisharticle/:id", &controllers.BackendArticleController{}, "post:Unpublish") + beego.Router("/backend/articleRecommend/:id", &controllers.BackendArticleController{}, "post:Recommend") + beego.Router("/backend/unArticleRecommend/:id", &controllers.BackendArticleController{}, "post:Unrecommend") + beego.Router("/backend/articleTop/:id", &controllers.BackendArticleController{}, "post:Top") + beego.Router("/backend/unArticleTop/:id", &controllers.BackendArticleController{}, "post:Untop") + + beego.Router("/backend/categories", &controllers.BackendArticleCategoryController{}, "get:List") + beego.Router("/backend/allcategories", &controllers.BackendArticleCategoryController{}, "get:ListAll") + beego.Router("/backend/categories/:id", &controllers.BackendArticleCategoryController{}, "get:Detail;delete:Delete") + beego.Router("/backend/createCategory", &controllers.BackendArticleCategoryController{}, "post:Create") + beego.Router("/backend/editCategory/:id", &controllers.BackendArticleCategoryController{}, "post:Update") + beego.Router("/backend/categories/:id/status", &controllers.BackendArticleCategoryController{}, "patch:UpdateStatus") + + // 域名管理(主域名池 / 租户域名) + beego.Router("/backend/domain/pool/index", &controllers.BackendDomainPoolController{}, "get:Index") + beego.Router("/backend/domain/pool/getEnabledDomains", &controllers.BackendDomainPoolController{}, "get:GetEnabledDomains") + beego.Router("/backend/domain/pool/create", &controllers.BackendDomainPoolController{}, "post:Create") + beego.Router("/backend/domain/pool/update", &controllers.BackendDomainPoolController{}, "post:Update") + beego.Router("/backend/domain/pool/delete/:id", &controllers.BackendDomainPoolController{}, "delete:Delete") + beego.Router("/backend/domain/pool/toggleStatus", &controllers.BackendDomainPoolController{}, "post:ToggleStatus") + + beego.Router("/backend/domain/tenant/index", &controllers.BackendTenantDomainController{}, "get:Index") + beego.Router("/backend/domain/tenant/myDomains", &controllers.BackendTenantDomainController{}, "get:MyDomains") + beego.Router("/backend/domain/tenant/apply", &controllers.BackendTenantDomainController{}, "post:Apply") + beego.Router("/backend/domain/tenant/audit", &controllers.BackendTenantDomainController{}, "post:Audit") + beego.Router("/backend/domain/tenant/toggleStatus", &controllers.BackendTenantDomainController{}, "post:ToggleStatus") + beego.Router("/backend/domain/tenant/delete/:id", &controllers.BackendTenantDomainController{}, "delete:Delete") + + // 记事本管理 + beego.Router("/backend/notebook/list", &controllers.BackendNotebookController{}, "get:List") + beego.Router("/backend/notebook/detail/:id", &controllers.BackendNotebookController{}, "get:Detail") + beego.Router("/backend/notebook/create", &controllers.BackendNotebookController{}, "post:Create") + beego.Router("/backend/notebook/update/:id", &controllers.BackendNotebookController{}, "post:Update") + beego.Router("/backend/notebook/delete/:id", &controllers.BackendNotebookController{}, "delete:Delete") + + // 报销与审批管理 + beego.Router("/backend/reimbursements", &controllers.BackendReimburseController{}, "get:List;post:Create") + beego.Router("/backend/reimbursements/types", &controllers.BackendReimburseController{}, "get:Types") + beego.Router("/backend/reimbursements/flows", &controllers.BackendReimburseController{}, "get:Flows") + beego.Router("/backend/reimbursements/:id/items", &controllers.BackendReimburseController{}, "post:AddItem") + beego.Router("/backend/reimbursements/:id/items/:item_id", &controllers.BackendReimburseController{}, "post:UpdateItem;delete:DeleteItem") + beego.Router("/backend/reimbursements/:id", &controllers.BackendReimburseController{}, "get:Detail;post:Update;delete:Delete") + beego.Router("/backend/reimbursements/:id/submit", &controllers.BackendReimburseController{}, "post:Submit") + beego.Router("/backend/reimbursements/:id/records", &controllers.BackendReimburseController{}, "get:Records") + + // 日程提醒管理 + beego.Router("/backend/reminder/list", &controllers.BackendReminderController{}, "get:GetReminderList") + beego.Router("/backend/reminder/test", &controllers.BackendReminderController{}, "post:TestReminder") + beego.Router("/backend/reminder/:id", &controllers.BackendReminderController{}, "get:GetReminderDetail;put:UpdateReminder;delete:DeleteReminder") + beego.Router("/backend/reminder", &controllers.BackendReminderController{}, "post:CreateReminder") + beego.Router("/backend/reminder/batchDelete", &controllers.BackendReminderController{}, "post:BatchDeleteReminder") + beego.Router("/backend/reminder/finish/:id", &controllers.BackendReminderController{}, "post:FinishReminder") +} diff --git a/sql/alter_reimbursement_items_add_receipt_url.sql b/sql/alter_reimbursement_items_add_receipt_url.sql new file mode 100644 index 0000000..70d3a35 --- /dev/null +++ b/sql/alter_reimbursement_items_add_receipt_url.sql @@ -0,0 +1,3 @@ +ALTER TABLE `yz_backend_reimbursement_items` + ADD COLUMN `receipt_url` varchar(500) NOT NULL DEFAULT '' COMMENT '票据文件地址' AFTER `receipt_flag`, + ADD KEY `idx_receipt_url` (`receipt_url`); diff --git a/sql/yz_backend_reimbursement_expense_types.sql b/sql/yz_backend_reimbursement_expense_types.sql new file mode 100644 index 0000000..860491e --- /dev/null +++ b/sql/yz_backend_reimbursement_expense_types.sql @@ -0,0 +1,34 @@ +-- 报销费用类型表 +-- 每个租户维护自己的费用类型,uid 保留用于数据隔离扩展。 +CREATE TABLE IF NOT EXISTS `yz_backend_reimbursement_expense_types` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID', + `uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)', + `name` varchar(50) NOT NULL DEFAULT '' COMMENT '费用类型名称', + `sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序值,越小越靠前', + `is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用:0-禁用 1-启用', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_tid_name` (`tid`, `name`), + KEY `idx_uid` (`uid`) USING BTREE, + KEY `idx_sort_order` (`sort_order`) USING BTREE, + KEY `idx_is_active` (`is_active`) USING BTREE, + KEY `idx_is_deleted` (`is_deleted`) USING BTREE, + KEY `idx_create_time` (`create_time`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='报销费用类型表'; + +-- 可按需为已有租户初始化默认类型: +-- INSERT INTO `yz_backend_reimbursement_expense_types` +-- (`tid`, `name`, `sort_order`) +-- VALUES +-- (1, '差旅费', 10), +-- (1, '交通费', 20), +-- (1, '餐饮费', 30), +-- (1, '办公费', 40), +-- (1, '通讯费', 50), +-- (1, '业务招待费', 60), +-- (1, '培训费', 70), +-- (1, '其他', 999); diff --git a/sql/yz_backend_reimbursements.sql b/sql/yz_backend_reimbursements.sql new file mode 100644 index 0000000..8a21029 --- /dev/null +++ b/sql/yz_backend_reimbursements.sql @@ -0,0 +1,110 @@ +-- 报销模块表结构 +-- 执行前请确认当前数据库字符集为 utf8mb4 + +CREATE TABLE IF NOT EXISTS `yz_backend_approval_flows` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID', + `uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)', + `flow_name` varchar(100) NOT NULL DEFAULT '' COMMENT '流程名称', + `step_order` tinyint(1) NOT NULL DEFAULT '0' COMMENT '步骤顺序(从1开始)', + `approver_role` varchar(50) NOT NULL DEFAULT '' COMMENT '审批角色(如: dept_manager, finance, gm)', + `approver_rule` varchar(200) NOT NULL DEFAULT '' COMMENT '审批人规则(如: 部门主管, 财务总监)', + `timeout_hours` int(11) NOT NULL DEFAULT '48' COMMENT '超时提醒时间(小时)', + `is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用:0-禁用 1-启用', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_flow_name` (`flow_name`) USING BTREE, + KEY `idx_step_order` (`step_order`) USING BTREE, + KEY `idx_is_active` (`is_active`) USING BTREE, + KEY `idx_create_time` (`create_time`) USING BTREE, + KEY `idx_delete_time` (`delete_time`) USING BTREE, + KEY `idx_tid` (`tid`) USING BTREE, + KEY `idx_uid` (`uid`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='审批流程配置表'; + +CREATE TABLE IF NOT EXISTS `yz_backend_reimbursements` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID', + `uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)', + `user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '申请人ID', + `department_id` int(11) unsigned DEFAULT '0' COMMENT '部门ID', + `title` varchar(200) NOT NULL DEFAULT '' COMMENT '报销事由', + `total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '总金额', + `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态:0-草稿 1-审批中 2-已通过 3-已驳回 4-已撤回 5-已打款', + `expense_type` varchar(50) NOT NULL DEFAULT '' COMMENT '费用类型主分类', + `apply_date` date NOT NULL COMMENT '申请日期', + `description` text COMMENT '备注说明', + `current_approver_id` int(11) unsigned DEFAULT '0' COMMENT '当前审批人ID', + `current_step` tinyint(1) NOT NULL DEFAULT '0' COMMENT '当前审批步骤(0:未提交)', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_user_id` (`user_id`) USING BTREE, + KEY `idx_department_id` (`department_id`) USING BTREE, + KEY `idx_status` (`status`) USING BTREE, + KEY `idx_expense_type` (`expense_type`) USING BTREE, + KEY `idx_apply_date` (`apply_date`) USING BTREE, + KEY `idx_current_approver_id` (`current_approver_id`) USING BTREE, + KEY `idx_create_time` (`create_time`) USING BTREE, + KEY `idx_update_time` (`update_time`) USING BTREE, + KEY `idx_delete_time` (`delete_time`) USING BTREE, + KEY `idx_tid` (`tid`) USING BTREE, + KEY `idx_uid` (`uid`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='报销单主表'; + +CREATE TABLE IF NOT EXISTS `yz_backend_reimbursement_items` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID', + `uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)', + `reimbursement_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '关联报销单ID', + `expense_type` varchar(50) NOT NULL DEFAULT '' COMMENT '费用类型(具体子类)', + `expense_date` date NOT NULL COMMENT '费用发生日期', + `amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '金额', + `quantity` int(11) NOT NULL DEFAULT '1' COMMENT '数量', + `unit_price` decimal(10,2) DEFAULT '0.00' COMMENT '单价', + `description` varchar(200) NOT NULL DEFAULT '' COMMENT '费用描述', + `receipt_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否有发票:0-否 1-是', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_reimbursement_id` (`reimbursement_id`) USING BTREE, + KEY `idx_expense_type` (`expense_type`) USING BTREE, + KEY `idx_expense_date` (`expense_date`) USING BTREE, + KEY `idx_create_time` (`create_time`) USING BTREE, + KEY `idx_delete_time` (`delete_time`) USING BTREE, + KEY `idx_tid` (`tid`) USING BTREE, + KEY `idx_uid` (`uid`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='报销费用明细表'; + +CREATE TABLE IF NOT EXISTS `yz_backend_approval_records` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID', + `uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)', + `reimbursement_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '关联报销单ID', + `approver_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '审批人ID', + `approver_name` varchar(100) NOT NULL DEFAULT '' COMMENT '审批人姓名(冗余)', + `action` tinyint(1) NOT NULL DEFAULT '0' COMMENT '操作:1-提交 2-通过 3-驳回 4-转审 5-撤回', + `comment` text COMMENT '审批意见', + `step` tinyint(1) NOT NULL DEFAULT '0' COMMENT '审批步骤序号', + `prev_step` tinyint(1) NOT NULL DEFAULT '0' COMMENT '上一步骤序号', + `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_reimbursement_id` (`reimbursement_id`) USING BTREE, + KEY `idx_approver_id` (`approver_id`) USING BTREE, + KEY `idx_action` (`action`) USING BTREE, + KEY `idx_step` (`step`) USING BTREE, + KEY `idx_create_time` (`create_time`) USING BTREE, + KEY `idx_delete_time` (`delete_time`) USING BTREE, + KEY `idx_tid` (`tid`) USING BTREE, + KEY `idx_uid` (`uid`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='审批记录表';