package controllers import ( "encoding/csv" "encoding/json" "fmt" "io" "strconv" "strings" "time" "server/models" "github.com/beego/beego/v2/client/orm" ) // 本文件承载组织架构模块的设置读写、CSV 导入导出,以及 DTO 组装与通用工具函数。 // --------------------------------------------------------------------------- // 组织架构设置(按租户存放在 yz_backend_normal_setting 中) // --------------------------------------------------------------------------- // GetOrgSettings 获取当前租户的组织架构设置。 // GET /backend/{erp|oa}/getOrgSettings func (c *BackendOrganizationController) GetOrgSettings() { tid, ok := c.tenantID() if !ok { return } c.jsonOK(c.loadOrgSettings(tid)) } // SaveOrgSettings 保存当前租户的组织架构设置。 // POST /backend/{erp|oa}/saveOrgSettings func (c *BackendOrganizationController) SaveOrgSettings() { tid, ok := c.tenantID() if !ok { return } body := c.parseJSONBody() settings := c.loadOrgSettings(tid) if v, has := c.getStringValue(body, "org_code_prefix", "code_prefix"); has { settings.OrgCodePrefix = strings.TrimSpace(v) } if v, has := c.getStringValue(body, "employee_code_prefix"); has { settings.EmployeeCodePrefix = strings.TrimSpace(v) } if v, has := c.getStringValue(body, "position_code_prefix"); has { settings.PositionCodePrefix = strings.TrimSpace(v) } if v, has := c.getBoolValue(body, "auto_generate_codes", "auto_generate_code"); has { settings.AutoGenerateCodes = v } if v, has := c.getIntValue(body, "code_length"); has { settings.CodeLength = clampInt(v, 4, 32) } if v, has := c.getIntValue(body, "default_org_type"); has { settings.DefaultOrgType = v } if v, has := c.getIntValue(body, "default_sort"); has { settings.DefaultSort = maxInt(v, 0) } if v, has := c.getIntValue(body, "default_status"); has { settings.DefaultStatus = v } if v, has := c.getIntValue(body, "max_org_levels", "max_level"); has { settings.MaxOrgLevels = clampInt(v, 1, 32) } if v, has := c.getIntValue(body, "max_org_children"); has { settings.MaxOrgChildren = clampInt(v, 1, 1000) } if v, has := c.getBoolValue(body, "allow_duplicate_codes"); has { settings.AllowDuplicateCode = v } if v, has := c.getBoolValue(body, "batch_operations"); has { settings.BatchOperations = v } if v, has := c.getBoolValue(body, "export_enabled"); has { settings.ExportEnabled = v } if v, has := c.getBoolValue(body, "import_enabled"); has { settings.ImportEnabled = v } if err := c.persistOrgSettings(tid, settings); err != nil { c.jsonError(500, "保存组织设置失败: "+err.Error()) return } c.jsonOK(settings) } func (c *BackendOrganizationController) orgSettingsCode(tid uint64) string { return fmt.Sprintf("%s_%d", orgSettingsCodePrefix, tid) } // loadOrgSettings 读取租户设置;无记录或解析失败时回退到默认值,保证接口始终可用。 func (c *BackendOrganizationController) loadOrgSettings(tid uint64) orgSettings { settings := defaultOrgSettings() var row models.BackendNormalSetting err := models.Orm.QueryTable(new(models.BackendNormalSetting)). Filter("code", c.orgSettingsCode(tid)). Filter("delete_time__isnull", true). One(&row) if err != nil || strings.TrimSpace(row.Value) == "" { return settings } if err := json.Unmarshal([]byte(row.Value), &settings); err != nil { return defaultOrgSettings() } if settings.CodeLength <= 0 { settings.CodeLength = 8 } return settings } func (c *BackendOrganizationController) persistOrgSettings(tid uint64, settings orgSettings) error { raw, err := json.Marshal(settings) if err != nil { return err } code := c.orgSettingsCode(tid) var row models.BackendNormalSetting err = models.Orm.QueryTable(new(models.BackendNormalSetting)). Filter("code", code). Filter("delete_time__isnull", true). One(&row) if err == nil { now := time.Now() row.Value = string(raw) row.UpdateTime = &now _, err = models.Orm.Update(&row, "value", "update_time") return err } row = models.BackendNormalSetting{ Name: "组织架构设置", Code: code, Value: string(raw), Remark: fmt.Sprintf("租户 %d 的组织架构设置", tid), } _, err = models.Orm.Insert(&row) return err } // --------------------------------------------------------------------------- // 导入 / 导出(CSV,带 UTF-8 BOM,Excel 可直接打开) // --------------------------------------------------------------------------- var organizationExportHeader = []string{ "组织编码", "组织名称", "上级组织编码", "是否公司(1是0否)", "排序", "状态(1启用0禁用)", "备注", } // ExportOrganization 导出当前租户组织架构为 CSV。 // GET /backend/{erp|oa}/exportOrganization func (c *BackendOrganizationController) ExportOrganization() { tid, ok := c.tenantID() if !ok { return } if !c.loadOrgSettings(tid).ExportEnabled { c.jsonError(400, "导出功能已关闭") return } var rows []models.BackendOrganization if _, err := c.orgQuery(tid).OrderBy("sort", "id").All(&rows); err != nil { c.jsonError(500, "导出组织架构失败: "+err.Error()) return } codeByID := map[uint64]string{} for _, row := range rows { codeByID[row.ID] = row.OrgCode } c.Ctx.Output.Header("Content-Type", "text/csv; charset=utf-8") c.Ctx.Output.Header("Content-Disposition", fmt.Sprintf("attachment; filename=organization_%s.csv", time.Now().Format("20060102150405"))) // UTF-8 BOM,避免 Excel 打开中文乱码 _, _ = c.Ctx.ResponseWriter.Write([]byte{0xEF, 0xBB, 0xBF}) writer := csv.NewWriter(c.Ctx.ResponseWriter) _ = writer.Write(organizationExportHeader) for _, row := range rows { _ = writer.Write([]string{ row.OrgCode, row.OrgName, codeByID[row.ParentID], strconv.Itoa(row.IsCompany), strconv.FormatUint(uint64(row.Sort), 10), strconv.Itoa(int(row.Status)), derefString(row.Remark), }) } writer.Flush() } // ImportOrganization 从 CSV 导入组织架构。 // 已存在的组织编码执行更新,不存在的新增;上级组织通过编码关联, // 上级关系在所有行入库后统一回填,因此 CSV 行序不影响结果。 // POST /backend/{erp|oa}/importOrganization (multipart/form-data, field=file) func (c *BackendOrganizationController) ImportOrganization() { tid, ok := c.tenantID() if !ok { return } if !c.loadOrgSettings(tid).ImportEnabled { c.jsonError(400, "导入功能已关闭") return } file, _, err := c.GetFile("file") if err != nil { c.jsonError(400, "请上传 CSV 文件") return } defer file.Close() reader := csv.NewReader(newBOMTrimReader(file)) reader.FieldsPerRecord = -1 records, err := reader.ReadAll() if err != nil { c.jsonError(400, "解析 CSV 失败: "+err.Error()) return } if len(records) <= 1 { c.jsonError(400, "CSV 中没有可导入的数据") return } idByCode := map[string]uint64{} var existing []models.BackendOrganization if _, err := c.orgQuery(tid).All(&existing); err != nil { c.jsonError(500, "读取已有组织失败: "+err.Error()) return } for _, row := range existing { idByCode[row.OrgCode] = row.ID } type pendingParent struct { code string parentCode string } created, updated := 0, 0 failures := make([]string, 0) pending := make([]pendingParent, 0, len(records)) for i, record := range records[1:] { lineNo := i + 2 if len(record) < 2 { failures = append(failures, fmt.Sprintf("第 %d 行:列数不足", lineNo)) continue } orgCode := strings.TrimSpace(record[0]) orgName := strings.TrimSpace(record[1]) if orgCode == "" || orgName == "" { failures = append(failures, fmt.Sprintf("第 %d 行:组织编码与名称不能为空", lineNo)) continue } parentCode := csvField(record, 2) isCompany := csvInt(record, 3, 0) sortVal := csvInt(record, 4, 0) status := csvInt(record, 5, 1) remark := csvField(record, 6) if id, exists := idByCode[orgCode]; exists { update := orm.Params{ "org_name": orgName, "is_company": isCompany, "sort": uint(maxInt(sortVal, 0)), "status": int8(status), "remark": nullableString(remark), } if _, err := c.orgQuery(tid).Filter("id", id).Update(update); err != nil { failures = append(failures, fmt.Sprintf("第 %d 行:更新失败 %s", lineNo, err.Error())) continue } updated++ } else { row := models.BackendOrganization{ Tid: tid, OrgName: orgName, OrgCode: orgCode, IsCompany: isCompany, Sort: uint(maxInt(sortVal, 0)), Status: int8(status), Remark: strPtrIfNotEmpty(remark), } id, err := models.Orm.Insert(&row) if err != nil { failures = append(failures, fmt.Sprintf("第 %d 行:创建失败 %s", lineNo, err.Error())) continue } idByCode[orgCode] = uint64(id) created++ } pending = append(pending, pendingParent{code: orgCode, parentCode: parentCode}) } for _, item := range pending { selfID := idByCode[item.code] if selfID == 0 { continue } parentID := uint64(0) if item.parentCode != "" { parentID = idByCode[item.parentCode] if parentID == 0 { failures = append(failures, fmt.Sprintf("组织 %s:上级编码 %s 不存在", item.code, item.parentCode)) continue } if parentID == selfID { failures = append(failures, fmt.Sprintf("组织 %s:上级不能是自己", item.code)) continue } } _, _ = c.orgQuery(tid).Filter("id", selfID). Update(orm.Params{"parent_id": parentID, "is_company": boolInt(parentID == 0)}) } c.jsonOK(map[string]interface{}{ "created": created, "updated": updated, "failed": len(failures), "failures": failures, }) } // GetImportTemplate 下载导入模板(仅表头 + 一行示例)。 // GET /backend/{erp|oa}/organizationImportTemplate func (c *BackendOrganizationController) GetImportTemplate() { if _, ok := c.tenantID(); !ok { return } c.Ctx.Output.Header("Content-Type", "text/csv; charset=utf-8") c.Ctx.Output.Header("Content-Disposition", "attachment; filename=organization_template.csv") _, _ = c.Ctx.ResponseWriter.Write([]byte{0xEF, 0xBB, 0xBF}) writer := csv.NewWriter(c.Ctx.ResponseWriter) _ = writer.Write(organizationExportHeader) _ = writer.Write([]string{"COM001", "示例总公司", "", "1", "0", "1", "顶级组织,上级编码留空"}) _ = writer.Write([]string{"DEP001", "示例研发部", "COM001", "0", "1", "1", "隶属 COM001"}) writer.Flush() } // bomTrimReader 去掉 CSV 文件开头可能存在的 UTF-8 BOM。 type bomTrimReader struct { reader io.Reader checked bool buf []byte } func newBOMTrimReader(r io.Reader) io.Reader { return &bomTrimReader{reader: r} } func (r *bomTrimReader) Read(p []byte) (int, error) { if !r.checked { r.checked = true head := make([]byte, 3) n, err := io.ReadFull(r.reader, head) if n == 3 && head[0] == 0xEF && head[1] == 0xBB && head[2] == 0xBF { r.buf = nil } else { r.buf = head[:n] } if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF { return 0, err } } if len(r.buf) > 0 { n := copy(p, r.buf) r.buf = r.buf[n:] return n, nil } return r.reader.Read(p) } func csvField(record []string, index int) string { if index >= len(record) { return "" } return strings.TrimSpace(record[index]) } func csvInt(record []string, index int, fallback int) int { raw := csvField(record, index) if raw == "" { return fallback } v, err := strconv.Atoi(raw) if err != nil { return fallback } return v }