Files
yunzerwebsiteallinone/go/controllers/backend_crm_unit.go
T
2026-09-15 10:44:31 +08:00

474 lines
14 KiB
Go

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 租户首次使用(从未有过任何单位记录,含已软删除的)时写入默认单位。
// 【租户级初始化】新租户首次使用时补建默认业务数据(非表结构/全局预置数据;结构变更须走 docs/sql/ SQL 文件)。
// 用户手动清空后不再自动补,避免越删越有;需要恢复时用「恢复默认单位」接口。
// 已有记录时顺带修复存量默认单位的 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})
}