diff --git a/backend/src/api/crmDashboard.js b/backend/src/api/crmDashboard.js new file mode 100644 index 0000000..965b575 --- /dev/null +++ b/backend/src/api/crmDashboard.js @@ -0,0 +1,14 @@ +import request from "@/utils/request"; + +/** + * CRM 数据仪表盘接口 + * + * 说明: + * - range 维度:day(天) / week(周) / month(月) / quarter(季度) / year(年); + * - 后端按租户 + 时间维度聚合真实业务数据(客户/联系人/项目/合同/线索/商机/回访/回款)。 + */ + +/** 仪表盘汇总数据 */ +export function getCrmDashboard(params) { + return request({ url: "/backend/crm/dashboard", method: "get", params }); +} diff --git a/backend/src/api/crmPayback.js b/backend/src/api/crmPayback.js new file mode 100644 index 0000000..272497f --- /dev/null +++ b/backend/src/api/crmPayback.js @@ -0,0 +1,49 @@ +import request from "@/utils/request"; + +/** + * CRM 回款管理接口(针对于合同) + * + * 说明: + * - 回款计划必须挂靠合同(contract_id); + * - plan_type 回款周期:1月度/2季度/3年度/4进度/5自定义,决定 items 明细结构; + * 月度/季度/年度:{seq,name,amount}; + * 进度:{seq,name,percent,amount}; + * 自定义:{seq,name,plan_date,amount}; + * - pay_method 回款方式:1对公转账/2网银转账/3现金/4支票/5支付宝/6微信/7其他; + * - items 每行带 received_amount/received_date/status,用于回款进度登记。 + */ + +/** 回款计划列表 */ +export function getPaybackList(params) { + return request({ url: "/backend/crm/payback/list", method: "get", params }); +} + +/** 回款计划详情 */ +export function getPaybackDetail(id) { + return request({ url: `/backend/crm/payback/${id}`, method: "get" }); +} + +/** 创建回款计划 */ +export function createPayback(data) { + return request({ url: "/backend/crm/payback", method: "post", data }); +} + +/** 更新回款计划 */ +export function updatePayback(id, data) { + return request({ url: `/backend/crm/payback/${id}`, method: "put", data }); +} + +/** 删除回款计划 */ +export function deletePayback(id) { + return request({ url: `/backend/crm/payback/${id}`, method: "delete" }); +} + +/** 回款进度列表(跨计划摊平的分期明细 + 汇总) */ +export function getPaybackProgress(params) { + return request({ url: "/backend/crm/payback/progress/list", method: "get", params }); +} + +/** 回款进度登记:按明细序号写入实收金额 / 回款日期 */ +export function registerPaybackReceive(id, data) { + return request({ url: `/backend/crm/payback/${id}/receive`, method: "post", data }); +} diff --git a/backend/src/views/apps/crm/contract/components/DevModuleTree.vue b/backend/src/views/apps/crm/contract/components/DevModuleTree.vue new file mode 100644 index 0000000..eb58f2d --- /dev/null +++ b/backend/src/views/apps/crm/contract/components/DevModuleTree.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/backend/src/views/apps/crm/contract/components/ModuleNode.vue b/backend/src/views/apps/crm/contract/components/ModuleNode.vue new file mode 100644 index 0000000..1e2d89d --- /dev/null +++ b/backend/src/views/apps/crm/contract/components/ModuleNode.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/backend/src/views/apps/crm/contract/components/ProductList.vue b/backend/src/views/apps/crm/contract/components/ProductList.vue index ef8c228..4c0e72d 100644 --- a/backend/src/views/apps/crm/contract/components/ProductList.vue +++ b/backend/src/views/apps/crm/contract/components/ProductList.vue @@ -2,9 +2,18 @@
+ + + + @@ -132,7 +160,7 @@
添加产品 - 先选产品类别,再在该分类下搜索/选择产品(自动带出规格、单位、成本、税率);分类内无对应名称可输入新增,提交时自动建档到产品管理 + 每行可选「成品」或「软件开发」:成品按数量×单价计价;软件开发点「模块报价」编辑分层模块树(叶子填人天/人天单价,父级自动汇总),统一计入合同总价
@@ -170,6 +198,15 @@
+ + @@ -177,9 +214,10 @@ import { ref, computed, onMounted } from "vue"; import { Plus } from "@element-plus/icons-vue"; import { formatMoney } from "../../dict"; -import { buildSummary } from "./utils"; +import { buildSummary, lineAmount } from "./utils"; import { getProductList } from "@/api/crmProduct"; import { getProductCategoryList } from "@/api/crmProductCategory"; +import DevModuleTree from "./DevModuleTree.vue"; const props = defineProps({ /** 产品行数组(父组件持有) */ @@ -196,6 +234,7 @@ let rowSeed = 0; const addRow = () => { props.products.push({ __key: `row_${Date.now()}_${rowSeed++}`, + line_type: "product", name: "", product_id: null, category: "", @@ -205,6 +244,8 @@ const addRow = () => { price: 0, cost_price: 0, tax_rate: 0, + dev_tree: [], + dev_unit_price: 0, remark: "", }); }; @@ -213,8 +254,23 @@ const removeRow = (index) => { props.products.splice(index, 1); }; -const rowAmount = (row) => - Math.round(((Number(row.quantity) || 0) * (Number(row.price) || 0) || 0) * 100) / 100; +/** 切换行类型:软件开发行确保有模块树容器 */ +function onLineTypeChange(row) { + if (row.line_type === "dev") { + if (!Array.isArray(row.dev_tree)) row.dev_tree = []; + if (row.dev_unit_price == null) row.dev_unit_price = 0; + } +} + +/* ------------------------------ 软件开发模块树编辑 ------------------------------ */ +const devVisible = ref(false); +const editingRow = ref(null); + +function openDev(row) { + if (!Array.isArray(row.dev_tree)) row.dev_tree = []; + editingRow.value = row; + devVisible.value = true; +} const specPlaceholder = (row) => { const cat = String(row.category || ""); @@ -273,6 +329,15 @@ function onProductPick(row, val) { const hit = productOptions.value.find((p) => p.product_name === val); if (hit) { row.product_id = hit.id; + // 软件开发产品:带入模块树与人天单价,行类型自动切为 dev + if (hit.line_type === "dev" || hit.dev_tree) { + row.line_type = "dev"; + row.dev_unit_price = Number(hit.dev_unit_price) || 0; + row.dev_tree = Array.isArray(hit.dev_tree) + ? JSON.parse(JSON.stringify(hit.dev_tree)) + : safeParseTree(hit.dev_tree); + return; + } row.spec = hit.spec || ""; row.unit = hit.unit || ""; row.price = hit.price || 0; @@ -284,6 +349,17 @@ function onProductPick(row, val) { } } +/** 安全解析产品管理返回的 dev_tree(可能为字符串或数组) */ +function safeParseTree(v) { + if (Array.isArray(v)) return v; + try { + const parsed = typeof v === "string" ? JSON.parse(v) : v; + return Array.isArray(parsed) ? parsed : []; + } catch (e) { + return []; + } +} + onMounted(() => { fetchCategories(); // 为已加载的合同产品预置下拉选项,保证名称回显 @@ -311,6 +387,10 @@ onMounted(() => { color: var(--el-text-color-primary); } + .muted { + color: var(--el-text-color-placeholder); + } + .opt-name { float: left; } diff --git a/backend/src/views/apps/crm/contract/components/create.vue b/backend/src/views/apps/crm/contract/components/create.vue index e5f5c98..477de09 100644 --- a/backend/src/views/apps/crm/contract/components/create.vue +++ b/backend/src/views/apps/crm/contract/components/create.vue @@ -8,11 +8,16 @@ @update:model-value="handleClose" @closed="handleClosed" > - +
- - - + + +
@@ -103,8 +108,8 @@ - - + + @@ -145,6 +150,7 @@ value-format="YYYY-MM-DD" placeholder="签订日期" style="width: 100%" + @change="onSignDateChange" /> @@ -173,6 +179,18 @@ + + + + 技术日期到期且回款未回完,合同状态将自动转为「执行异常」 + + @@ -272,10 +290,11 @@ const emptyForm = () => ({ party_count: 2, // 合同形式:2双方 3三方 4四方,默认双方 project_id: null, project_name: "", - owner_name: authStore.user?.name || "", + owner_user_name: authStore.user?.name || "", sign_date: "", effective_date: "", expire_date: "", + tech_date: "", remark: "", status: 1, step: 1, @@ -395,6 +414,19 @@ watch( } ); +/** 选择签订日期:生效日期为空时默认带入(生效日期仍可手动修改,改过则不再被覆盖) */ +watch( + () => form.sign_date, + (nv) => { + if (nv && !form.effective_date) form.effective_date = nv; + } +); + +/** 签订日期变更(用户操作):自动带入生效日期 */ +function onSignDateChange(val) { + if (val && !form.effective_date) form.effective_date = val; +} + /** 合同形式文案:2双方 3三方 4四方 */ const partyCountName = (v) => ({ 2: "双方", 3: "三方", 4: "四方" }[Number(v)] || "双方"); @@ -455,6 +487,7 @@ watch( form.sign_date = normalizeDate(props.editData.sign_date); form.effective_date = normalizeDate(props.editData.effective_date); form.expire_date = normalizeDate(props.editData.expire_date); + form.tech_date = normalizeDate(props.editData.tech_date); parties.value = (props.editData.parties || []).map((p) => ({ ...emptyParty(p.role), ...p })); products.value = (props.editData.products || []).map((row, i) => ({ ...row, @@ -524,6 +557,13 @@ const prevStep = () => { if (activeStep.value > 0) activeStep.value--; }; +/** 点击上方步骤条直接跳转(无需按上一步 / 下一步) */ +const goStep = (index) => { + const target = Math.max(0, Math.min(MAX_STEP, Number(index) || 0)); + if (target === activeStep.value) return; + activeStep.value = target; +}; + /** 组装保存 payload;数值字段统一转数字(后端 int8 解析),summary 由产品清单实时计算 */ const buildPayload = (step, status) => ({ ...form, @@ -531,6 +571,7 @@ const buildPayload = (step, status) => ({ sign_date: normalizeDate(form.sign_date), effective_date: normalizeDate(form.effective_date), expire_date: normalizeDate(form.expire_date), + tech_date: normalizeDate(form.tech_date), our_role: Number(form.our_role) || 2, party_count: Number(form.party_count) || 2, parties: parties.value.map((p) => ({ ...p })), @@ -633,6 +674,18 @@ const handleClosed = () => { padding: 0 24px; } +/* 步骤条可点击跳转 */ +.clickable-steps { + :deep(.step-clickable) { + cursor: pointer; + } + + :deep(.el-step__head), + :deep(.el-step__main) { + cursor: pointer; + } +} + .dialog-footer { display: flex; justify-content: space-between; diff --git a/backend/src/views/apps/crm/contract/components/detail.vue b/backend/src/views/apps/crm/contract/components/detail.vue index 9fc1ebd..3122b34 100644 --- a/backend/src/views/apps/crm/contract/components/detail.vue +++ b/backend/src/views/apps/crm/contract/components/detail.vue @@ -1,143 +1,88 @@ + + diff --git a/backend/src/views/apps/crm/payback/components/detail.vue b/backend/src/views/apps/crm/payback/components/detail.vue new file mode 100644 index 0000000..61e2ad2 --- /dev/null +++ b/backend/src/views/apps/crm/payback/components/detail.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/backend/src/views/apps/crm/payback/index.vue b/backend/src/views/apps/crm/payback/index.vue index 923a537..9b4f36a 100644 --- a/backend/src/views/apps/crm/payback/index.vue +++ b/backend/src/views/apps/crm/payback/index.vue @@ -1,5 +1,662 @@ - + + + + + diff --git a/backend/src/views/apps/crm/product/components/edit.vue b/backend/src/views/apps/crm/product/components/edit.vue index 73878f3..563a794 100644 --- a/backend/src/views/apps/crm/product/components/edit.vue +++ b/backend/src/views/apps/crm/product/components/edit.vue @@ -19,6 +19,21 @@ + + + + + + + + + + + + + + + - + + + + 📁 编辑模块树({{ (form.dev_tree || []).length }} 项 · ¥{{ formatMoney(treeSellTotal(form.dev_tree, form.dev_unit_price)) }}) + +
叶子模块只填「人天」,人天单价统一;父级自动汇总金额
+
+
+ - + - + @@ -78,6 +101,15 @@ + + + diff --git a/backend/src/views/apps/crm/supplier/components/edit.vue b/backend/src/views/apps/crm/supplier/components/edit.vue index 012484b..3fac841 100644 --- a/backend/src/views/apps/crm/supplier/components/edit.vue +++ b/backend/src/views/apps/crm/supplier/components/edit.vue @@ -7,13 +7,13 @@ @update:model-value="handleClose" @closed="handleClosed" > - +
- - - - - + + + + +
@@ -382,6 +382,13 @@ function prevStep() { } } +/** 点击上方步骤条直接跳转(无需按上一步 / 下一步) */ +function goStep(index) { + const target = Math.max(0, Math.min(3, Number(index) || 0)); + if (target === activeStep.value) return; + activeStep.value = target; +} + async function handleSave(isDraft) { if (!formRef.value) return; if (isDraft) { @@ -441,6 +448,15 @@ function handleSaveDraft() { padding: 0 20px; } +/* 步骤条可点击跳转 */ +.clickable-steps { + :deep(.step-clickable), + :deep(.el-step__head), + :deep(.el-step__main) { + cursor: pointer; + } +} + .dialog-footer { display: flex; justify-content: space-between; diff --git a/go/controllers/backend_crm_contract.go b/go/controllers/backend_crm_contract.go index b5d655c..704b8fd 100644 --- a/go/controllers/backend_crm_contract.go +++ b/go/controllers/backend_crm_contract.go @@ -47,6 +47,7 @@ type contractPayload struct { SignDate string `json:"sign_date"` EffectiveDate string `json:"effective_date"` ExpireDate string `json:"expire_date"` + TechDate string `json:"tech_date"` Parties json.RawMessage `json:"parties"` Products json.RawMessage `json:"products"` Step int8 `json:"step"` @@ -126,6 +127,7 @@ func (c *BackendCrmContractController) List() { } items := make([]contractResp, 0, len(list)) for i := range list { + c.syncContractExceptionStatus(&list[i]) items = append(items, buildContractResp(&list[i])) } pipelineOk(&c.Controller, map[string]interface{}{ @@ -135,6 +137,8 @@ func (c *BackendCrmContractController) List() { // Stats GET /backend/crm/contract/stats // 全租户合同统计(排除已作废),供列表页顶部汇总卡片使用。 +// 支持按周期筛选:period=month/quarter/year(当期)或 custom(start_date~end_date), +// 按合同有效期 [生效日期, 结束日期] 与所选周期重叠统计。 func (c *BackendCrmContractController) Stats() { claims, err := pipelineClaims(&c.Controller) if err != nil { @@ -142,13 +146,34 @@ func (c *BackendCrmContractController) Stats() { return } tenantID := pipelineTenantID(claims) + period := strings.TrimSpace(c.GetString("period")) + start := strings.TrimSpace(c.GetString("start_date")) + end := strings.TrimSpace(c.GetString("end_date")) + if period != "" && period != "custom" { + start, end = periodRange(period) + } + table := new(models.TenantCrmContract).TableName() + where := "tenant_id = ? AND delete_time IS NULL AND status <> 3" + args := []interface{}{tenantID} + // 按合同有效期 [生效日期, 结束日期] 与所选周期 [start, end] 重叠来统计: + // 合同生效日 <= 周期末 且 合同结束日 >= 周期初;日期为空视为不限制对应边界。 + if start != "" { + where += " AND (expire_date IS NULL OR expire_date >= ?)" + args = append(args, start) + } + if end != "" { + where += " AND (effective_date IS NULL OR effective_date <= ?)" + args = append(args, end) + } raw := "SELECT COUNT(*) AS total, IFNULL(SUM(total_amount),0) AS total_amount, " + "IFNULL(SUM(total_cost),0) AS total_cost, IFNULL(SUM(total_profit),0) AS total_profit " + - "FROM " + table + " WHERE tenant_id = ? AND delete_time IS NULL AND status <> 3" + "FROM " + table + " WHERE " + where var rows []orm.Params - if _, err := models.Orm.Raw(raw, tenantID).Values(&rows); err != nil || len(rows) == 0 { - pipelineOk(&c.Controller, contractSummary{TotalProfit: 0}) + if _, err := models.Orm.Raw(raw, args...).Values(&rows); err != nil || len(rows) == 0 { + pipelineOk(&c.Controller, map[string]interface{}{ + "total": 0, "total_amount": 0, "total_cost": 0, "total_profit": 0, + }) return } r := rows[0] @@ -160,6 +185,28 @@ func (c *BackendCrmContractController) Stats() { }) } +// periodRange 根据 period 返回 [start, end] 签约日期区间(YYYY-MM-DD):month/quarter/year 取当期。 +func periodRange(period string) (string, string) { + now := time.Now() + loc := now.Location() + y, m, _ := now.Date() + switch period { + case "quarter": + q := (int(m) - 1) / 3 + start := time.Date(y, time.Month(q*3+1), 1, 0, 0, 0, 0, loc) + end := start.AddDate(0, 3, 0).Add(-time.Nanosecond) + return start.Format("2006-01-02"), end.Format("2006-01-02") + case "year": + start := time.Date(y, 1, 1, 0, 0, 0, 0, loc) + end := start.AddDate(1, 0, 0).Add(-time.Nanosecond) + return start.Format("2006-01-02"), end.Format("2006-01-02") + default: // month + start := time.Date(y, m, 1, 0, 0, 0, 0, loc) + end := start.AddDate(0, 1, 0).Add(-time.Nanosecond) + return start.Format("2006-01-02"), end.Format("2006-01-02") + } +} + // Detail GET /backend/crm/contract/:id func (c *BackendCrmContractController) Detail() { claims, err := pipelineClaims(&c.Controller) @@ -179,6 +226,7 @@ func (c *BackendCrmContractController) Detail() { pipelineErr(&c.Controller, 404, 404, "合同未找到") return } + c.syncContractExceptionStatus(&row) pipelineOk(&c.Controller, buildContractResp(&row)) } @@ -214,6 +262,7 @@ func (c *BackendCrmContractController) Create() { SignDate: parsePipelineDate(p.SignDate), EffectiveDate: parsePipelineDate(p.EffectiveDate), ExpireDate: parsePipelineDate(p.ExpireDate), + TechDate: parsePipelineDate(p.TechDate), Status: pickInt8(p.Status, 1, 5), Step: pickInt8(p.Step, 1, 2), Remark: p.Remark, @@ -332,6 +381,7 @@ func (c *BackendCrmContractController) Update() { row.SignDate = parsePipelineDate(p.SignDate) row.EffectiveDate = parsePipelineDate(p.EffectiveDate) row.ExpireDate = parsePipelineDate(p.ExpireDate) + row.TechDate = parsePipelineDate(p.TechDate) if p.Parties != nil { parties, err := normalizeContractJSON(p.Parties) if err != nil { @@ -532,6 +582,23 @@ func normalizeContractJSON(raw json.RawMessage) (string, error) { // 合同总金额(含税)=硬件+软件+其他;产品总成本=Σ(数量×成本单价)(成本通常不含税,原值汇总); // 合同总利润=总金额-总成本。 // 归类口径:分类代码 1/2 或名称含「硬件」/「软件」分别计入硬件/软件,其余计入其他。 +// contractTreeSellTotal 软件开发模块树售价合计:子节点汇总,叶子=人天×统一人天单价。 +func contractTreeSellTotal(nodes []interface{}, unitPrice float64) float64 { + var sum float64 + for _, n := range nodes { + m, ok := n.(map[string]interface{}) + if !ok { + continue + } + if kids, ok := m["children"].([]interface{}); ok && len(kids) > 0 { + sum += contractTreeSellTotal(kids, unitPrice) + continue + } + sum += round2(toFloat64(m["man_days"]) * unitPrice) + } + return round2(sum) +} + func applyContractAmounts(row *models.TenantCrmContract) { var items []map[string]interface{} if strings.TrimSpace(row.Products) != "" { @@ -539,6 +606,14 @@ func applyContractAmounts(row *models.TenantCrmContract) { } var hardware, software, other, cost float64 for _, item := range items { + // 软件开发行:金额来自模块树汇总(统一人天单价×人天,不计税率/成本,与前端 buildSummary 一致) + if fmt.Sprintf("%v", item["line_type"]) == "dev" { + if tree, ok := item["dev_tree"].([]interface{}); ok { + unitPrice := toFloat64(item["dev_unit_price"]) + software = round2(software + contractTreeSellTotal(tree, unitPrice)) + } + continue + } qty := toFloat64(item["quantity"]) price := toFloat64(item["price"]) costPrice := toFloat64(item["cost_price"]) @@ -563,6 +638,42 @@ func applyContractAmounts(row *models.TenantCrmContract) { row.TotalProfit = round2(row.TotalAmount - cost) } +// contractReceivedAmount 汇总合同已回款金额(该合同下所有未删除回款计划的 received_amount 之和)。 +func contractReceivedAmount(tenantID string, contractID uint64) float64 { + if contractID == 0 { + return 0 + } + var rows []orm.Params + sql := "SELECT IFNULL(SUM(received_amount),0) AS received FROM " + + new(models.TenantCrmPayback).TableName() + + " WHERE tenant_id = ? AND contract_id = ? AND delete_time IS NULL" + if _, err := models.Orm.Raw(sql, tenantID, contractID).Values(&rows); err != nil || len(rows) == 0 { + return 0 + } + return toFloat64(rows[0]["received"]) +} + +// syncContractExceptionStatus 履约中合同状态对账:技术日期已过但回款未回完(已收 < 合同总额, +// 允许 0.005 浮点容差)时,自动流转为「执行异常」(5)。无技术日期或不满足翻转条件则不动。 +// 在列表 / 详情读取时触发,也可在回款登记后调用,使状态及时反映。 +func (c *BackendCrmContractController) syncContractExceptionStatus(row *models.TenantCrmContract) { + if row == nil || row.Status != 4 || row.TechDate == nil || row.ID == 0 { + return + } + now := time.Now() + if !row.TechDate.Before(now) { + return + } + received := contractReceivedAmount(row.TenantID, row.ID) + if row.TotalAmount > 0 && received+0.005 < row.TotalAmount { + row.Status = 5 + row.UpdateTime = now + _, _ = models.Orm.QueryTable(new(models.TenantCrmContract)). + Filter("id", row.ID).Filter("tenant_id", row.TenantID). + Update(orm.Params{"status": int8(5), "update_time": now}) + } +} + // genContractNo 生成合同编号:HT-YYYYMMDD-4位随机,租户内查重,最多重试 5 次。 func genContractNo(tenantID string) string { for i := 0; i < 5; i++ { diff --git a/go/controllers/backend_crm_dashboard.go b/go/controllers/backend_crm_dashboard.go new file mode 100644 index 0000000..9bf0a6b --- /dev/null +++ b/go/controllers/backend_crm_dashboard.go @@ -0,0 +1,387 @@ +package controllers + +import ( + "fmt" + "strings" + "time" + + "server/models" + + beego "github.com/beego/beego/v2/server/web" +) + +// BackendCrmDashboardController CRM 数据仪表盘:按时间维度聚合真实业务数据。 +type BackendCrmDashboardController struct { + beego.Controller +} + +/* ------------------------------ 时间维度 ------------------------------ */ + +// crmPeriod 返回当前周期与上一个周期的起止时间(含当天)。 +func crmPeriod(r string) (curStart, curEnd, prevStart, prevEnd time.Time) { + now := time.Now() + y, m, d := now.Date() + loc := now.Location() + switch r { + case "day": + curStart = time.Date(y, m, d, 0, 0, 0, 0, loc) + curEnd = curStart.Add(24*time.Hour - time.Second) + prevStart = curStart.AddDate(0, 0, -1) + prevEnd = curEnd.AddDate(0, 0, -1) + case "week": + wd := int(now.Weekday()) // 0 周日 ~ 6 周六 + if wd == 0 { + wd = 7 + } + monday := time.Date(y, m, d, 0, 0, 0, 0, loc).AddDate(0, 0, -(wd - 1)) + curStart = monday + curEnd = monday.AddDate(0, 0, 7).Add(-time.Second) + prevStart = monday.AddDate(0, 0, -7) + prevEnd = curStart.Add(-time.Second) + case "month": + curStart = time.Date(y, m, 1, 0, 0, 0, 0, loc) + curEnd = time.Date(y, m+1, 1, 0, 0, 0, 0, loc).Add(-time.Second) + prevStart = time.Date(y, m-1, 1, 0, 0, 0, 0, loc) + prevEnd = curStart.Add(-time.Second) + case "quarter": + q := (int(m) - 1) / 3 + qs := time.Date(y, time.Month(q*3+1), 1, 0, 0, 0, 0, loc) + curStart = qs + curEnd = qs.AddDate(0, 3, 0).Add(-time.Second) + prevStart = qs.AddDate(0, -3, 0) + prevEnd = curStart.Add(-time.Second) + default: // year + curStart = time.Date(y, 1, 1, 0, 0, 0, 0, loc) + curEnd = time.Date(y+1, 1, 1, 0, 0, 0, 0, loc).Add(-time.Second) + prevStart = time.Date(y-1, 1, 1, 0, 0, 0, 0, loc) + prevEnd = curStart.Add(-time.Second) + } + return +} + +// crmBucketExpr 按时间维度返回分桶表达式(0..n-1 的整数桶序号)。 +func crmBucketExpr(r, dateCol string) string { + switch r { + case "day": + return fmt.Sprintf("FLOOR(HOUR(%s)/2)", dateCol) // 每 2 小时一桶,0~11 + case "week": + return fmt.Sprintf("WEEKDAY(%s)", dateCol) // 周一=0 ~ 周日=6 + case "month": + return fmt.Sprintf("DAY(%s)-1", dateCol) // 1日=0 + case "quarter": + return fmt.Sprintf("(MONTH(%s)-1) %% 3", dateCol) // 季度内月序号 0~2 + default: + return fmt.Sprintf("MONTH(%s)-1", dateCol) // 年内月序号 0~11 + } +} + +// crmAxis 返回与分桶对齐的 X 轴文案。 +func crmAxis(r string, start time.Time) []string { + loc := start.Location() + switch r { + case "day": + ax := make([]string, 12) + for i := 0; i < 12; i++ { + ax[i] = fmt.Sprintf("%02d:00", i*2) + } + return ax + case "week": + return []string{"周一", "周二", "周三", "周四", "周五", "周六", "周日"} + case "month": + last := time.Date(start.Year(), start.Month()+1, 0, 0, 0, 0, 0, loc).Day() + ax := make([]string, last) + for i := 0; i < last; i++ { + ax[i] = fmt.Sprintf("%d日", i+1) + } + return ax + case "quarter": + q := (int(start.Month()) - 1) / 3 + ax := make([]string, 3) + for i := 0; i < 3; i++ { + ax[i] = fmt.Sprintf("%d月", q*3+i+1) + } + return ax + default: + ax := make([]string, 12) + for i := 0; i < 12; i++ { + ax[i] = fmt.Sprintf("%d月", i+1) + } + return ax + } +} + +/* ------------------------------ 聚合查询 ------------------------------ */ + +func crmCountWhere(tenantID, table, dateCol, extra string, start, end time.Time) float64 { + var n int64 + sql := fmt.Sprintf( + "SELECT COUNT(*) FROM %s WHERE tenant_id=? AND delete_time IS NULL AND %s BETWEEN ? AND ? %s", + table, dateCol, extra, + ) + if err := models.Orm.Raw(sql, tenantID, start, end).QueryRow(&n); err != nil { + return 0 + } + return float64(n) +} + +func crmSumWhere(tenantID, table, dateCol, sumCol, extra string, start, end time.Time) float64 { + var s float64 + sql := fmt.Sprintf( + "SELECT COALESCE(SUM(%s),0) FROM %s WHERE tenant_id=? AND delete_time IS NULL AND %s BETWEEN ? AND ? %s", + sumCol, table, dateCol, extra, + ) + if err := models.Orm.Raw(sql, tenantID, start, end).QueryRow(&s); err != nil { + return 0 + } + return s +} + +// crmSeriesWhere 按时间分桶返回序列(长度 = bucketCount,缺失桶补 0)。 +func crmSeriesWhere(tenantID, table, dateCol, metric, sumCol, extra, bucketExpr string, bucketCount int, start, end time.Time) []float64 { + out := make([]float64, bucketCount) + var rows []struct { + B int `orm:"column(b)"` + V float64 `orm:"column(v)"` + } + var sel string + if metric == "sum" { + sel = fmt.Sprintf("SELECT %s AS b, COALESCE(SUM(%s),0) AS v", bucketExpr, sumCol) + } else { + sel = fmt.Sprintf("SELECT %s AS b, COUNT(*) AS v", bucketExpr) + } + sql := fmt.Sprintf( + "%s FROM %s WHERE tenant_id=? AND delete_time IS NULL AND %s BETWEEN ? AND ? %s GROUP BY b", + sel, table, dateCol, extra, + ) + if _, err := models.Orm.Raw(sql, tenantID, start, end).QueryRows(&rows); err != nil { + return out + } + for _, row := range rows { + if row.B >= 0 && row.B < bucketCount { + out[row.B] = row.V + } + } + return out +} + +func crmRound1(v float64) float64 { + return float64(int64(v*10+0.5)) / 10 +} + +/* ------------------------------ 实体定义 ------------------------------ */ + +type dashEntity struct { + table string + dateCol string + extra string +} + +// crmCountEntities 计数的实体(新增客户/联系人/项目/合同/线索/商机/回访)。 +func crmCountEntities() map[string]dashEntity { + return map[string]dashEntity{ + "newCustomer": {"yz_backend_erp_customer", "create_time", " AND in_pool=0 AND is_draft=0"}, + "newContact": {"yz_backend_contact_company", "create_time", " AND company_type='customer'"}, + "newProject": {"yz_backend_crm_project", "create_time", ""}, + "newContract": {"yz_backend_crm_contract", "create_time", ""}, + "newClue": {"yz_backend_crm_clue", "create_time", ""}, + "newChance": {"yz_backend_crm_business", "create_time", ""}, + "newVisit": {"yz_backend_crm_follow", "follow_time", " AND follow_time IS NOT NULL"}, + } +} + +// crmSumEntities 求和的实体(各金额)。 +func crmSumEntities() map[string]dashEntity { + return map[string]dashEntity{ + "projectAmount": {"yz_backend_crm_project", "create_time", ""}, + "contractAmount": {"yz_backend_crm_contract", "create_time", ""}, + "chanceAmount": {"yz_backend_crm_business", "create_time", ""}, + "paymentAmount": {"yz_backend_crm_payback", "create_time", ""}, + } +} + +func crmAmountCol(key string) string { + switch key { + case "projectAmount": + return "amount" + case "contractAmount": + return "total_amount" + case "chanceAmount": + return "amount" + case "paymentAmount": + return "received_amount" + } + return "amount" +} + +/* ------------------------------ 主接口 ------------------------------ */ + +// Summary GET /backend/crm/dashboard?range=day|week|month|quarter|year +// 返回仪表盘所需的全部真实统计数据。 +func (c *BackendCrmDashboardController) Summary() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + tenantID := pipelineTenantID(claims) + + r := strings.TrimSpace(c.GetString("range")) + valid := map[string]bool{"day": true, "week": true, "month": true, "quarter": true, "year": true} + if !valid[r] { + r = "month" + } + + curStart, curEnd, prevStart, prevEnd := crmPeriod(r) + axis := crmAxis(r, curStart) + bucketCount := len(axis) + bucketExpr := crmBucketExpr(r, "create_time") + + counts := crmCountEntities() + sums := crmSumEntities() + statKeys := []string{ + "newCustomer", "newContact", "newProject", "newContract", "newClue", + "newChance", "projectAmount", "contractAmount", "chanceAmount", "paymentAmount", "newVisit", + } + + stats := map[string]map[string]float64{} + for _, k := range statKeys { + var cur, prev float64 + if e, ok := counts[k]; ok { + cur = crmCountWhere(tenantID, e.table, e.dateCol, e.extra, curStart, curEnd) + prev = crmCountWhere(tenantID, e.table, e.dateCol, e.extra, prevStart, prevEnd) + } else if e, ok := sums[k]; ok { + col := crmAmountCol(k) + cur = crmSumWhere(tenantID, e.table, e.dateCol, col, e.extra, curStart, curEnd) + prev = crmSumWhere(tenantID, e.table, e.dateCol, col, e.extra, prevStart, prevEnd) + } + var trend float64 + if prev > 0 { + trend = (cur - prev) / prev * 100 + } else if cur > 0 { + trend = 100 + } + stats[k] = map[string]float64{"value": crmRound1(cur), "trend": crmRound1(trend)} + } + + // 趋势图:新增客户 / 新增商机 按桶分布 + customerSeries := crmSeriesWhere(tenantID, counts["newCustomer"].table, counts["newCustomer"].dateCol, + "count", "", counts["newCustomer"].extra, crmBucketExpr(r, counts["newCustomer"].dateCol), bucketCount, curStart, curEnd) + chanceSeries := crmSeriesWhere(tenantID, counts["newChance"].table, counts["newChance"].dateCol, + "count", "", counts["newChance"].extra, crmBucketExpr(r, counts["newChance"].dateCol), bucketCount, curStart, curEnd) + + // 金额对比图:合同金额 / 回款金额 按桶分布 + contractSeries := crmSeriesWhere(tenantID, sums["contractAmount"].table, sums["contractAmount"].dateCol, + "sum", "total_amount", sums["contractAmount"].extra, bucketExpr, bucketCount, curStart, curEnd) + paymentSeries := crmSeriesWhere(tenantID, sums["paymentAmount"].table, sums["paymentAmount"].dateCol, + "sum", "received_amount", sums["paymentAmount"].extra, crmBucketExpr(r, sums["paymentAmount"].dateCol), bucketCount, curStart, curEnd) + + // 销售漏斗:线索 / 商机 / 合同 / 回款(期内各实体新增数) + funnel := []map[string]interface{}{ + {"name": "线索", "value": crmCountWhere(tenantID, counts["newClue"].table, counts["newClue"].dateCol, counts["newClue"].extra, curStart, curEnd)}, + {"name": "商机", "value": crmCountWhere(tenantID, counts["newChance"].table, counts["newChance"].dateCol, counts["newChance"].extra, curStart, curEnd)}, + {"name": "合同", "value": crmCountWhere(tenantID, counts["newContract"].table, counts["newContract"].dateCol, counts["newContract"].extra, curStart, curEnd)}, + {"name": "回款", "value": crmCountWhere(tenantID, sums["paymentAmount"].table, sums["paymentAmount"].dateCol, sums["paymentAmount"].extra, curStart, curEnd)}, + } + + // 线索来源分布 + var srcRows []struct { + Source string `orm:"column(source)"` + Cnt float64 `orm:"column(cnt)"` + } + srcSQL := "SELECT source, COUNT(*) AS cnt FROM yz_backend_crm_clue WHERE tenant_id=? AND delete_time IS NULL AND create_time BETWEEN ? AND ? GROUP BY source ORDER BY cnt DESC" + _, _ = models.Orm.Raw(srcSQL, tenantID, curStart, curEnd).QueryRows(&srcRows) + source := make([]map[string]interface{}, 0, len(srcRows)) + for _, row := range srcRows { + name := strings.TrimSpace(row.Source) + if name == "" { + name = "未填写" + } + source = append(source, map[string]interface{}{"name": name, "value": row.Cnt}) + } + + // 销售业绩排行:按负责人合同金额 Top8,并补齐回款金额 + var rankRows []struct { + OwnerUserID string `orm:"column(owner_user_id)"` + OwnerUserName string `orm:"column(owner_user_name)"` + Contract float64 `orm:"column(contract)"` + } + rankSQL := "SELECT owner_user_id, owner_user_name, COALESCE(SUM(total_amount),0) AS contract FROM yz_backend_crm_contract WHERE tenant_id=? AND delete_time IS NULL AND create_time BETWEEN ? AND ? GROUP BY owner_user_id, owner_user_name ORDER BY contract DESC LIMIT 8" + _, _ = models.Orm.Raw(rankSQL, tenantID, curStart, curEnd).QueryRows(&rankRows) + + paymentMap := map[string]float64{} + var payRows []struct { + OwnerUserID string `orm:"column(owner_user_id)"` + Payment float64 `orm:"column(payment)"` + } + paySQL := "SELECT owner_user_id, COALESCE(SUM(received_amount),0) AS payment FROM yz_backend_crm_payback WHERE tenant_id=? AND delete_time IS NULL AND create_time BETWEEN ? AND ? GROUP BY owner_user_id" + _, _ = models.Orm.Raw(paySQL, tenantID, curStart, curEnd).QueryRows(&payRows) + for _, row := range payRows { + paymentMap[row.OwnerUserID] = row.Payment + } + + rank := make([]map[string]interface{}, 0, len(rankRows)) + for _, row := range rankRows { + name := strings.TrimSpace(row.OwnerUserName) + if name == "" { + name = "未知" + } + rank = append(rank, map[string]interface{}{ + "name": name, + "dept": "", + "contract": crmRound1(row.Contract), + "payment": crmRound1(paymentMap[row.OwnerUserID]), + }) + } + + // 最新回访 + var visitRows []struct { + RelatedName string `orm:"column(related_name)"` + FollowType string `orm:"column(follow_type)"` + Content string `orm:"column(content)"` + OwnerUserName string `orm:"column(owner_user_name)"` + FollowTime *time.Time `orm:"column(follow_time)"` + } + visitSQL := "SELECT related_name, follow_type, content, owner_user_name, follow_time FROM yz_backend_crm_follow WHERE tenant_id=? AND delete_time IS NULL AND follow_time IS NOT NULL ORDER BY follow_time DESC LIMIT 6" + _, _ = models.Orm.Raw(visitSQL, tenantID).QueryRows(&visitRows) + visits := make([]map[string]interface{}, 0, len(visitRows)) + for _, row := range visitRows { + way := strings.TrimSpace(row.FollowType) + if way == "" { + way = "其他" + } + user := strings.TrimSpace(row.OwnerUserName) + if user == "" { + user = "未知" + } + timeStr := "" + if row.FollowTime != nil { + timeStr = row.FollowTime.Format("01-02 15:04") + } + visits = append(visits, map[string]interface{}{ + "customer": row.RelatedName, + "way": way, + "content": row.Content, + "user": user, + "time": timeStr, + }) + } + + pipelineOk(&c.Controller, map[string]interface{}{ + "range": r, + "stats": stats, + "trend": map[string]interface{}{ + "axis": axis, + "customer": customerSeries, + "chance": chanceSeries, + }, + "funnel": funnel, + "amount": map[string]interface{}{ + "axis": axis, + "contract": contractSeries, + "payment": paymentSeries, + }, + "source": source, + "rank": rank, + "visits": visits, + }) +} diff --git a/go/controllers/backend_crm_payback.go b/go/controllers/backend_crm_payback.go new file mode 100644 index 0000000..c184b70 --- /dev/null +++ b/go/controllers/backend_crm_payback.go @@ -0,0 +1,646 @@ +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" +) + +// BackendCrmPaybackController CRM 回款管理 +// +// 回款计划针对于合同创建: +// - plan_type 回款周期:1月度/2季度/3年度/4进度/5自定义; +// - 月度/季度/年度:明细为「期数 + 支付金额」; +// - 进度:明细为「进度名称 + 进度百分比 + 进度金额」; +// - 自定义:明细为「名称 + 计划日期 + 金额」; +// - 回款进度:按计划明细逐期登记实收金额/日期。 +type BackendCrmPaybackController struct { + beego.Controller +} + +// paybackItem 计划明细行(items JSON 元素)。 +type paybackItem struct { + Seq int `json:"seq"` + Name string `json:"name"` + Percent float64 `json:"percent"` + Amount float64 `json:"amount"` + PlanDate string `json:"plan_date"` + ReceivedAmount float64 `json:"received_amount"` + ReceivedDate string `json:"received_date"` + ReceiptURL string `json:"receipt_url"` // 流水回执地址(选填) + ReceiptName string `json:"receipt_name"` // 流水回执文件名(选填) + Status int8 `json:"status"` // 1未回款/2部分回款/3已回款 +} + +// paybackPayload 创建 / 更新请求体。 +type paybackPayload struct { + ContractID uint64 `json:"contract_id"` + ContractNo string `json:"contract_no"` + ContractName string `json:"contract_name"` + CustomerID uint64 `json:"customer_id"` + CustomerName string `json:"customer_name"` + PlanType int8 `json:"plan_type"` + PayMethod int8 `json:"pay_method"` + RemindDays int `json:"remind_days"` + OwnerUserID string `json:"owner_user_id"` + OwnerUserName string `json:"owner_user_name"` + ContractAmount float64 `json:"contract_amount"` + Items json.RawMessage `json:"items"` + Remark string `json:"remark"` + Status int8 `json:"status"` + ReceiptURL string `json:"receipt_url"` // 流水回执地址(选填) + ReceiptName string `json:"receipt_name"` // 流水回执文件名(选填) +} + +// paybackResp 列表 / 详情响应:items 解析为 JSON 数组透出。 +type paybackResp struct { + models.TenantCrmPayback + Items json.RawMessage `json:"items"` +} + +// paybackProgress 回款进度行(跨计划摊平的计划明细)。 +type paybackProgress struct { + PaybackID uint64 `json:"payback_id"` + ContractID *uint64 `json:"contract_id"` + ContractNo string `json:"contract_no"` + ContractName string `json:"contract_name"` + CustomerName string `json:"customer_name"` + OwnerUserName string `json:"owner_user_name"` + PlanType int8 `json:"plan_type"` + PayMethod int8 `json:"pay_method"` + Seq int `json:"seq"` + Name string `json:"name"` + Percent float64 `json:"percent"` + Amount float64 `json:"amount"` + PlanDate string `json:"plan_date"` + ReceivedAmount float64 `json:"received_amount"` + ReceivedDate string `json:"received_date"` + Status int8 `json:"status"` +} + +// List GET /backend/crm/payback/list +func (c *BackendCrmPaybackController) 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")) + planType := strings.TrimSpace(c.GetString("plan_type")) + payMethod := strings.TrimSpace(c.GetString("pay_method")) + status := strings.TrimSpace(c.GetString("status")) + contractID := strings.TrimSpace(c.GetString("contract_id")) + + tenantID := pipelineTenantID(claims) + cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true) + if keyword != "" { + cond = cond.AndCond(orm.NewCondition(). + Or("contract_name__contains", keyword). + Or("contract_no__contains", keyword). + Or("customer_name__contains", keyword). + Or("owner_user_name__contains", keyword)) + } + if planType != "" { + cond = cond.And("plan_type", planType) + } + if payMethod != "" { + cond = cond.And("pay_method", payMethod) + } + if status != "" { + cond = cond.And("status", status) + } + if contractID != "" { + if cid, err := strconv.ParseUint(contractID, 10, 64); err == nil && cid > 0 { + cond = cond.And("contract_id", cid) + } + } + qs := models.Orm.QueryTable(new(models.TenantCrmPayback)).SetCond(cond) + + total, _ := qs.Count() + var list []models.TenantCrmPayback + if total > 0 { + _, _ = qs.OrderBy("-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list) + } + items := make([]paybackResp, 0, len(list)) + for i := range list { + items = append(items, buildPaybackResp(&list[i])) + } + + // 汇总(当前筛选条件下全部计划的计划总额 / 已回款 / 待回款),用于列表页统计卡片 + var all []models.TenantCrmPayback + _, _ = models.Orm.QueryTable(new(models.TenantCrmPayback)).SetCond(cond).All(&all) + var summaryTotal, summaryReceived float64 + for i := range all { + summaryTotal = round2(summaryTotal + all[i].TotalAmount) + summaryReceived = round2(summaryReceived + all[i].ReceivedAmount) + } + pipelineOk(&c.Controller, map[string]interface{}{ + "list": items, "total": total, "page": page, "pageSize": pageSize, + "summary": map[string]interface{}{ + "total_amount": summaryTotal, + "received_amount": summaryReceived, + "pending_amount": round2(summaryTotal - summaryReceived), + }, + }) +} + +// Detail GET /backend/crm/payback/:id +func (c *BackendCrmPaybackController) 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.TenantCrmPayback + if err := models.Orm.QueryTable(new(models.TenantCrmPayback)). + 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, buildPaybackResp(&row)) +} + +// Create POST /backend/crm/payback +func (c *BackendCrmPaybackController) 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 paybackPayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + if p.ContractID == 0 { + pipelineErr(&c.Controller, 400, 400, "请先选择关联合同") + return + } + tenantID := pipelineTenantID(claims) + + itemsJSON, totalAmount, err := normalizePaybackItems(p.Items) + if err != nil { + pipelineErr(&c.Controller, 400, 400, "计划明细格式错误") + return + } + if totalAmount <= 0 { + pipelineErr(&c.Controller, 400, 400, "计划回款总额不能为 0") + return + } + + now := time.Now() + row := models.TenantCrmPayback{ + TenantID: tenantID, + ContractNo: strings.TrimSpace(p.ContractNo), + ContractName: strings.TrimSpace(p.ContractName), + CustomerName: strings.TrimSpace(p.CustomerName), + PlanType: pickInt8(p.PlanType, 1, 5), + PayMethod: pickInt8(p.PayMethod, 1, 7), + RemindDays: p.RemindDays, + OwnerUserID: firstNonEmpty(p.OwnerUserID, pipelineUID(claims)), + OwnerUserName: firstNonEmpty(p.OwnerUserName, resolveUserName(claims)), + ContractAmount: round2(p.ContractAmount), + TotalAmount: totalAmount, + Status: pickInt8(p.Status, 1, 2), + Items: itemsJSON, + Remark: p.Remark, + ReceiptURL: strings.TrimSpace(p.ReceiptURL), + ReceiptName: strings.TrimSpace(p.ReceiptName), + CreateUserID: pipelineUID(claims), + CreateTime: now, + UpdateTime: now, + } + c.fillFromContract(tenantID, p.ContractID, &row) + applyPaybackReceived(&row) + + if _, err := models.Orm.Insert(&row); err != nil { + pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) + return + } + crmWriteLog(tenantID, 4, row.ID, "create", "创建回款计划:"+row.ContractName, claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": row.ID}) +} + +// Update PUT /backend/crm/payback/:id +func (c *BackendCrmPaybackController) 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 paybackPayload + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + tenantID := pipelineTenantID(claims) + var row models.TenantCrmPayback + if err := models.Orm.QueryTable(new(models.TenantCrmPayback)). + Filter("id", id).Filter("tenant_id", tenantID). + Filter("delete_time__isnull", true).One(&row); err != nil { + pipelineErr(&c.Controller, 404, 404, "回款计划未找到") + return + } + + if p.ContractID > 0 { + row.ContractID = &p.ContractID + row.ContractNo = strings.TrimSpace(p.ContractNo) + row.ContractName = strings.TrimSpace(p.ContractName) + row.CustomerName = strings.TrimSpace(p.CustomerName) + c.fillFromContract(tenantID, p.ContractID, &row) + } + if p.PlanType != 0 { + row.PlanType = pickInt8(p.PlanType, 1, 5) + } + if p.PayMethod != 0 { + row.PayMethod = pickInt8(p.PayMethod, 1, 7) + } + row.RemindDays = p.RemindDays + if strings.TrimSpace(p.OwnerUserID) != "" { + row.OwnerUserID = strings.TrimSpace(p.OwnerUserID) + } + if strings.TrimSpace(p.OwnerUserName) != "" { + row.OwnerUserName = strings.TrimSpace(p.OwnerUserName) + } + if p.ContractAmount > 0 { + row.ContractAmount = round2(p.ContractAmount) + } + row.Remark = p.Remark + row.ReceiptURL = strings.TrimSpace(p.ReceiptURL) + row.ReceiptName = strings.TrimSpace(p.ReceiptName) + if p.Status != 0 { + row.Status = pickInt8(p.Status, 1, 2) + } + if p.Items != nil { + itemsJSON, _, err := normalizePaybackItems(p.Items) + if err != nil { + pipelineErr(&c.Controller, 400, 400, "计划明细格式错误") + return + } + row.Items = itemsJSON + } + applyPaybackReceived(&row) + row.UpdateTime = time.Now() + + if _, err := models.Orm.Update(&row); err != nil { + pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error()) + return + } + crmWriteLog(tenantID, 4, row.ID, "update", "更新回款计划:"+row.ContractName, claims) + pipelineOk(&c.Controller, map[string]interface{}{"id": row.ID}) +} + +// Delete DELETE /backend/crm/payback/:id +func (c *BackendCrmPaybackController) 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.TenantCrmPayback + if err := models.Orm.QueryTable(new(models.TenantCrmPayback)). + Filter("id", id).Filter("tenant_id", tenantID). + Filter("delete_time__isnull", true).One(&row); err != nil { + pipelineErr(&c.Controller, 404, 404, "回款计划未找到") + return + } + if !canDeleteCrmRecord(claims, row.CreateUserID) { + pipelineErr(&c.Controller, 403, 403, "只有创建人、租户管理员或平台管理员可以删除该回款计划") + return + } + now := time.Now() + _, err = models.Orm.QueryTable(new(models.TenantCrmPayback)). + Filter("id", id).Filter("tenant_id", tenantID). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + pipelineOk(&c.Controller, nil) +} + +// ProgressList GET /backend/crm/payback/progress/list +// 回款进度:把全部回款计划的分期明细摊平返回,并汇总计划总额 / 已回款总额。 +func (c *BackendCrmPaybackController) ProgressList() { + claims, err := pipelineClaims(&c.Controller) + if err != nil { + pipelineErr(&c.Controller, 401, 401, err.Error()) + return + } + tenantID := pipelineTenantID(claims) + keyword := strings.TrimSpace(c.GetString("keyword")) + status := strings.TrimSpace(c.GetString("status")) + contractID := strings.TrimSpace(c.GetString("contract_id")) + + cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true) + if keyword != "" { + cond = cond.AndCond(orm.NewCondition(). + Or("contract_name__contains", keyword). + Or("customer_name__contains", keyword). + Or("owner_user_name__contains", keyword)) + } + if contractID != "" { + if cid, err := strconv.ParseUint(contractID, 10, 64); err == nil && cid > 0 { + cond = cond.And("contract_id", cid) + } + } + var list []models.TenantCrmPayback + _, _ = models.Orm.QueryTable(new(models.TenantCrmPayback)).SetCond(cond).OrderBy("-id").All(&list) + + rows := make([]paybackProgress, 0) + var totalPlanned, totalReceived float64 + for i := range list { + p := list[i] + for _, it := range parsePaybackItems(p.Items) { + st := it.Status + if st == 0 { + st = itemStatus(it.ReceivedAmount, it.Amount) + } + if status != "" && fmt.Sprintf("%d", st) != status { + continue + } + totalPlanned = round2(totalPlanned + it.Amount) + totalReceived = round2(totalReceived + it.ReceivedAmount) + rows = append(rows, paybackProgress{ + PaybackID: p.ID, + ContractID: p.ContractID, + ContractNo: p.ContractNo, + ContractName: p.ContractName, + CustomerName: p.CustomerName, + OwnerUserName: p.OwnerUserName, + PlanType: p.PlanType, + PayMethod: p.PayMethod, + Seq: it.Seq, + Name: it.Name, + Percent: it.Percent, + Amount: it.Amount, + PlanDate: it.PlanDate, + ReceivedAmount: it.ReceivedAmount, + ReceivedDate: it.ReceivedDate, + Status: st, + }) + } + } + pipelineOk(&c.Controller, map[string]interface{}{ + "list": rows, "total": len(rows), + "total_planned": totalPlanned, "total_received": totalReceived, + }) +} + +// Receive POST /backend/crm/payback/:id/receive +// 回款进度登记:按明细序号写入实收金额 / 回款日期,并重算计划汇总。 +func (c *BackendCrmPaybackController) Receive() { + 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 struct { + Seq int `json:"seq"` + ReceivedAmount float64 `json:"received_amount"` + ReceivedDate string `json:"received_date"` + ReceiptURL string `json:"receipt_url"` // 流水回执地址(选填) + ReceiptName string `json:"receipt_name"` // 流水回执文件名(选填) + } + if err := json.Unmarshal(raw, &p); err != nil { + pipelineErr(&c.Controller, 400, 400, "参数错误") + return + } + tenantID := pipelineTenantID(claims) + var row models.TenantCrmPayback + if err := models.Orm.QueryTable(new(models.TenantCrmPayback)). + Filter("id", id).Filter("tenant_id", tenantID). + Filter("delete_time__isnull", true).One(&row); err != nil { + pipelineErr(&c.Controller, 404, 404, "回款计划未找到") + return + } + items := parsePaybackItems(row.Items) + matched := false + for i := range items { + if items[i].Seq != p.Seq { + continue + } + items[i].ReceivedAmount = round2(p.ReceivedAmount) + items[i].ReceivedDate = strings.TrimSpace(p.ReceivedDate) + items[i].ReceiptURL = strings.TrimSpace(p.ReceiptURL) + items[i].ReceiptName = strings.TrimSpace(p.ReceiptName) + items[i].Status = itemStatus(items[i].ReceivedAmount, items[i].Amount) + matched = true + break + } + if !matched { + pipelineErr(&c.Controller, 404, 404, "计划明细不存在") + return + } + out, err := json.Marshal(items) + if err != nil { + pipelineErr(&c.Controller, 500, 500, "保存失败") + return + } + row.Items = string(out) + applyPaybackReceived(&row) + row.UpdateTime = time.Now() + if _, err := models.Orm.Update(&row); err != nil { + pipelineErr(&c.Controller, 500, 500, "保存失败: "+err.Error()) + return + } + crmWriteLog(tenantID, 4, row.ID, "receive", fmt.Sprintf("回款登记:%s 第 %d 期 %s 元", row.ContractName, p.Seq, formatAmount(p.ReceivedAmount)), claims) + pipelineOk(&c.Controller, buildPaybackResp(&row)) +} + +// ========================== 内部辅助 ========================== + +// buildPaybackResp 组装响应:items 透传 JSON 数组。 +func buildPaybackResp(row *models.TenantCrmPayback) paybackResp { + items := json.RawMessage("[]") + if strings.TrimSpace(row.Items) != "" { + items = json.RawMessage(row.Items) + } + return paybackResp{TenantCrmPayback: *row, Items: items} +} + +// parsePaybackItems 解析明细 JSON(容错:空 / 非法时返回空数组)。 +func parsePaybackItems(raw string) []paybackItem { + items := make([]paybackItem, 0) + if strings.TrimSpace(raw) == "" { + return items + } + _ = json.Unmarshal([]byte(raw), &items) + return items +} + +// normalizePaybackItems 校验并规范化明细 JSON:补齐序号、金额取两位小数,返回紧凑 JSON 与计划总额。 +func normalizePaybackItems(raw json.RawMessage) (string, float64, error) { + if len(raw) == 0 || strings.TrimSpace(string(raw)) == "" || strings.TrimSpace(string(raw)) == "null" { + return "[]", 0, nil + } + var arr []map[string]interface{} + if err := json.Unmarshal(raw, &arr); err != nil { + return "", 0, err + } + items := make([]paybackItem, 0, len(arr)) + var total float64 + for i, m := range arr { + it := paybackItem{ + Seq: i + 1, + Name: strings.TrimSpace(strVal(m["name"])), + Percent: round2(toFloat64(m["percent"])), + Amount: round2(toFloat64(m["amount"])), + PlanDate: strings.TrimSpace(strVal(m["plan_date"])), + ReceivedAmount: round2(toFloat64(m["received_amount"])), + ReceivedDate: strings.TrimSpace(strVal(m["received_date"])), + } + if v, ok := m["receipt_url"]; ok && v != nil { + it.ReceiptURL = strings.TrimSpace(fmt.Sprintf("%v", v)) + } + if v, ok := m["receipt_name"]; ok && v != nil { + it.ReceiptName = strings.TrimSpace(fmt.Sprintf("%v", v)) + } + if v, ok := m["seq"]; ok { + if s := int(toFloat64(v)); s > 0 { + it.Seq = s + } + } + it.Status = itemStatus(it.ReceivedAmount, it.Amount) + total = round2(total + it.Amount) + items = append(items, it) + } + out, err := json.Marshal(items) + if err != nil { + return "", 0, err + } + return string(out), total, nil +} + +// itemStatus 由实收金额推导明细状态:0/空=未回款 1、部分回款 2、已回款 3。 +func itemStatus(received, amount float64) int8 { + if amount > 0 && received >= amount { + return 3 + } + if received > 0 { + return 2 + } + return 1 +} + +// strVal 安全取字符串:nil / 非字符串时返回空串,避免写出 ""。 +func strVal(v interface{}) string { + if v == nil { + return "" + } + if s, ok := v.(string); ok { + return s + } + return fmt.Sprintf("%v", v) +} + +// applyPaybackReceived 按明细重算计划总额、已回款金额与状态。 +func applyPaybackReceived(row *models.TenantCrmPayback) { + items := parsePaybackItems(row.Items) + var total, received float64 + allDone := len(items) > 0 + for i := range items { + if items[i].Status == 0 { + items[i].Status = itemStatus(items[i].ReceivedAmount, items[i].Amount) + } + if items[i].Status != 3 { + allDone = false + } + total = round2(total + items[i].Amount) + received = round2(received + items[i].ReceivedAmount) + } + row.TotalAmount = total + row.ReceivedAmount = received + if out, err := json.Marshal(items); err == nil { + row.Items = string(out) + } + // 状态:1进行中 / 2已完成(全部明细回款完成) + if allDone { + row.Status = 2 + } else if row.Status == 0 { + row.Status = 1 + } +} + +// fillFromContract 由关联合同补齐合同编号 / 名称 / 客户 / 合同金额(仅在字段为空时填充)。 +func (c *BackendCrmPaybackController) fillFromContract(tenantID string, contractID uint64, row *models.TenantCrmPayback) { + if contractID == 0 { + return + } + var ct models.TenantCrmContract + if err := models.Orm.QueryTable(new(models.TenantCrmContract)). + Filter("id", contractID).Filter("tenant_id", tenantID). + Filter("delete_time__isnull", true).One(&ct); err != nil { + return + } + row.ContractID = &ct.ID + if row.ContractNo == "" { + row.ContractNo = ct.ContractNo + } + if row.ContractName == "" { + row.ContractName = ct.ContractName + } + if row.ContractAmount <= 0 { + row.ContractAmount = ct.TotalAmount + } + if row.CustomerName == "" { + var parties []map[string]interface{} + _ = json.Unmarshal([]byte(ct.Parties), &parties) + for _, party := range parties { + if fmt.Sprintf("%v", party["ref_type"]) != "1" { + continue + } + row.CustomerName = strings.TrimSpace(strVal(party["ref_name"])) + if cid := uint64(toFloat64(party["ref_id"])); cid > 0 { + row.CustomerID = &cid + } + break + } + } +} + +// formatAmount 金额文案(用于操作日志)。 +func formatAmount(n float64) string { + return strconv.FormatFloat(round2(n), 'f', 2, 64) +} diff --git a/go/controllers/backend_crm_product.go b/go/controllers/backend_crm_product.go index a51807d..7458af2 100644 --- a/go/controllers/backend_crm_product.go +++ b/go/controllers/backend_crm_product.go @@ -20,16 +20,19 @@ type BackendCrmProductController struct { } 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"` + 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"` + LineType string `json:"line_type"` + DevUnitPrice float64 `json:"dev_unit_price"` + DevTree json.RawMessage `json:"dev_tree"` + Status int8 `json:"status"` + Remark string `json:"remark"` } // crmProductItem 项目产品清单中的单行(也用于项目生成时写入产品管理)。 @@ -139,6 +142,10 @@ func (c *BackendCrmProductController) Create() { if status != 0 && status != 1 { status = 1 } + lineType := strings.TrimSpace(p.LineType) + if lineType != "dev" { + lineType = "product" + } now := time.Now() row := models.TenantCrmProduct{ TenantID: pipelineTenantID(claims), @@ -150,6 +157,9 @@ func (c *BackendCrmProductController) Create() { Price: p.Price, CostPrice: p.CostPrice, TaxRate: p.TaxRate, + LineType: lineType, + DevUnitPrice: p.DevUnitPrice, + DevTree: string(p.DevTree), Status: status, Remark: p.Remark, CreateUserID: pipelineUID(claims), @@ -198,21 +208,28 @@ func (c *BackendCrmProductController) Update() { if status != 0 && status != 1 { status = row.Status } + lineType := strings.TrimSpace(p.LineType) + if lineType != "dev" { + lineType = "product" + } 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, + "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, + "line_type": lineType, + "dev_unit_price": p.DevUnitPrice, + "dev_tree": p.DevTree, + "status": status, + "remark": p.Remark, + "update_time": now, }) if err != nil { pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error()) @@ -371,6 +388,10 @@ func SyncContractProducts(tenantID, uid string, raw json.RawMessage) (json.RawMe } now := time.Now() for _, it := range items { + // 软件开发行(line_type=dev)不走产品管理建档,仅保留模块树 + if fmt.Sprintf("%v", it["line_type"]) == "dev" { + continue + } name := strings.TrimSpace(fmt.Sprintf("%v", it["name"])) if name == "" { continue diff --git a/go/controllers/backend_crm_support.go b/go/controllers/backend_crm_support.go index d6d4c46..897c3bf 100644 --- a/go/controllers/backend_crm_support.go +++ b/go/controllers/backend_crm_support.go @@ -287,9 +287,20 @@ func (c *BackendCrmAttachController) List() { includeSource := strings.TrimSpace(c.GetString("include_source")) == "1" tenantID := pipelineTenantID(claims) + page, _ := strconv.Atoi(c.GetString("page")) + pageSize, _ := strconv.Atoi(c.GetString("pageSize")) + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 20 + } + offset := (page - 1) * pageSize + var list []models.TenantCrmAttach + total := 0 if includeSource && relatedType != "" && relatedID != "" && relatedID != "0" { - // 链路模式:线索 → 商机 → 项目 累积 + // 链路模式:线索 → 商机 → 项目 累积(跨表汇总后内存分页) rid, _ := strconv.ParseUint(relatedID, 10, 64) rt, _ := strconv.Atoi(relatedType) for _, r := range relatedChain(tenantID, int8(rt), rid) { @@ -303,6 +314,16 @@ func (c *BackendCrmAttachController) List() { list = append(list, rows...) } sort.Slice(list, func(i, j int) bool { return list[i].ID > list[j].ID }) + total = len(list) + start := offset + if start > total { + start = total + } + end := start + pageSize + if end > total { + end = total + } + list = list[start:end] } else { cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true) if relatedType != "" { @@ -311,9 +332,18 @@ func (c *BackendCrmAttachController) List() { if relatedID != "" { cond = cond.And("related_id", relatedID) } - _, _ = models.Orm.QueryTable(new(models.TenantCrmAttach)).SetCond(cond).OrderBy("-id").All(&list) + qs := models.Orm.QueryTable(new(models.TenantCrmAttach)).SetCond(cond) + if cnt, err := qs.Count(); err == nil { + total = int(cnt) + } + _, _ = qs.OrderBy("-id").Limit(pageSize, offset).All(&list) } - pipelineOk(&c.Controller, map[string]interface{}{"list": list, "total": len(list)}) + pipelineOk(&c.Controller, map[string]interface{}{ + "list": list, + "total": total, + "page": page, + "pageSize": pageSize, + }) } // Add POST /backend/crm/attach/add diff --git a/go/docs/sql/add_storage_config_table.sql b/go/docs/sql/add_storage_config_table.sql deleted file mode 100644 index 388ab82..0000000 --- a/go/docs/sql/add_storage_config_table.sql +++ /dev/null @@ -1,18 +0,0 @@ --- 创建存储配置表 -CREATE TABLE IF NOT EXISTS `yz_system_storage_config` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `storage_type` varchar(20) NOT NULL DEFAULT 'local' COMMENT '存储类型: local-本地存储, qiniu-七牛云', - `qiniu_access_key` varchar(255) DEFAULT NULL COMMENT '七牛云AccessKey', - `qiniu_secret_key` varchar(255) DEFAULT NULL COMMENT '七牛云SecretKey', - `qiniu_bucket` varchar(128) DEFAULT NULL COMMENT '七牛云Bucket名称', - `qiniu_domain` varchar(255) DEFAULT NULL COMMENT '七牛云CDN域名', - `qiniu_region` varchar(50) DEFAULT NULL COMMENT '七牛云存储区域', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统存储配置表'; - --- 插入默认配置(本地存储) -INSERT INTO `yz_system_storage_config` (`storage_type`, `create_time`) -VALUES ('local', NOW()) -ON DUPLICATE KEY UPDATE `storage_type` = 'local'; diff --git a/go/docs/sql/alter_backend_ai_chat_message_images.sql b/go/docs/sql/alter_backend_ai_chat_message_images.sql deleted file mode 100644 index fe9121c..0000000 --- a/go/docs/sql/alter_backend_ai_chat_message_images.sql +++ /dev/null @@ -1,3 +0,0 @@ --- 为 AI 聊天消息表增加图片字段(用户上传的图片,base64 dataURL 的 JSON 数组) -ALTER TABLE `yz_backend_ai_chat_message` - ADD COLUMN `images` TEXT NULL COMMENT '用户上传的图片(base64 dataURL 的 JSON 数组)'; diff --git a/go/docs/sql/alter_backend_ai_chat_message_stats.sql b/go/docs/sql/alter_backend_ai_chat_message_stats.sql deleted file mode 100644 index 5e7d87d..0000000 --- a/go/docs/sql/alter_backend_ai_chat_message_stats.sql +++ /dev/null @@ -1,6 +0,0 @@ --- 为 AI 聊天消息表增加 token 用量与响应耗时字段 --- 用途:在对话界面展示每条助手回复的响应时间与消耗的 tokens -ALTER TABLE yz_backend_ai_chat_message - ADD COLUMN tokens INT NOT NULL DEFAULT 0 COMMENT '本次响应消耗的 token 总数', - ADD COLUMN duration_ms INT NOT NULL DEFAULT 0 COMMENT 'AI 响应耗时(毫秒)', - ADD COLUMN model VARCHAR(64) NOT NULL DEFAULT '' COMMENT '生成该消息使用的模型'; diff --git a/go/docs/sql/alter_backend_crm_overdue.sql b/go/docs/sql/alter_backend_crm_overdue.sql new file mode 100644 index 0000000..edf4d20 --- /dev/null +++ b/go/docs/sql/alter_backend_crm_overdue.sql @@ -0,0 +1,63 @@ +-- ============================================================= +-- CRM 逾期(overdue)判定与索引 +-- 说明: +-- 逾期以「当前日期」实时判定,无需新增存储字段。 +-- 前端在列表/详情中展示「逾期 N 天」标签,规则如下: +-- - 项目 : 结束日期(end_date) < 今天 且 状态 != 已完成(3) +-- - 合同 : 到期日期(expire_date) < 今天 且 状态 != 已完成(2) / 已作废(3) +-- - 回款明细: 计划回款日期(plan_date) < 今天 且 明细状态 != 已回款(3) +-- 本脚本仅补充判定所需的日期索引,并给出等价 SQL 供统计/定时任务使用。 +-- ============================================================= + +-- 1) 项目:结束日期索引(逾期判定 + 按结束日期排序) +ALTER TABLE `yz_backend_crm_project` + ADD INDEX `idx_end_date` (`end_date`); + +-- 2) 合同:到期日期索引(逾期判定 + 按到期日期排序) +ALTER TABLE `yz_backend_crm_contract` + ADD INDEX `idx_expire_date` (`expire_date`); + +-- 3) 回款明细逾期:plan_date 内嵌于 items(JSON),无法建单列索引。 +-- 如需按「逾期回款明细」做服务端统计,建议将回款明细拆为独立子表 +-- (如 yz_backend_crm_payback_item,含 plan_date DATE 列),再建索引。 +-- 当前版本逾期在应用层(前端/接口)实时计算,items 已含 plan_date。 + +-- ============================================================= +-- 等价查询(统计「逾期」记录,供运营/定时任务参考) +-- 注意:DATE(end_date) < CURDATE() 表示「结束日期在今天之前」。 +-- ============================================================= + +-- 逾期项目(结束日期已过且未完成) +SELECT id, project_name, end_date, status +FROM `yz_backend_crm_project` +WHERE `delete_time` IS NULL + AND `end_date` IS NOT NULL + AND DATE(`end_date`) < CURDATE() + AND `status` <> 3; -- 3=已完成 + +-- 逾期合同(到期日期已过且非已完成/已作废) +SELECT id, contract_name, expire_date, status +FROM `yz_backend_crm_contract` +WHERE `delete_time` IS NULL + AND `expire_date` IS NOT NULL + AND DATE(`expire_date`) < CURDATE() + AND `status` NOT IN (2, 3); -- 2=已完成, 3=已作废 + +-- 逾期回款明细(MySQL 5.7+ 可用 JSON 函数;低版本建议在应用层计算) +-- 列出「存在任一逾期明细」的回款计划 +SELECT p.id, p.contract_name, + j.seq, j.name, j.plan_date, j.status +FROM `yz_backend_crm_payback` p, + JSON_TABLE( + p.items, + '$[*]' COLUMNS ( + seq INT PATH '$.seq', + name VARCHAR(100) PATH '$.name', + plan_date DATE PATH '$.plan_date', + status INT PATH '$.status' + ) + ) j +WHERE p.`delete_time` IS NULL + AND j.plan_date IS NOT NULL + AND DATE(j.plan_date) < CURDATE() + AND j.status <> 3; -- 3=已回款 diff --git a/go/docs/sql/alter_backend_crm_payback_plan_date.sql b/go/docs/sql/alter_backend_crm_payback_plan_date.sql new file mode 100644 index 0000000..a231c86 --- /dev/null +++ b/go/docs/sql/alter_backend_crm_payback_plan_date.sql @@ -0,0 +1,42 @@ +-- ============================================================= +-- 回款计划表(yz_backend_crm_payback)建表 / 核对脚本 +-- ============================================================= +-- 说明: +-- 1. 计划明细 items 以 JSON 文本存储,plan_date(计划回款日期)已内置于 +-- 各类 plan_type 的明细结构中,无需新增独立列,故无 ALTER TABLE 语句。 +-- 2. 各 plan_type 的 items 明细结构: +-- 月度/季度/年度:{"seq":int,"name":"第1期","amount":0,"plan_date":"2026-10-01"} +-- 进度: {"seq":int,"name":"预付款","percent":30,"amount":0,"plan_date":"2026-10-01"} +-- 自定义: {"seq":int,"name":"首付款","plan_date":"2026-10-01","amount":0} +-- 每行另含 received_amount / received_date / status(回款进度登记用)。 +-- 3. 执行下方建表脚本可创建或核对表结构(幂等,已存在则跳过)。 +-- ============================================================= + +CREATE TABLE IF NOT EXISTS `yz_backend_crm_payback` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `tenant_id` VARCHAR(64) NOT NULL DEFAULT '', + `contract_id` BIGINT UNSIGNED NULL, + `contract_no` VARCHAR(50) NOT NULL DEFAULT '', + `contract_name` VARCHAR(100) NOT NULL DEFAULT '', + `customer_id` BIGINT UNSIGNED NULL, + `customer_name` VARCHAR(128) NOT NULL DEFAULT '', + `plan_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1月度/2季度/3年度/4进度/5自定义', + `pay_method` TINYINT NOT NULL DEFAULT 1 COMMENT '1对公/2网银/3现金/4支票/5支付宝/6微信/7其他', + `remind_days` INT NOT NULL DEFAULT 0 COMMENT '提前几天提醒', + `owner_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `owner_user_name` VARCHAR(128) NOT NULL DEFAULT '', + `contract_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '合同总金额', + `total_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '计划回款总额', + `received_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '已回款金额', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1进行中/2已完成', + `items` TEXT NULL COMMENT '计划明细 JSON 数组(含 plan_date 计划回款日期)', + `remark` TEXT NULL, + `create_user_id` VARCHAR(64) NOT NULL DEFAULT '', + `create_time` DATETIME NOT NULL, + `update_time` DATETIME NOT NULL, + `delete_time` DATETIME NULL, + PRIMARY KEY (`id`), + KEY `idx_yz_backend_crm_payback_tenant` (`tenant_id`), + KEY `idx_yz_backend_crm_payback_contract` (`contract_id`), + KEY `idx_yz_backend_crm_payback_delete` (`delete_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='回款计划表'; diff --git a/go/docs/sql/alter_backend_crm_payback_receipt.sql b/go/docs/sql/alter_backend_crm_payback_receipt.sql new file mode 100644 index 0000000..2fe3354 --- /dev/null +++ b/go/docs/sql/alter_backend_crm_payback_receipt.sql @@ -0,0 +1,12 @@ +-- ============================================================= +-- CRM 回款计划:流水回执(receipt)字段 +-- 说明: +-- 回款登记支持上传「流水回执」(银行流水 / 转账凭证等),非必填。 +-- 新增两列用于保存回执文件地址与文件名,随回款计划主表一并存取。 +-- 文件本身经 /backend/uploadfile 上传到文件存储,此处仅存其可访问地址。 +-- ============================================================= + +-- 回款计划表:流水回执地址 + 文件名(均允许为空) +ALTER TABLE `yz_backend_crm_payback` + ADD COLUMN `receipt_url` VARCHAR(512) NULL COMMENT '流水回执文件地址(选填)' AFTER `remark`, + ADD COLUMN `receipt_name` VARCHAR(255) NULL COMMENT '流水回执文件名(选填)' AFTER `receipt_url`; diff --git a/go/docs/sql/alter_backend_crm_project_products_column.sql b/go/docs/sql/alter_backend_crm_project_products_column.sql deleted file mode 100644 index 3eb1b53..0000000 --- a/go/docs/sql/alter_backend_crm_project_products_column.sql +++ /dev/null @@ -1,15 +0,0 @@ --- 项目表新增「产品清单」字段: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; diff --git a/go/docs/sql/alter_system_files_storage_columns.sql b/go/docs/sql/alter_system_files_storage_columns.sql deleted file mode 100644 index ba783a2..0000000 --- a/go/docs/sql/alter_system_files_storage_columns.sql +++ /dev/null @@ -1,93 +0,0 @@ --- 文件存储分层改造:yz_system_files 新增归属与存储字段 --- 对应计划文档:go/docs/文件存储分层改造计划.md S1 --- --- 幂等脚本:用存储过程逐项判断,已存在的列/索引自动跳过,可反复执行,不会报 --- 1060 Duplicate column / 1061 Duplicate key。 --- --- 背景:md5 在部分历史库中是 TEXT 类型,MySQL 不允许对 TEXT/BLOB 建整列索引, --- 因此 idx_file_dedup 使用前缀长度 md5(32)(MD5 十六进制串固定 32 字符,等价全值)。 - -DROP PROCEDURE IF EXISTS `yz_alter_system_files_storage`; - -DELIMITER $$ - -CREATE PROCEDURE `yz_alter_system_files_storage`() -BEGIN - DECLARE v_db VARCHAR(64); - SET v_db = DATABASE(); - - -- 1. 字段:source - IF NOT EXISTS ( - SELECT 1 FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = v_db AND TABLE_NAME = 'yz_system_files' AND COLUMN_NAME = 'source' - ) THEN - ALTER TABLE `yz_system_files` - ADD COLUMN `source` varchar(16) NOT NULL DEFAULT 'backend' - COMMENT '来源端: backend-租户后台 platform-平台端'; - END IF; - - -- 2. 字段:scope - IF NOT EXISTS ( - SELECT 1 FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = v_db AND TABLE_NAME = 'yz_system_files' AND COLUMN_NAME = 'scope' - ) THEN - ALTER TABLE `yz_system_files` - ADD COLUMN `scope` varchar(16) NOT NULL DEFAULT 'tenant' - COMMENT '归属: tenant-租户共享 user-用户个人'; - END IF; - - -- 3. 字段:storage - IF NOT EXISTS ( - SELECT 1 FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = v_db AND TABLE_NAME = 'yz_system_files' AND COLUMN_NAME = 'storage' - ) THEN - ALTER TABLE `yz_system_files` - ADD COLUMN `storage` varchar(16) NOT NULL DEFAULT '' - COMMENT '存储类型: local/qiniu(冗余,便于迁移与排查)'; - END IF; - - -- 4. 字段:object_key - IF NOT EXISTS ( - SELECT 1 FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = v_db AND TABLE_NAME = 'yz_system_files' AND COLUMN_NAME = 'object_key' - ) THEN - ALTER TABLE `yz_system_files` - ADD COLUMN `object_key` varchar(512) NOT NULL DEFAULT '' - COMMENT '存储相对路径(不含域名),用于迁移与精确删除'; - END IF; - - -- 5. 索引:查重(source + scope + tid + tuid + md5 前缀) - -- 不加 UNIQUE:软删(delete_time)与并发上传下唯一索引会直接报错,去重在代码层完成 - IF NOT EXISTS ( - SELECT 1 FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = v_db AND TABLE_NAME = 'yz_system_files' AND INDEX_NAME = 'idx_file_dedup' - ) THEN - ALTER TABLE `yz_system_files` - ADD KEY `idx_file_dedup` (`source`, `scope`, `tid`, `tuid`, `md5`(32)); - END IF; - - -- 6. 索引:归属列表过滤 - IF NOT EXISTS ( - SELECT 1 FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = v_db AND TABLE_NAME = 'yz_system_files' AND INDEX_NAME = 'idx_file_owner' - ) THEN - ALTER TABLE `yz_system_files` - ADD KEY `idx_file_owner` (`source`, `tid`, `scope`, `tuid`, `delete_time`); - END IF; -END$$ - -DELIMITER ; - -CALL `yz_alter_system_files_storage`(); -DROP PROCEDURE IF EXISTS `yz_alter_system_files_storage`; - --- 7. 老数据初始化:迁移脚本执行前先打底,避免 source/scope 为空导致查询遗漏 -UPDATE `yz_system_files` SET `source` = 'backend' WHERE `source` = '' OR `source` IS NULL; -UPDATE `yz_system_files` SET `scope` = 'tenant' WHERE `scope` = '' OR `scope` IS NULL; - --- 8. 校验(应看到 4 个字段 + 2 个索引) --- SHOW COLUMNS FROM yz_system_files LIKE 'source'; --- SHOW COLUMNS FROM yz_system_files LIKE 'scope'; --- SHOW COLUMNS FROM yz_system_files LIKE 'storage'; --- SHOW COLUMNS FROM yz_system_files LIKE 'object_key'; --- SHOW INDEX FROM yz_system_files WHERE Key_name IN ('idx_file_dedup','idx_file_owner'); diff --git a/go/docs/sql/yz_backend_crm_product.sql b/go/docs/sql/yz_backend_crm_product.sql deleted file mode 100644 index 788647a..0000000 --- a/go/docs/sql/yz_backend_crm_product.sql +++ /dev/null @@ -1,29 +0,0 @@ --- 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产品管理表'; diff --git a/go/docs/sql/yz_backend_crm_product_category.sql b/go/docs/sql/yz_backend_crm_product_category.sql deleted file mode 100644 index 4dd1789..0000000 --- a/go/docs/sql/yz_backend_crm_product_category.sql +++ /dev/null @@ -1,19 +0,0 @@ --- 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产品分类表'; diff --git a/go/docs/sql/yz_complaint.sql b/go/docs/sql/yz_complaint.sql deleted file mode 100644 index c05f933..0000000 --- a/go/docs/sql/yz_complaint.sql +++ /dev/null @@ -1,45 +0,0 @@ --- 投诉建议「产品分类」:区分用户针对哪类产品提建议 --- 请在目标库手动执行(utf8mb4) - -CREATE TABLE IF NOT EXISTS `yz_system_complaint_category` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `name` varchar(64) NOT NULL COMMENT '分类名称,如:官网、租户后台、小程序', - `code` varchar(32) DEFAULT NULL COMMENT '可选编码,便于程序识别', - `sort` int NOT NULL DEFAULT 0 COMMENT '排序,越小越靠前', - `status` tinyint NOT NULL DEFAULT 1 COMMENT '1启用 0禁用', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - `delete_time` datetime DEFAULT NULL COMMENT '软删', - PRIMARY KEY (`id`), - KEY `idx_delete_time` (`delete_time`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='投诉建议-产品分类'; - -CREATE TABLE IF NOT EXISTS `yz_system_platform_complaint` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `category_id` bigint unsigned NOT NULL COMMENT '产品分类ID', - `title` varchar(200) NOT NULL COMMENT '标题', - `content` text NOT NULL COMMENT '建议/投诉内容', - `contact_name` varchar(64) DEFAULT NULL COMMENT '联系人', - `contact_phone` varchar(32) DEFAULT NULL COMMENT '联系电话', - `contact_email` varchar(128) DEFAULT NULL COMMENT '联系邮箱', - `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待处理 1处理中 2已回复 3已关闭', - `reply_content` text COMMENT '平台回复内容', - `reply_time` datetime DEFAULT NULL COMMENT '回复时间', - `tid` bigint unsigned DEFAULT NULL COMMENT '可选:关联租户ID', - `remark` varchar(512) DEFAULT NULL COMMENT '管理员内部备注', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - `delete_time` datetime DEFAULT NULL COMMENT '软删', - PRIMARY KEY (`id`), - KEY `idx_category_id` (`category_id`), - KEY `idx_status` (`status`), - KEY `idx_delete_time` (`delete_time`), - KEY `idx_tid` (`tid`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台端-投诉建议'; - --- 可选:示例分类(执行完建表后按需取消注释) --- INSERT INTO `yz_system_complaint_category` (`name`,`code`,`sort`,`status`) VALUES --- ('官网','site',0,1), --- ('租户后台','tenant_admin',10,1), --- ('小程序','miniapp',20,1); diff --git a/go/docs/sql/yz_oa_document.sql b/go/docs/sql/yz_oa_document.sql deleted file mode 100644 index 6acc55f..0000000 --- a/go/docs/sql/yz_oa_document.sql +++ /dev/null @@ -1,129 +0,0 @@ --- OA 文档管理(文档库 / 文档图谱)建表脚本 --- --- 说明:正常情况【不需要执行本脚本】。 --- 服务在首次访问 /backend/oa/document/* 接口时,会由 --- controllers/backend_oa_document.go 的 Prepare() 调用 --- models.EnsureOaDocumentTables() 自动建表(CREATE TABLE IF NOT EXISTS, --- sync.Once 保证单进程只建一次)。 --- --- 本脚本仅用于以下场景手工预建: --- 1. 生产库账号没有 DDL 权限,自动建表失败; --- 2. DBA 要求先在预发/生产环境审核建表语句; --- 3. 需要排查表结构是否符合预期。 --- --- 幂等:全部使用 IF NOT EXISTS,可反复执行,不会覆盖已有数据。 --- 与代码保持一致:models/oa_document.go - --- 1. 文档分类表(树形) -CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_category ( - id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - tid int NOT NULL DEFAULT 0 COMMENT '租户ID', - user_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '分类归属用户:共享空间共享=0(租户级);私密空间=创建者的 yz_system_tenant_user.uid', - parent_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '父级ID,0为一级分类', - name varchar(128) NOT NULL DEFAULT '' COMMENT '分类名称', - sort int NOT NULL DEFAULT 0 COMMENT '排序,越小越靠前', - remark varchar(255) NOT NULL DEFAULT '' COMMENT '备注', - is_system tinyint NOT NULL DEFAULT 0 COMMENT '是否系统内置分类 0-否(可删除) 1-是(不可删除)', - scope varchar(16) NOT NULL DEFAULT 'shared' COMMENT '所属空间 shared-共享文档空间 personal-私密文档空间(绑定用户级别)', - is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是', - create_time datetime DEFAULT NULL COMMENT '创建时间', - update_time datetime DEFAULT NULL COMMENT '更新时间', - delete_time datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (id), - KEY idx_tid_parent (tid, parent_id), - KEY idx_tid_scope_user (tid, scope, user_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档分类表'; - --- 1.1 历史库补齐列(特性上线前已建表时执行,已存在则报错可忽略) --- ALTER TABLE yz_backend_oa_doc_category --- ADD COLUMN is_system tinyint NOT NULL DEFAULT 0 COMMENT '是否系统内置分类 0-否(可删除) 1-是(不可删除)'; --- ALTER TABLE yz_backend_oa_doc_category --- ADD COLUMN scope varchar(16) NOT NULL DEFAULT 'shared' COMMENT '所属空间 shared-共享 personal-私密'; --- ALTER TABLE yz_backend_oa_doc_category --- ADD COLUMN user_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '分类归属用户(私密空间=租户用户 uid)'; - --- 1.2 分类归属规则(双空间模型,2026-09 起): --- 文档库按「共享文档 / 私密文档」两个空间组织,每个空间各自一棵分类树: --- - 共享空间:scope=shared、user_id=0(租户级,全租户共用一套,任何人可建/删,内置分类除外); --- - 私密空间:scope=personal、user_id=当前用户的 yz_system_tenant_user.uid(绑定到用户级别,各用户独立一套)。 --- 文档可见性(OaDoc.visibility)与所在空间一一对应:共享空间→0(租户公开),私密空间→1(私密,仅创建者与被共享者可见)。 --- 系统内置分类「项目文档」(scope=shared, user_id=0, is_system=1) 属于共享空间,按租户自动补建 --- (models.EnsureOaDocDefaultCategories),一般无需手工插入。 --- 历史脏数据(scope 不在 shared/personal)在 EnsureOaDocumentTables 中自动归为共享空间(user_id=0, scope=shared)。 --- 说明:「未分类」是虚拟分类,文档 category_id = 0 即表示未分类,两个空间各自独立计算,不落库、不可删除。 - --- 2. 文档表 --- doc_type: 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频 --- status : 0-草稿 1-已发布 2-已归档 -CREATE TABLE IF NOT EXISTS yz_backend_oa_doc ( - id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - tid int NOT NULL DEFAULT 0 COMMENT '租户ID', - category_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '所属分类ID,0为未分类', - title varchar(255) NOT NULL DEFAULT '' COMMENT '文档标题', - file_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '关联附件ID(yz_system_files)', - file_url varchar(512) NOT NULL DEFAULT '' COMMENT '附件访问地址', - file_name varchar(255) NOT NULL DEFAULT '' COMMENT '原始文件名', - ext varchar(16) NOT NULL DEFAULT '' COMMENT '扩展名', - size bigint unsigned NOT NULL DEFAULT 0 COMMENT '文件大小(字节)', - doc_type tinyint NOT NULL DEFAULT 0 COMMENT '类型 0-其他 1-文档 2-表格 3-演示 4-PDF 5-图片 6-压缩包 7-视频 8-音频', - tags varchar(255) NOT NULL DEFAULT '' COMMENT '标签,逗号分隔', - summary varchar(1000) NOT NULL DEFAULT '' COMMENT '摘要/描述', - status tinyint NOT NULL DEFAULT 0 COMMENT '状态 0-草稿 1-已发布 2-已归档', - version int NOT NULL DEFAULT 1 COMMENT '版本号', - is_star tinyint NOT NULL DEFAULT 0 COMMENT '是否收藏 0-否 1-是', - owner_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '负责人ID', - owner_name varchar(100) NOT NULL DEFAULT '' COMMENT '负责人姓名', - view_count int NOT NULL DEFAULT 0 COMMENT '查看次数', - download_count int NOT NULL DEFAULT 0 COMMENT '下载次数', - creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID', - creator_name varchar(100) NOT NULL DEFAULT '' COMMENT '创建人姓名', - visibility tinyint NOT NULL DEFAULT 0 COMMENT '可见性 0-租户公开 1-私密(仅创建者与被共享者)', - is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是', - create_time datetime DEFAULT NULL COMMENT '创建时间', - update_time datetime DEFAULT NULL COMMENT '更新时间', - delete_time datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (id), - KEY idx_tid_cate (tid, category_id, is_deleted), - KEY idx_tid_status (tid, status, is_deleted), - KEY idx_tid_title (tid, title) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档表'; - --- 3. 文档关联表(文档图谱的连线来源) --- relation: reference-引用 related-相关 version-版本 belong-从属 --- 说明:代码在建立关联时会同时写入正向与反向两条记录,便于图谱无向检索; --- uk_doc_pair 保证同一对文档的同一关系不重复。 -CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_link ( - id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - tid int NOT NULL DEFAULT 0 COMMENT '租户ID', - source_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '源文档ID', - target_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '目标文档ID', - relation varchar(32) NOT NULL DEFAULT 'related' COMMENT '关系 reference-引用 related-相关 version-版本 belong-从属', - remark varchar(255) NOT NULL DEFAULT '' COMMENT '关系说明', - creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '创建人ID', - create_time datetime DEFAULT NULL COMMENT '创建时间', - PRIMARY KEY (id), - UNIQUE KEY uk_doc_pair (tid, source_id, target_id, relation), - KEY idx_tid_source (tid, source_id), - KEY idx_tid_target (tid, target_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档关联表'; - --- 4. 文档共享表(私密文档可见性授权) --- share_type: 0-用户(target_id=uid) 1-部门(target_id=org_id,含子部门) --- uk_doc_target 保证同一文档对同一目标不重复授权。 -CREATE TABLE IF NOT EXISTS yz_backend_oa_doc_share ( - id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - tid int NOT NULL DEFAULT 0 COMMENT '租户ID', - doc_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '文档ID', - share_type tinyint NOT NULL DEFAULT 0 COMMENT '共享类型 0-用户(target_id=uid) 1-部门(target_id=org_id,含子部门)', - target_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '共享目标ID', - creator_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '授权人ID', - create_time datetime DEFAULT NULL COMMENT '创建时间', - PRIMARY KEY (id), - UNIQUE KEY uk_doc_target (tid, doc_id, share_type, target_id), - KEY idx_tid_doc (tid, doc_id), - KEY idx_target (tid, share_type, target_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA文档共享表'; - --- 5. 校验(应看到 4 张表) --- SHOW TABLES LIKE 'yz_backend_oa_doc%'; - diff --git a/go/docs/sql/yz_platform_cursor_activation_code.sql b/go/docs/sql/yz_platform_cursor_activation_code.sql deleted file mode 100644 index e411140..0000000 --- a/go/docs/sql/yz_platform_cursor_activation_code.sql +++ /dev/null @@ -1,31 +0,0 @@ --- Cursor 激活码管理 --- status: 0 未使用 1 已使用 2 已过期 3 已禁用 --- type: 0 自定义 1 天卡 7 周卡 30 月卡 90 季卡 365 年卡 - -CREATE TABLE IF NOT EXISTS `yz_platform_cursor_activation_code` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `code` varchar(128) NOT NULL COMMENT '激活码', - `type` int NOT NULL DEFAULT 30 COMMENT '卡密类型:0自定义 1天卡 7周卡 30月卡 90季卡 365年卡', - `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0未使用 1已使用 2已过期 3已禁用', - `duration_days` int NOT NULL DEFAULT 30 COMMENT '有效天数', - `bind_account` varchar(128) DEFAULT NULL COMMENT '绑定账号', - `bind_device_id` bigint unsigned DEFAULT NULL COMMENT '绑定设备ID,关联 yz_platform_cursor_equipment.id', - `machine_code` varchar(128) DEFAULT NULL COMMENT '绑定设备机器码', - `device_info` varchar(1000) DEFAULT NULL COMMENT '绑定设备信息', - `owner_user_id` bigint unsigned DEFAULT NULL COMMENT '归属用户ID', - `owner_user_name` varchar(128) DEFAULT NULL COMMENT '归属用户名称', - `activated_at` datetime DEFAULT NULL COMMENT '激活时间', - `expired_at` datetime DEFAULT NULL COMMENT '过期时间', - `remark` varchar(1000) DEFAULT NULL COMMENT '备注', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_code` (`code`), - KEY `idx_status_delete` (`status`,`delete_time`), - KEY `idx_type_status` (`type`,`status`), - KEY `idx_bind_account` (`bind_account`), - KEY `idx_bind_device_id` (`bind_device_id`), - KEY `idx_owner_user_id` (`owner_user_id`), - KEY `idx_expired_at` (`expired_at`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Cursor续杯激活码'; diff --git a/go/docs/sql/yz_software_upgrade.sql b/go/docs/sql/yz_software_upgrade.sql deleted file mode 100644 index 17892a6..0000000 --- a/go/docs/sql/yz_software_upgrade.sql +++ /dev/null @@ -1,26 +0,0 @@ --- 软件升级产品(客户端拉取版本与下载地址) --- 安装包建议上传到文件管理,分类使用「appsupgrade」(或任意分类,记录 file_id 即可) - -CREATE TABLE IF NOT EXISTS `yz_system_software_upgrade` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `name` varchar(128) NOT NULL COMMENT '软件显示名称', - `code` varchar(64) NOT NULL COMMENT '客户端唯一标识,与 check 接口 code 一致', - `latest_version` varchar(32) NOT NULL DEFAULT '0.0.0' COMMENT '当前发布的最新版本号', - `file_id` bigint unsigned DEFAULT NULL COMMENT '关联 yz_system_files.id,安装包', - `download_url` varchar(512) DEFAULT NULL COMMENT '兼容旧客户端的单安装包地址;为空则用 file_id 对应 src 拼公开 URL', - `download_urls` text DEFAULT NULL COMMENT '多运行环境安装包地址 JSON,如 {"windows":"...","mac":"...","ubuntu":"...","linux":"..."}', - `force_update` tinyint NOT NULL DEFAULT 0 COMMENT '1 建议强制更新', - `release_notes` varchar(2000) DEFAULT NULL COMMENT '更新说明', - `status` tinyint NOT NULL DEFAULT 1 COMMENT '1 启用 0 停用', - `sort` int NOT NULL DEFAULT 0, - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - `delete_time` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `uk_code` (`code`), - KEY `idx_status_delete` (`status`,`delete_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='软件升级产品'; - --- 已有表升级: --- ALTER TABLE `yz_system_software_upgrade` --- ADD COLUMN `download_urls` text DEFAULT NULL COMMENT '多运行环境安装包地址 JSON,如 {"windows":"...","mac":"...","ubuntu":"...","linux":"..."}' AFTER `download_url`; diff --git a/go/models/init.go b/go/models/init.go index 8c35f33..6b9b1a8 100644 --- a/go/models/init.go +++ b/go/models/init.go @@ -84,6 +84,7 @@ func Init(_ string) { new(TenantCrmEntityContact), new(TenantCrmOperateLog), new(TenantCrmContract), + new(TenantCrmPayback), new(ErpAccountSet), new(ErpNormalSetting), new(ErpCompanyContact), @@ -147,7 +148,49 @@ func Init(_ string) { EnsureCrmProjectDocColumn() EnsureCrmContractTable() EnsureCrmContractOurRoleColumn() + EnsureCrmContractTechDateColumn() EnsureCrmProjectProductsColumn() + EnsureCrmPaybackTable() + EnsureCrmProductDevColumns() +} + +// EnsureCrmPaybackTable 回款计划表建表(CREATE TABLE IF NOT EXISTS,可重复执行; +// 建表失败(如表已存在但结构不一致)时静默忽略,可用 sql/yz_backend_crm_payback.sql 手动修复)。 +func EnsureCrmPaybackTable() { + sql := `CREATE TABLE IF NOT EXISTS yz_backend_crm_payback ( + id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID', + tenant_id varchar(64) NOT NULL COMMENT '租户ID', + contract_id bigint(20) DEFAULT NULL COMMENT '关联合同ID(yz_backend_crm_contract.id)', + contract_no varchar(50) NOT NULL DEFAULT '' COMMENT '合同编号', + contract_name varchar(100) NOT NULL DEFAULT '' COMMENT '合同名称', + customer_id bigint(20) DEFAULT NULL COMMENT '客户ID', + customer_name varchar(128) NOT NULL DEFAULT '' COMMENT '客户名称', + plan_type tinyint(4) NOT NULL DEFAULT '1' COMMENT '回款周期:1月度/2季度/3年度/4进度/5自定义', + pay_method tinyint(4) NOT NULL DEFAULT '1' COMMENT '回款方式:1对公转账/2网银转账/3现金/4支票/5支付宝/6微信/7其他', + remind_days int(11) NOT NULL DEFAULT '0' COMMENT '提前几天提醒', + owner_user_id varchar(64) NOT NULL DEFAULT '' COMMENT '负责人用户ID', + owner_user_name varchar(128) NOT NULL DEFAULT '' COMMENT '负责人姓名', + contract_amount decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '合同总金额', + total_amount decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '计划回款总额', + received_amount decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '已回款金额', + status tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1进行中/2已完成', + items text COMMENT '计划明细JSON数组', + 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_contract (tenant_id,contract_id), + KEY idx_customer (tenant_id,customer_id), + KEY idx_status (tenant_id,status), + KEY idx_delete_time (delete_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM回款计划表'` + if _, err := Orm.Raw(sql).Exec(); err != nil { + // 表已存在或执行失败时忽略(完整表结构见 sql/yz_backend_crm_payback.sql) + return + } } // EnsureCrmContractOurRoleColumn 补齐合同表的我方角色字段(存量表已建时新增; @@ -158,6 +201,14 @@ func EnsureCrmContractOurRoleColumn() { _, _ = Orm.Raw(sql).Exec() } +// EnsureCrmContractTechDateColumn 补齐合同表的技术日期字段(开发/交付完成日期; +// 存量表已建时新增,表不存在或列已存在时忽略错误)。 +func EnsureCrmContractTechDateColumn() { + sql := "ALTER TABLE " + new(TenantCrmContract).TableName() + + " ADD COLUMN tech_date date DEFAULT NULL COMMENT '技术日期(开发/交付完成日期;到期且回款未满则自动转执行异常)'" + _, _ = Orm.Raw(sql).Exec() +} + // EnsureCrmProjectProductsColumn 补齐项目表的产品清单字段(存量表已建时新增; // 表不存在或列已存在时忽略错误,可用 docs/sql/alter_backend_crm_project_products_column.sql 手动修复)。 func EnsureCrmProjectProductsColumn() { @@ -221,6 +272,24 @@ func EnsureCrmProjectDocColumn() { _, _ = Orm.Raw(sql).Exec() } +// EnsureCrmProductDevColumns 补齐产品表的软件开发字段(line_type / dev_unit_price / dev_tree); +// 表不存在或列已存在时忽略错误,可用 docs/sql/alter_backend_crm_product_dev_columns.sql 手动修复。 +func EnsureCrmProductDevColumns() { + table := new(TenantCrmProduct).TableName() + cols := []string{ + "line_type varchar(20) NOT NULL DEFAULT 'product' COMMENT '行类型:product成品/dev软件开发'", + "dev_unit_price decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '软件开发统一人天单价'", + "dev_tree text COMMENT '软件开发模块树JSON(叶子人天×统一人天单价)'", + } + for _, col := range cols { + sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", table, col) + if _, err := Orm.Raw(sql).Exec(); err != nil { + // 列已存在或表不存在时忽略 + continue + } + } +} + diff --git a/go/models/tenant_crm_contract.go b/go/models/tenant_crm_contract.go index aefa242..b66fdac 100644 --- a/go/models/tenant_crm_contract.go +++ b/go/models/tenant_crm_contract.go @@ -25,6 +25,7 @@ type TenantCrmContract struct { SignDate *time.Time `orm:"column(sign_date);type(date);null" json:"sign_date"` EffectiveDate *time.Time `orm:"column(effective_date);type(date);null" json:"effective_date"` ExpireDate *time.Time `orm:"column(expire_date);type(date);null" json:"expire_date"` + TechDate *time.Time `orm:"column(tech_date);type(date);null" json:"tech_date"` // 技术日期(开发/交付完成日期;到期且回款未满则自动转执行异常) Parties string `orm:"column(parties);type(text);null" json:"-"` // 各方签约主体 JSON 数组 Products string `orm:"column(products);type(text);null" json:"-"` // 产品清单 JSON 数组 HardwareAmount float64 `orm:"column(hardware_amount);digits(14);decimals(2);default(0)" json:"hardware_amount"` diff --git a/go/models/tenant_crm_payback.go b/go/models/tenant_crm_payback.go new file mode 100644 index 0000000..1be73fe --- /dev/null +++ b/go/models/tenant_crm_payback.go @@ -0,0 +1,47 @@ +package models + +import "time" + +// TenantCrmPayback 回款计划表: yz_backend_crm_payback +// +// 说明: +// - 回款计划针对于合同创建,contract_id 为关联合同(yz_backend_crm_contract.id); +// - plan_type 回款周期:1=月度 2=季度 3=年度 4=进度 5=自定义; +// - pay_method 回款方式:1=对公转账 2=网银转账 3=现金 4=支票 5=支付宝 6=微信 7=其他; +// - items 计划明细以 JSON 文本存储,结构随 plan_type 变化: +// 月度/季度/年度:{seq, name, amount, plan_date} —— 期数 + 支付金额 + 计划回款日期 +// 进度: {seq, name, percent, amount, plan_date} —— 进度名称 + 进度百分比 + 进度金额 + 计划回款日期 +// 自定义: {seq, name, plan_date, amount} —— 名称 + 计划日期 + 金额 +// 每行另带 received_amount / received_date / status,用于「回款进度」登记实收。 +// - plan_date 计划回款日期随各 plan_type 的明细行一并存储于 items JSON,无需独立列。 +// - total_amount 计划回款总额;received_amount 已回款金额,均由 items 汇总。 +type TenantCrmPayback struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"` + ContractID *uint64 `orm:"column(contract_id);null" json:"contract_id"` // 关联合同ID + ContractNo string `orm:"column(contract_no);size(50)" json:"contract_no"` + ContractName string `orm:"column(contract_name);size(100)" json:"contract_name"` + CustomerID *uint64 `orm:"column(customer_id);null" json:"customer_id"` + CustomerName string `orm:"column(customer_name);size(128)" json:"customer_name"` + PlanType int8 `orm:"column(plan_type);default(1)" json:"plan_type"` // 回款周期:1月度/2季度/3年度/4进度/5自定义 + PayMethod int8 `orm:"column(pay_method);default(1)" json:"pay_method"` // 回款方式:1对公/2网银/3现金/4支票/5支付宝/6微信/7其他 + RemindDays int `orm:"column(remind_days);default(0)" json:"remind_days"` // 提前几天提醒 + OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"` + OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"` + ContractAmount float64 `orm:"column(contract_amount);digits(14);decimals(2);default(0)" json:"contract_amount"` // 合同总金额 + TotalAmount float64 `orm:"column(total_amount);digits(14);decimals(2);default(0)" json:"total_amount"` // 计划回款总额 + ReceivedAmount float64 `orm:"column(received_amount);digits(14);decimals(2);default(0)" json:"received_amount"` // 已回款金额 + Status int8 `orm:"column(status);default(1)" json:"status"` // 1进行中/2已完成 + Items string `orm:"column(items);type(text);null" json:"-"` // 计划明细 JSON 数组 + Remark string `orm:"column(remark);type(text);null" json:"remark"` + ReceiptURL string `orm:"column(receipt_url);size(512);null" json:"receipt_url"` // 流水回执文件地址(选填) + ReceiptName string `orm:"column(receipt_name);size(255);null" json:"receipt_name"` // 流水回执文件名(选填) + 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 *TenantCrmPayback) TableName() string { + return "yz_backend_crm_payback" +} diff --git a/go/models/tenant_crm_product.go b/go/models/tenant_crm_product.go index c32094b..d35d85c 100644 --- a/go/models/tenant_crm_product.go +++ b/go/models/tenant_crm_product.go @@ -1,6 +1,8 @@ package models -import "time" +import ( + "time" +) // TenantCrmProduct CRM 产品管理(产品台账/目录): yz_backend_crm_product // @@ -19,6 +21,9 @@ type TenantCrmProduct struct { 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"` // 税率(%) + LineType string `orm:"column(line_type);size(20);default(product)" json:"line_type"` // 行类型 product/dev + DevUnitPrice float64 `orm:"column(dev_unit_price);digits(14);decimals(2);default(0)" json:"dev_unit_price"` // 软件开发统一人天单价 + DevTree string `orm:"column(dev_tree);type(text);null" json:"dev_tree"` // 软件开发模块树JSON文本 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"` // 来源项目名称 diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go index 4a33c30..d1b2572 100644 --- a/go/routers/backend/backend.go +++ b/go/routers/backend/backend.go @@ -426,12 +426,22 @@ func registerOrganizationRoutes(module string) { beego.Router("/backend/crm/contract/:id", &controllers.BackendCrmContractController{}, "get:Detail;put:Update;delete:Delete") beego.Router("/backend/crm/contract/:id/status", &controllers.BackendCrmContractController{}, "post:ChangeStatus") + // CRM回款管理(针对于合同:回款计划创建 + 回款进度登记) + beego.Router("/backend/crm/payback/list", &controllers.BackendCrmPaybackController{}, "get:List") + beego.Router("/backend/crm/payback/progress/list", &controllers.BackendCrmPaybackController{}, "get:ProgressList") + beego.Router("/backend/crm/payback", &controllers.BackendCrmPaybackController{}, "post:Create") + beego.Router("/backend/crm/payback/:id", &controllers.BackendCrmPaybackController{}, "get:Detail;put:Update;delete:Delete") + beego.Router("/backend/crm/payback/:id/receive", &controllers.BackendCrmPaybackController{}, "post:Receive") + // CRM回访记录(贯穿线索/商机/项目) beego.Router("/backend/crm/follow/list", &controllers.BackendCrmFollowController{}, "get:List") beego.Router("/backend/crm/follow/add", &controllers.BackendCrmFollowController{}, "post:Add") beego.Router("/backend/crm/follow/edit", &controllers.BackendCrmFollowController{}, "post:Edit") beego.Router("/backend/crm/follow/delete", &controllers.BackendCrmFollowController{}, "post:Delete") + // CRM数据仪表盘(按时间维度聚合真实业务数据) + beego.Router("/backend/crm/dashboard", &controllers.BackendCrmDashboardController{}, "get:Summary") + // CRM附件(线索/商机/项目资料) beego.Router("/backend/crm/attach/list", &controllers.BackendCrmAttachController{}, "get:List") beego.Router("/backend/crm/attach/add", &controllers.BackendCrmAttachController{}, "post:Add") diff --git a/sql/add_contacts_table.sql b/sql/add_contacts_table.sql deleted file mode 100644 index 8e94259..0000000 --- a/sql/add_contacts_table.sql +++ /dev/null @@ -1,38 +0,0 @@ --- 通讯录表(全局表,已从 yz_backend_erp_contact 改名):与组织架构联动,支持内部员工(自动同步)和外部联系人 --- 已合并 alter_contact_add_dept_position.sql 的 dept_name / position_id 字段 --- tenant 隔离:通过 tid 字段实现租户间数据隔离 - -CREATE TABLE IF NOT EXISTS `yz_backend_contact` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tid` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '租户ID', - `employee_id` bigint(20) unsigned DEFAULT NULL COMMENT '关联员工ID(内部联系人自动关联)', - `org_id` bigint(20) unsigned DEFAULT NULL COMMENT '所属组织/部门ID(关联yz_backend_organization)', - `contact_type` tinyint(4) NOT NULL DEFAULT 1 COMMENT '联系人类型:1=内部员工 2=外部联系人', - `contact_name` varchar(64) NOT NULL COMMENT '联系人姓名', - `gender` tinyint(4) NOT NULL DEFAULT 0 COMMENT '性别:0=未知 1=男 2=女', - `phone` varchar(20) DEFAULT NULL COMMENT '手机号', - `work_phone` varchar(20) DEFAULT NULL COMMENT '办公电话', - `email` varchar(128) DEFAULT NULL COMMENT '邮箱', - `wechat` varchar(64) DEFAULT NULL COMMENT '微信号', - `avatar` varchar(512) DEFAULT NULL COMMENT '头像URL', - `company_name` varchar(128) DEFAULT NULL COMMENT '公司名称(外部联系人)', - `dept_name` varchar(128) DEFAULT NULL COMMENT '外部联系人手填部门名称', - `position_id` bigint(20) unsigned DEFAULT NULL COMMENT '职位ID,关联yz_backend_position', - `position_title` varchar(100) DEFAULT NULL COMMENT '职位名称', - `address` varchar(512) DEFAULT NULL COMMENT '地址', - `remark` varchar(512) DEFAULT NULL COMMENT '备注', - `is_starred` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否收藏:0=否 1=是', - `sort` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '排序值', - `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态:1=正常 0=停用', - `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_tid` (`tid`), - KEY `idx_employee_id` (`employee_id`), - KEY `idx_org_id` (`org_id`), - KEY `idx_contact_type` (`contact_type`), - KEY `idx_contact_name` (`contact_name`), - KEY `idx_phone` (`phone`), - KEY `idx_delete_time` (`delete_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通讯录表'; diff --git a/sql/alter_company_contact_unify.sql b/sql/alter_company_contact_unify.sql deleted file mode 100644 index 535478a..0000000 --- a/sql/alter_company_contact_unify.sql +++ /dev/null @@ -1,198 +0,0 @@ --- ============================================================================= --- 联系人正式库统一:正式客户/供应商联系人通用表 yz_backend_contact_company --- --- 目标: --- 1. 正式客户/供应商联系人统一到 `yz_backend_contact_company` --- (backend 端「公司联系人」通用表,命名与 yz_backend_contact 保持一致); --- 2. 兼容历史表名 `yz_backend_erp_company_contact`: --- - 若新表不存在、旧表存在,直接 RENAME(结构与数据 100% 保留); --- - 若两表并存,则去重合并进新表,旧表改名为 *_bak 备份保留; --- 3. 迁移历史死表 `yz_tenant_crm_contact`(若存在且有数据):去重并入新表, --- 原表改名为 `yz_tenant_crm_contact_bak` 保留; --- 4. 线索/商机联系人仍然只存过程库 `yz_backend_crm_entity_contact`(本脚本不动)。 --- --- 去重键(判定为同一联系人):tenant_id + company_type + company_id + name + mobiles --- 说明: --- - 脚本幂等,可重复执行; --- - 任何迁移都是「先备份原表 / 或直接改名」再按去重键补插,绝不删除数据; --- - 执行本脚本的同时需要部署新版后端(模型表名已指向 yz_backend_contact_company)。 --- ============================================================================= - -SET NAMES utf8mb4; - --- 记录初始状态 -SET @has_new := ( - SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_contact_company' -); -SET @has_old_company := ( - SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_company_contact' -); - --- ----------------------------------------------------------------------------- --- 1. 新表不存在、旧表存在 → 直接改名(保留全部结构、索引、自增、数据) --- ----------------------------------------------------------------------------- -SET @sql := IF(@has_new = 0 AND @has_old_company > 0, - 'RENAME TABLE `yz_backend_erp_company_contact` TO `yz_backend_contact_company`', - 'SELECT ''step1: 跳过直接改名'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 重新计算状态 -SET @has_new := ( - SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_contact_company' -); -SET @has_old_company := ( - SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_company_contact' -); - --- ----------------------------------------------------------------------------- --- 2. 确保正式表存在(新表也不存在、旧表也不存在时创建) --- ----------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS `yz_backend_contact_company` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID', - `tenant_id` varchar(64) NOT NULL DEFAULT '' COMMENT '租户ID', - `company_type` varchar(20) NOT NULL DEFAULT '' COMMENT '公司类型:customer=客户,supplier=供应商', - `company_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '公司ID(客户/供应商ID)', - `name` varchar(50) NOT NULL DEFAULT '' COMMENT '联系人姓名', - `gender` tinyint(4) NOT NULL DEFAULT 0 COMMENT '性别:0-未知 1-男 2-女', - `phone` varchar(20) NOT NULL DEFAULT '' COMMENT '座机', - `mobiles` text COMMENT '手机号,JSON数组,最多5个', - `wechat` varchar(50) NOT NULL DEFAULT '' COMMENT '微信', - `qq` varchar(50) NOT NULL DEFAULT '' COMMENT 'QQ', - `dingtalk` varchar(50) NOT NULL DEFAULT '' COMMENT '钉钉', - `email` varchar(100) NOT NULL DEFAULT '' COMMENT '邮箱', - `department` varchar(50) NOT NULL DEFAULT '' COMMENT '部门', - `position` varchar(50) NOT NULL DEFAULT '' COMMENT '职位', - `home_address` varchar(255) NOT NULL DEFAULT '' COMMENT '家庭住址', - `is_primary` tinyint(4) NOT NULL DEFAULT 0 COMMENT '是否主联系人:0-否 1-是', - `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态:0-离职 1-在职', - `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`), - KEY `idx_tenant_company` (`tenant_id`,`company_type`,`company_id`) COMMENT '按公司查询联系人', - KEY `idx_name` (`tenant_id`,`name`) COMMENT '按姓名查询', - KEY `idx_delete_time` (`delete_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='正式客户/供应商联系人通用表(backend 公司联系人)'; - --- ----------------------------------------------------------------------------- --- 3. 两表并存时:旧表 yz_backend_erp_company_contact 去重并入新表,再改名备份 --- ----------------------------------------------------------------------------- -SET @sql := IF(@has_old_company > 0, -'INSERT INTO `yz_backend_contact_company` - (`tenant_id`,`company_type`,`company_id`,`name`,`gender`,`phone`,`mobiles`,`wechat`,`qq`,`dingtalk`,`email`,`department`,`position`,`home_address`,`is_primary`,`status`,`remark`,`create_time`,`update_time`,`delete_time`) - SELECT - CAST(o.`tenant_id` AS CHAR), o.`company_type`, o.`company_id`, o.`name`, o.`gender`, o.`phone`, o.`mobiles`, - IFNULL(o.`wechat`, ''''), IFNULL(o.`qq`, ''''), IFNULL(o.`dingtalk`, ''''), IFNULL(o.`email`, ''''), - IFNULL(o.`department`, ''''), IFNULL(o.`position`, ''''), IFNULL(o.`home_address`, ''''), - IFNULL(o.`is_primary`, 0), IFNULL(o.`status`, 1), o.`remark`, - IFNULL(o.`create_time`, NOW()), IFNULL(o.`update_time`, NOW()), o.`delete_time` - FROM `yz_backend_erp_company_contact` o - WHERE NOT EXISTS ( - SELECT 1 FROM `yz_backend_contact_company` n - WHERE n.`tenant_id` = CAST(o.`tenant_id` AS CHAR) - AND n.`company_type` = o.`company_type` - AND n.`company_id` = o.`company_id` - AND n.`name` = o.`name` - AND IFNULL(n.`mobiles`, '''') = IFNULL(o.`mobiles`, '''') - )', -'SELECT ''step3: 无并存旧公司联系人表,跳过合并'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - -SET @bak_company_exists := ( - SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_company_contact_bak' -); -SET @sql := IF(@has_old_company > 0 AND @bak_company_exists = 0, - 'RENAME TABLE `yz_backend_erp_company_contact` TO `yz_backend_erp_company_contact_bak`', - 'SELECT ''step3: 跳过改名备份'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- ----------------------------------------------------------------------------- --- 4. 历史死表 yz_tenant_crm_contact(related_type 1=客户/2=供应商)→ 新表,原表备份 --- mobiles 由单个 mobile 转为 JSON 数组;相关字段做空值兜底。 --- ----------------------------------------------------------------------------- -SET @has_dead := ( - SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_contact' -); - -SET @sql := IF(@has_dead > 0, -'INSERT INTO `yz_backend_contact_company` - (`tenant_id`,`company_type`,`company_id`,`name`,`gender`,`phone`,`mobiles`,`wechat`,`qq`,`dingtalk`,`email`,`department`,`position`,`home_address`,`is_primary`,`status`,`remark`,`create_time`,`update_time`,`delete_time`) - SELECT - CAST(o.`tenant_id` AS CHAR), - IF(o.`related_type` = 2, ''supplier'', ''customer''), - o.`related_id`, o.`contact_name`, IFNULL(o.`gender`, 0), IFNULL(o.`phone`, ''''), - CASE WHEN o.`mobile` IS NULL OR o.`mobile` = '''' THEN NULL ELSE CONCAT(''["'', o.`mobile`, ''"]'') END, - '''', '''', '''', IFNULL(o.`email`, ''''), IFNULL(o.`department`, ''''), IFNULL(o.`position`, ''''), '''', - IFNULL(o.`is_primary`, 0), 1, IFNULL(o.`remark`, ''''), - IFNULL(o.`create_time`, NOW()), IFNULL(o.`update_time`, NOW()), - CASE WHEN IFNULL(o.`is_deleted`, 0) = 1 THEN NOW() ELSE NULL END - FROM `yz_tenant_crm_contact` o - WHERE o.`contact_name` IS NOT NULL AND o.`contact_name` <> '''' - AND NOT EXISTS ( - SELECT 1 FROM `yz_backend_contact_company` n - WHERE n.`tenant_id` = CAST(o.`tenant_id` AS CHAR) - AND n.`company_type` = IF(o.`related_type` = 2, ''supplier'', ''customer'') - AND n.`company_id` = o.`related_id` - AND n.`name` = o.`contact_name` - AND IFNULL(n.`mobiles`, '''') = IFNULL( - CASE WHEN o.`mobile` IS NULL OR o.`mobile` = '''' THEN NULL ELSE CONCAT(''["'', o.`mobile`, ''"]'') END, '''') - )', -'SELECT ''步骤4: 无 yz_tenant_crm_contact,跳过'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - -SET @bak_dead_exists := ( - SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_contact_bak' -); -SET @sql := IF(@has_dead > 0 AND @bak_dead_exists = 0, - 'RENAME TABLE `yz_tenant_crm_contact` TO `yz_tenant_crm_contact_bak`', - 'SELECT ''步骤4: 跳过改名备份'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- ----------------------------------------------------------------------------- --- 5. 重复数据检测(只查询、不修改/不删除,供人工核对) --- ----------------------------------------------------------------------------- -SELECT - `tenant_id`, - `company_type`, - `company_id`, - `name`, - IFNULL(`mobiles`, '') AS `mobiles`, - COUNT(*) AS `dup_count`, - GROUP_CONCAT(`id` ORDER BY `id`) AS `dup_ids` -FROM `yz_backend_contact_company` -WHERE `delete_time` IS NULL -GROUP BY `tenant_id`, `company_type`, `company_id`, `name`, IFNULL(`mobiles`, '') -HAVING COUNT(*) > 1; - --- ----------------------------------------------------------------------------- --- 6. 迁移结果核对(新表与备份表条数) --- ----------------------------------------------------------------------------- -SET @bak_company_exists2 := ( - SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_erp_company_contact_bak' -); -SET @bak_dead_exists2 := ( - SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_contact_bak' -); - -SELECT 'yz_backend_contact_company' AS `table_name`, COUNT(*) AS `row_count` -FROM `yz_backend_contact_company`; - -SET @sql := IF(@bak_company_exists2 > 0, - 'SELECT ''yz_backend_erp_company_contact_bak'' AS `table_name`, COUNT(*) AS `row_count` FROM `yz_backend_erp_company_contact_bak`', - 'SELECT ''yz_backend_erp_company_contact_bak(不存在)'' AS `table_name`, 0 AS `row_count`'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - -SET @sql := IF(@bak_dead_exists2 > 0, - 'SELECT ''yz_tenant_crm_contact_bak'' AS `table_name`, COUNT(*) AS `row_count` FROM `yz_tenant_crm_contact_bak`', - 'SELECT ''yz_tenant_crm_contact_bak(不存在)'' AS `table_name`, 0 AS `row_count`'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/sql/alter_contact_add_dept_position.sql b/sql/alter_contact_add_dept_position.sql deleted file mode 100644 index c28f767..0000000 --- a/sql/alter_contact_add_dept_position.sql +++ /dev/null @@ -1,6 +0,0 @@ --- 通讯录表新增字段:外部联系人手填部门、内部员工职位关联 --- 对应模型:go/models/contact.go BackendErpContact - -ALTER TABLE `yz_backend_contact` - ADD COLUMN `dept_name` VARCHAR(128) NULL COMMENT '外部联系人手填部门名称' AFTER `company_name`, - ADD COLUMN `position_id` BIGINT UNSIGNED NULL COMMENT '职位ID,关联 yz_backend_position' AFTER `dept_name`; diff --git a/sql/alter_contact_add_qq.sql b/sql/alter_contact_add_qq.sql deleted file mode 100644 index d684dfa..0000000 --- a/sql/alter_contact_add_qq.sql +++ /dev/null @@ -1,42 +0,0 @@ --- 通讯录表新增 QQ 字段 --- 对应模型:go/models/contact.go BackendErpContact --- --- 说明:历史迁移 alter_contact_rename_to_global.sql 把表名从 yz_backend_erp_contact --- 改名为 yz_backend_contact,但模型 BackendErpContact.TableName() 仍返回旧表名。 --- 为避免不同环境表名不一致导致字段加不上,这里按实际存在的表逐个补齐,可重复执行。 - -SET @db := DATABASE(); - --- 旧表名(模型当前使用的表名) -SET @tbl := 'yz_backend_erp_contact'; -SET @table_exists := ( - SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl -); -SET @column_exists := ( - SELECT COUNT(*) FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'qq' -); -SET @sql := IF(@table_exists > 0 AND @column_exists = 0, - CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `qq` VARCHAR(20) NULL COMMENT ''QQ号'' AFTER `wechat`'), - 'SELECT 1'); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; - --- 新表名(执行过改名迁移的环境) -SET @tbl := 'yz_backend_contact'; -SET @table_exists := ( - SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl -); -SET @column_exists := ( - SELECT COUNT(*) FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = @db AND TABLE_NAME = @tbl AND COLUMN_NAME = 'qq' -); -SET @sql := IF(@table_exists > 0 AND @column_exists = 0, - CONCAT('ALTER TABLE `', @tbl, '` ADD COLUMN `qq` VARCHAR(20) NULL COMMENT ''QQ号'' AFTER `wechat`'), - 'SELECT 1'); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; diff --git a/sql/alter_contact_rename_to_global.sql b/sql/alter_contact_rename_to_global.sql deleted file mode 100644 index c436440..0000000 --- a/sql/alter_contact_rename_to_global.sql +++ /dev/null @@ -1,5 +0,0 @@ --- 通讯录表全局化:从 ERP 模块独立为全局基础功能,表名去掉 erp 前缀 --- 对应模型:go/models/contact.go(BackendErpContact) --- RENAME 会保留全部数据与索引,对存量环境执行本脚本即可完成迁移 - -RENAME TABLE `yz_backend_erp_contact` TO `yz_backend_contact`; diff --git a/sql/alter_crm_follow_add_stage.sql b/sql/alter_crm_follow_add_stage.sql deleted file mode 100644 index c282bdc..0000000 --- a/sql/alter_crm_follow_add_stage.sql +++ /dev/null @@ -1,40 +0,0 @@ --- ============================================================================= --- 跟进记录(回访)增加「创建阶段」字段 stage --- --- 目的: --- 过程数据(跟进记录)不做搬迁,按 线索 → 商机 → 项目 链路累积展示; --- stage 记录该条记录创建时所处的阶段,用于详情列表标明数据来自哪个阶段。 --- --- 取值:1=线索,2=商机,3=项目 --- 说明: --- - 脚本幂等:列已存在则跳过新增; --- - 回填只在首次新增列时执行一次(历史数据按当前 related_type 近似回填)。 --- ============================================================================= - -SET NAMES utf8mb4; - -SET @has_stage := ( - SELECT COUNT(*) FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = 'yz_backend_crm_follow' - AND COLUMN_NAME = 'stage' -); - --- 1. 新增列(已存在则跳过) -SET @sql := IF(@has_stage = 0, - 'ALTER TABLE `yz_backend_crm_follow` ADD COLUMN `stage` tinyint(4) NOT NULL DEFAULT 1 COMMENT ''创建阶段:1线索/2商机/3项目'' AFTER `related_name`', - 'SELECT ''skip: stage 列已存在'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 2. 历史数据回填(仅在首次新增列时执行;按当前 related_type 近似推断) -SET @sql := IF(@has_stage = 0, - 'UPDATE `yz_backend_crm_follow` SET `stage` = `related_type` WHERE `related_type` IN (2, 3)', - 'SELECT ''skip: 回填已完成过'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 3. 核对:各阶段记录数 -SELECT `stage`, COUNT(*) AS `cnt` -FROM `yz_backend_crm_follow` -WHERE `delete_time` IS NULL -GROUP BY `stage` -ORDER BY `stage`; diff --git a/sql/alter_crm_pipeline_add_contact_position.sql b/sql/alter_crm_pipeline_add_contact_position.sql deleted file mode 100644 index 41ca374..0000000 --- a/sql/alter_crm_pipeline_add_contact_position.sql +++ /dev/null @@ -1,42 +0,0 @@ --- ============================================================================= --- 为 CRM 业务管线(线索/商机/项目)增加「对接人职位」字段 contact_position --- --- 说明: --- - 幂等:列已存在时自动跳过,不会报 Duplicate column 错误; --- - 只增加字段,不改动任何已有数据; --- - 若尚未建表,直接执行 yz_backend_crm_pipeline.sql 已包含该字段,无需本脚本。 --- ============================================================================= - -SET NAMES utf8mb4; - --- 1. 线索 -SET @has_col := (SELECT COUNT(*) FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_clue' AND COLUMN_NAME = 'contact_position'); -SET @sql := IF(@has_col = 0, - 'ALTER TABLE `yz_backend_crm_clue` ADD COLUMN `contact_position` varchar(50) NOT NULL DEFAULT '''' COMMENT ''对接人职位'' AFTER `contact_person`', - 'SELECT ''skip: yz_backend_crm_clue.contact_position 已存在'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 2. 商机 -SET @has_col := (SELECT COUNT(*) FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_business' AND COLUMN_NAME = 'contact_position'); -SET @sql := IF(@has_col = 0, - 'ALTER TABLE `yz_backend_crm_business` ADD COLUMN `contact_position` varchar(50) NOT NULL DEFAULT '''' COMMENT ''对接人职位'' AFTER `contact_person`', - 'SELECT ''skip: yz_backend_crm_business.contact_position 已存在'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 3. 项目 -SET @has_col := (SELECT COUNT(*) FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_project' AND COLUMN_NAME = 'contact_position'); -SET @sql := IF(@has_col = 0, - 'ALTER TABLE `yz_backend_crm_project` ADD COLUMN `contact_position` varchar(50) NOT NULL DEFAULT '''' COMMENT ''对接人职位'' AFTER `contact_person`', - 'SELECT ''skip: yz_backend_crm_project.contact_position 已存在'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 4. 核对:三张表的 contact_position 字段 -SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT -FROM information_schema.COLUMNS -WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME IN ('yz_backend_crm_clue', 'yz_backend_crm_business', 'yz_backend_crm_project') - AND COLUMN_NAME = 'contact_position' -ORDER BY TABLE_NAME; diff --git a/sql/alter_crm_project_add_doc_category.sql b/sql/alter_crm_project_add_doc_category.sql deleted file mode 100644 index b496f6f..0000000 --- a/sql/alter_crm_project_add_doc_category.sql +++ /dev/null @@ -1,14 +0,0 @@ --- ----------------------------------------------------------------------------- --- 项目文档(CRM 项目 ↔ OA 文档库绑定) --- --- 在项目表上增加文档库文件夹分类ID: --- 指向 yz_backend_oa_doc_category 中「共享文档 / 项目文档 / {项目名称}」这条分类。 --- 项目建立时自动在文档库中创建同名文件夹并回写该列; --- 存量项目(为 0)在打开「项目文档」或项目详情时自动补建。 --- 项目改名时同步重命名文档库文件夹。 --- --- 注:新建库时该列已包含在 yz_backend_crm_pipeline.sql 中,此脚本用于已有库升级。 --- 后端启动时也会自动补列(models.EnsureCrmProjectDocColumn),可不必手工执行。 --- ----------------------------------------------------------------------------- -ALTER TABLE `yz_backend_crm_project` - ADD COLUMN `doc_category_id` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联OA文档库项目文件夹分类ID(共享文档/项目文档/{项目名称})'; diff --git a/sql/alter_crm_rename_to_backend_prefix.sql b/sql/alter_crm_rename_to_backend_prefix.sql deleted file mode 100644 index 4411448..0000000 --- a/sql/alter_crm_rename_to_backend_prefix.sql +++ /dev/null @@ -1,69 +0,0 @@ --- ============================================================================= --- CRM 业务管线表名规范化:yz_tenant_crm_* → yz_backend_crm_* --- --- 目的:与 yz_backend_erp_*、yz_backend_contact_company 等保持统一前缀,便于查找维护。 --- --- 映射: --- yz_tenant_crm_clue → yz_backend_crm_clue --- yz_tenant_crm_business → yz_backend_crm_business --- yz_tenant_crm_project → yz_backend_crm_project --- yz_tenant_crm_follow → yz_backend_crm_follow --- yz_tenant_crm_attach → yz_backend_crm_attach --- yz_tenant_crm_entity_contact → yz_backend_crm_entity_contact --- yz_tenant_crm_operate_log → yz_backend_crm_operate_log --- --- 说明: --- - RENAME TABLE 是原子改名,数据、索引、自增全部原样保留,不会丢数据; --- - 幂等:新表已存在或旧表不存在时自动跳过; --- - 注意:需与新版后端一起发布(模型表名已指向 yz_backend_crm_*)。 --- ============================================================================= - -SET NAMES utf8mb4; - --- 1. 线索 -SET @has_old := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_clue'); -SET @has_new := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_clue'); -SET @sql := IF(@has_old > 0 AND @has_new = 0, 'RENAME TABLE `yz_tenant_crm_clue` TO `yz_backend_crm_clue`', 'SELECT ''skip: clue'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 2. 商机 -SET @has_old := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_business'); -SET @has_new := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_business'); -SET @sql := IF(@has_old > 0 AND @has_new = 0, 'RENAME TABLE `yz_tenant_crm_business` TO `yz_backend_crm_business`', 'SELECT ''skip: business'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 3. 项目 -SET @has_old := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_project'); -SET @has_new := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_project'); -SET @sql := IF(@has_old > 0 AND @has_new = 0, 'RENAME TABLE `yz_tenant_crm_project` TO `yz_backend_crm_project`', 'SELECT ''skip: project'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 4. 回访/跟进记录 -SET @has_old := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_follow'); -SET @has_new := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_follow'); -SET @sql := IF(@has_old > 0 AND @has_new = 0, 'RENAME TABLE `yz_tenant_crm_follow` TO `yz_backend_crm_follow`', 'SELECT ''skip: follow'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 5. 附件 -SET @has_old := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_attach'); -SET @has_new := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_attach'); -SET @sql := IF(@has_old > 0 AND @has_new = 0, 'RENAME TABLE `yz_tenant_crm_attach` TO `yz_backend_crm_attach`', 'SELECT ''skip: attach'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 6. 联系人 -SET @has_old := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_entity_contact'); -SET @has_new := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_entity_contact'); -SET @sql := IF(@has_old > 0 AND @has_new = 0, 'RENAME TABLE `yz_tenant_crm_entity_contact` TO `yz_backend_crm_entity_contact`', 'SELECT ''skip: entity_contact'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 7. 操作日志 -SET @has_old := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_tenant_crm_operate_log'); -SET @has_new := (SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'yz_backend_crm_operate_log'); -SET @sql := IF(@has_old > 0 AND @has_new = 0, 'RENAME TABLE `yz_tenant_crm_operate_log` TO `yz_backend_crm_operate_log`', 'SELECT ''skip: operate_log'' AS note'); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- 8. 核对结果 -SELECT TABLE_NAME, TABLE_ROWS, CREATE_TIME -FROM information_schema.TABLES -WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME LIKE 'yz_%crm_%' -ORDER BY TABLE_NAME; diff --git a/sql/alter_employee_file_add_cert_photos.sql b/sql/alter_employee_file_add_cert_photos.sql deleted file mode 100644 index 6a35875..0000000 --- a/sql/alter_employee_file_add_cert_photos.sql +++ /dev/null @@ -1,8 +0,0 @@ --- 员工档案主表增加证照字段:身份证正反面、学历照片。 --- 仅在已执行过 yz_backend_employee_file.sql 建表时执行本脚本; --- 未建表的库直接执行最新版 yz_backend_employee_file.sql 即可,无需本脚本。 - -ALTER TABLE `yz_backend_employee_file` - ADD COLUMN `id_card_front` varchar(500) NOT NULL DEFAULT '' COMMENT '身份证正面(人像面)图片URL' AFTER `id_card`, - ADD COLUMN `id_card_back` varchar(500) NOT NULL DEFAULT '' COMMENT '身份证反面(国徽面)图片URL' AFTER `id_card_front`, - ADD COLUMN `education_photo` varchar(500) NOT NULL DEFAULT '' COMMENT '学历照片URL' AFTER `id_card_back`; diff --git a/sql/alter_equipment_add_heartbeat.sql b/sql/alter_equipment_add_heartbeat.sql deleted file mode 100644 index 91587c1..0000000 --- a/sql/alter_equipment_add_heartbeat.sql +++ /dev/null @@ -1,8 +0,0 @@ --- 为设备表添加心跳时间字段,用于实时判断设备是否在线 --- 在线判断逻辑:last_heartbeat_at 在 5 分钟以内 = 在线,否则 = 离线 -ALTER TABLE `yz_platform_cursor_equipment` - ADD COLUMN `last_heartbeat_at` DATETIME NULL DEFAULT NULL COMMENT '最后心跳时间(客户端定期上报)' AFTER `update_time`; - --- 可选:加索引,便于管理后台统计在线设备数 -ALTER TABLE `yz_platform_cursor_equipment` - ADD INDEX `idx_last_heartbeat_at` (`last_heartbeat_at`); diff --git a/sql/alter_notebook_add_pinned.sql b/sql/alter_notebook_add_pinned.sql deleted file mode 100644 index 652b834..0000000 --- a/sql/alter_notebook_add_pinned.sql +++ /dev/null @@ -1,14 +0,0 @@ --- 记事本表增加置顶字段(兼容已有数据库) --- 已存在字段时请跳过对应 ALTER 语句。 - -ALTER TABLE `yz_platform_notebook` - ADD COLUMN `pinned` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否置顶 0-否 1-是' AFTER `content`; - -ALTER TABLE `yz_platform_notebook` - ADD KEY `idx_pinned` (`pinned`); - -ALTER TABLE `yz_backend_notebook` - ADD COLUMN `pinned` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否置顶 0-否 1-是' AFTER `content`; - -ALTER TABLE `yz_backend_notebook` - ADD KEY `idx_pinned` (`pinned`); diff --git a/sql/alter_oa_schedule_add_carry.sql b/sql/alter_oa_schedule_add_carry.sql deleted file mode 100644 index 01a6183..0000000 --- a/sql/alter_oa_schedule_add_carry.sql +++ /dev/null @@ -1,19 +0,0 @@ --- 日程表增加"顺延到下一天"相关字段(兼容已有数据库) --- 已存在字段时请跳过对应 ALTER 语句。 --- 作用:当天没做完的日程可一键顺延,原日程标记完成(carry_over=1), --- 并在目标日期自动生成一条待办(source_id 指向源头日程)继续跟进。 - -ALTER TABLE `yz_backend_oa_schedule` - ADD COLUMN `source_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '顺延来源日程ID 0-原始日程' AFTER `finish_time`; - -ALTER TABLE `yz_backend_oa_schedule` - ADD COLUMN `carry_over` tinyint NOT NULL DEFAULT '0' COMMENT '是否已顺延到下一天 0-否 1-是' AFTER `source_id`; - -ALTER TABLE `yz_backend_oa_schedule` - ADD COLUMN `carry_count` int NOT NULL DEFAULT '0' COMMENT '累计顺延次数' AFTER `carry_over`; - -ALTER TABLE `yz_backend_oa_schedule` - ADD COLUMN `carry_to_date` varchar(10) NOT NULL DEFAULT '' COMMENT '顺延目标日期(YYYY-MM-DD)' AFTER `carry_count`; - -ALTER TABLE `yz_backend_oa_schedule` - ADD KEY `idx_source_id` (`source_id`); diff --git a/sql/alter_password_store_to_multi_accounts.sql b/sql/alter_password_store_to_multi_accounts.sql deleted file mode 100644 index a041376..0000000 --- a/sql/alter_password_store_to_multi_accounts.sql +++ /dev/null @@ -1,78 +0,0 @@ --- ============================================================ --- 密码存储:单账号结构 -> 一个平台挂载多个账号(accounts JSON) --- --- 使用说明: --- 1. 先备份: --- mysqldump -u用户 -p 库名 yz_platform_password_store yz_backend_password_store > pwd_backup.sql --- 2. 按顺序执行下面的「第 1 步」「第 2 步」「第 3 步」。 --- 3. 如果某条 ALTER 报 "Duplicate column name 'accounts'",说明该列已存在, --- 忽略该条错误继续往下执行即可。 --- 4. 「第 4 步」删除旧列不可逆,确认页面数据显示正常后再手动执行。 --- ============================================================ - - --- ---------- 第 1 步:新增 accounts 列 ---------- --- 报 Duplicate column name 表示已加过,忽略即可。 - -ALTER TABLE `yz_platform_password_store` - ADD COLUMN `accounts` longtext NULL - COMMENT '账号列表JSON: [{"username":"","password":"","registration_info":"","remark":""}]' - AFTER `url`; - -ALTER TABLE `yz_backend_password_store` - ADD COLUMN `accounts` longtext NULL - COMMENT '账号列表JSON: [{"username":"","password":"","registration_info":"","remark":""}]' - AFTER `url`; - --- 旧表可能没有 delete_time 列,模型里有该字段,一并补上。 --- 报 Duplicate column name 同样忽略。 -ALTER TABLE `yz_platform_password_store` ADD COLUMN `delete_time` datetime NULL; -ALTER TABLE `yz_backend_password_store` ADD COLUMN `delete_time` datetime NULL; - - --- ---------- 第 2 步:把旧的单账号数据搬进 accounts ---------- --- 只处理 accounts 仍为空、且旧列确实有内容的行,包装成只含一条账号的 JSON 数组。 --- 如果报 Unknown column 'username',说明旧列已经删掉了,无需迁移,跳过这两条。 - -UPDATE `yz_platform_password_store` - SET `accounts` = JSON_ARRAY(JSON_OBJECT( - 'username', IFNULL(`username`, ''), - 'password', IFNULL(`password`, ''), - 'registration_info', IFNULL(`registration_info`, ''), - 'remark', '' - )) - WHERE (`accounts` IS NULL OR `accounts` = '' OR `accounts` = '[]') - AND (IFNULL(`username`, '') <> '' - OR IFNULL(`password`, '') <> '' - OR IFNULL(`registration_info`, '') <> ''); - -UPDATE `yz_backend_password_store` - SET `accounts` = JSON_ARRAY(JSON_OBJECT( - 'username', IFNULL(`username`, ''), - 'password', IFNULL(`password`, ''), - 'registration_info', IFNULL(`registration_info`, ''), - 'remark', '' - )) - WHERE (`accounts` IS NULL OR `accounts` = '' OR `accounts` = '[]') - AND (IFNULL(`username`, '') <> '' - OR IFNULL(`password`, '') <> '' - OR IFNULL(`registration_info`, '') <> ''); - - --- ---------- 第 3 步:accounts 为空的行统一补成空数组 ---------- - -UPDATE `yz_platform_password_store` SET `accounts` = '[]' - WHERE `accounts` IS NULL OR `accounts` = ''; - -UPDATE `yz_backend_password_store` SET `accounts` = '[]' - WHERE `accounts` IS NULL OR `accounts` = ''; - - --- ---------- 第 4 步(可选,不可逆):删除旧的单账号列 ---------- --- 先在页面上确认历史数据都能正常显示,再手动执行下面两条。 --- 旧表这三列是 NOT NULL DEFAULT '',不删也不影响新代码运行。 - --- ALTER TABLE `yz_platform_password_store` --- DROP COLUMN `username`, DROP COLUMN `password`, DROP COLUMN `registration_info`; --- ALTER TABLE `yz_backend_password_store` --- DROP COLUMN `username`, DROP COLUMN `password`, DROP COLUMN `registration_info`; diff --git a/sql/alter_reimbursement_items_add_receipt_amounts.sql b/sql/alter_reimbursement_items_add_receipt_amounts.sql deleted file mode 100644 index 38ac2d2..0000000 --- a/sql/alter_reimbursement_items_add_receipt_amounts.sql +++ /dev/null @@ -1,12 +0,0 @@ --- 报销明细:每张发票对应一个确认金额,与 receipt_file_ids 顺序一一对应(逗号分隔,空位可留空) --- 示例:receipt_file_ids=357,358 receipt_amounts=98.00,145.00 - --- 若已有 receipt_urls 列,用这一条: -ALTER TABLE `yz_backend_reimbursement_items` - ADD COLUMN `receipt_amounts` varchar(500) NOT NULL DEFAULT '' COMMENT '票据金额,逗号分隔,与 receipt_file_ids 对应' AFTER `receipt_urls`; - --- 若上面报错 Unknown column 'receipt_urls',改用这一条(放在 receipt_file_ids 后面): --- ALTER TABLE `yz_backend_reimbursement_items` --- ADD COLUMN `receipt_amounts` varchar(500) NOT NULL DEFAULT '' COMMENT '票据金额,逗号分隔,与 receipt_file_ids 对应' AFTER `receipt_file_ids`; - --- 若报错 Duplicate column name 'receipt_amounts',说明字段已存在,无需重复执行。 diff --git a/sql/alter_reimbursement_items_add_receipt_file_ids.sql b/sql/alter_reimbursement_items_add_receipt_file_ids.sql deleted file mode 100644 index 16fdd5b..0000000 --- a/sql/alter_reimbursement_items_add_receipt_file_ids.sql +++ /dev/null @@ -1,3 +0,0 @@ --- 报销明细发票关联统一文件表 yz_system_files(逗号分隔多个文件 ID) -ALTER TABLE `yz_backend_reimbursement_items` - ADD COLUMN `receipt_file_ids` varchar(500) NOT NULL DEFAULT '' COMMENT '关联 yz_system_files.id,逗号分隔' AFTER `receipt_flag`; diff --git a/sql/alter_reimbursement_items_add_receipt_urls.sql b/sql/alter_reimbursement_items_add_receipt_urls.sql deleted file mode 100644 index 0c4ff72..0000000 --- a/sql/alter_reimbursement_items_add_receipt_urls.sql +++ /dev/null @@ -1,3 +0,0 @@ --- 票据地址(多个 URL 逗号分隔,与 receipt_file_ids 对应) -ALTER TABLE `yz_backend_reimbursement_items` - ADD COLUMN `receipt_urls` varchar(500) NOT NULL DEFAULT '' COMMENT '票据文件地址,逗号分隔' AFTER `receipt_file_ids`; diff --git a/sql/alter_reimbursement_items_drop_receipt_file_id.sql b/sql/alter_reimbursement_items_drop_receipt_file_id.sql deleted file mode 100644 index 652b9d8..0000000 --- a/sql/alter_reimbursement_items_drop_receipt_file_id.sql +++ /dev/null @@ -1,12 +0,0 @@ --- 报销明细发票:统一使用 receipt_file_ids,移除 receipt_file_id --- 若已存在 receipt_file_id 列,先合并数据再删除 - -UPDATE `yz_backend_reimbursement_items` -SET `receipt_file_ids` = CAST(`receipt_file_id` AS CHAR) -WHERE (`receipt_file_ids` = '' OR `receipt_file_ids` IS NULL) - AND `receipt_file_id` IS NOT NULL - AND `receipt_file_id` > 0; - -ALTER TABLE `yz_backend_reimbursement_items` - DROP INDEX `idx_receipt_file_id`, - DROP COLUMN `receipt_file_id`; diff --git a/sql/alter_reimbursement_items_rename_receipt_url.sql b/sql/alter_reimbursement_items_rename_receipt_url.sql deleted file mode 100644 index 3812d39..0000000 --- a/sql/alter_reimbursement_items_rename_receipt_url.sql +++ /dev/null @@ -1,6 +0,0 @@ --- receipt_url 重命名为 receipt_urls(与代码字段一致) -ALTER TABLE `yz_backend_reimbursement_items` - CHANGE COLUMN `receipt_url` `receipt_urls` varchar(500) NOT NULL DEFAULT '' COMMENT '票据文件地址,逗号分隔'; - -ALTER TABLE `yz_backend_reimbursement_items` - DROP INDEX `idx_receipt_url`; diff --git a/sql/alter_reimbursements_drop_title_expense_type.sql b/sql/alter_reimbursements_drop_title_expense_type.sql deleted file mode 100644 index 7225f08..0000000 --- a/sql/alter_reimbursements_drop_title_expense_type.sql +++ /dev/null @@ -1,7 +0,0 @@ --- 移除报销主表不再使用的字段:报销事由(title)、主表费用类型(expense_type) --- 费用明细表 yz_backend_reimbursement_items 的 expense_type 保留不变 - -ALTER TABLE `yz_backend_reimbursements` - DROP INDEX `idx_expense_type`, - DROP COLUMN `title`, - DROP COLUMN `expense_type`; diff --git a/sql/alter_tenant_domain_add_custom.sql b/sql/alter_tenant_domain_add_custom.sql deleted file mode 100644 index 6605d63..0000000 --- a/sql/alter_tenant_domain_add_custom.sql +++ /dev/null @@ -1,39 +0,0 @@ --- 租户域名表增加「自有域名绑定 + 自动 HTTPS」所需字段(兼容已有数据库) --- 已存在字段时请跳过对应 ALTER 语句。 --- 对应模型:go/models/system_tenant_domain.go - --- 域名类型:1-平台二级域名(历史数据默认值即为 1)2-租户自有域名 -ALTER TABLE `yz_system_tenant_domain` - ADD COLUMN `domain_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '域名类型 1-平台二级域名 2-自有域名' AFTER `tid`; - --- 证书状态:0-待处理 1-签发中 2-已签发 3-签发失败 -ALTER TABLE `yz_system_tenant_domain` - ADD COLUMN `ssl_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '证书状态 0-待处理 1-签发中 2-已签发 3-失败' AFTER `status`; - -ALTER TABLE `yz_system_tenant_domain` - ADD COLUMN `ssl_expire_at` datetime DEFAULT NULL COMMENT '证书到期时间' AFTER `ssl_status`; - -ALTER TABLE `yz_system_tenant_domain` - ADD COLUMN `ssl_error` varchar(500) DEFAULT NULL COMMENT '最近一次签发失败原因' AFTER `ssl_expire_at`; - --- DNS 解析校验:0-未检测 1-已通过 2-未通过 -ALTER TABLE `yz_system_tenant_domain` - ADD COLUMN `verify_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'DNS校验 0-未检测 1-已通过 2-未通过' AFTER `ssl_error`; - -ALTER TABLE `yz_system_tenant_domain` - ADD COLUMN `verify_msg` varchar(500) DEFAULT NULL COMMENT '最近一次DNS检测说明' AFTER `verify_status`; - -ALTER TABLE `yz_system_tenant_domain` - ADD COLUMN `last_verify_at` datetime DEFAULT NULL COMMENT '最近一次DNS检测时间' AFTER `verify_msg`; - --- 索引:按 Host 查租户是官网渲染的热路径;域名类型用于「同类型只能一条」的判断 --- 注意:这里用普通索引而非唯一索引,避免历史脏数据(含软删重复域名)导致 ALTER 失败; --- 域名唯一性由应用层 bindCustom / apply 接口校验。 -ALTER TABLE `yz_system_tenant_domain` - ADD KEY `idx_full_domain` (`full_domain`); - -ALTER TABLE `yz_system_tenant_domain` - ADD KEY `idx_tid_domain_type` (`tid`, `domain_type`); - -ALTER TABLE `yz_system_tenant_domain` - ADD KEY `idx_ssl_status` (`ssl_status`); diff --git a/sql/cleanup_cms_auto_categories.sql b/sql/cleanup_cms_auto_categories.sql deleted file mode 100644 index 195062d..0000000 --- a/sql/cleanup_cms_auto_categories.sql +++ /dev/null @@ -1,7 +0,0 @@ --- 清理旧版本自动插入的文章分类(旧代码 cmsEnsureTables 每次请求都会调 --- EnsureCmsArticleDefaultCategories,向 yz_cms_article_category 插入 --- tid=0 的「文章中心」「新闻中心」,导致刷新页面分类不断增多)。 --- 新代码已移除该逻辑,分类完全由后台手动维护。 --- 执行前请先备份;如需保留其中一条,请自行注释对应语句。 - -DELETE FROM yz_cms_article_category WHERE tid = 0 AND `name` IN ('文章中心', '新闻中心'); diff --git a/sql/fix_crm_owner_user_name.sql b/sql/fix_crm_owner_user_name.sql deleted file mode 100644 index c3b1c0b..0000000 --- a/sql/fix_crm_owner_user_name.sql +++ /dev/null @@ -1,102 +0,0 @@ --- ============================================================================= --- 修正「负责人 / 回访人 / 上传人」姓名:把登录账号刷成真实姓名 --- --- 背景: --- 历史代码把 JWT 里的 Username(登录账号,如 hero920103)写进了 --- owner_user_name / last_owner_name 等字段,导致界面显示账号而不是人名。 --- 后端已改为按 uid 查 yz_system_tenant_user.name 写入真实姓名, --- 本脚本用于修正存量数据。 --- --- 说明: --- - 按 tenant_id + owner_user_id(uid) 关联 yz_system_tenant_user 取 name; --- - 只更新「能匹配到用户且姓名非空」的记录,不动其它数据; --- - 幂等,可重复执行(姓名已一致的行不会被更新)。 --- ============================================================================= - -SET NAMES utf8mb4; - --- 1. 客户:负责人姓名 -UPDATE `yz_backend_erp_customer` c -JOIN `yz_system_tenant_user` u - ON u.`tid` = CAST(c.`tenant_id` AS UNSIGNED) - AND u.`uid` = CAST(c.`owner_user_id` AS UNSIGNED) -SET c.`owner_user_name` = u.`name` -WHERE c.`owner_user_id` IS NOT NULL AND c.`owner_user_id` <> '' - AND u.`name` IS NOT NULL AND u.`name` <> '' - AND (c.`owner_user_name` IS NULL OR c.`owner_user_name` <> u.`name`); - --- 2. 客户:公海记录里的「原负责人」姓名 -UPDATE `yz_backend_erp_customer` c -JOIN `yz_system_tenant_user` u - ON u.`tid` = CAST(c.`tenant_id` AS UNSIGNED) - AND u.`uid` = CAST(c.`owner_user_id` AS UNSIGNED) -SET c.`last_owner_name` = u.`name` -WHERE c.`owner_user_id` IS NOT NULL AND c.`owner_user_id` <> '' - AND u.`name` IS NOT NULL AND u.`name` <> '' - AND c.`last_owner_name` IS NOT NULL AND c.`last_owner_name` <> '' - AND c.`last_owner_name` <> u.`name`; - --- 3. 线索:负责人姓名 -UPDATE `yz_backend_crm_clue` c -JOIN `yz_system_tenant_user` u - ON u.`tid` = CAST(c.`tenant_id` AS UNSIGNED) - AND u.`uid` = CAST(c.`owner_user_id` AS UNSIGNED) -SET c.`owner_user_name` = u.`name` -WHERE c.`owner_user_id` <> '' - AND u.`name` IS NOT NULL AND u.`name` <> '' - AND (c.`owner_user_name` IS NULL OR c.`owner_user_name` <> u.`name`); - --- 4. 商机:负责人姓名 -UPDATE `yz_backend_crm_business` c -JOIN `yz_system_tenant_user` u - ON u.`tid` = CAST(c.`tenant_id` AS UNSIGNED) - AND u.`uid` = CAST(c.`owner_user_id` AS UNSIGNED) -SET c.`owner_user_name` = u.`name` -WHERE c.`owner_user_id` <> '' - AND u.`name` IS NOT NULL AND u.`name` <> '' - AND (c.`owner_user_name` IS NULL OR c.`owner_user_name` <> u.`name`); - --- 5. 项目:负责人姓名 -UPDATE `yz_backend_crm_project` c -JOIN `yz_system_tenant_user` u - ON u.`tid` = CAST(c.`tenant_id` AS UNSIGNED) - AND u.`uid` = CAST(c.`owner_user_id` AS UNSIGNED) -SET c.`owner_user_name` = u.`name` -WHERE c.`owner_user_id` <> '' - AND u.`name` IS NOT NULL AND u.`name` <> '' - AND (c.`owner_user_name` IS NULL OR c.`owner_user_name` <> u.`name`); - --- 6. 回访记录:回访人姓名 -UPDATE `yz_backend_crm_follow` c -JOIN `yz_system_tenant_user` u - ON u.`tid` = CAST(c.`tenant_id` AS UNSIGNED) - AND u.`uid` = CAST(c.`owner_user_id` AS UNSIGNED) -SET c.`owner_user_name` = u.`name` -WHERE c.`owner_user_id` <> '' - AND u.`name` IS NOT NULL AND u.`name` <> '' - AND (c.`owner_user_name` IS NULL OR c.`owner_user_name` <> u.`name`); - --- 7. 附件:上传人姓名 -UPDATE `yz_backend_crm_attach` c -JOIN `yz_system_tenant_user` u - ON u.`tid` = CAST(c.`tenant_id` AS UNSIGNED) - AND u.`uid` = CAST(c.`uploader_id` AS UNSIGNED) -SET c.`uploader_name` = u.`name` -WHERE c.`uploader_id` <> '' - AND u.`name` IS NOT NULL AND u.`name` <> '' - AND (c.`uploader_name` IS NULL OR c.`uploader_name` <> u.`name`); - --- 8. 操作日志:操作人姓名 -UPDATE `yz_backend_crm_operate_log` c -JOIN `yz_system_tenant_user` u - ON u.`tid` = CAST(c.`tenant_id` AS UNSIGNED) - AND u.`uid` = CAST(c.`operator_id` AS UNSIGNED) -SET c.`operator_name` = u.`name` -WHERE c.`operator_id` <> '' - AND u.`name` IS NOT NULL AND u.`name` <> '' - AND (c.`operator_name` IS NULL OR c.`operator_name` <> u.`name`); - --- 9. 核对:客户负责人分布 -SELECT `owner_user_id`, `owner_user_name`, COUNT(*) AS `cnt` -FROM `yz_backend_erp_customer` -GROUP BY `owner_user_id`, `owner_user_name`; diff --git a/sql/rename_erp_tables_to_backend_organization.sql b/sql/rename_erp_tables_to_backend_organization.sql deleted file mode 100644 index 60f72d0..0000000 --- a/sql/rename_erp_tables_to_backend_organization.sql +++ /dev/null @@ -1,193 +0,0 @@ --- 组织架构模块:表名去掉 erp 字样,改为租户端通用命名 --- yz_backend_erp_organization -> yz_backend_organization --- yz_backend_erp_employee -> yz_backend_employee --- yz_backend_erp_position -> yz_backend_position --- --- 组织架构由进销存(ERP)与办公自动化(OA)共用同一份数据,通过 tid 实现租户间隔离。 --- 执行前请确认当前数据库字符集为 utf8mb4,并先备份这三张表。 --- --- 说明:脚本可重复执行。如果旧表不存在(全新环境),RENAME 会被跳过, --- 后面的 CREATE TABLE IF NOT EXISTS 会建出目标表结构。 - --- --------------------------------------------------------------------------- --- 1. 重命名已有表(仅当旧表存在且新表不存在时执行) --- --------------------------------------------------------------------------- - -SET @schema := DATABASE(); - -SET @sql := IF( - (SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_erp_organization') = 1 - AND (SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_organization') = 0, - 'RENAME TABLE `yz_backend_erp_organization` TO `yz_backend_organization`', - 'SELECT "skip: yz_backend_organization"' -); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - -SET @sql := IF( - (SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_erp_employee') = 1 - AND (SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_employee') = 0, - 'RENAME TABLE `yz_backend_erp_employee` TO `yz_backend_employee`', - 'SELECT "skip: yz_backend_employee"' -); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - -SET @sql := IF( - (SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_erp_position') = 1 - AND (SELECT COUNT(*) FROM information_schema.TABLES - WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_position') = 0, - 'RENAME TABLE `yz_backend_erp_position` TO `yz_backend_position`', - 'SELECT "skip: yz_backend_position"' -); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- --------------------------------------------------------------------------- --- 2. 全新环境建表(旧表不存在时生效) --- --------------------------------------------------------------------------- - -CREATE TABLE IF NOT EXISTS `yz_backend_organization` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tid` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '租户ID(数据隔离)', - `org_name` varchar(128) NOT NULL DEFAULT '' COMMENT '组织名称', - `org_code` varchar(64) NOT NULL DEFAULT '' COMMENT '组织编码(租户内唯一)', - `parent_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '上级组织ID,0为顶级', - `sort` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '排序,越小越靠前', - `leader_id` bigint(20) unsigned DEFAULT NULL COMMENT '负责人(员工ID)', - `is_company` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否企业单位:1-是 0-部门', - `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1-启用 0-禁用', - `remark` varchar(512) DEFAULT NULL 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`) USING BTREE, - KEY `idx_tid` (`tid`) USING BTREE, - KEY `idx_tid_code` (`tid`,`org_code`) USING BTREE, - KEY `idx_parent_id` (`parent_id`) USING BTREE, - KEY `idx_is_company` (`is_company`) USING BTREE, - KEY `idx_status` (`status`) USING BTREE, - KEY `idx_delete_time` (`delete_time`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='组织架构表(ERP与OA共用)'; - -CREATE TABLE IF NOT EXISTS `yz_backend_employee` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tid` int(11) DEFAULT NULL COMMENT '租户ID(数据隔离)', - `account` varchar(50) NOT NULL DEFAULT '' COMMENT '登录账号(租户内唯一)', - `password` varchar(64) NOT NULL DEFAULT '' COMMENT '密码(sha256 hex)', - `name` varchar(30) NOT NULL DEFAULT '' COMMENT '姓名', - `gender` tinyint(1) NOT NULL DEFAULT '0' COMMENT '性别:0-未知 1-男 2-女', - `birthday` date DEFAULT NULL COMMENT '生日', - `affiliate_unit` varchar(100) DEFAULT NULL COMMENT '隶属单位(组织ID)', - `department` varchar(50) DEFAULT NULL COMMENT '部门(组织ID)', - `position` varchar(50) DEFAULT NULL COMMENT '职位', - `education` varchar(20) DEFAULT NULL COMMENT '学历', - `nation` varchar(20) DEFAULT NULL COMMENT '民族', - `phone` varchar(20) DEFAULT NULL COMMENT '手机号', - `wechat` varchar(50) DEFAULT NULL COMMENT '微信', - `email` varchar(100) DEFAULT NULL COMMENT '邮箱', - `home_address` varchar(255) DEFAULT NULL COMMENT '家庭住址', - `account_status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '账号状态:1-启用 0-禁用 2-离职', - `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`) USING BTREE, - KEY `idx_tid` (`tid`) USING BTREE, - KEY `idx_tid_account` (`tid`,`account`) USING BTREE, - KEY `idx_department` (`department`) USING BTREE, - KEY `idx_affiliate_unit` (`affiliate_unit`) USING BTREE, - KEY `idx_account_status` (`account_status`) USING BTREE, - KEY `idx_delete_time` (`delete_time`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='员工信息表(ERP与OA共用)'; - -CREATE TABLE IF NOT EXISTS `yz_backend_position` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tid` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '租户ID(数据隔离)', - `department_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '所属部门(组织ID)', - `position_code` varchar(50) NOT NULL DEFAULT '' COMMENT '职位编码(租户内唯一)', - `position_name` varchar(100) NOT NULL DEFAULT '' COMMENT '职位名称', - `position_type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '职位类型:0-普通 1-主管 2-负责人', - `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1-启用 0-禁用', - `sort` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '排序,越小越靠前', - `remark` varchar(512) DEFAULT NULL 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`) USING BTREE, - KEY `idx_tid` (`tid`) USING BTREE, - KEY `idx_tid_code` (`tid`,`position_code`) USING BTREE, - KEY `idx_department_id` (`department_id`) USING BTREE, - KEY `idx_status` (`status`) USING BTREE, - KEY `idx_delete_time` (`delete_time`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='职位表(ERP与OA共用)'; - --- --------------------------------------------------------------------------- --- 3. 补齐旧表缺失的字段与索引(重命名后的表可能来自更早的版本) --- --------------------------------------------------------------------------- - --- yz_backend_position 新增 remark / delete_time -SET @sql := IF( - (SELECT COUNT(*) FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_position' - AND COLUMN_NAME = 'remark') = 0, - 'ALTER TABLE `yz_backend_position` ADD COLUMN `remark` varchar(512) DEFAULT NULL COMMENT ''备注'' AFTER `sort`', - 'SELECT "skip: yz_backend_position.remark"' -); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - -SET @sql := IF( - (SELECT COUNT(*) FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_position' - AND COLUMN_NAME = 'delete_time') = 0, - 'ALTER TABLE `yz_backend_position` ADD COLUMN `delete_time` datetime DEFAULT NULL COMMENT ''删除时间(软删除)''', - 'SELECT "skip: yz_backend_position.delete_time"' -); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - -SET @sql := IF( - (SELECT COUNT(*) FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_position' - AND INDEX_NAME = 'idx_tid_code') = 0, - 'ALTER TABLE `yz_backend_position` ADD INDEX `idx_tid_code` (`tid`,`position_code`) USING BTREE', - 'SELECT "skip: yz_backend_position.idx_tid_code"' -); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - -SET @sql := IF( - (SELECT COUNT(*) FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_organization' - AND INDEX_NAME = 'idx_tid_code') = 0, - 'ALTER TABLE `yz_backend_organization` ADD INDEX `idx_tid_code` (`tid`,`org_code`) USING BTREE', - 'SELECT "skip: yz_backend_organization.idx_tid_code"' -); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - -SET @sql := IF( - (SELECT COUNT(*) FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = 'yz_backend_employee' - AND INDEX_NAME = 'idx_tid_account') = 0, - 'ALTER TABLE `yz_backend_employee` ADD INDEX `idx_tid_account` (`tid`,`account`) USING BTREE', - 'SELECT "skip: yz_backend_employee.idx_tid_account"' -); -PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; - --- --------------------------------------------------------------------------- --- 4. 组织架构设置存放在 yz_backend_normal_setting,按 backend_org_settings_{tid} 区分租户 --- 这里只保证该表存在,具体记录由后端首次保存设置时写入。 --- --------------------------------------------------------------------------- - -CREATE TABLE IF NOT EXISTS `yz_backend_normal_setting` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `name` varchar(128) NOT NULL DEFAULT '' COMMENT '配置名称', - `value` text COMMENT '配置内容(JSON)', - `code` varchar(64) NOT NULL DEFAULT '' COMMENT '配置标识', - `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)', - PRIMARY KEY (`id`) USING BTREE, - KEY `idx_code` (`code`) USING BTREE, - KEY `idx_delete_time` (`delete_time`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='管理端通用配置表'; diff --git a/sql/seed_employee_file_menu.sql b/sql/seed_employee_file_menu.sql deleted file mode 100644 index 75544dd..0000000 --- a/sql/seed_employee_file_menu.sql +++ /dev/null @@ -1,27 +0,0 @@ --- 员工档案菜单(租户端):挂在办公自动化模块目录下。 --- --- 前置:yz_system_menu 中已存在 path 为 /apps/oa 的模块目录菜单; --- 若不存在,脚本会自动创建。 --- views 字段:[2] 租户端。type 字段:2-页面。 --- --- 脚本可重复执行:按 path 判重,已存在的菜单会更新组件路径与标题。 - -INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT 0, '办公自动化', '/apps/oa', '', 'Document', 31, 1, 1, '[2]', 1, '办公自动化模块' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/oa' -); - -SET @oa_pid := (SELECT `id` FROM `yz_system_menu` WHERE `path` = '/apps/oa' ORDER BY `id` LIMIT 1); - -INSERT INTO `yz_system_menu` (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT @oa_pid, '员工档案', '/apps/oa/employeefile', '/apps/oa/employeefile/index.vue', 'Postcard', 4, 1, 1, '[2]', 2, '一人一档的人事档案管理' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t WHERE t.`path` = '/apps/oa/employeefile' -); - --- 修正已有菜单的组件路径(历史数据可能指向旧组件) -UPDATE `yz_system_menu` SET `component_path` = '/apps/oa/employeefile/index.vue', `type` = 2, `views` = '[2]' - WHERE `path` = '/apps/oa/employeefile'; diff --git a/sql/seed_oa_compensation_menu.sql b/sql/seed_oa_compensation_menu.sql deleted file mode 100644 index 5f1c0ec..0000000 --- a/sql/seed_oa_compensation_menu.sql +++ /dev/null @@ -1,69 +0,0 @@ --- 薪酬管理菜单(租户端):挂在“办公自动化”模块目录下。 --- --- 说明: --- 1. 本脚本只创建/修正 yz_system_menu 菜单记录,不涉及薪酬业务数据表。 --- 2. 路由由前端根据登录接口返回的菜单数据动态注册; --- 不需要在 backend/src/router/index.js 中增加静态业务路由。 --- 3. 执行完成后,请在角色菜单权限关联表中为目标角色授予“薪酬管理”菜单权限, --- 然后退出重登或清理菜单缓存。 --- --- views: [2] = 租户端 --- type: 1 = 目录,2 = 页面 --- 脚本可重复执行:通过 path 判重,已有记录会被修正为当前配置。 - --- 确保“办公自动化”父级目录存在。 -INSERT INTO `yz_system_menu` - (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT - 0, '办公自动化', '/apps/oa', '', 'Document', 31, 1, 1, '[2]', 1, '办公自动化模块' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 - FROM (SELECT * FROM `yz_system_menu`) AS t - WHERE t.`path` = '/apps/oa' -); - -SET @oa_pid := ( - SELECT `id` - FROM `yz_system_menu` - WHERE `path` = '/apps/oa' - ORDER BY `id` - LIMIT 1 -); - --- 新增“薪酬管理”页面菜单。 -INSERT INTO `yz_system_menu` - (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT - @oa_pid, - '薪酬管理', - '/apps/oa/compensation', - '/apps/oa/compensation/index.vue', - 'Money', - 6, - 1, - 1, - '[2]', - 2, - '员工薪资档案与月度工资核算' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 - FROM (SELECT * FROM `yz_system_menu`) AS t - WHERE t.`path` = '/apps/oa/compensation' -); - --- 修正历史或手工新增菜单的动态组件路径及基础属性。 -UPDATE `yz_system_menu` -SET - `pid` = @oa_pid, - `title` = '薪酬管理', - `component_path` = '/apps/oa/compensation/index.vue', - `icon` = 'Money', - `sort` = 6, - `status` = 1, - `is_visible` = 1, - `views` = '[2]', - `type` = 2, - `remark` = '员工薪资档案与月度工资核算' -WHERE `path` = '/apps/oa/compensation'; diff --git a/sql/seed_oa_notice_menu.sql b/sql/seed_oa_notice_menu.sql deleted file mode 100644 index d086185..0000000 --- a/sql/seed_oa_notice_menu.sql +++ /dev/null @@ -1,69 +0,0 @@ --- 通知公告菜单(租户端):挂在“办公自动化”模块目录下。 --- --- 说明: --- 1. 本脚本只创建/修正 yz_system_menu 菜单记录,不涉及通知公告业务数据表。 --- 2. 路由由前端根据登录接口返回的菜单数据动态注册; --- 不需要在 backend/src/router/index.js 中增加静态业务路由。 --- 3. 执行完成后,请在角色菜单权限关联表中为目标角色授予“通知公告”菜单权限, --- 然后退出重登或清理菜单缓存。 --- --- views: [2] = 租户端 --- type: 1 = 目录,2 = 页面 --- 脚本可重复执行:通过 path 判重,已有记录会被修正为当前配置。 - --- 确保“办公自动化”父级目录存在。 -INSERT INTO `yz_system_menu` - (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT - 0, '办公自动化', '/apps/oa', '', 'Document', 31, 1, 1, '[2]', 1, '办公自动化模块' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 - FROM (SELECT * FROM `yz_system_menu`) AS t - WHERE t.`path` = '/apps/oa' -); - -SET @oa_pid := ( - SELECT `id` - FROM `yz_system_menu` - WHERE `path` = '/apps/oa' - ORDER BY `id` - LIMIT 1 -); - --- 新增“通知公告”页面菜单。 -INSERT INTO `yz_system_menu` - (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT - @oa_pid, - '通知公告', - '/apps/oa/notice', - '/apps/oa/notice/index.vue', - 'Bell', - 7, - 1, - 1, - '[2]', - 2, - '通知公告发布与管理' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 - FROM (SELECT * FROM `yz_system_menu`) AS t - WHERE t.`path` = '/apps/oa/notice' -); - --- 修正历史或手工新增菜单的动态组件路径及基础属性。 -UPDATE `yz_system_menu` -SET - `pid` = @oa_pid, - `title` = '通知公告', - `component_path` = '/apps/oa/notice/index.vue', - `icon` = 'Bell', - `sort` = 7, - `status` = 1, - `is_visible` = 1, - `views` = '[2]', - `type` = 2, - `remark` = '通知公告发布与管理' -WHERE `path` = '/apps/oa/notice'; diff --git a/sql/seed_organization_menus.sql b/sql/seed_organization_menus.sql deleted file mode 100644 index 82ed7e7..0000000 --- a/sql/seed_organization_menus.sql +++ /dev/null @@ -1,91 +0,0 @@ --- 全局基础设置:组织架构、人员管理、职位管理 --- 三项功能使用统一组织数据,统一归入全局“基础设置”,不再隶属 OA 或 ERP。 --- views:[2] 租户后台;type:1-目录,2-页面。 --- 可重复执行。最后会删除 OA / ERP 下的历史重复页面菜单。 - --- 1. 确保全局“基础设置”目录存在 -INSERT INTO `yz_system_menu` - (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT - 0, '基础设置', '/basic-settings', '', 'Setting', 20, 1, 1, '[2]', 1, '全局基础数据维护' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t - WHERE t.`path` = '/basic-settings' -); - -SET @basic_settings_pid := ( - SELECT `id` FROM `yz_system_menu` - WHERE `path` = '/basic-settings' - ORDER BY `id` LIMIT 1 -); - --- 2. 创建全局入口 -INSERT INTO `yz_system_menu` - (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT - @basic_settings_pid, '组织架构', '/basic-settings/organization', - '/basicSettings/organization/index.vue', 'OfficeBuilding', 1, 1, 1, '[2]', 2, - '全局组织架构维护,与所有业务模块共用' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t - WHERE t.`path` = '/basic-settings/organization' -); - -INSERT INTO `yz_system_menu` - (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT - @basic_settings_pid, '人员管理', '/basic-settings/employee', - '/basicSettings/employee/index.vue', 'User', 2, 1, 1, '[2]', 2, - '全局人员维护,与所有业务模块共用' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t - WHERE t.`path` = '/basic-settings/employee' -); - -INSERT INTO `yz_system_menu` - (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `remark`) -SELECT - @basic_settings_pid, '职位管理', '/basic-settings/position', - '/basicSettings/position/index.vue', 'Postcard', 3, 1, 1, '[2]', 2, - '全局职位维护,与所有业务模块共用' -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM (SELECT * FROM `yz_system_menu`) AS t - WHERE t.`path` = '/basic-settings/position' -); - --- 3. 修正已存在的全局入口 -UPDATE `yz_system_menu` -SET `pid` = @basic_settings_pid, `title` = '组织架构', - `component_path` = '/basicSettings/organization/index.vue', - `icon` = 'OfficeBuilding', `sort` = 1, `status` = 1, `is_visible` = 1, - `views` = '[2]', `type` = 2, `remark` = '全局组织架构维护,与所有业务模块共用' -WHERE `path` = '/basic-settings/organization'; - -UPDATE `yz_system_menu` -SET `pid` = @basic_settings_pid, `title` = '人员管理', - `component_path` = '/basicSettings/employee/index.vue', - `icon` = 'User', `sort` = 2, `status` = 1, `is_visible` = 1, - `views` = '[2]', `type` = 2, `remark` = '全局人员维护,与所有业务模块共用' -WHERE `path` = '/basic-settings/employee'; - -UPDATE `yz_system_menu` -SET `pid` = @basic_settings_pid, `title` = '职位管理', - `component_path` = '/basicSettings/position/index.vue', - `icon` = 'Postcard', `sort` = 3, `status` = 1, `is_visible` = 1, - `views` = '[2]', `type` = 2, `remark` = '全局职位维护,与所有业务模块共用' -WHERE `path` = '/basic-settings/position'; - --- 4. 删除历史 OA / ERP 下的重复页面入口,不删除模块目录本身。 -DELETE FROM `yz_system_menu` -WHERE `path` IN ( - '/apps/erp/organization', - '/apps/erp/employee', - '/apps/erp/position', - '/apps/oa/organization', - '/apps/oa/employee', - '/apps/oa/position' -); diff --git a/sql/upgrade_yz_system_reminderlist.sql b/sql/upgrade_yz_system_reminderlist.sql deleted file mode 100644 index 43857ca..0000000 --- a/sql/upgrade_yz_system_reminderlist.sql +++ /dev/null @@ -1,12 +0,0 @@ --- 升级已有的 yz_system_reminderlist 表,添加 batch_id 以及发送目标相关字段 -ALTER TABLE `yz_system_reminderlist` - ADD COLUMN `batch_id` varchar(64) NOT NULL DEFAULT '' COMMENT '批次号/分组ID' AFTER `delete_time`, - ADD COLUMN `target_type` varchar(32) NOT NULL DEFAULT '' COMMENT '发送目标类型: platform, tenant_all, role, tenant' AFTER `batch_id`, - ADD COLUMN `target_role_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '目标角色ID' AFTER `target_type`, - ADD COLUMN `target_tenant_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '目标租户ID' AFTER `target_role_id`, - ADD INDEX `idx_batch` (`batch_id`); - --- 初始化已有记录的 batch_id (如果存在空值,使用 create_time 和 sender_id 进行分组初始化) -UPDATE `yz_system_reminderlist` -SET `batch_id` = CONCAT(UNIX_TIMESTAMP(COALESCE(`create_time`, NOW())), '_', `sender_id`) -WHERE `batch_id` = '' OR `batch_id` IS NULL; diff --git a/sql/yz_backend_crm_contract.sql b/sql/yz_backend_crm_contract.sql deleted file mode 100644 index 019256d..0000000 --- a/sql/yz_backend_crm_contract.sql +++ /dev/null @@ -1,53 +0,0 @@ --- ============================================================================= --- CRM 合同管理:进度式创建(合同信息 / 产品清单),绑定项目或无头合同 --- 对应租户端 backend 页面:/apps/crm/contract --- 说明: --- 1. project_id 为空即为「无头合同」,否则为「项目合同」; --- 2. our_role:我方角色 1=甲方 2=乙方 3=丙方 4=丁方(当前租户扮演的一方,默认乙方); --- party_count:2=双方(甲乙)3=三方(甲乙丙)4=四方(甲乙丙丁); --- 3. parties / products 以 JSON 文本存储; --- 4. 各部分金额冗余为表字段,由后端按 products 重算,便于统计与排序; --- 5. status:1=草稿(进度式保存中)2=已完成 3=已作废;step 记录创建进度。 --- ⚠️ 安全说明:本脚本只做 CREATE TABLE IF NOT EXISTS,不删除或覆盖任何已有数据,可重复执行。 --- ============================================================================= - -SET NAMES utf8mb4; - -CREATE TABLE IF NOT EXISTS `yz_backend_crm_contract` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID', - `tenant_id` varchar(64) NOT NULL COMMENT '租户ID', - `contract_no` varchar(50) NOT NULL DEFAULT '' COMMENT '合同编号', - `contract_name` varchar(100) NOT NULL COMMENT '合同名称', - `contract_category` varchar(20) NOT NULL DEFAULT '' COMMENT '合同分类:1开发/2服务/3销售/4租赁/5采购/6运维/7咨询/8其他', - `our_role` tinyint(4) NOT NULL DEFAULT '2' COMMENT '我方角色:1甲方/2乙方/3丙方/4丁方(当前租户扮演的一方)', - `party_count` tinyint(4) NOT NULL DEFAULT '2' COMMENT '合同形式:2双方(甲乙)/3三方(甲乙丙)/4四方(甲乙丙丁)', - `project_id` bigint(20) DEFAULT NULL COMMENT '关联项目ID(yz_backend_crm_project.id),空=无头合同', - `project_name` varchar(100) NOT NULL DEFAULT '' COMMENT '项目名称', - `owner_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '项目负责人用户ID', - `owner_user_name` varchar(128) NOT NULL DEFAULT '' COMMENT '项目负责人姓名(默认为创建人)', - `sign_date` date DEFAULT NULL COMMENT '签订日期', - `effective_date` date DEFAULT NULL COMMENT '生效日期', - `expire_date` date DEFAULT NULL COMMENT '结束日期', - `parties` text COMMENT '各方签约主体 JSON:[{role,ref_type,ref_id,ref_name,signer_name,signer_phone}]', - `products` text COMMENT '产品清单 JSON:[{name,category,spec,unit,quantity,price,cost_price,remark}]', - `hardware_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '硬件部分金额(元)', - `software_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '软件部分金额(元)', - `other_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '其他部分金额(服务/开发/其他)(元)', - `total_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '合同总金额(元)', - `total_cost` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '产品总成本(元)', - `total_profit` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '合同总利润(元)', - `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1草稿/2已完成/3已作废/4履约中/5执行异常', - `step` tinyint(4) NOT NULL DEFAULT '1' COMMENT '进度式创建步骤:1合同信息/2产品清单', - `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_contract_name` (`tenant_id`,`contract_name`), - KEY `idx_contract_no` (`tenant_id`,`contract_no`), - 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合同表'; diff --git a/sql/yz_backend_crm_pipeline.sql b/sql/yz_backend_crm_pipeline.sql deleted file mode 100644 index 2cb8b2b..0000000 --- a/sql/yz_backend_crm_pipeline.sql +++ /dev/null @@ -1,229 +0,0 @@ --- ============================================================================= --- CRM 业务管线:线索 → 商机 → 项目,回访贯穿全流程 --- 对应租户端 backend 页面:/apps/crm/clue、/apps/crm/business、/apps/crm/project、/apps/crm/follow --- 说明: --- 1. 线索的「客户名称」暂不关联客户管理(线索阶段尚无价值), --- 线索转化为商机时才把客户名称落地为正式客户(yz_backend_erp_customer)。 --- 2. 线索转化商机后会被锁定(locked=1),后续操作都在商机进行。 --- 3. 商机转化项目后同样锁定(locked=1)。 --- 4. 回访/跟进记录(yz_backend_crm_follow)与附件、联系人、操作日志均为通用多态表, --- 通过 related_type(1线索/2商机/3项目) + related_id 关联。 --- ⚠️ 安全说明(重要): --- 本脚本只做 CREATE TABLE IF NOT EXISTS,**不会删除或覆盖任何已有数据**,可重复执行。 --- 如需给已存在的表新增字段,请勿重跑本脚本,改用对应的 alter_*.sql 增量脚本。 --- ============================================================================= - -SET NAMES utf8mb4; - --- ----------------------------------------------------------------------------- --- 线索表 --- ----------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS `yz_backend_crm_clue` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID', - `tenant_id` varchar(64) NOT NULL COMMENT '租户ID', - `clue_name` varchar(100) NOT NULL COMMENT '线索名称', - `customer_name` varchar(100) NOT NULL DEFAULT '' COMMENT '客户名称(线索阶段不关联客户管理)', - `source` varchar(50) NOT NULL DEFAULT '' COMMENT '客户来源(1官网咨询/2电话咨询/3朋友介绍/4展会/5广告投放/6陌生拜访/7其他)', - `owner_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '负责人用户ID', - `owner_user_name` varchar(128) NOT NULL DEFAULT '' COMMENT '负责人姓名', - `industry` varchar(50) NOT NULL DEFAULT '' COMMENT '客户行业', - `next_contact_time` datetime DEFAULT NULL COMMENT '下次联系时间', - `clue_level` varchar(20) NOT NULL DEFAULT '2' COMMENT '客户级别:1重点/2普通/3非优先', - `contact_person` varchar(50) NOT NULL DEFAULT '' COMMENT '客户对接人', - `contact_position` varchar(50) NOT NULL DEFAULT '' COMMENT '对接人职位', - `contact_phone` varchar(20) NOT NULL DEFAULT '' COMMENT '对接人手机', - `contact_wechat` varchar(64) NOT NULL DEFAULT '' COMMENT '对接人微信', - `contact_qq` varchar(20) NOT NULL DEFAULT '' COMMENT '对接人QQ', - `address` varchar(255) NOT NULL DEFAULT '' COMMENT '公司地址', - `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1跟进中/2已转化/3已关闭', - `locked` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否锁定:0否/1是(转化后锁定不可编辑)', - `business_id` bigint(20) DEFAULT NULL COMMENT '转化后的商机ID', - `convert_time` datetime DEFAULT NULL 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_clue_name` (`tenant_id`,`clue_name`), - KEY `idx_owner` (`tenant_id`,`owner_user_id`), - KEY `idx_status` (`tenant_id`,`status`), - KEY `idx_delete_time` (`delete_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM线索表'; - --- ----------------------------------------------------------------------------- --- 商机表 --- ----------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS `yz_backend_crm_business` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID', - `tenant_id` varchar(64) NOT NULL COMMENT '租户ID', - `business_name` varchar(100) NOT NULL COMMENT '商机名称', - `clue_id` bigint(20) DEFAULT NULL COMMENT '来源线索ID', - `customer_id` bigint(20) DEFAULT NULL COMMENT '正式客户ID(yz_backend_erp_customer.id)', - `customer_name` varchar(100) NOT NULL DEFAULT '' COMMENT '客户名称', - `source` varchar(50) NOT NULL DEFAULT '' COMMENT '客户来源', - `owner_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '负责人用户ID', - `owner_user_name` varchar(128) NOT NULL DEFAULT '' COMMENT '负责人姓名', - `industry` varchar(50) NOT NULL DEFAULT '' COMMENT '客户行业', - `stage` varchar(20) NOT NULL DEFAULT '1' COMMENT '商机阶段:1初步接洽/2需求确认/3方案报价/4商务谈判/5赢单/6输单', - `amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '预计金额(元)', - `expect_deal_date` date DEFAULT NULL COMMENT '预计成交日期', - `next_contact_time` datetime DEFAULT NULL COMMENT '下次联系时间', - `level` varchar(20) NOT NULL DEFAULT '2' COMMENT '商机级别:1重点/2普通/3非优先', - `contact_person` varchar(50) NOT NULL DEFAULT '' COMMENT '对接人', - `contact_position` varchar(50) NOT NULL DEFAULT '' COMMENT '对接人职位', - `contact_phone` varchar(20) NOT NULL DEFAULT '' COMMENT '对接人手机', - `contact_wechat` varchar(64) NOT NULL DEFAULT '' COMMENT '对接人微信', - `contact_qq` varchar(20) NOT NULL DEFAULT '' COMMENT '对接人QQ', - `address` varchar(255) NOT NULL DEFAULT '' COMMENT '客户地址', - `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1跟进中/2已转化项目/3已关闭', - `locked` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否锁定:0否/1是(转化项目后锁定不可编辑)', - `project_id` bigint(20) DEFAULT NULL COMMENT '转化后的项目ID', - `convert_time` datetime DEFAULT NULL 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_business_name` (`tenant_id`,`business_name`), - KEY `idx_clue` (`clue_id`), - KEY `idx_customer` (`customer_id`), - KEY `idx_owner` (`tenant_id`,`owner_user_id`), - KEY `idx_status` (`tenant_id`,`status`), - KEY `idx_delete_time` (`delete_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM商机表'; - --- ----------------------------------------------------------------------------- --- 项目表 --- ----------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS `yz_backend_crm_project` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID', - `tenant_id` varchar(64) NOT NULL COMMENT '租户ID', - `project_name` varchar(100) NOT NULL COMMENT '项目名称', - `project_no` varchar(50) NOT NULL DEFAULT '' COMMENT '项目编号', - `business_id` bigint(20) DEFAULT NULL COMMENT '来源商机ID', - `customer_id` bigint(20) DEFAULT NULL COMMENT '客户ID', - `customer_name` varchar(100) NOT NULL DEFAULT '' COMMENT '客户名称', - `owner_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '负责人用户ID', - `owner_user_name` varchar(128) NOT NULL DEFAULT '' COMMENT '负责人姓名', - `industry` varchar(50) NOT NULL DEFAULT '' COMMENT '客户行业', - `amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '项目金额(元)', - `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1未开始/2进行中/3已完成/4已暂停', - `start_date` date DEFAULT NULL COMMENT '开始日期', - `end_date` date DEFAULT NULL COMMENT '结束日期', - `contact_person` varchar(50) NOT NULL DEFAULT '' COMMENT '对接人', - `contact_position` varchar(50) NOT NULL DEFAULT '' COMMENT '对接人职位', - `contact_phone` varchar(20) NOT NULL DEFAULT '' COMMENT '对接人手机', - `address` varchar(255) NOT NULL DEFAULT '' COMMENT '项目地址', - `remark` text COMMENT '备注', - `doc_category_id` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联OA文档库项目文件夹分类ID(共享文档/项目文档/{项目名称})', - `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_project_name` (`tenant_id`,`project_name`), - KEY `idx_business` (`business_id`), - KEY `idx_owner` (`tenant_id`,`owner_user_id`), - KEY `idx_status` (`tenant_id`,`status`), - KEY `idx_delete_time` (`delete_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM项目表'; - --- ----------------------------------------------------------------------------- --- 回访/跟进记录表(贯穿线索、商机、项目) --- ----------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS `yz_backend_crm_follow` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID', - `tenant_id` varchar(64) NOT NULL COMMENT '租户ID', - `related_type` tinyint(4) NOT NULL COMMENT '关联类型:1线索/2商机/3项目', - `related_id` bigint(20) NOT NULL COMMENT '关联记录ID', - `related_name` varchar(100) NOT NULL DEFAULT '' COMMENT '关联对象名称(冗余,便于回访列表展示)', - `stage` tinyint(4) NOT NULL DEFAULT '1' COMMENT '创建阶段:1线索/2商机/3项目', - `follow_type` varchar(20) NOT NULL DEFAULT '1' COMMENT '回访方式:1电话/2微信/3上门/4邮件/5其他', - `follow_time` datetime DEFAULT NULL COMMENT '回访时间', - `content` text COMMENT '回访内容', - `next_contact_time` datetime DEFAULT NULL COMMENT '下次联系时间', - `owner_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '回访人用户ID', - `owner_user_name` varchar(128) NOT NULL DEFAULT '' 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`), - KEY `idx_tenant_id` (`tenant_id`), - KEY `idx_related` (`tenant_id`,`related_type`,`related_id`), - KEY `idx_follow_time` (`tenant_id`,`follow_time`), - KEY `idx_delete_time` (`delete_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM回访/跟进记录表'; - --- ----------------------------------------------------------------------------- --- 附件表(多态:线索/商机/项目的资料) --- ----------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS `yz_backend_crm_attach` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID', - `tenant_id` varchar(64) NOT NULL COMMENT '租户ID', - `related_type` tinyint(4) NOT NULL COMMENT '关联类型:1线索/2商机/3项目', - `related_id` bigint(20) NOT NULL COMMENT '关联记录ID', - `file_id` bigint(20) DEFAULT NULL COMMENT '文件ID(yz_system_file.id,可空)', - `file_name` varchar(255) NOT NULL DEFAULT '' COMMENT '文件名', - `file_url` varchar(500) NOT NULL DEFAULT '' COMMENT '文件访问地址', - `file_size` bigint(20) NOT NULL DEFAULT '0' COMMENT '文件大小(字节)', - `uploader_id` varchar(64) NOT NULL DEFAULT '' COMMENT '上传人用户ID', - `uploader_name` varchar(128) NOT NULL DEFAULT '' 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`), - KEY `idx_tenant_id` (`tenant_id`), - KEY `idx_related` (`tenant_id`,`related_type`,`related_id`), - KEY `idx_delete_time` (`delete_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM附件表'; - --- ----------------------------------------------------------------------------- --- 联系人表(多态:线索/商机/项目的对接人列表) --- ----------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS `yz_backend_crm_entity_contact` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID', - `tenant_id` varchar(64) NOT NULL COMMENT '租户ID', - `related_type` tinyint(4) NOT NULL COMMENT '关联类型:1线索/2商机/3项目', - `related_id` bigint(20) NOT NULL COMMENT '关联记录ID', - `contact_name` varchar(50) NOT NULL COMMENT '联系人姓名', - `position` varchar(50) NOT NULL DEFAULT '' COMMENT '职位', - `mobile` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号', - `wechat` varchar(64) NOT NULL DEFAULT '' COMMENT '微信', - `qq` varchar(20) NOT NULL DEFAULT '' COMMENT 'QQ', - `email` varchar(100) NOT NULL DEFAULT '' COMMENT '邮箱', - `is_primary` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否主联系人:0否/1是', - `remark` varchar(500) NOT NULL DEFAULT '' 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`), - KEY `idx_tenant_id` (`tenant_id`), - KEY `idx_related` (`tenant_id`,`related_type`,`related_id`), - KEY `idx_delete_time` (`delete_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM联系人表(线索/商机/项目)'; - --- ----------------------------------------------------------------------------- --- 操作日志表(多态:记录线索/商机/项目的关键操作,用于详情页操作日志 Tab) --- ----------------------------------------------------------------------------- -CREATE TABLE IF NOT EXISTS `yz_backend_crm_operate_log` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键ID', - `tenant_id` varchar(64) NOT NULL COMMENT '租户ID', - `related_type` tinyint(4) NOT NULL COMMENT '关联类型:1线索/2商机/3项目', - `related_id` bigint(20) NOT NULL COMMENT '关联记录ID', - `action` varchar(50) NOT NULL DEFAULT '' COMMENT '操作动作:create/update/convert/delete 等', - `content` varchar(500) NOT NULL DEFAULT '' COMMENT '操作描述', - `operator_id` varchar(64) NOT NULL DEFAULT '' COMMENT '操作人用户ID', - `operator_name` varchar(128) NOT NULL DEFAULT '' COMMENT '操作人姓名', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`), - KEY `idx_tenant_id` (`tenant_id`), - KEY `idx_related` (`tenant_id`,`related_type`,`related_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户CRM操作日志表'; - -SET FOREIGN_KEY_CHECKS = 1; - diff --git a/sql/yz_backend_employee_file.sql b/sql/yz_backend_employee_file.sql deleted file mode 100644 index 0ab6b44..0000000 --- a/sql/yz_backend_employee_file.sql +++ /dev/null @@ -1,70 +0,0 @@ --- 员工档案表(租户端 OA 人事档案) --- 在租户业务库执行以下语句创建表结构。 --- 数据按租户 tid 隔离;档案与员工(yz_backend_employee)一对一, --- 子记录表统一存放教育经历 / 工作经历 / 合同信息 / 证照附件。 - --- --------------------------------------------------------------------------- --- 1. 员工档案主表 --- --------------------------------------------------------------------------- - -CREATE TABLE `yz_backend_employee_file` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tid` int NOT NULL DEFAULT '0' COMMENT '租户ID', - `employee_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '员工ID(yz_backend_employee.id)', - `file_no` varchar(50) NOT NULL DEFAULT '' COMMENT '档案编号(留空自动生成EF+时间戳)', - `id_card` varchar(30) NOT NULL DEFAULT '' COMMENT '身份证号', - `id_card_front` varchar(500) NOT NULL DEFAULT '' COMMENT '身份证正面(人像面)图片URL', - `id_card_back` varchar(500) NOT NULL DEFAULT '' COMMENT '身份证反面(国徽面)图片URL', - `education_photo` varchar(500) NOT NULL DEFAULT '' COMMENT '学历照片URL', - `political_status` varchar(30) NOT NULL DEFAULT '' COMMENT '政治面貌', - `marital_status` tinyint NOT NULL DEFAULT '0' COMMENT '婚姻状况 0-未知 1-未婚 2-已婚 3-离异 4-丧偶', - `native_place` varchar(100) NOT NULL DEFAULT '' COMMENT '籍贯', - `household_address` varchar(255) NOT NULL DEFAULT '' COMMENT '户籍地址', - `current_address` varchar(255) NOT NULL DEFAULT '' COMMENT '现居住址', - `emergency_contact` varchar(50) NOT NULL DEFAULT '' COMMENT '紧急联系人', - `emergency_phone` varchar(20) NOT NULL DEFAULT '' COMMENT '紧急联系电话', - `emergency_relation` varchar(30) NOT NULL DEFAULT '' COMMENT '与本人关系', - `work_email` varchar(100) NOT NULL DEFAULT '' COMMENT '工作邮箱', - `hire_date` date DEFAULT NULL COMMENT '入职日期', - `regular_date` date DEFAULT NULL COMMENT '转正日期', - `leave_date` date DEFAULT NULL COMMENT '离职日期', - `employment_status` tinyint NOT NULL DEFAULT '2' COMMENT '在职状态 1-试用 2-正式 3-离职', - `remark` text NULL COMMENT '备注', - `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uniq_tid_employee` (`tid`,`employee_id`), - KEY `idx_file_no` (`file_no`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='员工档案主表'; - --- --------------------------------------------------------------------------- --- 2. 员工档案子记录表(教育经历/工作经历/合同信息/证照附件) --- type 字段区分记录类型,title/sub_title/extra 的语义随类型变化: --- 教育经历(1):title=学校 sub_title=专业 extra=学历 --- 工作经历(2):title=公司 sub_title=职位 extra=所在部门 --- 合同信息(3):title=合同名称 sub_title=合同类型 extra=签订主体 --- 证照附件(4):title=证照名称 sub_title=证件号码 extra='' --- --------------------------------------------------------------------------- - -CREATE TABLE `yz_backend_employee_file_record` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tid` int NOT NULL DEFAULT '0' COMMENT '租户ID', - `employee_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '员工ID(yz_backend_employee.id)', - `type` tinyint NOT NULL DEFAULT '1' COMMENT '记录类型 1-教育经历 2-工作经历 3-合同信息 4-证照附件', - `title` varchar(100) NOT NULL DEFAULT '' COMMENT '名称(学校/公司/合同/证照)', - `sub_title` varchar(100) NOT NULL DEFAULT '' COMMENT '次级信息(专业/职位/合同类型/证件号码)', - `extra` varchar(100) NOT NULL DEFAULT '' COMMENT '附加信息(学历/部门/签订主体)', - `start_date` date DEFAULT NULL COMMENT '开始日期', - `end_date` date DEFAULT NULL COMMENT '结束日期', - `description` text NULL COMMENT '描述', - `attachment_url` varchar(500) NOT NULL DEFAULT '' COMMENT '附件地址(证照图片等)', - `sort` int NOT NULL DEFAULT '0' COMMENT '排序(越小越靠前)', - `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`), - KEY `idx_tid_employee_type` (`tid`,`employee_id`,`type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='员工档案子记录表'; diff --git a/sql/yz_backend_oa_notice.sql b/sql/yz_backend_oa_notice.sql deleted file mode 100644 index 3edd25e..0000000 --- a/sql/yz_backend_oa_notice.sql +++ /dev/null @@ -1,25 +0,0 @@ --- OA 通知公告表 --- 在租户业务库执行以下语句创建表结构。 --- 数据按租户 tid 隔离;发布后对租户内所有后台用户可见。 - -CREATE TABLE `yz_backend_oa_notice` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tid` int NOT NULL DEFAULT '0' COMMENT '租户ID', - `title` varchar(255) NOT NULL DEFAULT '' COMMENT '公告标题', - `content` text NULL COMMENT '公告内容', - `notice_type` tinyint NOT NULL DEFAULT '0' COMMENT '类型 0-通知 1-公告 2-活动', - `level` tinyint NOT NULL DEFAULT '0' COMMENT '紧急程度 0-普通 1-重要 2-紧急', - `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态 0-草稿 1-已发布 2-已下架', - `is_top` tinyint NOT NULL DEFAULT '0' COMMENT '是否置顶 0-否 1-是', - `publish_time` datetime DEFAULT NULL COMMENT '发布时间', - `publisher_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '发布人ID', - `publisher_name` varchar(100) NOT NULL DEFAULT '' COMMENT '发布人姓名', - `read_count` int NOT NULL DEFAULT '0' COMMENT '阅读次数', - `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`), - KEY `idx_tid_status` (`tid`,`status`), - KEY `idx_tid_top` (`tid`,`is_top`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA通知公告表'; diff --git a/sql/yz_backend_oa_schedule.sql b/sql/yz_backend_oa_schedule.sql deleted file mode 100644 index 3fa6876..0000000 --- a/sql/yz_backend_oa_schedule.sql +++ /dev/null @@ -1,32 +0,0 @@ --- OA 日程管理表(日历待办) --- 在租户业务库执行以下语句创建表结构。 --- 数据按租户 tid + 用户 user_id 双重隔离,仅创建者本人可见与操作。 - -CREATE TABLE `yz_backend_oa_schedule` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tid` int NOT NULL DEFAULT '0' COMMENT '租户ID', - `user_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '所属用户ID', - `user_name` varchar(100) NOT NULL DEFAULT '' COMMENT '所属用户名', - `title` varchar(255) NOT NULL DEFAULT '' COMMENT '日程标题', - `content` text NULL COMMENT '日程内容/备注', - `schedule_date` date NOT NULL COMMENT '日程日期(YYYY-MM-DD)', - `start_time` varchar(8) NOT NULL DEFAULT '' COMMENT '开始时间 HH:mm(全天日程为空)', - `end_time` varchar(8) NOT NULL DEFAULT '' COMMENT '结束时间 HH:mm(全天日程为空)', - `all_day` tinyint NOT NULL DEFAULT '1' COMMENT '是否全天 0-否 1-是', - `color` varchar(20) NOT NULL DEFAULT '#409EFF' COMMENT '标签颜色', - `priority` tinyint NOT NULL DEFAULT '0' COMMENT '优先级 0-普通 1-重要 2-紧急', - `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态 0-待办 1-完成', - `finish_time` datetime DEFAULT NULL COMMENT '完成时间', - `source_id` bigint unsigned NOT NULL DEFAULT '0' COMMENT '顺延来源日程ID 0-原始日程', - `carry_over` tinyint NOT NULL DEFAULT '0' COMMENT '是否已顺延到下一天 0-否 1-是', - `carry_count` int NOT NULL DEFAULT '0' COMMENT '累计顺延次数', - `carry_to_date` varchar(10) NOT NULL DEFAULT '' COMMENT '顺延目标日期(YYYY-MM-DD)', - `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`), - KEY `idx_tid_user_date` (`tid`,`user_id`,`schedule_date`), - KEY `idx_schedule_date` (`schedule_date`), - KEY `idx_source_id` (`source_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA日程管理表(日历待办)'; diff --git a/sql/yz_backend_organization.sql b/sql/yz_backend_organization.sql deleted file mode 100644 index 108c4db..0000000 --- a/sql/yz_backend_organization.sql +++ /dev/null @@ -1,69 +0,0 @@ --- 全局组织架构相关表:组织机构、员工、职位 --- 说明:组织架构已从 ERP 模块独立为全局基础功能,表名不带 erp 前缀 --- tenant 隔离:通过 tid 字段实现租户间数据隔离 --- 对应模型:go/models/erp.go(BackendErpOrganization / BackendErpEmployee / BackendErpPosition) --- DDL 与线上 go-platform 库实际结构一致(2026-08 导出) - --- 组织架构表(OA 组织机构、通讯录、组织机构管理页依赖) -CREATE TABLE IF NOT EXISTS `yz_backend_organization` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', - `tid` bigint(20) unsigned NOT NULL COMMENT '租户ID', - `org_name` varchar(128) NOT NULL COMMENT '组织/部门名称', - `org_code` varchar(64) NOT NULL COMMENT '组织/部门编码', - `parent_id` bigint(20) unsigned DEFAULT '0' COMMENT '上级组织ID', - `sort` int(10) unsigned DEFAULT '0' COMMENT '排序号', - `leader_id` bigint(20) unsigned DEFAULT NULL COMMENT '部门负责人ID', - `is_company` int(11) DEFAULT '0' COMMENT '是否是公司 0-不是 1-是', - `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1-启用,2-停用,0-删除', - `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 '删除时间', - `remark` varchar(512) DEFAULT NULL COMMENT '备注信息', - PRIMARY KEY (`id`) USING BTREE, - KEY `idx_tid_code` (`tid`,`org_code`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='组织架构表'; - --- 员工信息表(account 全局唯一) -CREATE TABLE IF NOT EXISTS `yz_backend_employee` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', - `tid` int(11) DEFAULT NULL COMMENT '租户ID', - `account` varchar(50) NOT NULL COMMENT '员工账号', - `password` varchar(64) NOT NULL DEFAULT '' COMMENT '密码', - `name` varchar(30) NOT NULL COMMENT '员工姓名', - `gender` tinyint(4) NOT NULL DEFAULT '0' COMMENT '性别 0=未知 1=男 2=女', - `birthday` date DEFAULT NULL COMMENT '生日', - `affiliate_unit` varchar(100) DEFAULT NULL COMMENT '隶属单位', - `department` varchar(50) DEFAULT NULL COMMENT '部门', - `position` varchar(50) DEFAULT NULL COMMENT '职位', - `education` varchar(20) DEFAULT NULL COMMENT '学历', - `nation` varchar(20) DEFAULT NULL COMMENT '民族', - `phone` varchar(20) DEFAULT NULL COMMENT '手机号', - `wechat` varchar(50) DEFAULT NULL COMMENT '微信号', - `email` varchar(100) DEFAULT NULL COMMENT '邮箱', - `home_address` varchar(255) DEFAULT NULL COMMENT '家庭住址', - `account_status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '账号状态 0=禁用 1=正常 2=离职', - `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`) USING BTREE, - UNIQUE KEY `uk_account` (`account`) USING BTREE, - KEY `idx_tid_account` (`tid`,`account`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='员工信息表'; - --- 职位表 -CREATE TABLE IF NOT EXISTS `yz_backend_position` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', - `tid` bigint(20) unsigned NOT NULL COMMENT '租户ID', - `department_id` bigint(20) unsigned NOT NULL COMMENT '部门ID', - `position_code` varchar(50) NOT NULL COMMENT '职位编码', - `position_name` varchar(100) NOT NULL COMMENT '职位名称', - `position_type` tinyint(4) DEFAULT '0' COMMENT '职位类型:0-普通职位 1-管理职位 2-技术职位', - `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-启用', - `sort` int(10) unsigned DEFAULT '0' COMMENT '排序', - `remark` varchar(512) DEFAULT NULL 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`) USING BTREE, - KEY `idx_tid_code` (`tid`,`position_code`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='职位表'; diff --git a/sql/yz_backend_reimbursement_expense_types.sql b/sql/yz_backend_reimbursement_expense_types.sql deleted file mode 100644 index 860491e..0000000 --- a/sql/yz_backend_reimbursement_expense_types.sql +++ /dev/null @@ -1,34 +0,0 @@ --- 报销费用类型表 --- 每个租户维护自己的费用类型,uid 保留用于数据隔离扩展。 -CREATE TABLE IF NOT EXISTS `yz_backend_reimbursement_expense_types` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID', - `uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)', - `name` varchar(50) NOT NULL DEFAULT '' COMMENT '费用类型名称', - `sort_order` int(11) NOT NULL DEFAULT '0' COMMENT '排序值,越小越靠前', - `is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用:0-禁用 1-启用', - `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`) USING BTREE, - UNIQUE KEY `uk_tid_name` (`tid`, `name`), - KEY `idx_uid` (`uid`) USING BTREE, - KEY `idx_sort_order` (`sort_order`) USING BTREE, - KEY `idx_is_active` (`is_active`) USING BTREE, - KEY `idx_is_deleted` (`is_deleted`) USING BTREE, - KEY `idx_create_time` (`create_time`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='报销费用类型表'; - --- 可按需为已有租户初始化默认类型: --- INSERT INTO `yz_backend_reimbursement_expense_types` --- (`tid`, `name`, `sort_order`) --- VALUES --- (1, '差旅费', 10), --- (1, '交通费', 20), --- (1, '餐饮费', 30), --- (1, '办公费', 40), --- (1, '通讯费', 50), --- (1, '业务招待费', 60), --- (1, '培训费', 70), --- (1, '其他', 999); diff --git a/sql/yz_backend_reimbursements.sql b/sql/yz_backend_reimbursements.sql deleted file mode 100644 index 91ce4c1..0000000 --- a/sql/yz_backend_reimbursements.sql +++ /dev/null @@ -1,107 +0,0 @@ --- 报销模块表结构 --- 执行前请确认当前数据库字符集为 utf8mb4 - -CREATE TABLE IF NOT EXISTS `yz_backend_approval_flows` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID', - `uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)', - `flow_name` varchar(100) NOT NULL DEFAULT '' COMMENT '流程名称', - `step_order` tinyint(1) NOT NULL DEFAULT '0' COMMENT '步骤顺序(从1开始)', - `approver_role` varchar(50) NOT NULL DEFAULT '' COMMENT '审批角色(如: dept_manager, finance, gm)', - `approver_rule` varchar(200) NOT NULL DEFAULT '' COMMENT '审批人规则(如: 部门主管, 财务总监)', - `timeout_hours` int(11) NOT NULL DEFAULT '48' COMMENT '超时提醒时间(小时)', - `is_active` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用:0-禁用 1-启用', - `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`) USING BTREE, - KEY `idx_flow_name` (`flow_name`) USING BTREE, - KEY `idx_step_order` (`step_order`) USING BTREE, - KEY `idx_is_active` (`is_active`) USING BTREE, - KEY `idx_create_time` (`create_time`) USING BTREE, - KEY `idx_delete_time` (`delete_time`) USING BTREE, - KEY `idx_tid` (`tid`) USING BTREE, - KEY `idx_uid` (`uid`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='审批流程配置表'; - -CREATE TABLE IF NOT EXISTS `yz_backend_reimbursements` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID', - `uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)', - `user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '申请人ID', - `department_id` int(11) unsigned DEFAULT '0' COMMENT '部门ID', - `total_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '总金额', - `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态:0-草稿 1-审批中 2-已通过 3-已驳回 4-已撤回 5-已打款', - `apply_date` date NOT NULL COMMENT '申请日期', - `description` text COMMENT '备注说明', - `current_approver_id` int(11) unsigned DEFAULT '0' COMMENT '当前审批人ID', - `current_step` tinyint(1) NOT NULL DEFAULT '0' COMMENT '当前审批步骤(0:未提交)', - `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`) USING BTREE, - KEY `idx_user_id` (`user_id`) USING BTREE, - KEY `idx_department_id` (`department_id`) USING BTREE, - KEY `idx_status` (`status`) USING BTREE, - KEY `idx_apply_date` (`apply_date`) USING BTREE, - KEY `idx_current_approver_id` (`current_approver_id`) USING BTREE, - KEY `idx_create_time` (`create_time`) USING BTREE, - KEY `idx_update_time` (`update_time`) USING BTREE, - KEY `idx_delete_time` (`delete_time`) USING BTREE, - KEY `idx_tid` (`tid`) USING BTREE, - KEY `idx_uid` (`uid`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='报销单主表'; - -CREATE TABLE IF NOT EXISTS `yz_backend_reimbursement_items` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID', - `uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)', - `reimbursement_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '关联报销单ID', - `expense_type` varchar(50) NOT NULL DEFAULT '' COMMENT '费用类型(具体子类)', - `expense_date` date NOT NULL COMMENT '费用发生日期', - `amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '金额', - `quantity` int(11) NOT NULL DEFAULT '1' COMMENT '数量', - `unit_price` decimal(10,2) DEFAULT '0.00' COMMENT '单价', - `description` varchar(200) NOT NULL DEFAULT '' COMMENT '费用描述', - `receipt_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否有发票:0-否 1-是', - `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`) USING BTREE, - KEY `idx_reimbursement_id` (`reimbursement_id`) USING BTREE, - KEY `idx_expense_type` (`expense_type`) USING BTREE, - KEY `idx_expense_date` (`expense_date`) USING BTREE, - KEY `idx_create_time` (`create_time`) USING BTREE, - KEY `idx_delete_time` (`delete_time`) USING BTREE, - KEY `idx_tid` (`tid`) USING BTREE, - KEY `idx_uid` (`uid`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='报销费用明细表'; - -CREATE TABLE IF NOT EXISTS `yz_backend_approval_records` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `tid` int(11) NOT NULL DEFAULT '0' COMMENT '租户ID', - `uid` int(11) DEFAULT NULL COMMENT '用户ID(数据隔离)', - `reimbursement_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '关联报销单ID', - `approver_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '审批人ID', - `approver_name` varchar(100) NOT NULL DEFAULT '' COMMENT '审批人姓名(冗余)', - `action` tinyint(1) NOT NULL DEFAULT '0' COMMENT '操作:1-提交 2-通过 3-驳回 4-转审 5-撤回', - `comment` text COMMENT '审批意见', - `step` tinyint(1) NOT NULL DEFAULT '0' COMMENT '审批步骤序号', - `prev_step` tinyint(1) NOT NULL DEFAULT '0' COMMENT '上一步骤序号', - `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0-否 1-是', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`) USING BTREE, - KEY `idx_reimbursement_id` (`reimbursement_id`) USING BTREE, - KEY `idx_approver_id` (`approver_id`) USING BTREE, - KEY `idx_action` (`action`) USING BTREE, - KEY `idx_step` (`step`) USING BTREE, - KEY `idx_create_time` (`create_time`) USING BTREE, - KEY `idx_delete_time` (`delete_time`) USING BTREE, - KEY `idx_tid` (`tid`) USING BTREE, - KEY `idx_uid` (`uid`) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='审批记录表'; diff --git a/sql/yz_cms_frontend_tables.sql b/sql/yz_cms_frontend_tables.sql deleted file mode 100644 index 50ddeb4..0000000 --- a/sql/yz_cms_frontend_tables.sql +++ /dev/null @@ -1,84 +0,0 @@ --- ===================================================================== --- 多租户官网标签模板引擎 - 数据库脚本 --- 说明:本脚本只交付,不代执行,请在数据库后台手动运行 --- 新建表统一使用 yz_cms_frontend_ 前缀(官网前台专用) --- ===================================================================== - --- 1. 模板登记表(platform 端登记,供租户选用) -CREATE TABLE IF NOT EXISTS `yz_cms_frontend_template` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `code` varchar(50) NOT NULL DEFAULT '' COMMENT '模板编码,对应 themes/{code}/ 目录', - `name` varchar(100) NOT NULL DEFAULT '' COMMENT '模板名称', - `description` varchar(500) NOT NULL DEFAULT '' COMMENT '模板描述', - `preview` varchar(500) NOT NULL DEFAULT '' COMMENT '预览图地址', - `status` tinyint NOT NULL DEFAULT 1 COMMENT '1启用 0停用', - `sort` int NOT NULL DEFAULT 0 COMMENT '排序', - `create_time` datetime NOT NULL, - `update_time` datetime DEFAULT NULL, - `delete_time` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `idx_code` (`code`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='官网模板登记表'; - --- 2. 官网 Banner 表(按租户隔离) -CREATE TABLE IF NOT EXISTS `yz_cms_frontend_banner` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `tid` bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID', - `title` varchar(255) NOT NULL DEFAULT '' COMMENT '标题', - `desc` varchar(500) NOT NULL DEFAULT '' COMMENT '简介', - `image` varchar(500) NOT NULL DEFAULT '' COMMENT '图片地址', - `url` varchar(500) NOT NULL DEFAULT '' COMMENT '跳转地址', - `sort` int NOT NULL DEFAULT 0 COMMENT '排序', - `status` tinyint NOT NULL DEFAULT 1 COMMENT '1启用 0停用', - `create_time` datetime NOT NULL, - `update_time` datetime DEFAULT NULL, - `delete_time` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `idx_tid_status` (`tid`, `status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='官网Banner表'; - --- 3. 友情链接表(按租户隔离) -CREATE TABLE IF NOT EXISTS `yz_cms_frontend_friendlink` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `tid` bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID', - `link_name` varchar(100) NOT NULL DEFAULT '' COMMENT '链接名称', - `link_url` varchar(500) NOT NULL DEFAULT '' COMMENT '链接地址', - `link_logo` varchar(500) NOT NULL DEFAULT '' COMMENT 'Logo地址', - `description` varchar(500) NOT NULL DEFAULT '' COMMENT '描述', - `sort` int NOT NULL DEFAULT 0 COMMENT '排序', - `status` tinyint NOT NULL DEFAULT 1 COMMENT '1启用 0禁用', - `create_time` datetime NOT NULL, - `update_time` datetime DEFAULT NULL, - `delete_time` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `idx_tid_status` (`tid`, `status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='官网友情链接表'; - --- 4. 单页表(按租户隔离,path 租户内唯一) -CREATE TABLE IF NOT EXISTS `yz_cms_frontend_onepage` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `tid` bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID', - `title` varchar(255) NOT NULL DEFAULT '' COMMENT '单页标题', - `path` varchar(100) NOT NULL DEFAULT '' COMMENT '访问路径标识,租户内唯一,如 about', - `content` longtext COMMENT '富文本内容', - `sort` int NOT NULL DEFAULT 0 COMMENT '排序', - `status` tinyint NOT NULL DEFAULT 1 COMMENT '1启用 0停用', - `create_time` datetime NOT NULL, - `update_time` datetime DEFAULT NULL, - `delete_time` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `idx_tid_path` (`tid`, `path`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='官网单页表'; - --- 5. 租户站点设置表新增模板编码字段(租户选用的官网模板) -ALTER TABLE `yz_system_tenant_site_setting` - ADD COLUMN `template_code` varchar(50) NOT NULL DEFAULT '' COMMENT '选用的官网模板编码'; - --- 6. 初始登记内置默认模板(可选,platform 端扫描目录也会自动登记) -INSERT INTO `yz_cms_frontend_template` (`code`, `name`, `description`, `preview`, `status`, `sort`, `create_time`) -SELECT 'default', '默认模板', '系统内置默认官网模板', '/themes/default/static/preview.png', 1, 0, NOW() -FROM DUAL -WHERE NOT EXISTS ( - SELECT 1 FROM `yz_cms_frontend_template` WHERE `code` = 'default' AND `delete_time` IS NULL -); diff --git a/sql/yz_cms_product.sql b/sql/yz_cms_product.sql deleted file mode 100644 index e04a143..0000000 --- a/sql/yz_cms_product.sql +++ /dev/null @@ -1,35 +0,0 @@ --- CMS 产品与产品分类表(对应 backend 租户端 apps/cms/product 模块) --- 接口:/backend/productsList、/backend/addProducts、/backend/editProducts/:id、/backend/deleteProducts/:id --- /backend/productsTypesList、/backend/addProductsTypes、/backend/editProductsTypes/:id、/backend/deleteProductsTypes/:id - -CREATE TABLE IF NOT EXISTS yz_cms_product ( - id bigint unsigned NOT NULL AUTO_INCREMENT, - tid bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID', - title varchar(100) NOT NULL DEFAULT '' COMMENT '产品名称', - thumb varchar(500) NOT NULL DEFAULT '' COMMENT '产品图片', - `desc` varchar(500) NOT NULL DEFAULT '' COMMENT '产品描述', - content mediumtext COMMENT '产品内容(富文本)', - url varchar(500) NOT NULL DEFAULT '' COMMENT '跳转地址', - sort int NOT NULL DEFAULT 0 COMMENT '排序', - status tinyint NOT NULL DEFAULT 1 COMMENT '状态 1启用 0禁用', - create_time datetime NOT NULL, - update_time datetime DEFAULT NULL, - delete_time datetime DEFAULT NULL COMMENT '软删除时间', - PRIMARY KEY (id), - KEY idx_tid_status (tid, status) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CMS产品'; - -CREATE TABLE IF NOT EXISTS yz_cms_product_category ( - id bigint unsigned NOT NULL AUTO_INCREMENT, - tid bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID', - title varchar(100) NOT NULL DEFAULT '' COMMENT '分类名称', - pid bigint unsigned NOT NULL DEFAULT 0 COMMENT '父级分类ID,0为顶级', - `desc` varchar(500) NOT NULL DEFAULT '' COMMENT '分类描述', - sort int NOT NULL DEFAULT 0 COMMENT '排序', - status tinyint NOT NULL DEFAULT 1 COMMENT '状态 1启用 0禁用', - create_time datetime NOT NULL, - update_time datetime DEFAULT NULL, - delete_time datetime DEFAULT NULL COMMENT '软删除时间', - PRIMARY KEY (id), - KEY idx_tid_pid (tid, pid) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CMS产品分类'; diff --git a/sql/yz_cms_solution.sql b/sql/yz_cms_solution.sql deleted file mode 100644 index d6f42d5..0000000 --- a/sql/yz_cms_solution.sql +++ /dev/null @@ -1,34 +0,0 @@ --- CMS 解决方案(特色服务)与分类表(对应 backend 租户端 apps/cms/solution 模块) --- 接口:/backend/servicesList、/backend/addServices、/backend/editServices/:id、/backend/deleteServices/:id --- /backend/servicesTypesList、/backend/addServicesTypes、/backend/editServicesTypes/:id、/backend/deleteServicesTypes/:id - -CREATE TABLE IF NOT EXISTS yz_cms_solution ( - id bigint unsigned NOT NULL AUTO_INCREMENT, - tid bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID', - title varchar(100) NOT NULL DEFAULT '' COMMENT '名称', - thumb varchar(500) NOT NULL DEFAULT '' COMMENT '图片', - `desc` varchar(500) NOT NULL DEFAULT '' COMMENT '描述', - url varchar(500) NOT NULL DEFAULT '' COMMENT '跳转地址', - sort int NOT NULL DEFAULT 0 COMMENT '排序', - status tinyint NOT NULL DEFAULT 1 COMMENT '状态 1启用 0禁用', - create_time datetime NOT NULL, - update_time datetime DEFAULT NULL, - delete_time datetime DEFAULT NULL COMMENT '软删除时间', - PRIMARY KEY (id), - KEY idx_tid_status (tid, status) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CMS解决方案(特色服务)'; - -CREATE TABLE IF NOT EXISTS yz_cms_solution_category ( - id bigint unsigned NOT NULL AUTO_INCREMENT, - tid bigint unsigned NOT NULL DEFAULT 0 COMMENT '租户ID', - title varchar(100) NOT NULL DEFAULT '' COMMENT '分类名称', - pid bigint unsigned NOT NULL DEFAULT 0 COMMENT '父级分类ID,0为顶级', - `desc` varchar(500) NOT NULL DEFAULT '' COMMENT '分类描述', - sort int NOT NULL DEFAULT 0 COMMENT '排序', - status tinyint NOT NULL DEFAULT 1 COMMENT '状态 1启用 0禁用', - create_time datetime NOT NULL, - update_time datetime DEFAULT NULL, - delete_time datetime DEFAULT NULL COMMENT '软删除时间', - PRIMARY KEY (id), - KEY idx_tid_pid (tid, pid) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CMS解决方案分类'; diff --git a/sql/yz_password_store.sql b/sql/yz_password_store.sql deleted file mode 100644 index 13d8477..0000000 --- a/sql/yz_password_store.sql +++ /dev/null @@ -1,37 +0,0 @@ --- 平台端和租户后台密码存储表。支持一个平台下挂载多个账号(每个账号包含账号、密码、注册信息、备注)。 -CREATE TABLE IF NOT EXISTS `yz_platform_password_store` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `platform` varchar(100) NOT NULL DEFAULT '' COMMENT '平台名称', - `url` varchar(500) NOT NULL DEFAULT '' COMMENT '平台网址/登录地址', - `accounts` longtext COMMENT '账号列表JSON: [{"username":"","password":"","registration_info":"","remark":""}]', - `remark` text COMMENT '平台备注', - `user_id` bigint unsigned NOT NULL COMMENT '所属用户ID', - `is_deleted` tinyint NOT NULL DEFAULT 0, - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime DEFAULT NULL, - `delete_time` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `idx_platform_user` (`platform`,`user_id`), - KEY `idx_user_deleted` (`user_id`,`is_deleted`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台密码存储'; - -CREATE TABLE IF NOT EXISTS `yz_backend_password_store` ( - `id` bigint unsigned NOT NULL AUTO_INCREMENT, - `tid` bigint NOT NULL COMMENT '租户ID', - `platform` varchar(100) NOT NULL DEFAULT '' COMMENT '平台名称', - `url` varchar(500) NOT NULL DEFAULT '' COMMENT '平台网址/登录地址', - `accounts` longtext COMMENT '账号列表JSON: [{"username":"","password":"","registration_info":"","remark":""}]', - `remark` text COMMENT '平台备注', - `user_id` bigint unsigned NOT NULL COMMENT '所属用户ID', - `is_deleted` tinyint NOT NULL DEFAULT 0, - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `update_time` datetime DEFAULT NULL, - `delete_time` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `idx_tid_user` (`tid`,`user_id`), - KEY `idx_tid_platform` (`tid`,`platform`), - KEY `idx_tid_deleted` (`tid`,`is_deleted`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户密码存储'; - --- 已经按旧的「单账号」结构建过表的库,请执行 --- sql/alter_password_store_to_multi_accounts.sql 完成升级与数据迁移。 diff --git a/sql/yz_platform_cursor_equipment_ip_log.sql b/sql/yz_platform_cursor_equipment_ip_log.sql deleted file mode 100644 index fb12ba7..0000000 --- a/sql/yz_platform_cursor_equipment_ip_log.sql +++ /dev/null @@ -1,28 +0,0 @@ --- 设备 IP 登录日志表 --- 每次客户端调用 report / activateByCode 接口时,若携带 ipInfo,则向本表插入一条记录 -CREATE TABLE IF NOT EXISTS `yz_platform_cursor_equipment_ip_log` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', - `equipment_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '关联 yz_platform_cursor_equipment.id,0 表示设备尚未创建时的记录', - `machine_code` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '机器码(冗余,便于无 equipment_id 时查询)', - `source` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '来源:report / activateByCode', - -- ip-api.com 返回的原始字段 - `status` VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'ip-api status', - `country` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '国家', - `country_code` VARCHAR(8) NOT NULL DEFAULT '' COMMENT '国家代码', - `region` VARCHAR(16) NOT NULL DEFAULT '' COMMENT '省/州代码', - `region_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '省/州名称', - `city` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '城市', - `zip` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '邮编', - `lat` DOUBLE NOT NULL DEFAULT 0 COMMENT '纬度', - `lon` DOUBLE NOT NULL DEFAULT 0 COMMENT '经度', - `timezone` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '时区', - `isp` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'ISP 运营商', - `org` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '组织', - `as_info` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'AS 信息', - `query` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '出口 IP 地址', - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间', - PRIMARY KEY (`id`), - KEY `idx_equipment_id` (`equipment_id`), - KEY `idx_machine_code` (`machine_code`), - KEY `idx_create_time` (`create_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备 IP 登录日志'; diff --git a/sql/yz_platform_notebook.sql b/sql/yz_platform_notebook.sql deleted file mode 100644 index 376680c..0000000 --- a/sql/yz_platform_notebook.sql +++ /dev/null @@ -1,18 +0,0 @@ --- 平台记事本表 -CREATE TABLE IF NOT EXISTS `yz_platform_notebook` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `title` varchar(255) NOT NULL DEFAULT '' COMMENT '笔记标题', - `content` longtext COMMENT '笔记内容(HTML格式)', - `pinned` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否置顶 0-否 1-是', - `user_id` bigint(20) unsigned DEFAULT NULL COMMENT '创建用户ID', - `user_name` varchar(100) DEFAULT NULL COMMENT '创建用户名', - `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是', - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_pinned` (`pinned`), - KEY `idx_create_time` (`create_time`), - KEY `idx_is_deleted` (`is_deleted`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台记事本表'; diff --git a/sql/yz_system_login_log.sql b/sql/yz_system_login_log.sql deleted file mode 100644 index bcb8c7f..0000000 --- a/sql/yz_system_login_log.sql +++ /dev/null @@ -1,20 +0,0 @@ --- 登录日志表 -CREATE TABLE IF NOT EXISTS `yz_system_login_log` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', - `tid` bigint(20) unsigned DEFAULT NULL COMMENT '租户ID', - `user_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '用户ID', - `account` varchar(64) NOT NULL DEFAULT '' COMMENT '登录账号', - `user_name` varchar(64) NOT NULL DEFAULT '' COMMENT '用户姓名', - `tenant_name` varchar(64) NOT NULL DEFAULT '' COMMENT '租户名称', - `login_type` varchar(20) NOT NULL DEFAULT 'password' COMMENT '登录方式: password-密码, sms-短信验证码, email-邮箱验证码', - `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '登录状态: 0-失败, 1-成功', - `message` varchar(255) NOT NULL DEFAULT '' COMMENT '结果信息/失败原因', - `ip` varchar(50) NOT NULL DEFAULT '' COMMENT '登录IP', - `user_agent` varchar(500) NOT NULL DEFAULT '' COMMENT '浏览器User-Agent', - `create_time` datetime DEFAULT NULL COMMENT '登录时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - PRIMARY KEY (`id`), - KEY `idx_tid` (`tid`), - KEY `idx_account` (`account`), - KEY `idx_create_time` (`create_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统登录日志表'; diff --git a/sql/yz_system_sitereminder.sql b/sql/yz_system_sitereminder.sql deleted file mode 100644 index bf65bc0..0000000 --- a/sql/yz_system_sitereminder.sql +++ /dev/null @@ -1,36 +0,0 @@ --- 站内信配置表 -CREATE TABLE IF NOT EXISTS `yz_system_sitereminder` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', - `retention_days` int(11) NOT NULL DEFAULT '30' COMMENT '消息保留天数', - `auto_read` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否自动标记已读: 0-否, 1-是', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='站内信配置表'; - --- 默认插入一条配置数据 -INSERT INTO `yz_system_sitereminder` (`id`, `retention_days`, `auto_read`, `create_time`, `update_time`) -VALUES (1, 30, 0, NOW(), NOW()) -ON DUPLICATE KEY UPDATE `update_time` = NOW(); - --- 站内信消息列表表 -CREATE TABLE IF NOT EXISTS `yz_system_reminderlist` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID', - `title` varchar(255) NOT NULL COMMENT '标题', - `content` text NOT NULL COMMENT '内容', - `sender_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '发送者ID (0为系统)', - `sender_type` varchar(32) NOT NULL DEFAULT 'system' COMMENT '发送者类型: system, platform, tenant', - `receiver_id` bigint(20) unsigned NOT NULL COMMENT '接收者ID', - `receiver_type` varchar(32) NOT NULL DEFAULT 'receiver' COMMENT '接收者类型: platform, tenant', - `is_read` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否已读: 0-未读, 1-已读', - `read_time` datetime DEFAULT NULL COMMENT '已读时间', - `create_time` datetime DEFAULT NULL COMMENT '创建时间/发送时间', - `delete_time` datetime DEFAULT NULL COMMENT '删除时间', - `batch_id` varchar(64) NOT NULL DEFAULT '' COMMENT '批次号/分组ID', - `target_type` varchar(32) NOT NULL DEFAULT '' COMMENT '发送目标类型: platform, tenant_all, role, tenant', - `target_role_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '目标角色ID', - `target_tenant_id` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '目标租户ID', - PRIMARY KEY (`id`), - KEY `idx_receiver` (`receiver_type`, `receiver_id`, `is_read`), - KEY `idx_batch` (`batch_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='站内信消息列表表';