package controllers import ( "encoding/json" "io" "strconv" "strings" "time" "server/models" "github.com/beego/beego/v2/client/orm" beego "github.com/beego/beego/v2/server/web" ) // BackendCrmUnitController CRM 基本设置-计量单位(产品等单位下拉取自该表) type BackendCrmUnitController struct { beego.Controller } type crmUnitPayload struct { Name string `json:"name"` Code string `json:"code"` Sort int `json:"sort"` Status int8 `json:"status"` Remark string `json:"remark"` } // crmUnitItem 列表项:单位信息 + 引用数(引用该单位的产品数量) type crmUnitItem struct { models.TenantCrmUnit UsedCount int64 `json:"used_count"` } // crmUnitUsageRow 单位引用统计(按产品表 unit 分组计数) type crmUnitUsageRow struct { Unit string `orm:"column(unit)"` Cnt int64 `orm:"column(cnt)"` } // crmDefaultUnits 默认单位:租户首次使用单位设置时自动写入(顺序即排序) var crmDefaultUnits = []struct { Name string Code string }{ {"套", "SET"}, {"台", "TAI"}, {"个", "PCS"}, {"件", "PIECE"}, {"批", "BATCH"}, {"次", "TIME"}, {"项", "ITEM"}, {"人月", "PM"}, {"人天", "PD"}, {"年", "YEAR"}, {"月", "MONTH"}, {"米", "M"}, {"平方米", "SQM"}, {"吨", "TON"}, {"千克", "KG"}, {"箱", "BOX"}, } // crmEnsureDefaultUnits 租户首次使用(从未有过任何单位记录,含已软删除的)时写入默认单位。 // 用户手动清空后不再自动补,避免越删越有;需要恢复时用「恢复默认单位」接口。 // 已有记录时顺带修复存量默认单位的 is_default 标记(历史数据升级)。 func crmEnsureDefaultUnits(tenantID string) { if models.Orm == nil || strings.TrimSpace(tenantID) == "" { return } cnt, err := models.Orm.QueryTable(new(models.TenantCrmUnit)). Filter("tenant_id", tenantID).Count() if err != nil { return } if cnt == 0 { crmInsertDefaultUnits(tenantID, nil) return } crmMarkDefaultUnits(tenantID) } // crmMarkDefaultUnits 将默认清单中已存在的存量单位标记为 is_default=1(历史数据升级自修复, // 避免早期写入的默认单位因未打标而被误编辑/删除)。 func crmMarkDefaultUnits(tenantID string) { names := make([]string, 0, len(crmDefaultUnits)) for _, u := range crmDefaultUnits { names = append(names, u.Name) } if len(names) == 0 { return } _, _ = models.Orm.QueryTable(new(models.TenantCrmUnit)). Filter("tenant_id", tenantID). Filter("name__in", names). Update(orm.Params{"is_default": 1}) } // crmInsertDefaultUnits 写入默认单位,跳过已存在的名称(existing 为已存在单位名集合)。 // 返回新增数量。 func crmInsertDefaultUnits(tenantID string, existing map[string]bool) int { now := time.Now() added := 0 for i, u := range crmDefaultUnits { if existing != nil && existing[u.Name] { continue } row := models.TenantCrmUnit{ TenantID: tenantID, Name: u.Name, Code: u.Code, Sort: i + 1, Status: 1, IsDefault: 1, // 系统默认单位:不可编辑、不可删除 CreateTime: now, UpdateTime: now, } if _, err := models.Orm.Insert(&row); err == nil { added++ } } return added } // crmInitDefaultUnits 补充缺失的默认单位: // - 已存在(启用/停用)的单位不动; // - 已软删除的同名单位直接恢复(清 delete_time)并置为启用。 // // 返回本次新增/恢复的数量。 func crmInitDefaultUnits(tenantID string) int { var rows []models.TenantCrmUnit _, _ = models.Orm.QueryTable(new(models.TenantCrmUnit)). Filter("tenant_id", tenantID).All(&rows) existing := make(map[string]bool, len(rows)) deleted := make(map[string]uint64, len(rows)) for _, r := range rows { if r.DeleteTime != nil { deleted[r.Name] = r.ID continue } existing[r.Name] = true } now := time.Now() table := new(models.TenantCrmUnit).TableName() added := 0 for i, u := range crmDefaultUnits { if existing[u.Name] { continue } if id, ok := deleted[u.Name]; ok { if _, err := models.Orm.Raw("UPDATE "+table+ " SET delete_time = NULL, status = 1, is_default = 1, sort = ?, update_time = ? WHERE id = ?", i+1, now, id).Exec(); err == nil { existing[u.Name] = true added++ } continue } row := models.TenantCrmUnit{ TenantID: tenantID, Name: u.Name, Code: u.Code, Sort: i + 1, Status: 1, IsDefault: 1, // 系统默认单位:不可编辑、不可删除 CreateTime: now, UpdateTime: now, } if _, err := models.Orm.Insert(&row); err == nil { existing[u.Name] = true added++ } } return added } // crmUnitUsage 统计各单位的引用数(引用该单位且未删除的产品数量)。 func crmUnitUsage(tenantID string) map[string]int64 { usage := make(map[string]int64, len(crmDefaultUnits)) if models.Orm == nil { return usage } var rows []crmUnitUsageRow _, err := models.Orm.Raw("SELECT unit, COUNT(*) AS cnt FROM "+ new(models.TenantCrmProduct).TableName()+ " WHERE tenant_id = ? AND delete_time IS NULL AND unit <> '' GROUP BY unit", tenantID).QueryRows(&rows) if err != nil { return usage } for _, r := range rows { usage[strings.TrimSpace(r.Unit)] = r.Cnt } return usage } // List GET /backend/crm/unit/list func (c *BackendCrmUnitController) List() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } page, _ := c.GetInt("page", 1) pageSize, _ := c.GetInt("pageSize", 20) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 200 { pageSize = 20 } keyword := strings.TrimSpace(c.GetString("keyword")) status := strings.TrimSpace(c.GetString("status")) tenantID := pipelineTenantID(claims) crmEnsureDefaultUnits(tenantID) cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true) if keyword != "" { kw := orm.NewCondition(). Or("name__contains", keyword). Or("code__contains", keyword) cond = cond.AndCond(kw) } if status != "" { cond = cond.And("status", status) } qs := models.Orm.QueryTable(new(models.TenantCrmUnit)).SetCond(cond) total, _ := qs.Count() items := make([]crmUnitItem, 0, 8) if total > 0 { usage := crmUnitUsage(tenantID) var list []models.TenantCrmUnit _, _ = qs.OrderBy("sort", "id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list) for _, row := range list { items = append(items, crmUnitItem{TenantCrmUnit: row, UsedCount: usage[row.Name]}) } } pipelineOk(&c.Controller, map[string]interface{}{ "list": items, "total": total, "page": page, "pageSize": pageSize, }) } // All GET /backend/crm/unit/all —— 下拉数据源:仅返回启用中的单位 func (c *BackendCrmUnitController) All() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } tenantID := pipelineTenantID(claims) crmEnsureDefaultUnits(tenantID) var list []models.TenantCrmUnit _, _ = models.Orm.QueryTable(new(models.TenantCrmUnit)). Filter("tenant_id", tenantID). Filter("delete_time__isnull", true). Filter("status", 1). OrderBy("sort", "id").Limit(200).All(&list) pipelineOk(&c.Controller, map[string]interface{}{"list": list}) } // Detail GET /backend/crm/unit/:id func (c *BackendCrmUnitController) Detail() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) if id == 0 { pipelineErr(&c.Controller, 400, 400, "无效的ID") return } var row models.TenantCrmUnit if err := models.Orm.QueryTable(new(models.TenantCrmUnit)). Filter("id", id).Filter("tenant_id", pipelineTenantID(claims)). Filter("delete_time__isnull", true).One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "单位未找到") return } pipelineOk(&c.Controller, row) } // Create POST /backend/crm/unit func (c *BackendCrmUnitController) Create() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } raw, _ := io.ReadAll(c.Ctx.Request.Body) var p crmUnitPayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } name := strings.TrimSpace(p.Name) if name == "" { pipelineErr(&c.Controller, 400, 400, "单位名称不能为空") return } status := p.Status if status != 0 && status != 1 { status = 1 } tenantID := pipelineTenantID(claims) now := time.Now() // 同租户下单位名称唯一:命中已存在的记录时,若已软删除则「复活」复用,避免唯一索引冲突 var old models.TenantCrmUnit if err := models.Orm.QueryTable(new(models.TenantCrmUnit)). Filter("tenant_id", tenantID).Filter("name", name).One(&old); err == nil { if old.DeleteTime == nil { pipelineErr(&c.Controller, 400, 400, "单位名称已存在") return } if _, err := models.Orm.Raw("UPDATE "+new(models.TenantCrmUnit).TableName()+ " SET code = ?, sort = ?, status = ?, remark = ?, update_time = ?, delete_time = NULL WHERE id = ?", strings.TrimSpace(p.Code), p.Sort, status, strings.TrimSpace(p.Remark), now, old.ID).Exec(); err != nil { pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) return } pipelineOk(&c.Controller, map[string]interface{}{"id": old.ID}) return } row := models.TenantCrmUnit{ TenantID: tenantID, Name: name, Code: strings.TrimSpace(p.Code), Sort: p.Sort, Status: status, Remark: strings.TrimSpace(p.Remark), CreateUserID: pipelineUID(claims), CreateTime: now, UpdateTime: now, } id, err := models.Orm.Insert(&row) if err != nil { if strings.Contains(err.Error(), "uk_tenant_name") { pipelineErr(&c.Controller, 400, 400, "单位名称已存在") return } pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) return } pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Update PUT /backend/crm/unit/:id func (c *BackendCrmUnitController) Update() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) if id == 0 { pipelineErr(&c.Controller, 400, 400, "无效的ID") return } raw, _ := io.ReadAll(c.Ctx.Request.Body) var p crmUnitPayload if err := json.Unmarshal(raw, &p); err != nil { pipelineErr(&c.Controller, 400, 400, "参数错误") return } name := strings.TrimSpace(p.Name) if name == "" { pipelineErr(&c.Controller, 400, 400, "单位名称不能为空") return } tenantID := pipelineTenantID(claims) var row models.TenantCrmUnit if err := models.Orm.QueryTable(new(models.TenantCrmUnit)). Filter("id", id).Filter("tenant_id", tenantID). Filter("delete_time__isnull", true).One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "单位未找到") return } // 系统默认单位只读:不可编辑 if row.IsDefault == 1 { pipelineErr(&c.Controller, 400, 400, "系统默认单位不可编辑") return } status := p.Status if status != 0 && status != 1 { status = row.Status } // 名称重复校验:与他人在用的单位冲突直接拒绝;命中的是历史软删除记录则彻底清理后继续 var dup models.TenantCrmUnit if err := models.Orm.QueryTable(new(models.TenantCrmUnit)). Filter("tenant_id", tenantID).Filter("name", name).Exclude("id", id).One(&dup); err == nil { if dup.DeleteTime == nil { pipelineErr(&c.Controller, 400, 400, "单位名称已存在") return } _, _ = models.Orm.QueryTable(new(models.TenantCrmUnit)).Filter("id", dup.ID).Delete() } now := time.Now() _, err = models.Orm.QueryTable(new(models.TenantCrmUnit)). Filter("id", id).Filter("tenant_id", tenantID). Update(orm.Params{ "name": name, "code": strings.TrimSpace(p.Code), "sort": p.Sort, "status": status, "remark": strings.TrimSpace(p.Remark), "update_time": now, }) if err != nil { if strings.Contains(err.Error(), "uk_tenant_name") { pipelineErr(&c.Controller, 400, 400, "单位名称已存在") return } pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error()) return } pipelineOk(&c.Controller, map[string]interface{}{"id": id}) } // Delete DELETE /backend/crm/unit/:id func (c *BackendCrmUnitController) Delete() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) if id == 0 { pipelineErr(&c.Controller, 400, 400, "无效的ID") return } tenantID := pipelineTenantID(claims) var row models.TenantCrmUnit if err := models.Orm.QueryTable(new(models.TenantCrmUnit)). Filter("id", id).Filter("tenant_id", tenantID). Filter("delete_time__isnull", true).One(&row); err != nil { pipelineErr(&c.Controller, 404, 404, "单位未找到") return } // 系统默认单位只读:不可删除 if row.IsDefault == 1 { pipelineErr(&c.Controller, 400, 400, "系统默认单位不可删除") return } now := time.Now() _, err = models.Orm.QueryTable(new(models.TenantCrmUnit)). Filter("id", id).Filter("tenant_id", tenantID). Update(orm.Params{"delete_time": now, "update_time": now}) if err != nil { pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) return } pipelineOk(&c.Controller, nil) } // InitDefault POST /backend/crm/unit/init-default —— 补齐缺失的系统默认单位 func (c *BackendCrmUnitController) InitDefault() { claims, err := pipelineClaims(&c.Controller) if err != nil { pipelineErr(&c.Controller, 401, 401, err.Error()) return } added := crmInitDefaultUnits(pipelineTenantID(claims)) pipelineOk(&c.Controller, map[string]interface{}{"added": added}) }