做双端审批流
This commit is contained in:
1 parent
5b4dbb2b46
commit
ffaadc26ae
69 files changed
+6662
-1071
No files matched your search
@@ -0,0 +1,264 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
"server/services"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// 移动端员工档案「全公司视角」接口(HR 使用)。
|
||||
//
|
||||
// 与 Web 端(/backend/oa/employeefile/*)的关系:
|
||||
// - 数据口径完全一致:一人一档(yz_backend_employee_file)+ 教育/工作/合同/证照子记录,
|
||||
// DTO 组装直接复用 Web 端 efAssembleDTOList,保证两端展示字段完全一致;
|
||||
// - 只读:建档、改档、离职等写操作仍只在 Web 端提供;
|
||||
// - 数据一律按 JWT 中的 TenantId 隔离,前端无法指定租户。
|
||||
//
|
||||
// 权限口径:租户角色的 rights 里勾选了「员工档案」菜单(或其祖先节点)即视为 HR,可见全公司档案;
|
||||
// 租户管理员 / 未分配角色 / 角色未配置权限等既有「全权限」情形同样放行。
|
||||
// 未放行的用户仍可查看本人档案(/api/oa/employee-file),前端据此展示不同视图。
|
||||
|
||||
// employeeFileMenuIDs 命中「员工档案」菜单的菜单ID集合(含其祖先节点)。
|
||||
// 菜单表由租户自行维护,标题/路径可能被改写,因此按 路径关键字 + 标题关键字 双重匹配,
|
||||
// 与移动端菜单跳转规则(uniapp/utils/nav.js 中的 employeefile 规则)保持同一识别口径。
|
||||
func employeeFileMenuIDs() map[uint64]bool {
|
||||
ids := map[uint64]bool{}
|
||||
if models.Orm == nil {
|
||||
return ids
|
||||
}
|
||||
var rows []models.SystemMenu
|
||||
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true).
|
||||
All(&rows); err != nil {
|
||||
return ids
|
||||
}
|
||||
|
||||
pidOf := make(map[uint64]int64, len(rows))
|
||||
hit := map[uint64]bool{}
|
||||
for _, m := range rows {
|
||||
pidOf[m.ID] = m.Pid
|
||||
hay := strings.ToLower(m.Title + " " + derefString(m.Path) + " " + derefString(m.ComponentPath))
|
||||
if strings.Contains(hay, "employeefile") || strings.Contains(hay, "employee-file") ||
|
||||
strings.Contains(m.Title, "员工档案") || strings.Contains(m.Title, "人事档案") {
|
||||
hit[m.ID] = true
|
||||
}
|
||||
}
|
||||
if len(hit) == 0 {
|
||||
return ids
|
||||
}
|
||||
|
||||
// 角色权限勾到父目录即代表含全部下级,因此把命中菜单的祖先一并纳入判定。
|
||||
for id := range hit {
|
||||
cur := id
|
||||
for i := 0; i < 32; i++ { // 限制深度,防御脏数据造成的环
|
||||
if _, ok := pidOf[cur]; !ok {
|
||||
break
|
||||
}
|
||||
ids[cur] = true
|
||||
pid := pidOf[cur]
|
||||
if pid <= 0 {
|
||||
break
|
||||
}
|
||||
cur = uint64(pid)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// canViewAllEmployeeFiles 判断当前用户是否有权查看全公司员工档案(HR 视角)。
|
||||
func canViewAllEmployeeFiles(claims *jwtutil.Claims) bool {
|
||||
if claims == nil || claims.TenantId <= 0 {
|
||||
return false
|
||||
}
|
||||
// 平台管理员
|
||||
if claims.UserType == "platform" {
|
||||
return true
|
||||
}
|
||||
// 租户管理员 / 未分配角色 / 角色未配置权限 —— 与 Web 端 isTenantAdmin 同一口径
|
||||
if isTenantAdmin(claims.UserID, claims.TenantId) {
|
||||
return true
|
||||
}
|
||||
|
||||
var tu models.SystemTenantUser
|
||||
if models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("uid", tenantUIDOf(claims)).
|
||||
Filter("tid", claims.TenantId).
|
||||
Filter("delete_time__isnull", true).One(&tu) != nil {
|
||||
return false
|
||||
}
|
||||
if tu.GroupID == 0 {
|
||||
return true
|
||||
}
|
||||
var role models.AdminRole
|
||||
if models.Orm.QueryTable(new(models.AdminRole)).
|
||||
Filter("id", tu.GroupID).
|
||||
Filter("cid", 2).One(&role) != nil {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(role.Name) == "租户管理员" {
|
||||
return true
|
||||
}
|
||||
rights := ""
|
||||
if role.Rights != nil {
|
||||
rights = strings.TrimSpace(*role.Rights)
|
||||
}
|
||||
if rights == "" || rights == "[]" || rights == "null" {
|
||||
return true
|
||||
}
|
||||
|
||||
allowed := parseRightsToSet(rights)
|
||||
if len(allowed) == 0 {
|
||||
return true
|
||||
}
|
||||
for id := range employeeFileMenuIDs() {
|
||||
if allowed[id] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// tenantUIDOf 令牌 user_id 是认证中心 identity_id,换算成本租户的老 uid。
|
||||
func tenantUIDOf(claims *jwtutil.Claims) uint64 {
|
||||
return services.ResolveTenantUID(uint64(claims.TenantId), uint64(claims.UserID))
|
||||
}
|
||||
|
||||
// EmployeeFileAccess GET /api/oa/employee-file/access
|
||||
// 返回当前用户是否具备 HR 视角(可见全公司档案),前端据此决定展示列表还是本人档案。
|
||||
func (c *AppOaController) EmployeeFileAccess() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
c.oaOk(map[string]interface{}{"can_view_all": canViewAllEmployeeFiles(claims)})
|
||||
}
|
||||
|
||||
// EmployeeFileList GET /api/oa/employee-file/list 全公司档案分页列表(需 HR 权限)。
|
||||
// 支持 keyword(档案编号/身份证/员工姓名/账号/手机)、status(在职状态)、org_id(部门,含下级)。
|
||||
func (c *AppOaController) EmployeeFileList() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
if !canViewAllEmployeeFiles(claims) {
|
||||
c.oaErr(403, 403, "您没有查看全部员工档案的权限")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 20)
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
statusRaw := strings.TrimSpace(c.GetString("status"))
|
||||
orgID, _ := c.GetUint64("org_id")
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
web := &BackendEmployeeFileController{}
|
||||
qs := web.efQuery(tid)
|
||||
|
||||
if orgID > 0 {
|
||||
empIDs := web.efEmployeeIDsInOrgs(tid, web.efSubtreeOrgIDs(tid, orgID))
|
||||
if len(empIDs) == 0 {
|
||||
c.oaOk(map[string]interface{}{"list": []employeeFileDTO{}, "total": 0, "page": page, "pageSize": pageSize})
|
||||
return
|
||||
}
|
||||
qs = qs.Filter("employee_id__in", empIDs)
|
||||
}
|
||||
if statusRaw != "" {
|
||||
status, convErr := strconv.Atoi(statusRaw)
|
||||
if convErr != nil || status < 1 || status > 3 {
|
||||
c.oaErr(400, 400, "在职状态参数无效")
|
||||
return
|
||||
}
|
||||
qs = qs.Filter("employment_status", int8(status))
|
||||
}
|
||||
if keyword != "" {
|
||||
// 关键词同时命中档案自身字段(编号/身份证)与员工字段(姓名/账号/手机)
|
||||
empIDs := web.efEmployeeIDsByKeyword(tid, keyword)
|
||||
cond := orm.NewCondition().
|
||||
Or("file_no__icontains", keyword).
|
||||
Or("id_card__icontains", keyword)
|
||||
if len(empIDs) > 0 {
|
||||
cond = cond.Or("employee_id__in", empIDs)
|
||||
}
|
||||
qs = qs.SetCond(orm.NewCondition().AndCond(cond))
|
||||
}
|
||||
|
||||
total, _ := qs.Count()
|
||||
var files []models.BackendEmployeeFile
|
||||
if _, err := qs.OrderBy("-id").Limit(pageSize).Offset((page - 1) * pageSize).All(&files); err != nil && err != orm.ErrNoRows {
|
||||
c.oaErr(500, 500, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.oaOk(map[string]interface{}{
|
||||
"list": web.efAssembleDTOList(tid, files),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"pageSize": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// EmployeeFileDetail GET /api/oa/employee-file/detail/:id 员工档案详情(含子记录)。
|
||||
//
|
||||
// 两种访问路径:
|
||||
// - HR 视角:可查看任意员工的档案(租户内隔离);
|
||||
// - 普通员工:仅可查看本人档案,看他人档案返回 403。
|
||||
func (c *AppOaController) EmployeeFileDetail() {
|
||||
claims, err := c.oaClaims()
|
||||
if err != nil {
|
||||
c.oaErr(401, 401, "未登录或无权限")
|
||||
return
|
||||
}
|
||||
tid := claims.TenantId
|
||||
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
c.oaErr(400, 400, "无效的档案ID")
|
||||
return
|
||||
}
|
||||
|
||||
web := &BackendEmployeeFileController{}
|
||||
var file models.BackendEmployeeFile
|
||||
if web.efQuery(tid).Filter("id", id).One(&file) != nil {
|
||||
c.oaErr(404, 404, "档案不存在")
|
||||
return
|
||||
}
|
||||
|
||||
if !canViewAllEmployeeFiles(claims) {
|
||||
emp, ok := c.oaMyEmployee(claims)
|
||||
if !ok || uint64(emp.ID) != file.EmployeeID {
|
||||
c.oaErr(403, 403, "您没有查看他人档案的权限")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var records []models.BackendEmployeeFileRecord
|
||||
if _, err := models.Orm.QueryTable(new(models.BackendEmployeeFileRecord)).
|
||||
Filter("tid", tid).
|
||||
Filter("is_deleted", 0).
|
||||
Filter("employee_id", file.EmployeeID).
|
||||
OrderBy("sort", "id").
|
||||
All(&records); err != nil && err != orm.ErrNoRows {
|
||||
c.oaErr(500, 500, "查询档案记录失败")
|
||||
return
|
||||
}
|
||||
if records == nil {
|
||||
records = []models.BackendEmployeeFileRecord{}
|
||||
}
|
||||
dtos := web.efAssembleDTOList(tid, []models.BackendEmployeeFile{file})
|
||||
c.oaOk(map[string]interface{}{"file": dtos[0], "records": records})
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/services"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// BackendApprovalController 租户端审批流配置(流程头 + 节点编排)。
|
||||
//
|
||||
// 与平台端差异:
|
||||
// - 数据按登录租户隔离(tid 取自 JWT,写入/查询均带 tid);
|
||||
// - 「功能」下拉取当前租户已开通的模块(SystemModules),流程 module_code / biz_type 均绑定模块 code,
|
||||
// 实现「选功能 → 命名 → 把审批流引用到该功能」;
|
||||
// - 审批人取本租户主体:租户用户 / 租户角色 / 部门主管 / 职位 / 发起人 / 发起人主管(引擎支持 6 种)。
|
||||
type BackendApprovalController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// readTenantBody 读取并解析 JSON 请求体
|
||||
func (c *BackendApprovalController) readTenantBody(dst interface{}) error {
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("参数错误")
|
||||
}
|
||||
if err := json.Unmarshal(raw, dst); err != nil {
|
||||
return fmt.Errorf("参数错误: %s", err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tenantFlowList GET /backend/approval/flow/list?module=oa
|
||||
// 当前租户的流程列表;按分类(module_code)过滤,附带节点数。
|
||||
func (c *BackendApprovalController) FlowList() {
|
||||
claims, err := requireBackend(&c.Controller)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := uint64(claims.TenantId)
|
||||
module := strings.TrimSpace(c.GetString("module"))
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.TenantApprovalFlow)).
|
||||
Filter("tid", tid).Filter("is_deleted", 0)
|
||||
if module != "" {
|
||||
qs = qs.Filter("module_code", module)
|
||||
}
|
||||
var rows []models.TenantApprovalFlow
|
||||
_, _ = qs.OrderBy("-is_default", "id").All(&rows)
|
||||
|
||||
list := make([]approvalFlowView, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
cnt, _ := models.Orm.QueryTable(new(models.TenantApprovalNode)).
|
||||
Filter("flow_id", r.ID).Filter("is_deleted", 0).Count()
|
||||
list = append(list, approvalFlowView{
|
||||
ID: r.ID,
|
||||
ModuleCode: r.ModuleCode,
|
||||
BizType: r.BizType,
|
||||
FlowCode: r.FlowCode,
|
||||
FlowName: r.FlowName,
|
||||
Description: r.Description,
|
||||
IsDefault: r.IsDefault,
|
||||
Status: r.Status,
|
||||
NodeCount: int(cnt),
|
||||
CreateTime: r.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// FlowDetail GET /backend/approval/flow/detail/:id
|
||||
// 返回本租户某流程及其全部节点(按 sort 升序),供编排界面回显。
|
||||
func (c *BackendApprovalController) FlowDetail() {
|
||||
claims, err := requireBackend(&c.Controller)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := uint64(claims.TenantId)
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
var f models.TenantApprovalFlow
|
||||
if err := models.Orm.QueryTable(new(models.TenantApprovalFlow)).
|
||||
Filter("id", id).Filter("tid", tid).Filter("is_deleted", 0).One(&f); err != nil {
|
||||
jsonErr(&c.Controller, 404, 404, "流程不存在")
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": tenantFlowToView(f)}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// tenantFlowToView 组装租户流程视图(含节点与连线)
|
||||
func tenantFlowToView(f models.TenantApprovalFlow) approvalFlowView {
|
||||
var nodes []models.TenantApprovalNode
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantApprovalNode)).
|
||||
Filter("flow_id", f.ID).Filter("is_deleted", 0).
|
||||
OrderBy("sort", "id").All(&nodes)
|
||||
view := approvalFlowView{
|
||||
ID: f.ID,
|
||||
ModuleCode: f.ModuleCode,
|
||||
BizType: f.BizType,
|
||||
FlowCode: f.FlowCode,
|
||||
FlowName: f.FlowName,
|
||||
Description: f.Description,
|
||||
IsDefault: f.IsDefault,
|
||||
Status: f.Status,
|
||||
NodeCount: len(nodes),
|
||||
CreateTime: f.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
Nodes: make([]approvalNodePayload, 0, len(nodes)),
|
||||
}
|
||||
edgeNodes := make([]flowEdgeNode, 0, len(nodes))
|
||||
for _, n := range nodes {
|
||||
view.Nodes = append(view.Nodes, approvalNodePayload{
|
||||
NodeKey: n.NodeKey,
|
||||
NodeName: n.NodeName,
|
||||
NodeType: n.NodeType,
|
||||
ApproverType: n.ApproverType,
|
||||
ApproverIDs: parseIDSlice(n.ApproverIDs),
|
||||
MultiMode: n.MultiMode,
|
||||
AllowReturn: n.AllowReturn,
|
||||
TimeoutHours: n.TimeoutHours,
|
||||
Sort: n.Sort,
|
||||
PosX: n.PosX,
|
||||
PosY: n.PosY,
|
||||
ConditionExpr: n.ConditionExpr,
|
||||
})
|
||||
edgeNodes = append(edgeNodes, flowEdgeNode{NodeKey: n.NodeKey, NextKeys: n.NextKeys, Condition: n.ConditionExpr})
|
||||
}
|
||||
view.Edges = rebuildEdges(edgeNodes)
|
||||
return view
|
||||
}
|
||||
|
||||
// FlowSave POST /backend/approval/flow/save
|
||||
// 新建或更新本租户流程;节点采用「全量覆盖」语义(先软删旧节点,再按传入顺序重建)。
|
||||
func (c *BackendApprovalController) FlowSave() {
|
||||
claims, err := requireBackend(&c.Controller)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := uint64(claims.TenantId)
|
||||
var p approvalFlowSave
|
||||
if err := c.readTenantBody(&p); err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.FlowName) == "" {
|
||||
jsonErr(&c.Controller, 400, 400, "流程名称不能为空")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.ModuleCode) == "" {
|
||||
jsonErr(&c.Controller, 400, 400, "请选择要配置的分类")
|
||||
return
|
||||
}
|
||||
// 分类与单据类型对齐:运行期按 biz_type=分类 code 命中默认流
|
||||
if strings.TrimSpace(p.BizType) == "" {
|
||||
p.BizType = p.ModuleCode
|
||||
}
|
||||
if err := validateTenantApprovalNodes(p.Nodes); err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if err := validateApprovalGraph(p.Nodes, p.Edges); err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
g := buildGraph(p.Nodes, p.Edges)
|
||||
now := time.Now()
|
||||
|
||||
o := models.Orm
|
||||
var flowID uint64
|
||||
var flowCode string
|
||||
if p.ID > 0 {
|
||||
var exist models.TenantApprovalFlow
|
||||
if err := o.QueryTable(new(models.TenantApprovalFlow)).
|
||||
Filter("id", p.ID).Filter("tid", tid).Filter("is_deleted", 0).One(&exist); err != nil {
|
||||
jsonErr(&c.Controller, 404, 404, "流程不存在")
|
||||
return
|
||||
}
|
||||
flowID = p.ID
|
||||
flowCode = exist.FlowCode
|
||||
_, err := o.QueryTable(new(models.TenantApprovalFlow)).
|
||||
Filter("id", flowID).Filter("tid", tid).Update(orm.Params{
|
||||
"module_code": p.ModuleCode,
|
||||
"biz_type": p.BizType,
|
||||
"flow_name": p.FlowName,
|
||||
"description": p.Description,
|
||||
"is_default": p.IsDefault,
|
||||
"status": p.Status,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
row := models.TenantApprovalFlow{
|
||||
Tid: tid,
|
||||
ModuleCode: p.ModuleCode,
|
||||
BizType: p.BizType,
|
||||
FlowName: p.FlowName,
|
||||
Description: p.Description,
|
||||
IsDefault: p.IsDefault,
|
||||
Status: p.Status,
|
||||
CreateTime: now,
|
||||
}
|
||||
id, err := o.Insert(&row)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
flowID = uint64(id)
|
||||
flowCode = genApprovalFlowCode(flowID)
|
||||
_, _ = o.QueryTable(new(models.TenantApprovalFlow)).Filter("id", flowID).Filter("tid", tid).
|
||||
Update(orm.Params{"flow_code": flowCode})
|
||||
}
|
||||
|
||||
// 默认流程唯一:同租户同单据类型下只允许一条 is_default=1
|
||||
if p.IsDefault == 1 {
|
||||
_, _ = o.QueryTable(new(models.TenantApprovalFlow)).
|
||||
Filter("tid", tid).Filter("biz_type", p.BizType).Filter("is_deleted", 0).
|
||||
Exclude("id", flowID).Update(orm.Params{"is_default": 0, "update_time": now})
|
||||
}
|
||||
|
||||
// 覆盖节点:软删旧节点后按 sort 重建
|
||||
_, _ = o.QueryTable(new(models.TenantApprovalNode)).
|
||||
Filter("flow_id", flowID).Filter("is_deleted", 0).
|
||||
Update(orm.Params{"is_deleted": 1, "delete_time": now})
|
||||
for i, nd := range p.Nodes {
|
||||
sort := nd.Sort
|
||||
if sort == 0 {
|
||||
sort = i + 1
|
||||
}
|
||||
nodeKey := strings.TrimSpace(nd.NodeKey)
|
||||
if nodeKey == "" {
|
||||
nodeKey = fmt.Sprintf("n%d", sort)
|
||||
}
|
||||
nodeType := nd.NodeType
|
||||
if nodeType == 0 {
|
||||
nodeType = models.ApprovalNodeApproval
|
||||
}
|
||||
multi := nd.MultiMode
|
||||
if multi != models.ApprovalMultiAll {
|
||||
multi = models.ApprovalMultiAny
|
||||
}
|
||||
timeout := nd.TimeoutHours
|
||||
if timeout <= 0 {
|
||||
timeout = 48
|
||||
}
|
||||
node := models.TenantApprovalNode{
|
||||
FlowID: flowID,
|
||||
Tid: tid,
|
||||
NodeKey: nodeKey,
|
||||
NodeName: strings.TrimSpace(nd.NodeName),
|
||||
NodeType: nodeType,
|
||||
ApproverType: nd.ApproverType,
|
||||
ApproverIDs: joinIDs(nd.ApproverIDs),
|
||||
MultiMode: multi,
|
||||
AllowReturn: nd.AllowReturn,
|
||||
TimeoutHours: timeout,
|
||||
Sort: sort,
|
||||
PosX: nd.PosX,
|
||||
PosY: nd.PosY,
|
||||
PrevKeys: strings.Join(g.prev[nodeKey], ","),
|
||||
NextKeys: strings.Join(g.next[nodeKey], ","),
|
||||
ConditionExpr: g.cond[nodeKey],
|
||||
CreateTime: now,
|
||||
}
|
||||
if _, err := o.Insert(&node); err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "保存节点失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功", "data": map[string]interface{}{"id": flowID, "flow_code": flowCode}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Pick GET /backend/approval/flow/pick
|
||||
// 轻量清单:按分类返回本租户启用流程的 {flow_code, flow_name},供其它功能“选/填编号调用”。
|
||||
func (c *BackendApprovalController) Pick() {
|
||||
claims, err := requireBackend(&c.Controller)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := uint64(claims.TenantId)
|
||||
module := strings.TrimSpace(c.GetString("module"))
|
||||
qs := models.Orm.QueryTable(new(models.TenantApprovalFlow)).
|
||||
Filter("tid", tid).Filter("is_deleted", 0).Filter("status", 1)
|
||||
if module != "" {
|
||||
qs = qs.Filter("module_code", module)
|
||||
}
|
||||
var rows []models.TenantApprovalFlow
|
||||
_, _ = qs.OrderBy("module_code", "id").All(&rows)
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, map[string]interface{}{
|
||||
"module_code": r.ModuleCode,
|
||||
"flow_code": r.FlowCode,
|
||||
"flow_name": r.FlowName,
|
||||
})
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// FlowDelete DELETE /backend/approval/flow/delete/:id
|
||||
// 软删本租户流程及其节点。存在在途实例时禁止删除,避免审批中断。
|
||||
func (c *BackendApprovalController) FlowDelete() {
|
||||
claims, err := requireBackend(&c.Controller)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := uint64(claims.TenantId)
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
var f models.TenantApprovalFlow
|
||||
if err := models.Orm.QueryTable(new(models.TenantApprovalFlow)).
|
||||
Filter("id", id).Filter("tid", tid).Filter("is_deleted", 0).One(&f); err != nil {
|
||||
jsonErr(&c.Controller, 404, 404, "流程不存在")
|
||||
return
|
||||
}
|
||||
running, _ := models.Orm.QueryTable(new(models.ApprovalInstance)).
|
||||
Filter("flow_id", id).Filter("scope", models.ApprovalScopeTenant).
|
||||
Filter("status", models.ApprovalInstanceRunning).Filter("is_deleted", 0).Count()
|
||||
if running > 0 {
|
||||
jsonErr(&c.Controller, 400, 400, fmt.Sprintf("该流程还有 %d 条在途审批,暂不能删除", running))
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantApprovalFlow)).Filter("id", id).Filter("tid", tid).
|
||||
Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now})
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantApprovalNode)).Filter("flow_id", id).Filter("tid", tid).
|
||||
Update(orm.Params{"is_deleted": 1, "delete_time": now})
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "已删除"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Options GET /backend/approval/options
|
||||
// 编排界面数据源:当前租户的功能模块 + 审批人候选(用户 / 角色 / 部门 / 职位)。
|
||||
func (c *BackendApprovalController) Options() {
|
||||
claims, err := requireBackend(&c.Controller)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := uint64(claims.TenantId)
|
||||
|
||||
// 功能模块:状态可用、Web 可见,并标记当前租户是否已开通
|
||||
var mods []models.SystemModules
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemModules)).
|
||||
Filter("delete_time__isnull", true).Filter("status", 1).Filter("is_show", 1).
|
||||
OrderBy("sort", "id").All(&mods)
|
||||
mods = services.FilterModulesByPlatform(mods, models.PlatformWeb)
|
||||
allowed := services.GetTenantModuleCodes(tid)
|
||||
moduleList := make([]map[string]interface{}, 0, len(mods))
|
||||
for _, m := range mods {
|
||||
enabled := true
|
||||
if len(allowed) > 0 {
|
||||
enabled = allowed[services.NormalizeModuleCode(m.Code)]
|
||||
}
|
||||
moduleList = append(moduleList, map[string]interface{}{
|
||||
"code": m.Code, "name": m.Name, "enabled": enabled,
|
||||
})
|
||||
}
|
||||
|
||||
// 租户用户
|
||||
var tu []models.SystemTenantUser
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemTenantUser)).
|
||||
Filter("tid", tid).Filter("status", 1).Filter("delete_time__isnull", true).
|
||||
OrderBy("id").All(&tu)
|
||||
userList := make([]map[string]interface{}, 0, len(tu))
|
||||
for _, u := range tu {
|
||||
userList = append(userList, map[string]interface{}{
|
||||
"id": u.Uid, "name": firstNonEmptyStr(u.Name, u.Account),
|
||||
})
|
||||
}
|
||||
|
||||
// 租户角色(cid=2,含平台预置 tenant_id=0 与本租户自定义)
|
||||
var ar []models.AdminRole
|
||||
_, _ = models.Orm.QueryTable(new(models.AdminRole)).
|
||||
Filter("cid", 2).Filter("tenant_id__in", []uint64{0, tid}).
|
||||
Filter("status", 1).Filter("delete_time__isnull", true).
|
||||
OrderBy("id").All(&ar)
|
||||
roleList := make([]map[string]interface{}, 0, len(ar))
|
||||
for _, r := range ar {
|
||||
roleList = append(roleList, map[string]interface{}{"id": r.ID, "name": r.Name})
|
||||
}
|
||||
|
||||
// 部门 / 职位
|
||||
var og []models.BackendOrganization
|
||||
_, _ = models.Orm.QueryTable(new(models.BackendOrganization)).
|
||||
Filter("tid", tid).Filter("status", 1).Filter("delete_time__isnull", true).
|
||||
OrderBy("sort", "id").All(&og)
|
||||
orgList := make([]map[string]interface{}, 0, len(og))
|
||||
for _, o := range og {
|
||||
orgList = append(orgList, map[string]interface{}{"id": o.ID, "name": o.OrgName})
|
||||
}
|
||||
|
||||
var ps []models.BackendPosition
|
||||
_, _ = models.Orm.QueryTable(new(models.BackendPosition)).
|
||||
Filter("tid", tid).Filter("status", 1).Filter("delete_time__isnull", true).
|
||||
OrderBy("sort", "id").All(&ps)
|
||||
posList := make([]map[string]interface{}, 0, len(ps))
|
||||
for _, p := range ps {
|
||||
posList = append(posList, map[string]interface{}{"id": p.ID, "name": p.PositionName})
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"modules": moduleList,
|
||||
"users": userList,
|
||||
"roles": roleList,
|
||||
"orgs": orgList,
|
||||
"positions": posList,
|
||||
},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ============================ 辅助函数 ============================
|
||||
|
||||
// firstNonEmptyStr 取字符串指针/普通字符串中第一个非空值
|
||||
func firstNonEmptyStr(vals ...*string) string {
|
||||
for _, v := range vals {
|
||||
if v != nil && strings.TrimSpace(*v) != "" {
|
||||
return strings.TrimSpace(*v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// validateTenantApprovalNodes 校验租户节点:审批/条件节点须合法选择审批人来源。
|
||||
// 租户侧支持 6 种审批人类型:1用户 2角色 3部门主管 4职位 5发起人本人 6发起人主管。
|
||||
func validateTenantApprovalNodes(nodes []approvalNodePayload) error {
|
||||
for i, nd := range nodes {
|
||||
if nd.NodeType == models.ApprovalNodeStart || nd.NodeType == models.ApprovalNodeEnd ||
|
||||
nd.NodeType == models.ApprovalNodeCondition {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(nd.NodeName) == "" {
|
||||
return fmt.Errorf("第 %d 个节点名称不能为空", i+1)
|
||||
}
|
||||
if nd.ApproverType < models.ApprovalApproverUser || nd.ApproverType > models.ApprovalApproverInitLeader {
|
||||
return fmt.Errorf("节点「%s」审批人类型非法", nd.NodeName)
|
||||
}
|
||||
// 发起人相关类型无需指定对象;其余必须选择非空来源
|
||||
needIDs := nd.ApproverType != models.ApprovalApproverInitiator &&
|
||||
nd.ApproverType != models.ApprovalApproverInitLeader
|
||||
if needIDs && len(nd.ApproverIDs) == 0 {
|
||||
return fmt.Errorf("节点「%s」未选择审批人", nd.NodeName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -564,58 +564,12 @@ func (c *BackendTenantDomainController) Apply() {
|
||||
jsonErr(&c.Controller, 500, 500, "申请失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "申请提交成功,等待审核", "data": map[string]interface{}{"id": uint64(id)}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Audit POST /backend/domain/tenant/audit body:{id,action} action=approve/reject
|
||||
func (c *BackendTenantDomainController) Audit() {
|
||||
if _, err := requireBackend(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
// 提交后进入平台侧审批流(backend 提起、platform 审核);严格:未配置流程/无有效审批人则提交失败并回滚
|
||||
if errMsg := submitDomainApprovalChecked(uint64(id), tenantID, uint64(claims.UserID), claims.Username, full); errMsg != "" {
|
||||
jsonErr(&c.Controller, 400, 400, errMsg)
|
||||
return
|
||||
}
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ID uint64 `json:"id"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var row models.SystemTenantDomain
|
||||
if err := models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).One(&row); err != nil {
|
||||
jsonErr(&c.Controller, 404, 404, "域名不存在")
|
||||
return
|
||||
}
|
||||
if row.DomainType == models.DomainTypeCustom {
|
||||
jsonErr(&c.Controller, 400, 400, "自有域名无需人工审核,解析检测与证书签发通过后自动生效")
|
||||
return
|
||||
}
|
||||
if row.Status != 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "该域名已审核过了")
|
||||
return
|
||||
}
|
||||
newStatus := 2
|
||||
msg := "已拒绝"
|
||||
if strings.ToLower(strings.TrimSpace(p.Action)) == "approve" {
|
||||
newStatus = 1
|
||||
msg = "审核通过"
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).Update(map[string]interface{}{
|
||||
"status": newStatus,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "审核失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": msg}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "申请提交成功,等待审批", "data": map[string]interface{}{"id": uint64(id)}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
|
||||
@@ -677,6 +677,44 @@ func (c *BackendOnepageController) ListAll() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetOnePageByPath GET /backend/onepage/path/:path 按路径读取租户单页(后端端独立接口)
|
||||
func (c *BackendOnepageController) GetOnePageByPath() {
|
||||
claims, err := c.cmsClaims()
|
||||
if err != nil {
|
||||
c.cmsJSONErr(401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := cmsEffectiveTid(&c.Controller, claims)
|
||||
if tid == 0 {
|
||||
c.cmsJSONErr(400, 400, "tid不能为空")
|
||||
return
|
||||
}
|
||||
path := strings.TrimSpace(c.Ctx.Input.Param(":path"))
|
||||
if path == "" {
|
||||
c.cmsJSONErr(400, 400, "路径标识不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var row models.CmsFrontendOnepage
|
||||
err = models.Orm.QueryTable(new(models.CmsFrontendOnepage)).
|
||||
Filter("tid", tid).
|
||||
Filter("path", path).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row)
|
||||
if err == orm.ErrNoRows {
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": nil}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.cmsJSONErr(500, 500, "获取单页失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": cmsOnepageToMap(row)}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Create POST /backend/createonepage
|
||||
func (c *BackendOnepageController) Create() {
|
||||
claims, err := c.cmsClaims()
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"server/models"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// IndexUpgradeNoticeController 官网端「平台更新公告」公开接口。
|
||||
// 数据同源于 models.PlatformUpgradeNotice,但作为官网端独立接口,
|
||||
// 不与平台端 /platform、移动端 /api 复用同一 handler。
|
||||
type IndexUpgradeNoticeController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// List GET /site/upgrade/list 官网首页公开展示的更新公告(仅启用、未删除)
|
||||
func (c *IndexUpgradeNoticeController) List() {
|
||||
limitStr := c.GetString("limit")
|
||||
limit := 10
|
||||
if limitStr != "" {
|
||||
val, err := strconv.Atoi(limitStr)
|
||||
if err == nil {
|
||||
limit = val
|
||||
}
|
||||
}
|
||||
if limit <= 0 || limit > 50 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
var upgradeList []models.PlatformUpgradeNotice
|
||||
qs := models.Orm.QueryTable(new(models.PlatformUpgradeNotice)).
|
||||
Filter("status", 1).
|
||||
Filter("delete_time__isnull", true)
|
||||
|
||||
_, err := qs.OrderBy("-sort", "-create_time").Limit(limit).All(&upgradeList)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 500,
|
||||
"msg": "查询失败",
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
data := make([]map[string]interface{}, 0, len(upgradeList))
|
||||
for _, item := range upgradeList {
|
||||
record := map[string]interface{}{
|
||||
"id": item.ID,
|
||||
"update_date": item.UpdateDate,
|
||||
"title": item.Title,
|
||||
"content": item.Content,
|
||||
"create_time": item.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
data = append(data, record)
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": data,
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
@@ -0,0 +1,660 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/services"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// PlatformApprovalController 平台端审批流配置(流程头 + 节点编排)。
|
||||
//
|
||||
// 当期面向「域名申请」这类平台侧管控场景,按 module_code + biz_type 联合唯一组织流程;
|
||||
// 平台账号不归属租户,故流程与节点均无 tid,审批人取平台用户 / 平台角色。
|
||||
type PlatformApprovalController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// ---- 配置侧 JSON 结构 ----
|
||||
|
||||
// approvalNodePayload 单个节点(前端画布节点)。
|
||||
// node_type:1开始 2审批 3结束 4条件网关;pos_x/pos_y 为画布坐标回显;
|
||||
// condition_expr 为该节点作为「条件网关出边目标」时的进入条件(由 edges 反推落库)。
|
||||
type approvalNodePayload struct {
|
||||
NodeKey string `json:"node_key"`
|
||||
NodeName string `json:"node_name"`
|
||||
NodeType int8 `json:"node_type"` // 1开始 2审批 3结束 4条件网关
|
||||
ApproverType int8 `json:"approver_type"` // 1指定用户 2指定角色 ...
|
||||
ApproverIDs []uint64 `json:"approver_ids"`
|
||||
MultiMode int8 `json:"multi_mode"` // 1或签 2会签
|
||||
AllowReturn int8 `json:"allow_return"` // 是否允许退回
|
||||
TimeoutHours int `json:"timeout_hours"` // 超时小时
|
||||
Sort int `json:"sort"` // 顺序(越小越靠前)
|
||||
PosX int `json:"pos_x"` // 画布 X 坐标
|
||||
PosY int `json:"pos_y"` // 画布 Y 坐标
|
||||
ConditionExpr string `json:"condition_expr"`
|
||||
}
|
||||
|
||||
// approvalEdge 画布连线:from→to,condition 为该出边的分支条件(落到 to 节点的 condition_expr)
|
||||
type approvalEdge struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Condition string `json:"condition"`
|
||||
}
|
||||
|
||||
// approvalFlowSave 保存流程(图:节点 + 连线,节点全量覆盖)
|
||||
type approvalFlowSave struct {
|
||||
ID uint64 `json:"id"`
|
||||
ModuleCode string `json:"module_code"`
|
||||
BizType string `json:"biz_type"`
|
||||
FlowName string `json:"flow_name"`
|
||||
Description string `json:"description"`
|
||||
IsDefault int8 `json:"is_default"`
|
||||
Status int8 `json:"status"`
|
||||
Nodes []approvalNodePayload `json:"nodes"`
|
||||
Edges []approvalEdge `json:"edges"`
|
||||
}
|
||||
|
||||
// approvalFlowView 流程 + 节点 + 连线(详情返回)
|
||||
type approvalFlowView struct {
|
||||
ID uint64 `json:"id"`
|
||||
ModuleCode string `json:"module_code"`
|
||||
BizType string `json:"biz_type"`
|
||||
FlowCode string `json:"flow_code"`
|
||||
FlowName string `json:"flow_name"`
|
||||
Description string `json:"description"`
|
||||
IsDefault int8 `json:"is_default"`
|
||||
Status int8 `json:"status"`
|
||||
NodeCount int `json:"node_count"`
|
||||
CreateTime string `json:"create_time"`
|
||||
Nodes []approvalNodePayload `json:"nodes"`
|
||||
Edges []approvalEdge `json:"edges"`
|
||||
}
|
||||
|
||||
// readApprovalBody 读取并解析 JSON 请求体
|
||||
func (c *PlatformApprovalController) readApprovalBody(dst interface{}) error {
|
||||
raw, err := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("参数错误")
|
||||
}
|
||||
if err := json.Unmarshal(raw, dst); err != nil {
|
||||
return fmt.Errorf("参数错误: %s", err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FlowList GET /platform/approval/flow/list?biz_type=domain
|
||||
// 平台端流程列表;可按单据类型过滤,附带节点数。
|
||||
func (c *PlatformApprovalController) FlowList() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
bizType := strings.TrimSpace(c.GetString("biz_type"))
|
||||
if bizType == "" {
|
||||
bizType = strings.TrimSpace(c.GetString("module")) // 兼容:分类即业务类型
|
||||
}
|
||||
qs := models.Orm.QueryTable(new(models.PlatformApprovalFlow)).
|
||||
Filter("is_deleted", 0)
|
||||
if bizType != "" {
|
||||
qs = qs.Filter("biz_type", bizType)
|
||||
}
|
||||
var rows []models.PlatformApprovalFlow
|
||||
_, _ = qs.OrderBy("-is_default", "id").All(&rows)
|
||||
|
||||
list := make([]approvalFlowView, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
cnt, _ := models.Orm.QueryTable(new(models.PlatformApprovalNode)).
|
||||
Filter("flow_id", r.ID).Filter("is_deleted", 0).Count()
|
||||
list = append(list, approvalFlowView{
|
||||
ID: r.ID,
|
||||
ModuleCode: r.ModuleCode,
|
||||
BizType: r.BizType,
|
||||
FlowCode: r.FlowCode,
|
||||
FlowName: r.FlowName,
|
||||
Description: r.Description,
|
||||
IsDefault: r.IsDefault,
|
||||
Status: r.Status,
|
||||
NodeCount: int(cnt),
|
||||
CreateTime: r.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// FlowDetail GET /platform/approval/flow/detail/:id
|
||||
// 返回流程及其全部节点(按 sort 升序),供编排界面回显。
|
||||
func (c *PlatformApprovalController) FlowDetail() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
var f models.PlatformApprovalFlow
|
||||
if err := models.Orm.QueryTable(new(models.PlatformApprovalFlow)).
|
||||
Filter("id", id).Filter("is_deleted", 0).One(&f); err != nil {
|
||||
jsonErr(&c.Controller, 404, 404, "流程不存在")
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": c.flowToView(f)}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// flowToView 组装流程视图(含节点与连线)
|
||||
func (c *PlatformApprovalController) flowToView(f models.PlatformApprovalFlow) approvalFlowView {
|
||||
var nodes []models.PlatformApprovalNode
|
||||
_, _ = models.Orm.QueryTable(new(models.PlatformApprovalNode)).
|
||||
Filter("flow_id", f.ID).Filter("is_deleted", 0).
|
||||
OrderBy("sort", "id").All(&nodes)
|
||||
view := approvalFlowView{
|
||||
ID: f.ID,
|
||||
ModuleCode: f.ModuleCode,
|
||||
BizType: f.BizType,
|
||||
FlowCode: f.FlowCode,
|
||||
FlowName: f.FlowName,
|
||||
Description: f.Description,
|
||||
IsDefault: f.IsDefault,
|
||||
Status: f.Status,
|
||||
NodeCount: len(nodes),
|
||||
CreateTime: f.CreateTime.Format("2006-01-02 15:04:05"),
|
||||
Nodes: make([]approvalNodePayload, 0, len(nodes)),
|
||||
}
|
||||
edgeNodes := make([]flowEdgeNode, 0, len(nodes))
|
||||
for _, n := range nodes {
|
||||
view.Nodes = append(view.Nodes, approvalNodePayload{
|
||||
NodeKey: n.NodeKey,
|
||||
NodeName: n.NodeName,
|
||||
NodeType: n.NodeType,
|
||||
ApproverType: n.ApproverType,
|
||||
ApproverIDs: parseIDSlice(n.ApproverIDs),
|
||||
MultiMode: n.MultiMode,
|
||||
AllowReturn: n.AllowReturn,
|
||||
TimeoutHours: n.TimeoutHours,
|
||||
Sort: n.Sort,
|
||||
PosX: n.PosX,
|
||||
PosY: n.PosY,
|
||||
ConditionExpr: n.ConditionExpr,
|
||||
})
|
||||
edgeNodes = append(edgeNodes, flowEdgeNode{NodeKey: n.NodeKey, NextKeys: n.NextKeys, Condition: n.ConditionExpr})
|
||||
}
|
||||
view.Edges = rebuildEdges(edgeNodes)
|
||||
return view
|
||||
}
|
||||
|
||||
// FlowSave POST /platform/approval/flow/save
|
||||
// 新建或更新流程;节点采用「全量覆盖」语义(先软删旧节点,再按传入顺序重建)。
|
||||
func (c *PlatformApprovalController) FlowSave() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
var p approvalFlowSave
|
||||
if err := c.readApprovalBody(&p); err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.FlowName) == "" {
|
||||
jsonErr(&c.Controller, 400, 400, "流程名称不能为空")
|
||||
return
|
||||
}
|
||||
// 分类(module_code)与单据类型(biz_type)对齐:二者留空时互补,至少其一非空
|
||||
if strings.TrimSpace(p.ModuleCode) == "" {
|
||||
p.ModuleCode = p.BizType
|
||||
}
|
||||
if strings.TrimSpace(p.BizType) == "" {
|
||||
p.BizType = p.ModuleCode
|
||||
}
|
||||
if strings.TrimSpace(p.BizType) == "" {
|
||||
jsonErr(&c.Controller, 400, 400, "请选择所属分类")
|
||||
return
|
||||
}
|
||||
if err := validateApprovalNodes(p.Nodes); err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if err := validateApprovalGraph(p.Nodes, p.Edges); err != nil {
|
||||
jsonErr(&c.Controller, 400, 400, err.Error())
|
||||
return
|
||||
}
|
||||
g := buildGraph(p.Nodes, p.Edges)
|
||||
now := time.Now()
|
||||
|
||||
o := models.Orm
|
||||
var flowID uint64
|
||||
var flowCode string
|
||||
if p.ID > 0 {
|
||||
var exist models.PlatformApprovalFlow
|
||||
if err := o.QueryTable(new(models.PlatformApprovalFlow)).
|
||||
Filter("id", p.ID).Filter("is_deleted", 0).One(&exist); err != nil {
|
||||
jsonErr(&c.Controller, 404, 404, "流程不存在")
|
||||
return
|
||||
}
|
||||
flowID = p.ID
|
||||
flowCode = exist.FlowCode
|
||||
_, err := o.QueryTable(new(models.PlatformApprovalFlow)).Filter("id", flowID).Update(orm.Params{
|
||||
"module_code": p.ModuleCode,
|
||||
"biz_type": p.BizType,
|
||||
"flow_name": p.FlowName,
|
||||
"description": p.Description,
|
||||
"is_default": p.IsDefault,
|
||||
"status": p.Status,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
row := models.PlatformApprovalFlow{
|
||||
ModuleCode: p.ModuleCode,
|
||||
BizType: p.BizType,
|
||||
FlowName: p.FlowName,
|
||||
Description: p.Description,
|
||||
IsDefault: p.IsDefault,
|
||||
Status: p.Status,
|
||||
CreateTime: now,
|
||||
}
|
||||
id, err := o.Insert(&row)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "保存失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
flowID = uint64(id)
|
||||
flowCode = genApprovalFlowCode(flowID)
|
||||
_, _ = o.QueryTable(new(models.PlatformApprovalFlow)).Filter("id", flowID).
|
||||
Update(orm.Params{"flow_code": flowCode})
|
||||
}
|
||||
|
||||
// 默认流程唯一:同 biz_type 下只允许一条 is_default=1
|
||||
if p.IsDefault == 1 {
|
||||
_, _ = o.QueryTable(new(models.PlatformApprovalFlow)).
|
||||
Filter("biz_type", p.BizType).Filter("is_deleted", 0).
|
||||
Exclude("id", flowID).Update(orm.Params{"is_default": 0, "update_time": now})
|
||||
}
|
||||
|
||||
// 覆盖节点:软删旧节点后按 sort 重建
|
||||
_, _ = o.QueryTable(new(models.PlatformApprovalNode)).
|
||||
Filter("flow_id", flowID).Filter("is_deleted", 0).
|
||||
Update(orm.Params{"is_deleted": 1, "delete_time": now})
|
||||
for i, nd := range p.Nodes {
|
||||
sort := nd.Sort
|
||||
if sort == 0 {
|
||||
sort = i + 1
|
||||
}
|
||||
nodeKey := strings.TrimSpace(nd.NodeKey)
|
||||
if nodeKey == "" {
|
||||
nodeKey = fmt.Sprintf("n%d", sort)
|
||||
}
|
||||
nodeType := nd.NodeType
|
||||
if nodeType == 0 {
|
||||
nodeType = models.ApprovalNodeApproval
|
||||
}
|
||||
multi := nd.MultiMode
|
||||
if multi != models.ApprovalMultiAll {
|
||||
multi = models.ApprovalMultiAny
|
||||
}
|
||||
timeout := nd.TimeoutHours
|
||||
if timeout <= 0 {
|
||||
timeout = 48
|
||||
}
|
||||
node := models.PlatformApprovalNode{
|
||||
FlowID: flowID,
|
||||
NodeKey: nodeKey,
|
||||
NodeName: strings.TrimSpace(nd.NodeName),
|
||||
NodeType: nodeType,
|
||||
ApproverType: nd.ApproverType,
|
||||
ApproverIDs: joinIDs(nd.ApproverIDs),
|
||||
MultiMode: multi,
|
||||
AllowReturn: nd.AllowReturn,
|
||||
TimeoutHours: timeout,
|
||||
Sort: sort,
|
||||
PosX: nd.PosX,
|
||||
PosY: nd.PosY,
|
||||
PrevKeys: strings.Join(g.prev[nodeKey], ","),
|
||||
NextKeys: strings.Join(g.next[nodeKey], ","),
|
||||
ConditionExpr: g.cond[nodeKey],
|
||||
CreateTime: now,
|
||||
}
|
||||
if _, err := o.Insert(&node); err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "保存节点失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "保存成功", "data": map[string]interface{}{"id": flowID, "flow_code": flowCode}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Pick GET /platform/approval/flow/pick
|
||||
// 轻量清单:按分类返回启用流程的 {flow_code, flow_name},供其它功能“选/填编号调用”。
|
||||
func (c *PlatformApprovalController) Pick() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
module := strings.TrimSpace(c.GetString("module"))
|
||||
if module == "" {
|
||||
module = strings.TrimSpace(c.GetString("biz_type"))
|
||||
}
|
||||
qs := models.Orm.QueryTable(new(models.PlatformApprovalFlow)).
|
||||
Filter("is_deleted", 0).Filter("status", 1)
|
||||
if module != "" {
|
||||
qs = qs.Filter("biz_type", module)
|
||||
}
|
||||
var rows []models.PlatformApprovalFlow
|
||||
_, _ = qs.OrderBy("biz_type", "id").All(&rows)
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, map[string]interface{}{
|
||||
"module_code": r.ModuleCode,
|
||||
"biz_type": r.BizType,
|
||||
"flow_code": r.FlowCode,
|
||||
"flow_name": r.FlowName,
|
||||
})
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": list}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// FlowDelete DELETE /platform/approval/flow/delete/:id
|
||||
// 软删流程及其节点。存在在途实例时禁止删除,避免审批中断。
|
||||
func (c *PlatformApprovalController) FlowDelete() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
var f models.PlatformApprovalFlow
|
||||
if err := models.Orm.QueryTable(new(models.PlatformApprovalFlow)).
|
||||
Filter("id", id).Filter("is_deleted", 0).One(&f); err != nil {
|
||||
jsonErr(&c.Controller, 404, 404, "流程不存在")
|
||||
return
|
||||
}
|
||||
running, _ := models.Orm.QueryTable(new(models.ApprovalInstance)).
|
||||
Filter("flow_id", id).Filter("scope", models.ApprovalScopePlatform).
|
||||
Filter("status", models.ApprovalInstanceRunning).Filter("is_deleted", 0).Count()
|
||||
if running > 0 {
|
||||
jsonErr(&c.Controller, 400, 400, fmt.Sprintf("该流程还有 %d 条在途审批,暂不能删除", running))
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, _ = models.Orm.QueryTable(new(models.PlatformApprovalFlow)).Filter("id", id).
|
||||
Update(orm.Params{"is_deleted": 1, "delete_time": now, "update_time": now})
|
||||
_, _ = models.Orm.QueryTable(new(models.PlatformApprovalNode)).Filter("flow_id", id).
|
||||
Update(orm.Params{"is_deleted": 1, "delete_time": now})
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "已删除"}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Options GET /platform/approval/options
|
||||
// 编排界面「选审批人」用:返回平台角色(cid=1)与平台用户列表。
|
||||
func (c *PlatformApprovalController) Options() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
var roles []models.AdminRole
|
||||
_, _ = models.Orm.QueryTable(new(models.AdminRole)).
|
||||
Filter("cid", 1).Filter("status", 1).Filter("delete_time__isnull", true).
|
||||
OrderBy("id").All(&roles)
|
||||
roleList := make([]map[string]interface{}, 0, len(roles))
|
||||
for _, r := range roles {
|
||||
roleList = append(roleList, map[string]interface{}{"id": r.ID, "name": r.Name})
|
||||
}
|
||||
|
||||
var users []models.AdminUser
|
||||
_, _ = models.Orm.QueryTable(new(models.AdminUser)).
|
||||
Filter("status", 1).Filter("delete_time__isnull", true).
|
||||
OrderBy("id").All(&users)
|
||||
userList := make([]map[string]interface{}, 0, len(users))
|
||||
for _, u := range users {
|
||||
userList = append(userList, map[string]interface{}{
|
||||
"id": u.ID,
|
||||
"name": approverDisplayName(u.Name, u.Account),
|
||||
"role": u.RoleID,
|
||||
})
|
||||
}
|
||||
|
||||
// 分类(复用功能模块):Web 可见、启用、未删除
|
||||
var mods []models.SystemModules
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemModules)).
|
||||
Filter("delete_time__isnull", true).Filter("status", 1).Filter("is_show", 1).
|
||||
OrderBy("sort", "id").All(&mods)
|
||||
mods = services.FilterModulesByPlatform(mods, models.PlatformWeb)
|
||||
moduleList := make([]map[string]interface{}, 0, len(mods))
|
||||
for _, m := range mods {
|
||||
moduleList = append(moduleList, map[string]interface{}{
|
||||
"code": m.Code, "name": m.Name, "enabled": true,
|
||||
})
|
||||
}
|
||||
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 200, "msg": "ok",
|
||||
"data": map[string]interface{}{"roles": roleList, "users": userList, "modules": moduleList},
|
||||
}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// BizTypes GET /platform/approval/biz-types
|
||||
// 可绑定业务类型字典(当期平台侧为「域名申请」),供配置界面顶级下拉。
|
||||
func (c *PlatformApprovalController) BizTypes() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": []map[string]interface{}{
|
||||
{"value": models.ApprovalBizDomain, "label": "域名申请"},
|
||||
}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ============================ 辅助函数 ============================
|
||||
|
||||
// approverDisplayName 优先姓名,回退账号(AdminUser.Account 为普通字符串)
|
||||
func approverDisplayName(name *string, account string) string {
|
||||
if name != nil && strings.TrimSpace(*name) != "" {
|
||||
return strings.TrimSpace(*name)
|
||||
}
|
||||
return strings.TrimSpace(account)
|
||||
}
|
||||
|
||||
// validateApprovalNodes 校验审批节点:必须选定审批人来源且非空(开始/结束/条件网关跳过)
|
||||
func validateApprovalNodes(nodes []approvalNodePayload) error {
|
||||
for i, nd := range nodes {
|
||||
if nd.NodeType == models.ApprovalNodeStart || nd.NodeType == models.ApprovalNodeEnd ||
|
||||
nd.NodeType == models.ApprovalNodeCondition {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(nd.NodeName) == "" {
|
||||
return fmt.Errorf("第 %d 个节点名称不能为空", i+1)
|
||||
}
|
||||
if nd.ApproverType != models.ApprovalApproverUser && nd.ApproverType != models.ApprovalApproverRole {
|
||||
return fmt.Errorf("节点「%s」审批人类型非法(仅支持指定用户/指定角色)", nd.NodeName)
|
||||
}
|
||||
if len(nd.ApproverIDs) == 0 {
|
||||
return fmt.Errorf("节点「%s」未选择审批人", nd.NodeName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// joinIDs 将 ID 数组拼成逗号分隔串(存库口径)
|
||||
func joinIDs(ids []uint64) string {
|
||||
parts := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if id > 0 {
|
||||
parts = append(parts, strconv.FormatUint(id, 10))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
// parseIDSlice 解析逗号分隔 ID 串为数组
|
||||
func parseIDSlice(raw string) []uint64 {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
out := make([]uint64, 0)
|
||||
for _, p := range strings.Split(raw, ",") {
|
||||
if v, err := strconv.ParseUint(strings.TrimSpace(p), 10, 64); err == nil && v > 0 {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ============================ 图(连线)辅助 ============================
|
||||
|
||||
// flowEdgeNode 连线重建输入:节点标识 + 出边(next_keys) + 本节点进入条件
|
||||
type flowEdgeNode struct {
|
||||
NodeKey string
|
||||
NextKeys string
|
||||
Condition string
|
||||
}
|
||||
|
||||
// splitKeyList 拆分逗号分隔 node_key 列表
|
||||
func splitKeyList(raw string) []string {
|
||||
out := make([]string, 0, 4)
|
||||
for _, p := range strings.Split(raw, ",") {
|
||||
if s := strings.TrimSpace(p); s != "" {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// containsStr 列表是否包含某字符串
|
||||
func containsStr(list []string, s string) bool {
|
||||
for _, x := range list {
|
||||
if x == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// approvalGraph 由画布连线反推的邻接与条件
|
||||
type approvalGraph struct {
|
||||
next map[string][]string
|
||||
prev map[string][]string
|
||||
cond map[string]string
|
||||
}
|
||||
|
||||
// buildGraph 由 edges 反推每个节点的 prev_keys/next_keys 与进入条件(条件落到目标节点)
|
||||
func buildGraph(nodes []approvalNodePayload, edges []approvalEdge) approvalGraph {
|
||||
g := approvalGraph{next: map[string][]string{}, prev: map[string][]string{}, cond: map[string]string{}}
|
||||
for _, e := range edges {
|
||||
f := strings.TrimSpace(e.From)
|
||||
t := strings.TrimSpace(e.To)
|
||||
if f == "" || t == "" || f == t {
|
||||
continue
|
||||
}
|
||||
if !containsStr(g.next[f], t) {
|
||||
g.next[f] = append(g.next[f], t)
|
||||
}
|
||||
if !containsStr(g.prev[t], f) {
|
||||
g.prev[t] = append(g.prev[t], f)
|
||||
}
|
||||
if c := strings.TrimSpace(e.Condition); c != "" {
|
||||
g.cond[t] = c
|
||||
}
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
// rebuildEdges 由节点 next_keys 邻接重建画布连线;连线条件取目标节点 condition_expr
|
||||
func rebuildEdges(nodes []flowEdgeNode) []approvalEdge {
|
||||
condByKey := make(map[string]string, len(nodes))
|
||||
for _, n := range nodes {
|
||||
condByKey[n.NodeKey] = n.Condition
|
||||
}
|
||||
edges := make([]approvalEdge, 0)
|
||||
for _, n := range nodes {
|
||||
for _, to := range splitKeyList(n.NextKeys) {
|
||||
edges = append(edges, approvalEdge{From: n.NodeKey, To: to, Condition: condByKey[to]})
|
||||
}
|
||||
}
|
||||
return edges
|
||||
}
|
||||
|
||||
// validateApprovalGraph 校验画布图的完整性:单一开始、审批节点≥1、网关出边≥2、全部可达、连线端点有效
|
||||
func validateApprovalGraph(nodes []approvalNodePayload, edges []approvalEdge) error {
|
||||
if len(nodes) == 0 {
|
||||
return fmt.Errorf("流程至少需包含开始与审批节点")
|
||||
}
|
||||
keySet := map[string]bool{}
|
||||
var startCount, approvalCount int
|
||||
var condKeys []string
|
||||
var startKey string
|
||||
for _, n := range nodes {
|
||||
k := strings.TrimSpace(n.NodeKey)
|
||||
if k == "" {
|
||||
return fmt.Errorf("存在缺少标识的节点,请重新连线后再保存")
|
||||
}
|
||||
if keySet[k] {
|
||||
return fmt.Errorf("节点标识重复:%s", k)
|
||||
}
|
||||
keySet[k] = true
|
||||
switch n.NodeType {
|
||||
case models.ApprovalNodeStart:
|
||||
startCount++
|
||||
startKey = k
|
||||
case models.ApprovalNodeApproval:
|
||||
approvalCount++
|
||||
case models.ApprovalNodeCondition:
|
||||
condKeys = append(condKeys, k)
|
||||
}
|
||||
}
|
||||
if startCount != 1 {
|
||||
return fmt.Errorf("流程需有且仅有一个「开始」节点")
|
||||
}
|
||||
if approvalCount < 1 {
|
||||
return fmt.Errorf("流程需至少包含一个「审批」节点")
|
||||
}
|
||||
nextAdj := map[string][]string{}
|
||||
for _, e := range edges {
|
||||
f := strings.TrimSpace(e.From)
|
||||
t := strings.TrimSpace(e.To)
|
||||
if !keySet[f] || !keySet[t] {
|
||||
return fmt.Errorf("存在连向未知节点的连线,请检查画布")
|
||||
}
|
||||
nextAdj[f] = append(nextAdj[f], t)
|
||||
}
|
||||
for _, k := range condKeys {
|
||||
if len(nextAdj[k]) < 2 {
|
||||
return fmt.Errorf("条件网关需至少两条分支连线")
|
||||
}
|
||||
}
|
||||
visited := map[string]bool{}
|
||||
queue := []string{startKey}
|
||||
for len(queue) > 0 {
|
||||
cur := queue[0]
|
||||
queue = queue[1:]
|
||||
if visited[cur] {
|
||||
continue
|
||||
}
|
||||
visited[cur] = true
|
||||
queue = append(queue, nextAdj[cur]...)
|
||||
}
|
||||
for _, n := range nodes {
|
||||
if !visited[strings.TrimSpace(n.NodeKey)] {
|
||||
return fmt.Errorf("节点「%s」未接入流程,请检查连线", strings.TrimSpace(n.NodeName))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// genApprovalFlowCode 由自增 id 生成审批流编号(全局唯一、供各程序按编号发起)
|
||||
func genApprovalFlowCode(id uint64) string {
|
||||
return fmt.Sprintf("FL%06d", id)
|
||||
}
|
||||
@@ -463,7 +463,8 @@ func (c *PlatformTenantDomainController) CustomDomainGuide() {
|
||||
|
||||
// Apply POST /platform/domain/tenant/apply body:{tid,sub_domain,main_domain}
|
||||
func (c *PlatformTenantDomainController) Apply() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
claims, err := requirePlatform(&c.Controller)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -551,13 +552,21 @@ func (c *PlatformTenantDomainController) Apply() {
|
||||
jsonErr(&c.Controller, 500, 500, "申请失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "申请提交成功,等待审核", "data": map[string]interface{}{"id": uint64(id)}}
|
||||
// 进入平台侧审批流(严格:未配置流程/无有效审批人则提交失败并回滚)
|
||||
if errMsg := submitDomainApprovalChecked(uint64(id), p.Tid, uint64(claims.UserID), claims.Username, full); errMsg != "" {
|
||||
jsonErr(&c.Controller, 400, 400, errMsg)
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "申请提交成功,等待审批", "data": map[string]interface{}{"id": uint64(id)}}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Audit POST /platform/domain/tenant/audit body:{id,action} action=approve/reject
|
||||
// Audit POST /platform/domain/tenant/audit body:{id,action,comment} action=approve/reject
|
||||
// 引擎驱动:已配置审批流则走多级流转,仅当前待办人可操作;
|
||||
// 未配置流程(无在途实例)则回退到单级直改状态,保证兼容。
|
||||
func (c *PlatformTenantDomainController) Audit() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
claims, err := requirePlatform(&c.Controller)
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -567,8 +576,9 @@ func (c *PlatformTenantDomainController) Audit() {
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ID uint64 `json:"id"`
|
||||
Action string `json:"action"`
|
||||
ID uint64 `json:"id"`
|
||||
Action string `json:"action"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil || p.ID == 0 {
|
||||
jsonErr(&c.Controller, 400, 400, "参数错误")
|
||||
@@ -587,25 +597,34 @@ func (c *PlatformTenantDomainController) Audit() {
|
||||
jsonErr(&c.Controller, 400, 400, "该域名已审核过了")
|
||||
return
|
||||
}
|
||||
newStatus := 2
|
||||
msg := "已拒绝"
|
||||
if strings.ToLower(strings.TrimSpace(p.Action)) == "approve" {
|
||||
newStatus = 1
|
||||
msg = "审核通过"
|
||||
pass := strings.ToLower(strings.TrimSpace(p.Action)) == "approve"
|
||||
var bizTid uint64
|
||||
if row.Tid != nil {
|
||||
bizTid = *row.Tid
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", p.ID).Update(map[string]interface{}{
|
||||
"status": newStatus,
|
||||
"update_time": now,
|
||||
})
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "审核失败: "+err.Error())
|
||||
|
||||
// 严格引擎驱动:必须命中在途实例与当前待办,否则拒绝(已移除单级兼容回退)
|
||||
msg, code := engineDomainAudit(p.ID, uint64(claims.UserID), pass, p.Comment, bizTid)
|
||||
if code != 200 {
|
||||
jsonErr(&c.Controller, code, code, msg)
|
||||
return
|
||||
}
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": msg}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// AuditProgress GET /platform/domain/tenant/audit/progress/:id
|
||||
// 返回该域名的审批进度(节点 + 各审批人状态),供平台端展示流转。
|
||||
func (c *PlatformTenantDomainController) AuditProgress() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
jsonErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "ok", "data": domainApprovalProgress(id)}
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// ToggleStatus POST /platform/domain/tenant/toggleStatus body:{id}
|
||||
func (c *PlatformTenantDomainController) ToggleStatus() {
|
||||
if _, err := requirePlatform(&c.Controller); err != nil {
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
"server/services/approval"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// =============================================================
|
||||
// 域名申请 × 审批流引擎 的接入层(严格模式,无单级兼容)
|
||||
//
|
||||
// 口径:
|
||||
// - 域名申请提交必须命中已配置的默认流程,走引擎多级流转;
|
||||
// - 未配置流程 / 节点无有效审批人 → 提交失败并回滚刚创建的域名行,绝不留下无法审批的脏数据;
|
||||
// - 审核只能通过引擎:必须存在在途实例且命中当前审批人的待办,否则拒绝操作。
|
||||
// =============================================================
|
||||
|
||||
// submitDomainApprovalChecked 将域名提交进平台侧审批流。
|
||||
// 成功返回空串;失败则软删回滚刚创建的域名行,并返回给前端的错误文案。
|
||||
func submitDomainApprovalChecked(domainRowID, bizTid, submitUID uint64, submitName, fullDomain string) string {
|
||||
_, err := approval.Submit(approval.SubmitInput{
|
||||
Scope: models.ApprovalScopePlatform,
|
||||
BizTid: bizTid,
|
||||
BizType: models.ApprovalBizDomain,
|
||||
BizID: domainRowID,
|
||||
BizTitle: fullDomain,
|
||||
SubmitUID: submitUID,
|
||||
SubmitName: submitName,
|
||||
})
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
// 提交失败:回滚刚创建的域名行(软删除),避免出现无法审批的待审僵尸数据
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemTenantDomain)).
|
||||
Filter("id", domainRowID).Update(map[string]interface{}{"delete_time": time.Now()})
|
||||
if _, ok := err.(*approval.ErrNoFlow); ok {
|
||||
return "平台尚未配置域名审批流程,无法提交申请"
|
||||
}
|
||||
if _, ok := err.(*approval.ErrNoApprover); ok {
|
||||
return "域名审批流程未配置有效审批人,无法提交申请"
|
||||
}
|
||||
return "提交审批失败:" + err.Error()
|
||||
}
|
||||
|
||||
// findRunningDomainInstance 取某域名当前在途的平台侧审批实例
|
||||
func findRunningDomainInstance(domainID uint64) (*models.ApprovalInstance, bool) {
|
||||
var inst models.ApprovalInstance
|
||||
err := models.Orm.QueryTable(new(models.ApprovalInstance)).
|
||||
Filter("scope", models.ApprovalScopePlatform).
|
||||
Filter("biz_type", models.ApprovalBizDomain).
|
||||
Filter("biz_id", domainID).
|
||||
Filter("status", models.ApprovalInstanceRunning).
|
||||
Filter("is_deleted", 0).
|
||||
OrderBy("-id").One(&inst)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, false
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &inst, true
|
||||
}
|
||||
|
||||
// myPendingDomainTask 取指定审批人在该实例上的当前待办任务
|
||||
func myPendingDomainTask(instID, approverID uint64) (*models.ApprovalTask, bool) {
|
||||
var task models.ApprovalTask
|
||||
err := models.Orm.QueryTable(new(models.ApprovalTask)).
|
||||
Filter("instance_id", instID).
|
||||
Filter("approver_id", approverID).
|
||||
Filter("status", models.ApprovalTaskPending).
|
||||
Filter("is_deleted", 0).
|
||||
OrderBy("sort", "id").One(&task)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &task, true
|
||||
}
|
||||
|
||||
// applyDomainApprovalResult 依据引擎结果回写域名状态(仅实例终结时改动)
|
||||
func applyDomainApprovalResult(domainID uint64, res *approval.ApproveResult) {
|
||||
if res == nil || !res.Finished {
|
||||
return
|
||||
}
|
||||
newStatus := 2 // 默认拒绝/驳回
|
||||
if res.InstanceStat == models.ApprovalInstanceApproved {
|
||||
newStatus = 1 // 通过生效
|
||||
}
|
||||
now := time.Now()
|
||||
_, _ = models.Orm.QueryTable(new(models.SystemTenantDomain)).Filter("id", domainID).
|
||||
Update(orm.Params{"status": newStatus, "update_time": now})
|
||||
}
|
||||
|
||||
// domainApprovalProgress 组装域名审批进度(节点 + 任务),供平台端展示
|
||||
func domainApprovalProgress(domainID uint64) map[string]interface{} {
|
||||
inst, ok := findRunningDomainInstance(domainID)
|
||||
if !ok {
|
||||
// 无在途实例:可能未走引擎(单级审核)或已结束
|
||||
return map[string]interface{}{"engine": false}
|
||||
}
|
||||
flow, err := approval.LoadFlow(models.ApprovalScopePlatform, 0, models.ApprovalBizDomain, inst.FlowID)
|
||||
nodes := []map[string]interface{}{}
|
||||
if err == nil && flow != nil {
|
||||
for _, nd := range flow.Nodes {
|
||||
nodes = append(nodes, map[string]interface{}{
|
||||
"node_key": nd.NodeKey,
|
||||
"node_name": nd.NodeName,
|
||||
"node_type": nd.NodeType,
|
||||
"sort": nd.Sort,
|
||||
})
|
||||
}
|
||||
}
|
||||
var tasks []models.ApprovalTask
|
||||
_, _ = models.Orm.QueryTable(new(models.ApprovalTask)).
|
||||
Filter("instance_id", inst.ID).Filter("is_deleted", 0).
|
||||
OrderBy("sort", "id").All(&tasks)
|
||||
taskList := make([]map[string]interface{}, 0, len(tasks))
|
||||
for _, t := range tasks {
|
||||
taskList = append(taskList, map[string]interface{}{
|
||||
"id": t.ID,
|
||||
"node_key": t.NodeKey,
|
||||
"node_name": t.NodeName,
|
||||
"approver_id": t.ApproverID,
|
||||
"approver_name": t.ApproverName,
|
||||
"status": t.Status,
|
||||
"action": t.Action,
|
||||
"comment": t.Comment,
|
||||
})
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"engine": true,
|
||||
"instance_id": inst.ID,
|
||||
"current_node": inst.CurrentNode,
|
||||
"status": inst.Status,
|
||||
"nodes": nodes,
|
||||
"tasks": taskList,
|
||||
}
|
||||
}
|
||||
|
||||
// engineDomainAudit 平台端审批入口(严格引擎驱动):
|
||||
// 必须存在在途实例且命中当前审批人的待办;否则拒绝,不做任何单级回退。
|
||||
func engineDomainAudit(domainID, adminID uint64, pass bool, comment string, bizTid uint64) (msg string, code int) {
|
||||
inst, ok := findRunningDomainInstance(domainID)
|
||||
if !ok {
|
||||
return "该域名申请未进入审批流程(未配置流程或已处理)", 400
|
||||
}
|
||||
task, ok := myPendingDomainTask(inst.ID, adminID)
|
||||
if !ok {
|
||||
return "该单据在审批流程中,当前不在您的待办节点", 403
|
||||
}
|
||||
res, err := approval.Approve(task.ID, approval.ApproveInput{
|
||||
ApproverID: adminID,
|
||||
Pass: pass,
|
||||
Comment: comment,
|
||||
BizTid: bizTid,
|
||||
})
|
||||
if err != nil {
|
||||
return err.Error(), 400
|
||||
}
|
||||
applyDomainApprovalResult(domainID, res)
|
||||
if res.Finished {
|
||||
if res.InstanceStat == models.ApprovalInstanceApproved {
|
||||
return "审批通过,域名已生效", 200
|
||||
}
|
||||
return "已驳回", 200
|
||||
}
|
||||
return fmt.Sprintf("已%s,流程继续(%s)", map[bool]string{true: "通过", false: "驳回"}[pass], res.Message), 200
|
||||
}
|
||||
@@ -9,14 +9,15 @@ import (
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// ApiPlatformUpgradeController 平台更新内容接口
|
||||
type ApiPlatformUpgradeController struct {
|
||||
// PlatformUpgradeNoticeController 平台端「平台更新公告」接口。
|
||||
// 数据同源于 models.PlatformUpgradeNotice,但作为平台端独立接口,
|
||||
// 不与移动端 /api、官网 /index 复用同一 handler。
|
||||
type PlatformUpgradeNoticeController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// List GET /api/platform/upgrade/list 获取平台更新列表(公开接口)
|
||||
func (c *ApiPlatformUpgradeController) List() {
|
||||
// 可选参数:limit - 返回数量限制,默认 10
|
||||
// List GET /platform/upgrade/list 获取平台更新列表
|
||||
func (c *PlatformUpgradeNoticeController) List() {
|
||||
limitStr := c.GetString("limit")
|
||||
limit := 10
|
||||
if limitStr != "" {
|
||||
@@ -29,7 +30,6 @@ func (c *ApiPlatformUpgradeController) List() {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
// 查询显示状态的更新记录,按排序字段和创建时间倒序
|
||||
var upgradeList []models.PlatformUpgradeNotice
|
||||
qs := models.Orm.QueryTable(new(models.PlatformUpgradeNotice)).
|
||||
Filter("status", 1).
|
||||
@@ -45,7 +45,6 @@ func (c *ApiPlatformUpgradeController) List() {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为前端需要的格式
|
||||
data := make([]map[string]interface{}, 0, len(upgradeList))
|
||||
for _, item := range upgradeList {
|
||||
record := map[string]interface{}{
|
||||
@@ -66,8 +65,8 @@ func (c *ApiPlatformUpgradeController) List() {
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Detail GET /api/platform/upgrade/detail?id={id} 获取单条更新详情
|
||||
func (c *ApiPlatformUpgradeController) Detail() {
|
||||
// Detail GET /platform/upgrade/detail?id={id} 获取单条更新详情
|
||||
func (c *PlatformUpgradeNoticeController) Detail() {
|
||||
idStr := strings.TrimSpace(c.GetString("id"))
|
||||
if idStr == "" {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
@@ -552,6 +553,32 @@ func (c *PlatformWebsitePageController) Get() {
|
||||
websiteOK(&c.Controller, row)
|
||||
}
|
||||
|
||||
// GetByPath GET /platform/website/page/path/:path — 按路径读取单页(平台端独立接口)
|
||||
func (c *PlatformWebsitePageController) GetByPath() {
|
||||
if _, ok := websiteAuth(&c.Controller); !ok {
|
||||
return
|
||||
}
|
||||
path := normalizePath(c.Ctx.Input.Param(":path"))
|
||||
if path == "" {
|
||||
jsonErr(&c.Controller, 400, 400, "路径标识不能为空")
|
||||
return
|
||||
}
|
||||
var row models.PlatformWebsitePage
|
||||
err := models.Orm.QueryTable(new(models.PlatformWebsitePage)).
|
||||
Filter("path", path).
|
||||
Filter("delete_time__isnull", true).
|
||||
One(&row)
|
||||
if err == orm.ErrNoRows {
|
||||
websiteOK(&c.Controller, nil)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
jsonErr(&c.Controller, 500, 500, "获取单页失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
websiteOK(&c.Controller, row)
|
||||
}
|
||||
|
||||
// Create POST /platform/website/page
|
||||
func (c *PlatformWebsitePageController) Create() {
|
||||
if _, ok := websiteAuth(&c.Controller); !ok {
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
-- =============================================================
|
||||
-- 自定义审批流(租户侧 + 平台侧)
|
||||
--
|
||||
-- 对应模型:go/models/approval.go
|
||||
--
|
||||
-- 设计要点:
|
||||
-- 1. 配置侧与实例侧分离:flow/node 是配置(租户可编辑),
|
||||
-- instance/task 是运行时(提交后生成,不可编辑)。
|
||||
-- 2. 节点表用 prev_keys / next_keys 存连线,当期顺序链只填 sort,
|
||||
-- 后续上拖拽图与条件分支无需改表。
|
||||
-- 3. scope 区分租户侧(tenant)与平台侧(platform):平台账号不归属租户,
|
||||
-- 其流程不带 tid,实例用 biz_tid 标记申请来源租户。
|
||||
--
|
||||
-- 注意:不显式指定 COLLATE,沿用数据库默认排序规则,避免与既有表
|
||||
-- (utf8mb4_0900_ai_ci)在 JOIN 比较字符串时报 Illegal mix of collations。
|
||||
-- =============================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ---------------- 1. 租户侧审批流程 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_approval_flow` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||
`module_code` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '所属功能模块 code(yz_system_modules.code):oa/erp/crm/cms',
|
||||
`biz_type` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '适用单据类型:reimburse/bid/domain/...',
|
||||
`flow_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '审批流编号(自动生成、全局唯一,供各程序按编号发起):FL000007',
|
||||
`flow_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '流程名称',
|
||||
`description` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '流程说明',
|
||||
`is_default` TINYINT NOT NULL DEFAULT 0 COMMENT '是否该单据类型的默认流程:1是 0否',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用',
|
||||
`is_deleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tid` (`tid`),
|
||||
KEY `idx_tid_module` (`tid`,`module_code`),
|
||||
KEY `idx_tid_biz` (`tid`,`biz_type`),
|
||||
KEY `idx_tid_code` (`tid`,`flow_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OA-自定义审批流程(租户侧)';
|
||||
|
||||
-- ---------------- 2. 租户侧审批节点 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_approval_node` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`flow_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '所属流程ID',
|
||||
`tid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID(冗余,便于按租户清理)',
|
||||
`node_key` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '节点唯一标识(流程内唯一)',
|
||||
`node_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '节点名称',
|
||||
`node_type` TINYINT NOT NULL DEFAULT 2 COMMENT '1开始 2审批 3结束 4条件分支',
|
||||
`approver_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1指定用户 2指定角色 3部门主管 4指定职位 5发起人本人 6发起人主管',
|
||||
`approver_ids` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '审批人ID列表,逗号分隔(user=SystemTenantUser.uid / role=AdminRole.id / position=BackendPosition.id)',
|
||||
`multi_mode` TINYINT NOT NULL DEFAULT 1 COMMENT '多人审批方式:1或签(任一通过) 2会签(全部通过)',
|
||||
`allow_return` TINYINT NOT NULL DEFAULT 0 COMMENT '是否允许退回上一节点:1是 0否',
|
||||
`timeout_hours` INT NOT NULL DEFAULT 48 COMMENT '超时小时数,超时后自动通过',
|
||||
`prev_keys` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '前置节点 node_key 列表,逗号分隔(为条件分支预留)',
|
||||
`next_keys` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '后继节点 node_key 列表,逗号分隔(为条件分支预留)',
|
||||
`condition_expr` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '进入条件表达式,如 amount>5000(条件分支网关的出边条件)',
|
||||
`sort` INT NOT NULL DEFAULT 0 COMMENT '顺序号,顺序链按此升序流转',
|
||||
`pos_x` INT NOT NULL DEFAULT 0 COMMENT '画布X坐标(图编辑器回显)',
|
||||
`pos_y` INT NOT NULL DEFAULT 0 COMMENT '画布Y坐标(图编辑器回显)',
|
||||
`is_deleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_flow` (`flow_id`),
|
||||
KEY `idx_tid` (`tid`),
|
||||
KEY `idx_flow_sort` (`flow_id`,`sort`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='OA-自定义审批节点(租户侧)';
|
||||
|
||||
-- ---------------- 3. 平台侧审批流程 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_platform_approval_flow` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`module_code` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '所属平台模块 code',
|
||||
`biz_type` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '适用单据类型:domain/tenant/...',
|
||||
`flow_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '审批流编号(自动生成、全局唯一):FL000007',
|
||||
`flow_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '流程名称',
|
||||
`description` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '流程说明',
|
||||
`is_default` TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认流程:1是 0否',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用',
|
||||
`is_deleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_module` (`module_code`),
|
||||
KEY `idx_biz` (`biz_type`),
|
||||
KEY `idx_code` (`flow_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台-自定义审批流程';
|
||||
|
||||
-- ---------------- 4. 平台侧审批节点 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_platform_approval_node` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`flow_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '所属流程ID',
|
||||
`node_key` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '节点唯一标识(流程内唯一)',
|
||||
`node_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '节点名称',
|
||||
`node_type` TINYINT NOT NULL DEFAULT 2 COMMENT '1开始 2审批 3结束 4条件分支',
|
||||
`approver_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1指定平台用户 2指定平台角色 5发起人本人',
|
||||
`approver_ids` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '审批人ID列表,逗号分隔(user=AdminUser.id / role=AdminRole.id)',
|
||||
`multi_mode` TINYINT NOT NULL DEFAULT 1 COMMENT '1或签 2会签',
|
||||
`allow_return` TINYINT NOT NULL DEFAULT 0 COMMENT '是否允许退回:1是 0否',
|
||||
`timeout_hours` INT NOT NULL DEFAULT 48 COMMENT '超时小时数',
|
||||
`prev_keys` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '前置节点(为条件分支预留)',
|
||||
`next_keys` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '后继节点(为条件分支预留)',
|
||||
`condition_expr` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '进入条件(条件分支网关出边条件)',
|
||||
`sort` INT NOT NULL DEFAULT 0 COMMENT '顺序号',
|
||||
`pos_x` INT NOT NULL DEFAULT 0 COMMENT '画布X坐标(图编辑器回显)',
|
||||
`pos_y` INT NOT NULL DEFAULT 0 COMMENT '画布Y坐标(图编辑器回显)',
|
||||
`is_deleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_flow` (`flow_id`),
|
||||
KEY `idx_flow_sort` (`flow_id`,`sort`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台-自定义审批节点';
|
||||
|
||||
-- ---------------- 5. 审批实例(运行时,租户侧与平台侧共用) ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_approval_instance` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`scope` TINYINT NOT NULL DEFAULT 1 COMMENT '1租户侧 2平台侧',
|
||||
`tid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID(租户侧=本租户;平台侧=发起申请的平台账号所属租户,平台账号通常为0)',
|
||||
`biz_tid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '业务来源租户ID(平台侧审批必填:申请由哪个租户提交)',
|
||||
`flow_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '流程ID(配置可能被改,实例内节点快照为准)',
|
||||
`biz_type` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '单据类型:reimburse/bid/domain',
|
||||
`biz_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '单据ID',
|
||||
`biz_title` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '单据标题快照',
|
||||
`submit_user_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '发起人ID',
|
||||
`submit_user_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '发起人姓名快照',
|
||||
`submit_dept_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '发起人部门ID快照(部门主管审批依赖)',
|
||||
`form_data` TEXT NULL COMMENT '发起表单数据快照(JSON),条件分支求值依据',
|
||||
`current_node` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '当前节点 node_key',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '0草稿 1审批中 2已通过 3已驳回 4已撤回 5已取消',
|
||||
`submit_time` DATETIME DEFAULT NULL COMMENT '提交时间',
|
||||
`finish_time` DATETIME DEFAULT NULL COMMENT '完成时间',
|
||||
`is_deleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_scope_tid` (`scope`,`tid`),
|
||||
KEY `idx_biz` (`biz_type`,`biz_id`),
|
||||
KEY `idx_biz_tid` (`scope`,`biz_tid`,`biz_type`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_flow` (`flow_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批实例(运行时)';
|
||||
|
||||
-- ---------------- 6. 审批任务(运行时,节点级待办) ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_approval_task` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`instance_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '实例ID',
|
||||
`scope` TINYINT NOT NULL DEFAULT 1 COMMENT '1租户侧 2平台侧',
|
||||
`tid` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID',
|
||||
`biz_type` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '单据类型(冗余,便于列表筛选)',
|
||||
`biz_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '单据ID(冗余)',
|
||||
`node_key` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '节点 node_key',
|
||||
`node_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '节点名称快照',
|
||||
`sort` INT NOT NULL DEFAULT 0 COMMENT '节点顺序号快照',
|
||||
`approver_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '审批人ID',
|
||||
`approver_name` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '审批人姓名快照',
|
||||
`multi_mode` TINYINT NOT NULL DEFAULT 1 COMMENT '1或签 2会签',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待审批 1已通过 2已驳回 3已退回 4已跳过 5已超时',
|
||||
`action` TINYINT NOT NULL DEFAULT 0 COMMENT '1提交 2通过 3驳回 4退回 5撤回 6超时自动通过',
|
||||
`comment` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '审批意见',
|
||||
`deadline` DATETIME DEFAULT NULL COMMENT '超时时间点',
|
||||
`finish_time` DATETIME DEFAULT NULL COMMENT '处理时间',
|
||||
`is_deleted` TINYINT NOT NULL DEFAULT 0,
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_instance` (`instance_id`),
|
||||
KEY `idx_approver` (`scope`,`approver_id`,`status`),
|
||||
KEY `idx_biz` (`biz_type`,`biz_id`),
|
||||
KEY `idx_tid` (`tid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审批任务(运行时·待办)';
|
||||
|
||||
-- =============================================================
|
||||
-- 旧表迁移说明:
|
||||
-- yz_backend_approval_flows(扁平步骤行结构)仅被 OA 报销 Submit 读取。
|
||||
-- 第一期接管报销后改走新引擎,旧表保留只读兼容,不再写入。
|
||||
-- 如需保留历史配置,可按如下语句转换(按需执行):
|
||||
--
|
||||
-- INSERT INTO yz_backend_approval_flow
|
||||
-- (tid, module_code, biz_type, flow_name, is_default, status)
|
||||
-- SELECT DISTINCT tid, 'oa', 'reimburse', flow_name, 1, 1
|
||||
-- FROM yz_backend_approval_flows
|
||||
-- WHERE is_active = 1 AND is_deleted = 0
|
||||
-- GROUP BY tid, flow_name;
|
||||
--
|
||||
-- INSERT INTO yz_backend_approval_node
|
||||
-- (flow_id, tid, node_key, node_name, node_type, approver_type, approver_ids, timeout_hours, sort)
|
||||
-- SELECT f.id, f.tid, CONCAT('step_', s.step_order), s.flow_name, 2, 2, '', s.timeout_hours, s.step_order
|
||||
-- FROM yz_backend_approval_flows s
|
||||
-- JOIN yz_backend_approval_flow f
|
||||
-- ON f.tid = s.tid AND f.biz_type = 'reimburse'
|
||||
-- WHERE s.is_active = 1 AND s.is_deleted = 0;
|
||||
-- =============================================================
|
||||
@@ -0,0 +1,38 @@
|
||||
-- ============================================================
|
||||
-- 审批流配置 —— 平台端菜单(yz_system_menu)
|
||||
-- 挂在「域名管理」目录(path = /system/domains)下,紧邻「域名审核」。
|
||||
-- 幂等脚本:按 path 判重,重复执行不会产生重复菜单。
|
||||
-- 执行:mysql -h <host> -P <port> -u <user> -p <db> < platform_approval_menu.sql
|
||||
-- ============================================================
|
||||
|
||||
-- 字段说明:
|
||||
-- type 1=目录 2=页面;views [1]=平台端;is_visible/status=1 显示并启用
|
||||
-- path 与 platform 前端静态路由 /system/approval 一致(component 同源,双保险)
|
||||
|
||||
SET @domain_dir := (
|
||||
SELECT id FROM yz_system_menu
|
||||
WHERE path = '/system/domains' AND delete_time IS NULL
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT @domain_dir, '审批流配置', '/system/approval', 'system/approval/index.vue',
|
||||
'SetUp', 5, 1, 1, '[1]', 2, 'system', '平台侧审批流节点编排(域名申请等管控场景)'
|
||||
FROM DUAL
|
||||
WHERE @domain_dir IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/system/approval' AND delete_time IS NULL) x);
|
||||
|
||||
-- 若未找到「域名管理」目录,则退化为平台端顶级菜单(pid=0)
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT 0, '审批流配置', '/system/approval', 'system/approval/index.vue',
|
||||
'SetUp', 90, 1, 1, '[1]', 2, 'system', '平台侧审批流节点编排(域名申请等管控场景)'
|
||||
FROM DUAL
|
||||
WHERE @domain_dir IS NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/system/approval' AND delete_time IS NULL) x);
|
||||
|
||||
-- 结果校验
|
||||
SELECT id, pid, title, path, component_path, views, type, is_visible, status
|
||||
FROM yz_system_menu
|
||||
WHERE delete_time IS NULL AND path = '/system/approval';
|
||||
+23
-14
@@ -25,9 +25,16 @@ const (
|
||||
)
|
||||
|
||||
// protectedPrefixes 需要鉴权的业务前缀。
|
||||
// 仅保护业务 API:官网(/ /site/ /index/…)、开放 API(/api/…)、
|
||||
// 仅保护业务 API:官网(/ /site/ /index/…)、开放/登录器接口(/api/ 下的公开项)、
|
||||
// 静态资源与 ACME 验证路径均不在其中,避免误伤公开接口。
|
||||
var protectedPrefixes = []string{"/backend/", "/platform/", "/app/"}
|
||||
// 移动端(uniapp)已并入 /api/,故显式列出其中需要鉴权的移动端前缀,
|
||||
// 而登录器公开接口(/api/getcard、/api/cursor/*、/api/softwareupgrade/check 等)
|
||||
// 不在下列前缀内,保持公开。
|
||||
var protectedPrefixes = []string{
|
||||
"/backend/", "/platform/",
|
||||
"/api/oa/", "/api/notebook/", "/api/schedule", "/api/currentUser",
|
||||
"/api/activity/", "/api/home", "/api/quick-menus",
|
||||
}
|
||||
|
||||
// authWhitelist 受保护前缀内无需鉴权的路径(前缀匹配)
|
||||
var authWhitelist = []string{
|
||||
@@ -44,23 +51,25 @@ var authWhitelist = []string{
|
||||
"/backend/verifyAccount",
|
||||
"/backend/logout",
|
||||
|
||||
// platform 登录/找回密码
|
||||
// platform 登录/注册/找回密码
|
||||
"/platform/login",
|
||||
"/platform/sendLoginCode",
|
||||
"/platform/register",
|
||||
"/platform/sendRegisterCode",
|
||||
"/platform/resetPassword",
|
||||
"/platform/logout",
|
||||
// 平台开放接口(客户端更新通知,无需登录)
|
||||
"/platform/api/",
|
||||
|
||||
// app 登录/注册/找回密码
|
||||
"/app/login",
|
||||
"/app/sendLoginCode",
|
||||
"/app/register",
|
||||
"/app/sendRegisterCode",
|
||||
"/app/resetPassword",
|
||||
"/app/sendResetCode",
|
||||
"/app/verifyAccount",
|
||||
"/app/logout",
|
||||
// 移动端(/api)登录/注册/找回密码(/api/login 前缀同时覆盖 loginBySms、极验配置)
|
||||
"/api/login",
|
||||
"/api/sendLoginCode",
|
||||
"/api/register",
|
||||
"/api/sendRegisterCode",
|
||||
"/api/resetPassword",
|
||||
"/api/sendResetCode",
|
||||
"/api/verifyAccount",
|
||||
"/api/logout",
|
||||
// 登录器日程提醒确认(公开回调)
|
||||
"/api/schedule/reminder/ack",
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -60,16 +60,16 @@ func BeginOperationLog(ctx *context.Context) {
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond) // 等待响应完成
|
||||
logRow := &models.SystemOperationLog{
|
||||
Tid: tid,
|
||||
UserID: uid,
|
||||
Module: module,
|
||||
Action: action,
|
||||
Method: method,
|
||||
URL: truncateString(url, 255),
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
RequestData: strPtr(reqBody),
|
||||
Status: 1,
|
||||
Tid: tid,
|
||||
UserID: uid,
|
||||
Module: module,
|
||||
Action: action,
|
||||
Method: method,
|
||||
URL: truncateString(url, 255),
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
RequestData: strPtr(reqBody),
|
||||
Status: 1,
|
||||
ExecutionTime: 0,
|
||||
}
|
||||
_, err := models.Orm.Insert(logRow)
|
||||
@@ -88,20 +88,40 @@ func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
// isMobileAppPath 判断是否属于移动端(/api/)功能接口前缀集合。
|
||||
// 登录器公开接口(/api/getcard、/api/cursor/*、/api/softwareupgrade/*、
|
||||
// /api/schedule/reminder/ack 等)不在此集合内。
|
||||
func isMobileAppPath(url string) bool {
|
||||
mobilePrefixes := []string{
|
||||
"/api/oa/", "/api/notebook/", "/api/currentUser",
|
||||
"/api/activity/", "/api/home", "/api/quick-menus",
|
||||
}
|
||||
// /api/schedule 需区分:/api/schedule/reminder/ack 属登录器,排除
|
||||
if strings.HasPrefix(url, "/api/schedule") {
|
||||
return !strings.HasPrefix(url, "/api/schedule/reminder/ack")
|
||||
}
|
||||
for _, p := range mobilePrefixes {
|
||||
if strings.HasPrefix(url, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WriteDeleteLog 控制器删除后直接写操作日志(带标题)
|
||||
func WriteDeleteLog(userID uint64, tid int, module, title string) {
|
||||
moduleCN := oplogModuleLabel(module)
|
||||
desc := fmt.Sprintf("您删除了%s:【%s】", moduleCN, truncateString(title, 20))
|
||||
tidVal := uint64(tid)
|
||||
logRow := &models.SystemOperationLog{
|
||||
UserID: userID,
|
||||
Tid: &tidVal,
|
||||
Module: module,
|
||||
Action: "删除",
|
||||
Method: "DELETE",
|
||||
URL: fmt.Sprintf("/app/%s", module),
|
||||
Status: 1,
|
||||
RequestData: &desc,
|
||||
UserID: userID,
|
||||
Tid: &tidVal,
|
||||
Module: module,
|
||||
Action: "删除",
|
||||
Method: "DELETE",
|
||||
URL: fmt.Sprintf("/api/%s", module),
|
||||
Status: 1,
|
||||
RequestData: &desc,
|
||||
ExecutionTime: 0,
|
||||
}
|
||||
models.Orm.Insert(logRow)
|
||||
@@ -291,12 +311,8 @@ func shouldSkipLogging(method, url string) bool {
|
||||
if strings.HasPrefix(url, "/api/softwareupgrade/check") {
|
||||
return true
|
||||
}
|
||||
// App 端读接口跳过(只记录写操作:POST/PUT/DELETE)
|
||||
if strings.HasPrefix(url, "/app/") && method == "GET" {
|
||||
return true
|
||||
}
|
||||
// App 端 DELETE 由控制器自行记录(需要标题信息)
|
||||
if strings.HasPrefix(url, "/app/") && method == "DELETE" {
|
||||
// 移动端(已并入 /api/)读接口跳过(只记录写操作:POST/PUT/DELETE)
|
||||
if isMobileAppPath(url) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
+42
-50
@@ -13,7 +13,7 @@ func PermissionMiddleware() func(ctx *context.Context) {
|
||||
return func(ctx *context.Context) {
|
||||
// 获取当前请求的路径
|
||||
path := ctx.Input.URL()
|
||||
|
||||
|
||||
// 不需要权限验证的路径列表
|
||||
publicPaths := []string{
|
||||
"/api/login",
|
||||
@@ -23,19 +23,19 @@ func PermissionMiddleware() func(ctx *context.Context) {
|
||||
"/api/program-infos/public",
|
||||
"/api/files/public",
|
||||
}
|
||||
|
||||
|
||||
// 检查是否为公开路径
|
||||
for _, p := range publicPaths {
|
||||
if path == p {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 检查是否为公开预览接口
|
||||
if strings.HasPrefix(path, "/api/files/public-preview/") {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 获取用户ID
|
||||
userIdData := ctx.Input.GetData("userId")
|
||||
if userIdData == nil {
|
||||
@@ -43,7 +43,7 @@ func PermissionMiddleware() func(ctx *context.Context) {
|
||||
// 这里直接返回,因为JWT中间件已经拦截了
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
userId, ok := userIdData.(int)
|
||||
if !ok {
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
@@ -52,15 +52,15 @@ func PermissionMiddleware() func(ctx *context.Context) {
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 获取当前路由对应的权限标识
|
||||
permission := getPermissionByPath(path, ctx.Input.Method())
|
||||
|
||||
|
||||
// 如果没有权限标识,说明该接口不需要权限控制
|
||||
if permission == "" {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 检查用户是否拥有该权限
|
||||
hasPermission, err := services.CheckUserPermission(userId, permission)
|
||||
if err != nil {
|
||||
@@ -71,7 +71,7 @@ func PermissionMiddleware() func(ctx *context.Context) {
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if !hasPermission {
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
@@ -90,26 +90,19 @@ func getPermissionByPath(path, method string) string {
|
||||
// 这里只列举了部分示例,实际应该从数据库中加载
|
||||
permissionMap := map[string]string{
|
||||
// 用户管理
|
||||
"GET:/api/allUsers": "user:list",
|
||||
"GET:/api/user/:id": "user:detail",
|
||||
"POST:/api/addUser": "user:add",
|
||||
"POST:/api/editUser/:id": "user:edit",
|
||||
"DELETE:/api/deleteUser/:id": "user:delete",
|
||||
"POST:/api/changePassword/:id":"user:changePassword",
|
||||
|
||||
// 角色管理
|
||||
"GET:/api/roles": "role:list",
|
||||
"POST:/api/roles": "role:create",
|
||||
"GET:/api/roles/:id": "role:detail",
|
||||
"POST:/api/roles/:id": "role:update",
|
||||
"DELETE:/api/roles/:id": "role:delete",
|
||||
|
||||
"GET:/api/allUsers": "user:list",
|
||||
"GET:/api/user/:id": "user:detail",
|
||||
"POST:/api/addUser": "user:add",
|
||||
"POST:/api/editUser/:id": "user:edit",
|
||||
"DELETE:/api/deleteUser/:id": "user:delete",
|
||||
"POST:/api/changePassword/:id": "user:changePassword",
|
||||
|
||||
// 菜单管理
|
||||
"GET:/api/allmenu": "menu:list",
|
||||
"POST:/api/menu": "menu:create",
|
||||
"PUT:/api/menu/:id": "menu:update",
|
||||
"DELETE:/api/menu/:id": "menu:delete",
|
||||
|
||||
"GET:/api/allmenu": "menu:list",
|
||||
"POST:/api/menu": "menu:create",
|
||||
"PUT:/api/menu/:id": "menu:update",
|
||||
"DELETE:/api/menu/:id": "menu:delete",
|
||||
|
||||
// 文件管理
|
||||
"GET:/api/files": "file:list",
|
||||
"POST:/api/files": "file:upload",
|
||||
@@ -121,29 +114,29 @@ func getPermissionByPath(path, method string) string {
|
||||
"DELETE:/api/files/:id": "file:delete",
|
||||
"GET:/api/files/search": "file:search",
|
||||
"GET:/api/files/statistics": "file:statistics",
|
||||
|
||||
|
||||
// 租户管理
|
||||
"GET:/api/tenant/list": "tenant:list",
|
||||
"POST:/api/tenant": "tenant:create",
|
||||
"PUT:/api/tenant/:id": "tenant:update",
|
||||
"DELETE:/api/tenant/:id": "tenant:delete",
|
||||
"POST:/api/tenant/:id/audit": "tenant:audit",
|
||||
"GET:/api/tenant/:id": "tenant:detail",
|
||||
|
||||
"GET:/api/tenant/list": "tenant:list",
|
||||
"POST:/api/tenant": "tenant:create",
|
||||
"PUT:/api/tenant/:id": "tenant:update",
|
||||
"DELETE:/api/tenant/:id": "tenant:delete",
|
||||
"POST:/api/tenant/:id/audit": "tenant:audit",
|
||||
"GET:/api/tenant/:id": "tenant:detail",
|
||||
|
||||
// 知识库
|
||||
"GET:/api/knowledge/list": "knowledge:list",
|
||||
"GET:/api/knowledge/detail": "knowledge:detail",
|
||||
"POST:/api/knowledge/create": "knowledge:create",
|
||||
"POST:/api/knowledge/update": "knowledge:update",
|
||||
"POST:/api/knowledge/delete": "knowledge:delete",
|
||||
"GET:/api/knowledge/list": "knowledge:list",
|
||||
"GET:/api/knowledge/detail": "knowledge:detail",
|
||||
"POST:/api/knowledge/create": "knowledge:create",
|
||||
"POST:/api/knowledge/update": "knowledge:update",
|
||||
"POST:/api/knowledge/delete": "knowledge:delete",
|
||||
}
|
||||
|
||||
|
||||
// 匹配路径(简化版本,不支持动态参数匹配)
|
||||
key := method + ":" + path
|
||||
if perm, ok := permissionMap[key]; ok {
|
||||
return perm
|
||||
}
|
||||
|
||||
|
||||
// 尝试匹配动态路由(简单的ID参数替换)
|
||||
// 例如:/api/user/123 -> /api/user/:id
|
||||
pathParts := strings.Split(path, "/")
|
||||
@@ -152,19 +145,19 @@ func getPermissionByPath(path, method string) string {
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
methodPart := parts[0]
|
||||
pathPattern := parts[1]
|
||||
|
||||
|
||||
if methodPart != method {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
patternParts := strings.Split(pathPattern, "/")
|
||||
if len(patternParts) != len(pathParts) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
match := true
|
||||
for i, part := range patternParts {
|
||||
if strings.HasPrefix(part, ":") {
|
||||
@@ -176,13 +169,12 @@ func getPermissionByPath(path, method string) string {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if match {
|
||||
return perm
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果没有找到匹配的权限标识,返回空字符串(表示不需要权限控制)
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// =============================================================
|
||||
// 自定义审批流(配置侧)
|
||||
//
|
||||
// 设计要点:
|
||||
// - 租户侧与平台侧配置分表隔离:平台账号不归属租户,其流程不带 tid;
|
||||
// - 节点表用 prev_keys / next_keys / condition_expr 承载连线与条件,
|
||||
// 当期顺序链只按 sort 流转,后续上拖拽图与条件分支无需改表。
|
||||
// =============================================================
|
||||
|
||||
// 审批适用范围:租户侧 / 平台侧
|
||||
const (
|
||||
ApprovalScopeTenant = 1 // 租户侧(backend / uniapp)
|
||||
ApprovalScopePlatform = 2 // 平台侧(platform,跨租户管控)
|
||||
)
|
||||
|
||||
// 节点类型
|
||||
const (
|
||||
ApprovalNodeStart = 1 // 开始
|
||||
ApprovalNodeApproval = 2 // 审批
|
||||
ApprovalNodeEnd = 3 // 结束
|
||||
ApprovalNodeCondition = 4 // 条件分支(预留)
|
||||
)
|
||||
|
||||
// 审批人类型
|
||||
const (
|
||||
ApprovalApproverUser = 1 // 指定用户
|
||||
ApprovalApproverRole = 2 // 指定角色
|
||||
ApprovalApproverOrgLeader = 3 // 部门主管
|
||||
ApprovalApproverPosition = 4 // 指定职位
|
||||
ApprovalApproverInitiator = 5 // 发起人本人
|
||||
ApprovalApproverInitLeader = 6 // 发起人所在部门主管
|
||||
)
|
||||
|
||||
// 多人审批方式
|
||||
const (
|
||||
ApprovalMultiAny = 1 // 或签:任一通过即通过
|
||||
ApprovalMultiAll = 2 // 会签:全部通过才通过
|
||||
)
|
||||
|
||||
// 单据类型
|
||||
const (
|
||||
ApprovalBizReimburse = "reimburse" // OA 报销单
|
||||
ApprovalBizBid = "bid" // CRM 投标立项
|
||||
ApprovalBizDomain = "domain" // 域名申请(平台侧审批)
|
||||
)
|
||||
|
||||
// ---------------- 租户侧配置 ----------------
|
||||
|
||||
// TenantApprovalFlow 租户侧审批流程 yz_backend_approval_flow
|
||||
type TenantApprovalFlow struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Tid uint64 `orm:"column(tid)" json:"tid"`
|
||||
ModuleCode string `orm:"column(module_code);size(50);default()" json:"module_code"`
|
||||
BizType string `orm:"column(biz_type);size(50);default()" json:"biz_type"`
|
||||
FlowCode string `orm:"column(flow_code);size(64);default()" json:"flow_code"`
|
||||
FlowName string `orm:"column(flow_name);size(100);default()" json:"flow_name"`
|
||||
Description string `orm:"column(description);size(500);default()" json:"description"`
|
||||
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
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);type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantApprovalFlow) TableName() string {
|
||||
return "yz_backend_approval_flow"
|
||||
}
|
||||
|
||||
// TenantApprovalNode 租户侧审批节点 yz_backend_approval_node
|
||||
type TenantApprovalNode struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
FlowID uint64 `orm:"column(flow_id)" json:"flow_id"`
|
||||
Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
|
||||
NodeKey string `orm:"column(node_key);size(50);default()" json:"node_key"`
|
||||
NodeName string `orm:"column(node_name);size(100);default()" json:"node_name"`
|
||||
NodeType int8 `orm:"column(node_type);default(2)" json:"node_type"`
|
||||
ApproverType int8 `orm:"column(approver_type);default(1)" json:"approver_type"`
|
||||
ApproverIDs string `orm:"column(approver_ids);size(500);default()" json:"approver_ids"`
|
||||
MultiMode int8 `orm:"column(multi_mode);default(1)" json:"multi_mode"`
|
||||
AllowReturn int8 `orm:"column(allow_return);default(0)" json:"allow_return"`
|
||||
TimeoutHours int `orm:"column(timeout_hours);default(48)" json:"timeout_hours"`
|
||||
PrevKeys string `orm:"column(prev_keys);size(500);default()" json:"prev_keys"`
|
||||
NextKeys string `orm:"column(next_keys);size(500);default()" json:"next_keys"`
|
||||
ConditionExpr string `orm:"column(condition_expr);size(500);default()" json:"condition_expr"`
|
||||
Sort int `orm:"column(sort);default(0)" json:"sort"`
|
||||
PosX int `orm:"column(pos_x);default(0)" json:"pos_x"`
|
||||
PosY int `orm:"column(pos_y);default(0)" json:"pos_y"`
|
||||
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);type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantApprovalNode) TableName() string {
|
||||
return "yz_backend_approval_node"
|
||||
}
|
||||
|
||||
// ---------------- 平台侧配置 ----------------
|
||||
|
||||
// PlatformApprovalFlow 平台侧审批流程 yz_platform_approval_flow
|
||||
// 平台账号不归属租户,因此无 tid 字段。
|
||||
type PlatformApprovalFlow struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
ModuleCode string `orm:"column(module_code);size(50);default()" json:"module_code"`
|
||||
BizType string `orm:"column(biz_type);size(50);default()" json:"biz_type"`
|
||||
FlowCode string `orm:"column(flow_code);size(64);default()" json:"flow_code"`
|
||||
FlowName string `orm:"column(flow_name);size(100);default()" json:"flow_name"`
|
||||
Description string `orm:"column(description);size(500);default()" json:"description"`
|
||||
IsDefault int8 `orm:"column(is_default);default(0)" json:"is_default"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
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);type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *PlatformApprovalFlow) TableName() string {
|
||||
return "yz_platform_approval_flow"
|
||||
}
|
||||
|
||||
// PlatformApprovalNode 平台侧审批节点 yz_platform_approval_node
|
||||
// approver_ids 存平台主体:user=AdminUser.id / role=AdminRole.id
|
||||
type PlatformApprovalNode struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
FlowID uint64 `orm:"column(flow_id)" json:"flow_id"`
|
||||
NodeKey string `orm:"column(node_key);size(50);default()" json:"node_key"`
|
||||
NodeName string `orm:"column(node_name);size(100);default()" json:"node_name"`
|
||||
NodeType int8 `orm:"column(node_type);default(2)" json:"node_type"`
|
||||
ApproverType int8 `orm:"column(approver_type);default(1)" json:"approver_type"`
|
||||
ApproverIDs string `orm:"column(approver_ids);size(500);default()" json:"approver_ids"`
|
||||
MultiMode int8 `orm:"column(multi_mode);default(1)" json:"multi_mode"`
|
||||
AllowReturn int8 `orm:"column(allow_return);default(0)" json:"allow_return"`
|
||||
TimeoutHours int `orm:"column(timeout_hours);default(48)" json:"timeout_hours"`
|
||||
PrevKeys string `orm:"column(prev_keys);size(500);default()" json:"prev_keys"`
|
||||
NextKeys string `orm:"column(next_keys);size(500);default()" json:"next_keys"`
|
||||
ConditionExpr string `orm:"column(condition_expr);size(500);default()" json:"condition_expr"`
|
||||
Sort int `orm:"column(sort);default(0)" json:"sort"`
|
||||
PosX int `orm:"column(pos_x);default(0)" json:"pos_x"`
|
||||
PosY int `orm:"column(pos_y);default(0)" json:"pos_y"`
|
||||
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);type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *PlatformApprovalNode) TableName() string {
|
||||
return "yz_platform_approval_node"
|
||||
}
|
||||
|
||||
// ---------------- 运行时(租户侧与平台侧共用) ----------------
|
||||
|
||||
// 实例状态
|
||||
const (
|
||||
ApprovalInstanceDraft = 0 // 草稿
|
||||
ApprovalInstanceRunning = 1 // 审批中
|
||||
ApprovalInstanceApproved = 2 // 已通过
|
||||
ApprovalInstanceRejected = 3 // 已驳回
|
||||
ApprovalInstanceWithdrawn = 4 // 已撤回
|
||||
ApprovalInstanceCanceled = 5 // 已取消
|
||||
)
|
||||
|
||||
// 任务状态
|
||||
const (
|
||||
ApprovalTaskPending = 0 // 待审批
|
||||
ApprovalTaskApproved = 1 // 已通过
|
||||
ApprovalTaskRejected = 2 // 已驳回
|
||||
ApprovalTaskReturned = 3 // 已退回
|
||||
ApprovalTaskSkipped = 4 // 已跳过
|
||||
ApprovalTaskTimeout = 5 // 已超时
|
||||
)
|
||||
|
||||
// 任务动作
|
||||
const (
|
||||
ApprovalActionSubmit = 1 // 提交
|
||||
ApprovalActionApprove = 2 // 通过
|
||||
ApprovalActionReject = 3 // 驳回
|
||||
ApprovalActionReturn = 4 // 退回
|
||||
ApprovalActionWithdraw = 5 // 撤回
|
||||
ApprovalActionAutoPass = 6 // 超时自动通过
|
||||
)
|
||||
|
||||
// ApprovalInstance 审批实例 yz_approval_instance
|
||||
//
|
||||
// scope 区分租户侧/平台侧。平台侧实例的 biz_tid 标记申请来源租户,
|
||||
// 使平台端能按租户筛选并代租户操作。
|
||||
type ApprovalInstance struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
Scope int8 `orm:"column(scope);default(1)" json:"scope"`
|
||||
Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
|
||||
BizTid uint64 `orm:"column(biz_tid);default(0)" json:"biz_tid"`
|
||||
FlowID uint64 `orm:"column(flow_id);default(0)" json:"flow_id"`
|
||||
BizType string `orm:"column(biz_type);size(50);default()" json:"biz_type"`
|
||||
BizID uint64 `orm:"column(biz_id);default(0)" json:"biz_id"`
|
||||
BizTitle string `orm:"column(biz_title);size(200);default()" json:"biz_title"`
|
||||
SubmitUserID uint64 `orm:"column(submit_user_id);default(0)" json:"submit_user_id"`
|
||||
SubmitUserName string `orm:"column(submit_user_name);size(64);default()" json:"submit_user_name"`
|
||||
SubmitDeptID uint64 `orm:"column(submit_dept_id);default(0)" json:"submit_dept_id"`
|
||||
FormData *string `orm:"column(form_data);type(text);null" json:"form_data"`
|
||||
CurrentNode string `orm:"column(current_node);size(50);default()" json:"current_node"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
SubmitTime *time.Time `orm:"column(submit_time);type(datetime);null" json:"submit_time"`
|
||||
FinishTime *time.Time `orm:"column(finish_time);type(datetime);null" json:"finish_time"`
|
||||
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);type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *ApprovalInstance) TableName() string {
|
||||
return "yz_approval_instance"
|
||||
}
|
||||
|
||||
// ApprovalTask 审批任务 yz_approval_task
|
||||
// 节点级待办:一个节点可展开多条任务(会签时每个审批人一条)。
|
||||
type ApprovalTask struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
InstanceID uint64 `orm:"column(instance_id)" json:"instance_id"`
|
||||
Scope int8 `orm:"column(scope);default(1)" json:"scope"`
|
||||
Tid uint64 `orm:"column(tid);default(0)" json:"tid"`
|
||||
BizType string `orm:"column(biz_type);size(50);default()" json:"biz_type"`
|
||||
BizID uint64 `orm:"column(biz_id);default(0)" json:"biz_id"`
|
||||
NodeKey string `orm:"column(node_key);size(50);default()" json:"node_key"`
|
||||
NodeName string `orm:"column(node_name);size(100);default()" json:"node_name"`
|
||||
Sort int `orm:"column(sort);default(0)" json:"sort"`
|
||||
ApproverID uint64 `orm:"column(approver_id);default(0)" json:"approver_id"`
|
||||
ApproverName string `orm:"column(approver_name);size(64);default()" json:"approver_name"`
|
||||
MultiMode int8 `orm:"column(multi_mode);default(1)" json:"multi_mode"`
|
||||
Status int8 `orm:"column(status);default(0)" json:"status"`
|
||||
Action int8 `orm:"column(action);default(0)" json:"action"`
|
||||
Comment string `orm:"column(comment);size(500);default()" json:"comment"`
|
||||
Deadline *time.Time `orm:"column(deadline);type(datetime);null" json:"deadline"`
|
||||
FinishTime *time.Time `orm:"column(finish_time);type(datetime);null" json:"finish_time"`
|
||||
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);type(datetime);null" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *ApprovalTask) TableName() string {
|
||||
return "yz_approval_task"
|
||||
}
|
||||
@@ -50,6 +50,13 @@ func Init(_ string) {
|
||||
new(BackendErpContact),
|
||||
new(BackendApprovalFlow),
|
||||
new(BackendApprovalRecord),
|
||||
// 自定义审批流(docs/sql/create_approval_flow.sql)
|
||||
new(TenantApprovalFlow),
|
||||
new(TenantApprovalNode),
|
||||
new(PlatformApprovalFlow),
|
||||
new(PlatformApprovalNode),
|
||||
new(ApprovalInstance),
|
||||
new(ApprovalTask),
|
||||
new(BackendReimbursement),
|
||||
new(BackendReimbursementItem),
|
||||
new(BackendReimbursementExpenseType),
|
||||
|
||||
+84
-18
@@ -6,40 +6,106 @@ import (
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// Register 注册移动端 / 开放 API(api)路由。
|
||||
// Register 注册移动端 / 客户端开放 API(api)路由。
|
||||
// 说明:/api 统一承载「所有移动端接口」(uniapp)与「登录器等客户端开放接口」,
|
||||
// 与 /platform、/backend、/index、/auth 各端完全隔离,不复用其它端的功能接口。
|
||||
//
|
||||
// 注意:字面量路由必须注册在 :id 通配之前(beego 按注册顺序匹配)。
|
||||
func Register() {
|
||||
// 平台更新通知列表(无需登录)
|
||||
beego.Router("/platform/api/upgrade/list", &controllers.ApiPlatformUpgradeController{}, "get:List")
|
||||
|
||||
// 平台更新通知详情(无需登录)
|
||||
beego.Router("/platform/api/upgrade/detail", &controllers.ApiPlatformUpgradeController{}, "get:Detail")
|
||||
|
||||
// 客户端检查更新(无需登录)
|
||||
// ===== 客户端登录器 / 开放接口(无需登录)=====
|
||||
// 客户端检查更新
|
||||
beego.Router("/api/softwareupgrade/check", &controllers.ApiSoftwareUpgradeController{}, "get:Check")
|
||||
|
||||
// 登录器上报 Cursor 设备信息(无需登录)
|
||||
// 登录器上报 Cursor 设备信息
|
||||
beego.Router("/api/cursor/equipment/report", &controllers.ApiCursorEquipmentController{}, "post:Report")
|
||||
|
||||
// 登录器使用激活码激活/续期 Cursor 设备(无需登录)
|
||||
// 登录器使用激活码激活/续期 Cursor 设备
|
||||
beego.Router("/api/cursor/equipment/activateByCode", &controllers.ApiCursorEquipmentController{}, "post:ActivateByCode")
|
||||
|
||||
// 登录器心跳接口,用于更新在线状态(无需登录)
|
||||
// 登录器心跳接口,用于更新在线状态
|
||||
beego.Router("/api/cursor/equipment/heartbeat", &controllers.ApiCursorEquipmentController{}, "post:Heartbeat")
|
||||
|
||||
// Cursor Token 顺序读取/检测接口(无需登录,peek 不改变号池状态)
|
||||
// Cursor Token 顺序读取/检测接口(peek 不改变号池状态)
|
||||
// GET /api/cursor/token/peek?id=11&data_type=tk
|
||||
beego.Router("/api/cursor/token/peek", &controllers.ApiCursorDetectController{}, "get:PeekToken")
|
||||
|
||||
// Cursor Token 可用状态标记接口(无需登录)
|
||||
// Cursor Token 可用状态标记接口
|
||||
// POST /api/cursor/token/available?id=11
|
||||
// POST /api/cursor/token/unavailable?id=11
|
||||
beego.Router("/api/cursor/token/available", &controllers.ApiCursorDetectController{}, "post:MarkTokenAvailable")
|
||||
beego.Router("/api/cursor/token/unavailable", &controllers.ApiCursorDetectController{}, "post:MarkTokenUnavailable")
|
||||
|
||||
// 对外提卡接口(无需登录)
|
||||
// 对外提卡接口
|
||||
// GET /api/getcard?type=xianyu&module=cursor&data_type=tk
|
||||
beego.Router("/api/getcard", &controllers.ApiGetCardController{}, "get:GetCard")
|
||||
|
||||
// 日程提醒确认接口(无需登录)
|
||||
// 日程提醒确认接口
|
||||
beego.Router("/api/schedule/reminder/ack", &controllers.ApiReminderController{}, "get:AckReminder")
|
||||
|
||||
// ===== 移动端(uniapp)登录相关(AppAuthController)=====
|
||||
beego.Router("/api/login", &controllers.AppAuthController{}, "post:LoginBackend")
|
||||
beego.Router("/api/sendLoginCode", &controllers.AppAuthController{}, "post:SendLoginCode")
|
||||
beego.Router("/api/loginBySms", &controllers.AppAuthController{}, "post:LoginBySms")
|
||||
beego.Router("/api/logout", &controllers.AppAuthController{}, "post:Logout")
|
||||
|
||||
// 当前用户信息
|
||||
beego.Router("/api/currentUser", &controllers.AppAuthController{}, "get:GetCurrentUser")
|
||||
|
||||
// 极验与登录验证配置
|
||||
beego.Router("/api/login/getGeetest3Infos", &controllers.AppAuthController{}, "get:GetGeetest3Infos")
|
||||
beego.Router("/api/login/getGeetest4Infos", &controllers.AppAuthController{}, "get:GetGeetest4Infos")
|
||||
beego.Router("/api/login/getOpenVerify", &controllers.AppAuthController{}, "get:GetOpenVerify")
|
||||
|
||||
// 找回密码(三步流程)
|
||||
beego.Router("/api/verifyAccount", &controllers.AppAuthController{}, "post:VerifyAccount")
|
||||
beego.Router("/api/sendResetCode", &controllers.AppAuthController{}, "post:SendResetCode")
|
||||
beego.Router("/api/resetPassword", &controllers.AppAuthController{}, "post:ResetPassword")
|
||||
|
||||
// 注册
|
||||
beego.Router("/api/register", &controllers.AppAuthController{}, "post:Register")
|
||||
beego.Router("/api/sendRegisterCode", &controllers.AppAuthController{}, "post:SendRegisterCode")
|
||||
|
||||
// 日程提醒
|
||||
beego.Router("/api/schedule/list", &controllers.AppReminderController{}, "get:GetList")
|
||||
beego.Router("/api/schedule", &controllers.AppReminderController{}, "post:Create")
|
||||
beego.Router("/api/schedule/:id", &controllers.AppReminderController{}, "get:GetDetail;put:Update;delete:Delete")
|
||||
beego.Router("/api/schedule/:id/toggle", &controllers.AppReminderController{}, "post:ToggleComplete")
|
||||
|
||||
// 记事本
|
||||
beego.Router("/api/notebook/list", &controllers.AppNotebookController{}, "get:GetList")
|
||||
beego.Router("/api/notebook", &controllers.AppNotebookController{}, "post:Create")
|
||||
beego.Router("/api/notebook/:id", &controllers.AppNotebookController{}, "get:GetDetail;put:Update;delete:Delete")
|
||||
beego.Router("/api/notebook/:id/togglePin", &controllers.AppNotebookController{}, "post:TogglePin")
|
||||
|
||||
// OA 协同办公(移动端只读:通知公告 / 文档库 / 薪酬工资单 / 员工档案)
|
||||
beego.Router("/api/oa/notice/list", &controllers.AppOaController{}, "get:NoticeList")
|
||||
beego.Router("/api/oa/notice/:id", &controllers.AppOaController{}, "get:NoticeDetail")
|
||||
|
||||
beego.Router("/api/oa/document/categories", &controllers.AppOaController{}, "get:DocCategories")
|
||||
beego.Router("/api/oa/document/list", &controllers.AppOaController{}, "get:DocList")
|
||||
beego.Router("/api/oa/document/download/:id", &controllers.AppOaController{}, "get:DocDownload")
|
||||
beego.Router("/api/oa/document/:id/star", &controllers.AppOaController{}, "post:DocStar")
|
||||
beego.Router("/api/oa/document/:id", &controllers.AppOaController{}, "get:DocDetail")
|
||||
|
||||
beego.Router("/api/oa/compensation/payrolls", &controllers.AppOaController{}, "get:PayrollList")
|
||||
beego.Router("/api/oa/compensation/payrolls/:id", &controllers.AppOaController{}, "get:PayrollDetail")
|
||||
|
||||
beego.Router("/api/oa/employee-file", &controllers.AppOaController{}, "get:MyEmployeeFile")
|
||||
// 员工档案 HR 视角:access 判定是否可看全公司档案,list 全公司档案列表,detail 档案详情
|
||||
beego.Router("/api/oa/employee-file/access", &controllers.AppOaController{}, "get:EmployeeFileAccess")
|
||||
beego.Router("/api/oa/employee-file/list", &controllers.AppOaController{}, "get:EmployeeFileList")
|
||||
beego.Router("/api/oa/employee-file/detail/:id", &controllers.AppOaController{}, "get:EmployeeFileDetail")
|
||||
|
||||
// OA 工作台仪表盘(移动端首屏顶部聚合:待办/报销统计 + 通知公告 + 今日日程)
|
||||
beego.Router("/api/oa/dashboard", &controllers.AppOaController{}, "get:Dashboard")
|
||||
|
||||
// 通讯录(移动端只读:列表 / 组织树 / 详情)
|
||||
beego.Router("/api/oa/contact/list", &controllers.AppOaController{}, "get:ContactList")
|
||||
beego.Router("/api/oa/contact/org-tree", &controllers.AppOaController{}, "get:ContactOrgTree")
|
||||
beego.Router("/api/oa/contact/:id", &controllers.AppOaController{}, "get:ContactDetail")
|
||||
|
||||
// 活动日志
|
||||
beego.Router("/api/activity/list", &controllers.AppActivityController{}, "get:GetList")
|
||||
|
||||
// 工作台聚合(ERP/OA/CRM/CMS 应用门户首屏数据)
|
||||
beego.Router("/api/home", &controllers.AppAggregateController{}, "get:Home")
|
||||
// 移动端用户自定义「常用功能」保存(读取随 /api/home 下发 quick_menu_ids)
|
||||
beego.Router("/api/quick-menus", &controllers.AppAggregateController{}, "post:SaveQuickMenus")
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"server/controllers"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// Register 注册移动端(app)路由。
|
||||
func Register() {
|
||||
// 登录相关(使用 AppAuthController)
|
||||
beego.Router("/app/login", &controllers.AppAuthController{}, "post:LoginBackend")
|
||||
beego.Router("/app/sendLoginCode", &controllers.AppAuthController{}, "post:SendLoginCode")
|
||||
beego.Router("/app/loginBySms", &controllers.AppAuthController{}, "post:LoginBySms")
|
||||
beego.Router("/app/logout", &controllers.AppAuthController{}, "post:Logout")
|
||||
|
||||
// 当前用户信息
|
||||
beego.Router("/app/currentUser", &controllers.AppAuthController{}, "get:GetCurrentUser")
|
||||
|
||||
// 极验与登录验证配置
|
||||
beego.Router("/app/login/getGeetest3Infos", &controllers.AppAuthController{}, "get:GetGeetest3Infos")
|
||||
beego.Router("/app/login/getGeetest4Infos", &controllers.AppAuthController{}, "get:GetGeetest4Infos")
|
||||
beego.Router("/app/login/getOpenVerify", &controllers.AppAuthController{}, "get:GetOpenVerify")
|
||||
|
||||
// 找回密码(三步流程)
|
||||
beego.Router("/app/verifyAccount", &controllers.AppAuthController{}, "post:VerifyAccount")
|
||||
beego.Router("/app/sendResetCode", &controllers.AppAuthController{}, "post:SendResetCode")
|
||||
beego.Router("/app/resetPassword", &controllers.AppAuthController{}, "post:ResetPassword")
|
||||
|
||||
// 注册
|
||||
beego.Router("/app/register", &controllers.AppAuthController{}, "post:Register")
|
||||
beego.Router("/app/sendRegisterCode", &controllers.AppAuthController{}, "post:SendRegisterCode")
|
||||
|
||||
// 日程提醒
|
||||
beego.Router("/app/schedule/list", &controllers.AppReminderController{}, "get:GetList")
|
||||
beego.Router("/app/schedule", &controllers.AppReminderController{}, "post:Create")
|
||||
beego.Router("/app/schedule/:id", &controllers.AppReminderController{}, "get:GetDetail;put:Update;delete:Delete")
|
||||
beego.Router("/app/schedule/:id/toggle", &controllers.AppReminderController{}, "post:ToggleComplete")
|
||||
|
||||
// 记事本
|
||||
beego.Router("/app/notebook/list", &controllers.AppNotebookController{}, "get:GetList")
|
||||
beego.Router("/app/notebook", &controllers.AppNotebookController{}, "post:Create")
|
||||
beego.Router("/app/notebook/:id", &controllers.AppNotebookController{}, "get:GetDetail;put:Update;delete:Delete")
|
||||
beego.Router("/app/notebook/:id/togglePin", &controllers.AppNotebookController{}, "post:TogglePin")
|
||||
|
||||
// OA 协同办公(移动端只读:通知公告 / 文档库 / 薪酬工资单 / 员工档案)
|
||||
// 注意:字面量路由必须注册在 :id 通配之前,beego 按注册顺序匹配。
|
||||
beego.Router("/app/oa/notice/list", &controllers.AppOaController{}, "get:NoticeList")
|
||||
beego.Router("/app/oa/notice/:id", &controllers.AppOaController{}, "get:NoticeDetail")
|
||||
|
||||
beego.Router("/app/oa/document/categories", &controllers.AppOaController{}, "get:DocCategories")
|
||||
beego.Router("/app/oa/document/list", &controllers.AppOaController{}, "get:DocList")
|
||||
beego.Router("/app/oa/document/download/:id", &controllers.AppOaController{}, "get:DocDownload")
|
||||
beego.Router("/app/oa/document/:id/star", &controllers.AppOaController{}, "post:DocStar")
|
||||
beego.Router("/app/oa/document/:id", &controllers.AppOaController{}, "get:DocDetail")
|
||||
|
||||
beego.Router("/app/oa/compensation/payrolls", &controllers.AppOaController{}, "get:PayrollList")
|
||||
beego.Router("/app/oa/compensation/payrolls/:id", &controllers.AppOaController{}, "get:PayrollDetail")
|
||||
|
||||
beego.Router("/app/oa/employee-file", &controllers.AppOaController{}, "get:MyEmployeeFile")
|
||||
|
||||
// OA 工作台仪表盘(移动端首屏顶部聚合:待办/报销统计 + 通知公告 + 今日日程)
|
||||
beego.Router("/app/oa/dashboard", &controllers.AppOaController{}, "get:Dashboard")
|
||||
|
||||
// 通讯录(移动端只读:列表 / 组织树 / 详情)
|
||||
// 同样:字面量路由必须排在 :id 通配之前。
|
||||
beego.Router("/app/oa/contact/list", &controllers.AppOaController{}, "get:ContactList")
|
||||
beego.Router("/app/oa/contact/org-tree", &controllers.AppOaController{}, "get:ContactOrgTree")
|
||||
beego.Router("/app/oa/contact/:id", &controllers.AppOaController{}, "get:ContactDetail")
|
||||
|
||||
// 活动日志
|
||||
beego.Router("/app/activity/list", &controllers.AppActivityController{}, "get:GetList")
|
||||
|
||||
// 工作台聚合(ERP/OA/CRM/CMS 应用门户首屏数据)
|
||||
beego.Router("/app/home", &controllers.AppAggregateController{}, "get:Home")
|
||||
// 移动端用户自定义「常用功能」保存(读取随 /app/home 下发 quick_menu_ids)
|
||||
beego.Router("/app/quick-menus", &controllers.AppAggregateController{}, "post:SaveQuickMenus")
|
||||
}
|
||||
@@ -15,6 +15,18 @@ func Register() {
|
||||
RegisterProductRoutes()
|
||||
RegisterWechatMpRoutes()
|
||||
RegisterDictRoutes()
|
||||
RegisterApprovalRoutes()
|
||||
}
|
||||
|
||||
// RegisterApprovalRoutes 注册租户端「审批流配置」路由。
|
||||
// 数据按登录租户隔离,审批人取本租户用户/角色/部门/职位。
|
||||
func RegisterApprovalRoutes() {
|
||||
beego.Router("/backend/approval/options", &controllers.BackendApprovalController{}, "get:Options")
|
||||
beego.Router("/backend/approval/flow/list", &controllers.BackendApprovalController{}, "get:FlowList")
|
||||
beego.Router("/backend/approval/flow/detail/:id", &controllers.BackendApprovalController{}, "get:FlowDetail")
|
||||
beego.Router("/backend/approval/flow/save", &controllers.BackendApprovalController{}, "post:FlowSave")
|
||||
beego.Router("/backend/approval/flow/delete/:id", &controllers.BackendApprovalController{}, "delete:FlowDelete")
|
||||
beego.Router("/backend/approval/flow/pick", &controllers.BackendApprovalController{}, "get:Pick")
|
||||
}
|
||||
|
||||
// RegisterProductRoutes 注册租户端「产品购买」路由。
|
||||
@@ -228,6 +240,7 @@ func RegisterAuthRoutes() {
|
||||
beego.Router("/backend/friendlinks/:id", &controllers.BackendFriendlinkController{}, "put:Update;delete:Delete")
|
||||
|
||||
beego.Router("/backend/allonepages", &controllers.BackendOnepageController{}, "get:ListAll")
|
||||
beego.Router("/backend/onepage/path/:path", &controllers.BackendOnepageController{}, "get:GetOnePageByPath")
|
||||
beego.Router("/backend/createonepage", &controllers.BackendOnepageController{}, "post:Create")
|
||||
beego.Router("/backend/editonepage/:id", &controllers.BackendOnepageController{}, "post:Update")
|
||||
beego.Router("/backend/deleteonepage/:id", &controllers.BackendOnepageController{}, "delete:Delete")
|
||||
@@ -251,7 +264,6 @@ func RegisterAuthRoutes() {
|
||||
beego.Router("/backend/domain/tenant/bindCustom", &controllers.BackendTenantDomainController{}, "post:BindCustom")
|
||||
beego.Router("/backend/domain/tenant/checkCustomDns", &controllers.BackendTenantDomainController{}, "post:CheckCustomDns")
|
||||
beego.Router("/backend/domain/tenant/customDomainGuide", &controllers.BackendTenantDomainController{}, "get:CustomDomainGuide")
|
||||
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")
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ func Register() {
|
||||
// 官网页头导航菜单(平台「官网管理 - 导航菜单」配置,仅返回启用且显示的项)
|
||||
beego.Router("/site/nav", &controllers.IndexWebsiteController{}, "get:Nav")
|
||||
|
||||
// 官网端平台更新公告(公开读取,与平台端 /platform/upgrade 各自独立)
|
||||
beego.Router("/site/upgrade/list", &controllers.IndexUpgradeNoticeController{}, "get:List")
|
||||
|
||||
// SPA 官网内容接口(读取平台官网管理 yz_platform_website_*,仅返回已发布内容)
|
||||
// 注意:静态段必须在 :id 之前注册,保证 /site/news/categories 不被当作详情
|
||||
beego.Router("/site/news", &controllers.IndexWebsiteController{}, "get:News")
|
||||
|
||||
@@ -64,6 +64,10 @@ func Register() {
|
||||
beego.Router("/platform/resetPassword", &controllers.PlatformAuthController{}, "post:ResetPassword")
|
||||
beego.Router("/platform/sendResetCode", &controllers.PlatformAuthController{}, "post:SendResetCode")
|
||||
|
||||
// 租户自助注册(平台端独立接口,与后端 /backend 不复用)
|
||||
beego.Router("/platform/register", &controllers.PlatformAuthController{}, "post:Register")
|
||||
beego.Router("/platform/sendRegisterCode", &controllers.PlatformAuthController{}, "post:SendRegisterCode")
|
||||
|
||||
// 平台菜单配置相关
|
||||
beego.Router("/platform/menu/:id", &controllers.AdminMenuController{}, "get:GetMenu")
|
||||
beego.Router("/platform/allmenu", &controllers.AdminMenuController{}, "get:GetAllMenus")
|
||||
@@ -162,9 +166,19 @@ func Register() {
|
||||
beego.Router("/platform/domain/tenant/checkCustomDns", &controllers.PlatformTenantDomainController{}, "post:CheckCustomDns")
|
||||
beego.Router("/platform/domain/tenant/customDomainGuide", &controllers.PlatformTenantDomainController{}, "get:CustomDomainGuide")
|
||||
beego.Router("/platform/domain/tenant/audit", &controllers.PlatformTenantDomainController{}, "post:Audit")
|
||||
beego.Router("/platform/domain/tenant/audit/progress/:id", &controllers.PlatformTenantDomainController{}, "get:AuditProgress")
|
||||
beego.Router("/platform/domain/tenant/toggleStatus", &controllers.PlatformTenantDomainController{}, "post:ToggleStatus")
|
||||
beego.Router("/platform/domain/tenant/delete/:id", &controllers.PlatformTenantDomainController{}, "delete:Delete")
|
||||
|
||||
// 审批流配置(平台侧:域名申请等管控场景)
|
||||
beego.Router("/platform/approval/flow/list", &controllers.PlatformApprovalController{}, "get:FlowList")
|
||||
beego.Router("/platform/approval/flow/detail/:id", &controllers.PlatformApprovalController{}, "get:FlowDetail")
|
||||
beego.Router("/platform/approval/flow/save", &controllers.PlatformApprovalController{}, "post:FlowSave")
|
||||
beego.Router("/platform/approval/flow/delete/:id", &controllers.PlatformApprovalController{}, "delete:FlowDelete")
|
||||
beego.Router("/platform/approval/options", &controllers.PlatformApprovalController{}, "get:Options")
|
||||
beego.Router("/platform/approval/biz-types", &controllers.PlatformApprovalController{}, "get:BizTypes")
|
||||
beego.Router("/platform/approval/flow/pick", &controllers.PlatformApprovalController{}, "get:Pick")
|
||||
|
||||
// 模块管理(yz_system_modules)
|
||||
beego.Router("/platform/modules/list", &controllers.PlatformModulesController{}, "get:GetList")
|
||||
beego.Router("/platform/modules/getTenantList", &controllers.PlatformModulesController{}, "get:GetTenantList")
|
||||
@@ -193,6 +207,10 @@ func Register() {
|
||||
beego.Router("/platform/softwareupgrade", &controllers.PlatformSoftwareUpgradeController{}, "post:Create")
|
||||
beego.Router("/platform/softwareupgrade/:id", &controllers.PlatformSoftwareUpgradeController{}, "get:Detail;post:Update;delete:Delete")
|
||||
|
||||
// 平台更新公告(yz_platform_upgrade_notice,平台端独立接口)
|
||||
beego.Router("/platform/upgrade/list", &controllers.PlatformUpgradeNoticeController{}, "get:List")
|
||||
beego.Router("/platform/upgrade/detail", &controllers.PlatformUpgradeNoticeController{}, "get:Detail")
|
||||
|
||||
// 租户站点设置(yz_tenant_site_setting)
|
||||
beego.Router("/platform/normalInfos", &controllers.PlatformSiteSettingsController{}, "get:GetNormalInfos")
|
||||
beego.Router("/platform/saveNormalInfos", &controllers.PlatformSiteSettingsController{}, "post:SaveNormalInfos")
|
||||
@@ -426,6 +444,7 @@ func Register() {
|
||||
|
||||
// 单页管理 yz_platform_website_page
|
||||
beego.Router("/platform/website/page/all", &controllers.PlatformWebsitePageController{}, "get:All")
|
||||
beego.Router("/platform/website/page/path/:path", &controllers.PlatformWebsitePageController{}, "get:GetByPath")
|
||||
beego.Router("/platform/website/page", &controllers.PlatformWebsitePageController{}, "post:Create")
|
||||
beego.Router("/platform/website/page/:id", &controllers.PlatformWebsitePageController{}, "get:Get;post:Update;delete:Delete")
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"server/controllers"
|
||||
"server/middleware"
|
||||
"server/routers/api"
|
||||
"server/routers/app"
|
||||
authrouter "server/routers/auth"
|
||||
"server/routers/backend"
|
||||
"server/routers/index"
|
||||
@@ -67,28 +66,25 @@ func init() {
|
||||
switch mode {
|
||||
case "platform":
|
||||
platform.Register()
|
||||
app.Register()
|
||||
api.Register()
|
||||
// 在 platform 模式下,仍保留 backend 登录相关路由,避免后台登录 404
|
||||
backend.RegisterAuthRoutes()
|
||||
case "backend":
|
||||
backend.Register()
|
||||
app.Register()
|
||||
api.Register()
|
||||
case "index":
|
||||
index.Register()
|
||||
app.Register()
|
||||
api.Register()
|
||||
case "api":
|
||||
api.Register()
|
||||
case "app":
|
||||
app.Register()
|
||||
case "all":
|
||||
platform.Register()
|
||||
backend.Register()
|
||||
index.Register()
|
||||
api.Register()
|
||||
app.Register()
|
||||
default:
|
||||
// 未知模式时,退回到只启用平台端,避免启动失败
|
||||
platform.Register()
|
||||
app.Register()
|
||||
api.Register()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user