批量优化产品和产品分类
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 项目表新增「产品清单」字段:yz_backend_crm_project.products
|
||||
-- 手动执行;可重复执行(列已存在时自动跳过,不再报 1060)。
|
||||
-- 该字段用于保存项目生成产品时写入产品管理的产品清单JSON。
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'yz_backend_crm_project';
|
||||
SET @columnname = 'products';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = @columnname) > 0,
|
||||
'SELECT 1',
|
||||
'ALTER TABLE yz_backend_crm_project ADD COLUMN products text COMMENT ''项目产品清单JSON(写入产品管理时使用)'''
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
@@ -0,0 +1,29 @@
|
||||
-- CRM 产品管理表:yz_backend_crm_product
|
||||
-- 手动执行(可重复执行);产品可由「项目生成」时自动写入,也可在「产品管理」中独立维护。
|
||||
CREATE TABLE IF NOT EXISTS yz_backend_crm_product (
|
||||
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
|
||||
tenant_id varchar(64) NOT NULL COMMENT '租户ID',
|
||||
product_no varchar(50) NOT NULL DEFAULT '' COMMENT '产品编号',
|
||||
product_name varchar(100) NOT NULL COMMENT '产品名称',
|
||||
category varchar(20) NOT NULL DEFAULT '' COMMENT '分类:1硬件/2软件/3服务/4开发/5其他',
|
||||
unit varchar(20) NOT NULL DEFAULT '' COMMENT '单位',
|
||||
spec varchar(255) NOT NULL DEFAULT '' COMMENT '规格型号',
|
||||
price decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '销售单价',
|
||||
cost_price decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '成本单价',
|
||||
tax_rate decimal(6,2) NOT NULL DEFAULT '0.00' COMMENT '税率(%)',
|
||||
status tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1启用/0停用',
|
||||
project_id bigint(20) DEFAULT NULL COMMENT '来源项目ID(由项目生成时填入)',
|
||||
project_name varchar(100) NOT NULL DEFAULT '' COMMENT '来源项目名称',
|
||||
remark text COMMENT '备注',
|
||||
create_user_id varchar(64) NOT NULL DEFAULT '' COMMENT '创建人用户ID',
|
||||
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
delete_time datetime DEFAULT NULL COMMENT '删除时间(软删除)',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_tenant_id (tenant_id),
|
||||
KEY idx_product_name (tenant_id,product_name),
|
||||
KEY idx_category (tenant_id,category),
|
||||
KEY idx_project (tenant_id,project_id),
|
||||
KEY idx_status (tenant_id,status),
|
||||
KEY idx_delete_time (delete_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM产品管理表';
|
||||
@@ -0,0 +1,19 @@
|
||||
-- CRM 产品分类表:yz_backend_crm_product_category
|
||||
-- 手动执行(可重复执行);建表后请在「产品管理-产品分类」中维护数据。
|
||||
CREATE TABLE IF NOT EXISTS yz_backend_crm_product_category (
|
||||
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID',
|
||||
tenant_id varchar(64) NOT NULL COMMENT '租户ID',
|
||||
name varchar(100) NOT NULL COMMENT '分类名称',
|
||||
code varchar(50) NOT NULL DEFAULT '' COMMENT '分类编码(选填)',
|
||||
sort int(11) NOT NULL DEFAULT '0' COMMENT '排序',
|
||||
status tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1启用/0停用',
|
||||
remark text COMMENT '备注',
|
||||
create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
delete_time datetime DEFAULT NULL COMMENT '删除时间(软删除)',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_tenant_name (tenant_id,name),
|
||||
KEY idx_tenant_id (tenant_id),
|
||||
KEY idx_status (tenant_id,status),
|
||||
KEY idx_delete_time (delete_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM产品分类表';
|
||||
@@ -77,6 +77,8 @@ func Init(_ string) {
|
||||
new(TenantCrmClue),
|
||||
new(TenantCrmBusiness),
|
||||
new(TenantCrmProject),
|
||||
new(TenantCrmProduct),
|
||||
new(TenantCrmProductCategory),
|
||||
new(TenantCrmFollow),
|
||||
new(TenantCrmAttach),
|
||||
new(TenantCrmEntityContact),
|
||||
@@ -145,6 +147,7 @@ func Init(_ string) {
|
||||
EnsureCrmProjectDocColumn()
|
||||
EnsureCrmContractTable()
|
||||
EnsureCrmContractOurRoleColumn()
|
||||
EnsureCrmProjectProductsColumn()
|
||||
}
|
||||
|
||||
// EnsureCrmContractOurRoleColumn 补齐合同表的我方角色字段(存量表已建时新增;
|
||||
@@ -155,6 +158,14 @@ func EnsureCrmContractOurRoleColumn() {
|
||||
_, _ = Orm.Raw(sql).Exec()
|
||||
}
|
||||
|
||||
// EnsureCrmProjectProductsColumn 补齐项目表的产品清单字段(存量表已建时新增;
|
||||
// 表不存在或列已存在时忽略错误,可用 docs/sql/alter_backend_crm_project_products_column.sql 手动修复)。
|
||||
func EnsureCrmProjectProductsColumn() {
|
||||
sql := "ALTER TABLE " + new(TenantCrmProject).TableName() +
|
||||
" ADD COLUMN products text COMMENT '项目产品清单JSON(写入产品管理时使用)'"
|
||||
_, _ = Orm.Raw(sql).Exec()
|
||||
}
|
||||
|
||||
// EnsureCrmContractTable 合同表建表(CREATE TABLE IF NOT EXISTS,可重复执行;
|
||||
// 建表失败(如表已存在但结构不一致)时静默忽略,可用 sql/yz_backend_crm_contract.sql 手动修复)。
|
||||
func EnsureCrmContractTable() {
|
||||
@@ -211,6 +222,8 @@ func EnsureCrmProjectDocColumn() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// EnsureCrmCreateUserColumn 补齐客户/供应商的创建人字段(用于删除权限校验)。
|
||||
// 同样兼容历史表名差异,表不存在或列已存在时忽略错误。
|
||||
func EnsureCrmCreateUserColumn() {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// TenantCrmProduct CRM 产品管理(产品台账/目录): yz_backend_crm_product
|
||||
//
|
||||
// 说明:
|
||||
// - 产品可独立维护,也可由「项目生成」时自动写入(project_id 记录来源项目);
|
||||
// - category 复用合同产品分类:1硬件/2软件/3服务/4开发/5其他;
|
||||
// - price 为销售单价,cost_price 为成本单价,tax_rate 为税率(%)。
|
||||
type TenantCrmProduct struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
ProductNo string `orm:"column(product_no);size(50)" json:"product_no"` // 产品编号
|
||||
ProductName string `orm:"column(product_name);size(100)" json:"product_name"` // 产品名称
|
||||
Category string `orm:"column(category);size(20)" json:"category"` // 分类 1硬件/2软件/3服务/4开发/5其他
|
||||
Unit string `orm:"column(unit);size(20)" json:"unit"` // 单位
|
||||
Spec string `orm:"column(spec);size(255)" json:"spec"` // 规格型号
|
||||
Price float64 `orm:"column(price);digits(14);decimals(2);default(0)" json:"price"` // 销售单价
|
||||
CostPrice float64 `orm:"column(cost_price);digits(14);decimals(2);default(0)" json:"cost_price"` // 成本单价
|
||||
TaxRate float64 `orm:"column(tax_rate);digits(6);decimals(2);default(0)" json:"tax_rate"` // 税率(%)
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"` // 1启用/0停用
|
||||
ProjectID *uint64 `orm:"column(project_id);null" json:"project_id"` // 来源项目ID(由项目生成时填入)
|
||||
ProjectName string `orm:"column(project_name);size(100)" json:"project_name"` // 来源项目名称
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmProduct) TableName() string {
|
||||
return "yz_backend_crm_product"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// TenantCrmProductCategory CRM 产品分类(用户自定义产品归类): yz_backend_crm_product_category
|
||||
//
|
||||
// 说明:
|
||||
// - 与 CMS 产品分类类似,是 CRM 产品台账的产品归类(如:服务器类 / 软件许可 / 实施服务 等);
|
||||
// - 产品表单的「产品分类」下拉取自该表;status 1启用/0停用。
|
||||
type TenantCrmProductCategory struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
Name string `orm:"column(name);size(100)" json:"name"` // 分类名称
|
||||
Code string `orm:"column(code);size(50)" json:"code"` // 分类编码(选填)
|
||||
Sort int `orm:"column(sort);default(0)" json:"sort"` // 排序
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"` // 1启用/0停用
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmProductCategory) TableName() string {
|
||||
return "yz_backend_crm_product_category"
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ type TenantCrmProject struct {
|
||||
// DocCategoryID 项目在 OA 文档库的文件夹分类ID:
|
||||
// 指向 yz_backend_oa_doc_category 中「共享文档 / 项目文档 / {项目名称}」这条记录(0 表示尚未建立)。
|
||||
DocCategoryID uint64 `orm:"column(doc_category_id);default(0)" json:"doc_category_id"`
|
||||
Products string `orm:"column(products);type(text);null" json:"products"` // 项目产品清单JSON(写入产品管理时使用)
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
|
||||
@@ -401,6 +401,17 @@ func registerOrganizationRoutes(module string) {
|
||||
beego.Router("/backend/crm/project", &controllers.BackendCrmProjectController{}, "post:Create")
|
||||
beego.Router("/backend/crm/project/:id", &controllers.BackendCrmProjectController{}, "get:Detail;put:Update;delete:Delete")
|
||||
|
||||
// CRM产品管理(产品台账/目录;可由项目生成时写入)
|
||||
beego.Router("/backend/crm/product/list", &controllers.BackendCrmProductController{}, "get:List")
|
||||
beego.Router("/backend/crm/product", &controllers.BackendCrmProductController{}, "post:Create")
|
||||
beego.Router("/backend/crm/product/:id", &controllers.BackendCrmProductController{}, "get:Detail;put:Update;delete:Delete")
|
||||
beego.Router("/backend/crm/product/sync-from-project", &controllers.BackendCrmProductController{}, "post:SyncFromProject")
|
||||
|
||||
// CRM产品分类管理(用户自定义产品归类;产品表单的分类下拉取自该表)
|
||||
beego.Router("/backend/crm/product/category/list", &controllers.BackendCrmProductCategoryController{}, "get:List")
|
||||
beego.Router("/backend/crm/product/category", &controllers.BackendCrmProductCategoryController{}, "post:Create")
|
||||
beego.Router("/backend/crm/product/category/:id", &controllers.BackendCrmProductCategoryController{}, "get:Detail;put:Update;delete:Delete")
|
||||
|
||||
// CRM项目文档(与OA文档库「共享文档/项目文档」绑定:列表/上传同步/删除同步/文件夹管理)
|
||||
beego.Router("/backend/crm/project/:id/docs", &controllers.BackendCrmProjectController{}, "get:DocList")
|
||||
beego.Router("/backend/crm/project/:id/doc-upload", &controllers.BackendCrmProjectController{}, "post:DocUpload")
|
||||
|
||||
Reference in New Issue
Block a user