批量优化产品和产品分类

This commit is contained in:
2026-09-12 00:44:13 +08:00
parent 184c604bee
commit 2b1874943f
33 changed files with 2595 additions and 59 deletions
+25 -6
View File
@@ -81,6 +81,7 @@ func (c *BackendCrmContractController) List() {
ourRole := strings.TrimSpace(c.GetString("our_role"))
category := strings.TrimSpace(c.GetString("contract_category"))
projectType := strings.TrimSpace(c.GetString("project_type"))
projectID := strings.TrimSpace(c.GetString("project_id"))
status := strings.TrimSpace(c.GetString("status"))
tenantID := pipelineTenantID(claims)
@@ -110,6 +111,12 @@ func (c *BackendCrmContractController) List() {
case "headless": // 无头合同
cond = cond.And("project_id__isnull", true)
}
// 按项目精确筛选(项目详情「合同管理」Tab:一个项目可关联多份合同)
if projectID != "" {
if pid, err := strconv.ParseUint(projectID, 10, 64); err == nil && pid > 0 {
cond = cond.And("project_id", pid)
}
}
qs := models.Orm.QueryTable(new(models.TenantCrmContract)).SetCond(cond)
total, _ := qs.Count()
@@ -244,6 +251,10 @@ func (c *BackendCrmContractController) Create() {
return
}
row.Products = products
// 合同产品清单同步到产品管理(新增产品自动建档、成本单价取产品管理)
if synced, serr := SyncContractProducts(tenantID, pipelineUID(claims), json.RawMessage(products)); serr == nil {
row.Products = string(synced)
}
applyContractAmounts(&row)
if _, err := models.Orm.Insert(&row); err != nil {
@@ -336,6 +347,10 @@ func (c *BackendCrmContractController) Update() {
return
}
row.Products = products
// 合同产品清单同步到产品管理(新增产品自动建档、成本单价取产品管理)
if synced, serr := SyncContractProducts(tenantID, pipelineUID(claims), json.RawMessage(products)); serr == nil {
row.Products = string(synced)
}
}
applyContractAmounts(&row)
if p.Status != 0 {
@@ -512,8 +527,11 @@ func normalizeContractJSON(raw json.RawMessage) (string, error) {
}
// applyContractAmounts 按产品清单重算各部分金额(与前端 ProductList 汇总口径一致):
// 硬件部分=Σ硬件小计;软件部分=Σ软件小计;其他部分=Σ服务/开发/其他小计;
// 合同总金额=硬件+软件+其他;产品总成本=Σ(数量×成本单价);合同总利润=总金额-总成本。
// 每行含税金额=数量×单价×(1+税率/100);
// 硬件部分=Σ硬件含税小计;软件部分=Σ软件含税小计;其他部分=Σ服务/开发/其他含税小计;
// 合同总金额(含税)=硬件+软件+其他;产品总成本=Σ(数量×成本单价)(成本通常不含税,原值汇总);
// 合同总利润=总金额-总成本。
// 归类口径:分类代码 1/2 或名称含「硬件」/「软件」分别计入硬件/软件,其余计入其他。
func applyContractAmounts(row *models.TenantCrmContract) {
var items []map[string]interface{}
if strings.TrimSpace(row.Products) != "" {
@@ -524,13 +542,14 @@ func applyContractAmounts(row *models.TenantCrmContract) {
qty := toFloat64(item["quantity"])
price := toFloat64(item["price"])
costPrice := toFloat64(item["cost_price"])
amount := round2(qty * price)
taxRate := toFloat64(item["tax_rate"])
amount := round2(qty * price * (1 + taxRate/100)) // 含税金额
cost = round2(cost + round2(qty*costPrice))
cat := fmt.Sprintf("%v", item["category"])
switch cat {
case "1":
switch {
case cat == "1" || strings.Contains(cat, "硬件"):
hardware = round2(hardware + amount)
case "2":
case cat == "2" || strings.Contains(cat, "软件"):
software = round2(software + amount)
default:
other = round2(other + amount)
@@ -83,6 +83,10 @@ func parsePipelineDateTime(s string) *time.Time {
}
layouts := []string{
"2006-01-02 15:04:05",
// 带时区偏移的 RFC3339:前端编辑时回显的是后端 JSON 序列化的 time.Time
// (如 2026-09-12T00:00:00+08:00),缺此布局会解析失败导致日期被写空。
time.RFC3339Nano,
time.RFC3339,
"2006-01-02T15:04:05",
"2006-01-02T15:04",
"2006-01-02 15:04",
+431
View File
@@ -0,0 +1,431 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"time"
"server/models"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// BackendCrmProductController CRM 产品管理(产品台账/目录)
type BackendCrmProductController struct {
beego.Controller
}
type crmProductPayload struct {
ProductNo string `json:"product_no"`
ProductName string `json:"product_name"`
Category string `json:"category"`
Unit string `json:"unit"`
Spec string `json:"spec"`
Price float64 `json:"price"`
CostPrice float64 `json:"cost_price"`
TaxRate float64 `json:"tax_rate"`
Status int8 `json:"status"`
Remark string `json:"remark"`
}
// crmProductItem 项目产品清单中的单行(也用于项目生成时写入产品管理)。
type crmProductItem struct {
ProductNo string `json:"product_no"`
Name string `json:"name"`
Category string `json:"category"`
Spec string `json:"spec"`
Unit string `json:"unit"`
Quantity interface{} `json:"quantity"`
Price float64 `json:"price"`
CostPrice float64 `json:"cost_price"`
TaxRate float64 `json:"tax_rate"`
Remark string `json:"remark"`
}
// List GET /backend/crm/product/list
func (c *BackendCrmProductController) 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 > 100 {
pageSize = 20
}
keyword := strings.TrimSpace(c.GetString("keyword"))
category := strings.TrimSpace(c.GetString("category"))
status := strings.TrimSpace(c.GetString("status"))
projectID, _ := c.GetUint64("project_id")
tenantID := pipelineTenantID(claims)
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
if keyword != "" {
kw := orm.NewCondition().
Or("product_name__contains", keyword).
Or("product_no__contains", keyword).
Or("spec__contains", keyword)
cond = cond.AndCond(kw)
}
if category != "" {
cond = cond.And("category", category)
}
if status != "" {
cond = cond.And("status", status)
}
if projectID > 0 {
cond = cond.And("project_id", projectID)
}
qs := models.Orm.QueryTable(new(models.TenantCrmProduct)).SetCond(cond)
total, _ := qs.Count()
var list []models.TenantCrmProduct
if total > 0 {
_, _ = qs.OrderBy("-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list)
}
pipelineOk(&c.Controller, map[string]interface{}{
"list": list, "total": total, "page": page, "pageSize": pageSize,
})
}
// Detail GET /backend/crm/product/:id
func (c *BackendCrmProductController) 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 p models.TenantCrmProduct
if err := models.Orm.QueryTable(new(models.TenantCrmProduct)).
Filter("id", id).Filter("tenant_id", pipelineTenantID(claims)).
Filter("delete_time__isnull", true).One(&p); err != nil {
pipelineErr(&c.Controller, 404, 404, "产品未找到")
return
}
pipelineOk(&c.Controller, p)
}
// Create POST /backend/crm/product
func (c *BackendCrmProductController) 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 crmProductPayload
if err := json.Unmarshal(raw, &p); err != nil {
pipelineErr(&c.Controller, 400, 400, "参数错误")
return
}
if strings.TrimSpace(p.ProductName) == "" {
pipelineErr(&c.Controller, 400, 400, "产品名称不能为空")
return
}
status := p.Status
if status != 0 && status != 1 {
status = 1
}
now := time.Now()
row := models.TenantCrmProduct{
TenantID: pipelineTenantID(claims),
ProductNo: strings.TrimSpace(p.ProductNo),
ProductName: strings.TrimSpace(p.ProductName),
Category: strings.TrimSpace(p.Category),
Unit: strings.TrimSpace(p.Unit),
Spec: strings.TrimSpace(p.Spec),
Price: p.Price,
CostPrice: p.CostPrice,
TaxRate: p.TaxRate,
Status: status,
Remark: p.Remark,
CreateUserID: pipelineUID(claims),
CreateTime: now,
UpdateTime: now,
}
id, err := models.Orm.Insert(&row)
if err != nil {
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
return
}
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
}
// Update PUT /backend/crm/product/:id
func (c *BackendCrmProductController) 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 crmProductPayload
if err := json.Unmarshal(raw, &p); err != nil {
pipelineErr(&c.Controller, 400, 400, "参数错误")
return
}
if strings.TrimSpace(p.ProductName) == "" {
pipelineErr(&c.Controller, 400, 400, "产品名称不能为空")
return
}
tenantID := pipelineTenantID(claims)
var row models.TenantCrmProduct
if err := models.Orm.QueryTable(new(models.TenantCrmProduct)).
Filter("id", id).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).One(&row); err != nil {
pipelineErr(&c.Controller, 404, 404, "产品未找到")
return
}
status := p.Status
if status != 0 && status != 1 {
status = row.Status
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.TenantCrmProduct)).
Filter("id", id).Filter("tenant_id", tenantID).
Update(orm.Params{
"product_no": strings.TrimSpace(p.ProductNo),
"product_name": strings.TrimSpace(p.ProductName),
"category": strings.TrimSpace(p.Category),
"unit": strings.TrimSpace(p.Unit),
"spec": strings.TrimSpace(p.Spec),
"price": p.Price,
"cost_price": p.CostPrice,
"tax_rate": p.TaxRate,
"status": status,
"remark": p.Remark,
"update_time": now,
})
if err != nil {
pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
}
// Delete DELETE /backend/crm/product/:id
func (c *BackendCrmProductController) 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.TenantCrmProduct
if err := models.Orm.QueryTable(new(models.TenantCrmProduct)).
Filter("id", id).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).One(&row); err != nil {
pipelineErr(&c.Controller, 404, 404, "产品未找到")
return
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.TenantCrmProduct)).
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)
}
// SyncFromProject POST /backend/crm/product/sync-from-project
// 项目生成(或保存)时,把项目产品清单里的产品参数写入产品管理(按 名称+分类 去重 upsert)。
func (c *BackendCrmProductController) SyncFromProject() {
claims, err := pipelineClaims(&c.Controller)
if err != nil {
pipelineErr(&c.Controller, 401, 401, err.Error())
return
}
raw, _ := io.ReadAll(c.Ctx.Request.Body)
var body struct {
ProjectID uint64 `json:"project_id"`
ProjectName string `json:"project_name"`
Products []crmProductItem `json:"products"`
}
if err := json.Unmarshal(raw, &body); err != nil {
pipelineErr(&c.Controller, 400, 400, "参数错误")
return
}
if body.ProjectID == 0 {
pipelineErr(&c.Controller, 400, 400, "缺少项目ID")
return
}
created, updated, err := SyncProjectProducts(pipelineTenantID(claims), body.ProjectID, strings.TrimSpace(body.ProjectName), body.Products)
if err != nil {
pipelineErr(&c.Controller, 500, 500, "同步失败: "+err.Error())
return
}
pipelineOk(&c.Controller, map[string]interface{}{"created": created, "updated": updated})
}
// SyncProjectProducts 把项目产品清单写入产品管理:按 产品名称+分类 在同一租户下去重。
// - 已存在:刷新单价/成本/税率/规格/单位/来源项目/备注;
// - 不存在:新建产品档案,登记来源项目。
// 返回新建数、更新数。
func SyncProjectProducts(tenantID string, projectID uint64, projectName string, items []crmProductItem) (created int, updated int, err error) {
if len(items) == 0 {
return 0, 0, nil
}
now := time.Now()
for _, it := range items {
name := strings.TrimSpace(it.Name)
if name == "" {
continue
}
cat := strings.TrimSpace(it.Category)
var exist models.TenantCrmProduct
e := models.Orm.QueryTable(new(models.TenantCrmProduct)).
Filter("tenant_id", tenantID).
Filter("product_name", name).
Filter("category", cat).
Filter("delete_time__isnull", true).
OrderBy("-id").Limit(1).One(&exist)
if e == nil && exist.ID > 0 {
// 已存在:更新参数
_, uerr := models.Orm.QueryTable(new(models.TenantCrmProduct)).
Filter("id", exist.ID).
Update(orm.Params{
"product_no": strings.TrimSpace(it.ProductNo),
"spec": strings.TrimSpace(it.Spec),
"unit": strings.TrimSpace(it.Unit),
"price": it.Price,
"cost_price": it.CostPrice,
"tax_rate": it.TaxRate,
"project_id": projectID,
"project_name": projectName,
"remark": it.Remark,
"update_time": now,
})
if uerr != nil {
err = uerr
return
}
updated++
continue
}
// 不存在:新建
row := models.TenantCrmProduct{
TenantID: tenantID,
ProductNo: strings.TrimSpace(it.ProductNo),
ProductName: name,
Category: cat,
Unit: strings.TrimSpace(it.Unit),
Spec: strings.TrimSpace(it.Spec),
Price: it.Price,
CostPrice: it.CostPrice,
TaxRate: it.TaxRate,
Status: 1,
ProjectID: &projectID,
ProjectName: projectName,
Remark: it.Remark,
CreateUserID: "",
CreateTime: now,
UpdateTime: now,
}
if _, ierr := models.Orm.Insert(&row); ierr != nil {
err = ierr
return
}
created++
}
return created, updated, nil
}
// SyncContractProducts 把合同产品清单同步到产品管理(供合同保存时调用):
// - 已关联 product_id 或名称命中现有产品:回填 product_id,并将成本单价强制取产品管理
// (成本追溯产品管理;销售单价不回写,允许合同溢价);
// - 名称未命中(产品管理中不存在):新建产品档案,销售单价/成本单价/规格/单位/分类/税率取自合同行。
//
// 返回(可能已回填 product_id 与成本单价)的清单 JSON,供合同落库。
func SyncContractProducts(tenantID, uid string, raw json.RawMessage) (json.RawMessage, error) {
var items []map[string]interface{}
if len(raw) == 0 || strings.TrimSpace(string(raw)) == "[]" || strings.TrimSpace(string(raw)) == "" {
return json.RawMessage("[]"), nil
}
if err := json.Unmarshal(raw, &items); err != nil {
return raw, err
}
now := time.Now()
for _, it := range items {
name := strings.TrimSpace(fmt.Sprintf("%v", it["name"]))
if name == "" {
continue
}
// 已关联产品:成本单价强制取产品管理,不回写销售单价
if pid := toUint64(it["product_id"]); pid > 0 {
var p models.TenantCrmProduct
if e := models.Orm.QueryTable(new(models.TenantCrmProduct)).
Filter("id", pid).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).One(&p); e == nil && p.ID > 0 {
it["product_id"] = p.ID
it["cost_price"] = p.CostPrice
}
continue
}
// 按名称命中现有产品(未启用也命中,便于回填)
var exist models.TenantCrmProduct
e := models.Orm.QueryTable(new(models.TenantCrmProduct)).
Filter("tenant_id", tenantID).Filter("product_name", name).
Filter("delete_time__isnull", true).OrderBy("-id").Limit(1).One(&exist)
if e == nil && exist.ID > 0 {
it["product_id"] = exist.ID
it["cost_price"] = exist.CostPrice
continue
}
// 未命中:新建产品档案(成本/单价等取自合同行)
row := models.TenantCrmProduct{
TenantID: tenantID,
ProductName: name,
Category: strings.TrimSpace(fmt.Sprintf("%v", it["category"])),
Unit: strings.TrimSpace(fmt.Sprintf("%v", it["unit"])),
Spec: strings.TrimSpace(fmt.Sprintf("%v", it["spec"])),
Price: toFloat64(it["price"]),
CostPrice: toFloat64(it["cost_price"]),
TaxRate: toFloat64(it["tax_rate"]),
Status: 1,
Remark: strings.TrimSpace(fmt.Sprintf("%v", it["remark"])),
CreateUserID: uid,
CreateTime: now,
UpdateTime: now,
}
id, ierr := models.Orm.Insert(&row)
if ierr != nil {
return raw, ierr
}
it["product_id"] = uint64(id)
}
out, err := json.Marshal(items)
if err != nil {
return raw, err
}
return json.RawMessage(out), nil
}
// toUint64 转为 uint64(依赖同包 toInt64)。
func toUint64(v interface{}) uint64 {
return uint64(toInt64(v))
}
@@ -0,0 +1,223 @@
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"
)
// BackendCrmProductCategoryController CRM 产品分类(用户自定义产品归类)
type BackendCrmProductCategoryController struct {
beego.Controller
}
type crmProductCategoryPayload struct {
Name string `json:"name"`
Code string `json:"code"`
Sort int `json:"sort"`
Status int8 `json:"status"`
Remark string `json:"remark"`
}
// List GET /backend/crm/product/category/list
func (c *BackendCrmProductCategoryController) 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)
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.TenantCrmProductCategory)).SetCond(cond)
total, _ := qs.Count()
var list []models.TenantCrmProductCategory
if total > 0 {
_, _ = qs.OrderBy("sort", "-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list)
}
pipelineOk(&c.Controller, map[string]interface{}{
"list": list, "total": total, "page": page, "pageSize": pageSize,
})
}
// Detail GET /backend/crm/product/category/:id
func (c *BackendCrmProductCategoryController) 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.TenantCrmProductCategory
if err := models.Orm.QueryTable(new(models.TenantCrmProductCategory)).
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/product/category
func (c *BackendCrmProductCategoryController) 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 crmProductCategoryPayload
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
}
now := time.Now()
row := models.TenantCrmProductCategory{
TenantID: pipelineTenantID(claims),
Name: name,
Code: strings.TrimSpace(p.Code),
Sort: p.Sort,
Status: status,
Remark: p.Remark,
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/product/category/:id
func (c *BackendCrmProductCategoryController) 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 crmProductCategoryPayload
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.TenantCrmProductCategory
if err := models.Orm.QueryTable(new(models.TenantCrmProductCategory)).
Filter("id", id).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).One(&row); err != nil {
pipelineErr(&c.Controller, 404, 404, "产品分类未找到")
return
}
status := p.Status
if status != 0 && status != 1 {
status = row.Status
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.TenantCrmProductCategory)).
Filter("id", id).Filter("tenant_id", tenantID).
Update(orm.Params{
"name": name,
"code": strings.TrimSpace(p.Code),
"sort": p.Sort,
"status": status,
"remark": 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/product/category/:id
func (c *BackendCrmProductCategoryController) 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.TenantCrmProductCategory
if err := models.Orm.QueryTable(new(models.TenantCrmProductCategory)).
Filter("id", id).Filter("tenant_id", tenantID).
Filter("delete_time__isnull", true).One(&row); err != nil {
pipelineErr(&c.Controller, 404, 404, "产品分类未找到")
return
}
now := time.Now()
_, err = models.Orm.QueryTable(new(models.TenantCrmProductCategory)).
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)
}
+20
View File
@@ -38,6 +38,7 @@ type projectPayload struct {
ContactPhone string `json:"contact_phone"`
Address string `json:"address"`
Remark string `json:"remark"`
Products string `json:"products"` // 项目产品清单JSON(生成时写入产品管理)
}
// List GET /backend/crm/project/list
@@ -173,6 +174,7 @@ func (c *BackendCrmProjectController) Create() {
ContactPhone: strings.TrimSpace(p.ContactPhone),
Address: strings.TrimSpace(p.Address),
Remark: p.Remark,
Products: p.Products,
CreateUserID: pipelineUID(claims),
CreateTime: now,
UpdateTime: now,
@@ -182,6 +184,16 @@ func (c *BackendCrmProjectController) Create() {
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
return
}
// 项目生成时,把产品清单里的产品参数写入产品管理(按 名称+分类 去重)
if strings.TrimSpace(p.Products) != "" {
var items []crmProductItem
if json.Unmarshal([]byte(p.Products), &items) == nil {
if _, _, serr := SyncProjectProducts(tenantID, uint64(id), proj.ProjectName, items); serr != nil {
// 同步失败不影响项目创建,仅记录
_ = serr
}
}
}
// 项目建立后,同步在文档库「共享文档 / 项目文档」下建立同名文件夹(失败不影响项目创建)
if cid, err := services.EnsureCrmProjectDocCategory(claims.TenantId, uint64(id), proj.ProjectName, 0); err == nil {
proj.DocCategoryID = cid
@@ -253,12 +265,20 @@ func (c *BackendCrmProjectController) Update() {
proj.ContactPhone = strings.TrimSpace(p.ContactPhone)
proj.Address = strings.TrimSpace(p.Address)
proj.Remark = p.Remark
proj.Products = p.Products
proj.UpdateTime = time.Now()
if _, err := models.Orm.Update(&proj); err != nil {
pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
return
}
// 项目保存时,把产品清单里的产品参数写入产品管理(按 名称+分类 去重)
if strings.TrimSpace(p.Products) != "" {
var items []crmProductItem
if json.Unmarshal([]byte(p.Products), &items) == nil {
_, _, _ = SyncProjectProducts(tenantID, proj.ID, proj.ProjectName, items)
}
}
// 项目改名时同步重命名文档库中的项目文件夹(失败静默,不影响项目更新)
if proj.DocCategoryID > 0 {
_ = services.RenameCrmProjectDocCategory(claims.TenantId, proj.DocCategoryID, proj.ProjectName)