低代码、开源系统、定制开发怎么选?用资产管理案例做技术取舍

低代码、开源系统、定制开发怎么选?用资产管理案例做技术取舍

很多企业准备做管理系统时,第一轮讨论常常会卡在一个问题上:到底用低代码平台、开源系统改造,还是直接定制开发?

如果只看演示页面,这个问题很容易被误判。低代码平台能很快拖出表单和流程,开源系统能很快跑起来一套后台,定制开发可以按需求完全设计。每一种方案都能讲出优点,但真正上线以后,决定成败的往往不是首页长什么样,而是数据模型能不能承接未来变化、权限边界能不能落地、二次开发责任能不能说清楚、源码和部署权能不能交接。

本文固定一个案例:客户要做固定资产系统,第一版只要求资产台账、领用、归还;三个月后,又提出折旧、盘点、多公司多部门权限、二维码标签和财务系统对接。我们不写泛泛的"低代码好不好、开源好不好",而是用一个可复现的评估模型,把技术选型拆成输入数据、评分维度、数据库结构、Java 服务层、JUnit 测试、SQL 验证和上线验收。

示例环境为 Java 17、Spring Boot 风格服务层、MySQL 8.x。低代码平台、开源系统和定制开发都可以接入这个评估模型;本文重点不在推荐某个具体厂商,而在怎样把"感觉上哪个更合适"变成可解释、可复核的技术决策。

目录

  1. 先固定一个资产管理选型案例
  2. 为什么不能只看首屏和报价
  3. 把选型拆成六个可评分维度
  4. MySQL表结构:选型单、评分项和决策快照
  5. Java实现:按维度计算推荐方案
  6. 预期输出和JUnit测试
  7. SQL验证:上线前怎么复核选型结论
  8. 异常边界和上线验收
  9. 小结和延伸阅读

一、先固定一个资产管理选型案例

本篇固定输入如下:

text 复制代码
评估单号:SEL-20260920-004
业务对象:固定资产管理系统
第一版范围:资产台账、领用、归还、基础报表
三个月后新增:折旧、盘点、多组织权限、二维码标签、财务系统对接
使用组织:3家公司、18个部门、约2600张资产卡
使用角色:普通员工、部门资产员、资产管理员、财务人员、系统管理员
上线目标:45天内上线第一版,后续持续迭代
预算倾向:不希望一次性投入过高,但要求源码、数据和部署可交接
评估结论候选:LOW_CODE、OPEN_SOURCE_CUSTOM、FULL_CUSTOM

三个候选方案先定义清楚:

方案 本文含义
LOW_CODE 以低代码平台配置表单、流程、报表为主,少量脚本或插件扩展
OPEN_SOURCE_CUSTOM 基于开源后台或开源业务系统做二次开发,保留源码和部署控制权
FULL_CUSTOM 从数据模型、后端、前端、权限到部署全部定制开发

这个案例的关键不是"哪个方案一定最好",而是当前输入已经透露出几个风险:第一版看起来很小,但后续需求变化快;资产生命周期长,状态、折旧、盘点和处置会不断扩展;多组织权限和财务对接意味着不能只靠页面配置;客户又要求源码和部署可交接。

图1:同一个资产系统需求,低代码、开源二开和定制开发关注的风险点不同。

二、为什么不能只看首屏和报价

很多选型失败,是因为评估时只比较了三个表面指标:

text 复制代码
谁演示页面更快
谁第一版报价更低
谁说自己功能更多

这三个指标都重要,但都不能单独决定方案。

第一,首屏页面不等于数据模型。固定资产系统如果只做台账列表,低代码平台很快;但一旦进入折旧、盘点、调拨、维修、报废,核心表之间会出现状态机、期间、凭证、附件和审计链路。页面拖得快,不代表这些业务事实能长期稳定保存。

第二,第一版报价不等于总成本。低代码平台前期配置快,但复杂逻辑可能变成平台脚本、插件和厂商服务费;开源系统初始成本低,但要评估源码质量、许可证、升级冲突和二开责任;定制开发前期投入高,但边界清晰时后续可控性更强。

第三,功能清单不等于可交接能力。供应商演示"有折旧、有盘点、有二维码",不代表交付时会给出数据字典、源码、部署文档、测试用例和二开边界。企业管理系统不是一次性页面,而是要被维护、迁移和继续开发的资产。

所以本文不按"喜欢哪个"来选,而是把选型变成六个维度的评分和证据:

维度 要问的问题
需求稳定度 三个月后会不会大改流程、字段、状态和报表
数据模型复杂度 是否有状态机、期间、金额、附件、审计、历史版本
权限复杂度 是否多公司、多部门、多角色、多数据范围
集成复杂度 是否对接财务、二维码、消息、SSO 或第三方平台
源码和部署控制 是否要求私有化、源码交付、独立部署和二开
时间和预算压力 第一版上线窗口和总投入边界

图2:选型不能只比较演示速度和报价,还要比较数据、权限、集成和交付责任。

三、把选型拆成六个可评分维度

为了让选型可复核,可以把每个维度按 1 到 5 分记录。分数不是越高越好,而是表示复杂度或约束强度。

text 复制代码
1分:简单、稳定、变化少
3分:有明确规则,但存在扩展
5分:复杂、变化快、需要持续开发

本案例可以先打一个样例分:

维度 分数 原因
需求稳定度 4 第一版很小,但三个月后明确新增折旧、盘点和权限
数据模型复杂度 5 资产卡、折旧期间、盘点差异、调拨、维修、报废都要保留历史
权限复杂度 4 三家公司、十八个部门,多角色且数据范围不同
集成复杂度 3 需要财务系统、二维码标签和消息提醒
源码和部署控制 5 客户要求源码、数据和部署可交接
时间和预算压力 3 45天上线第一版,但后续持续迭代

这组分数已经能解释为什么"纯低代码"风险较高:需求变化、数据模型、权限和源码控制都不低。如果客户只要一个内部登记表,低代码可能合适;但这个案例已经接近长期业务系统,至少要谨慎评估平台锁定和复杂逻辑的维护成本。

也不能简单说"那就全定制"。45 天要上线第一版,预算又不希望一次性过高,如果从零写完整权限、菜单、字典、导入导出、附件和审计,第一版周期会被基础能力吃掉。因此更稳的结论通常是:基于成熟开源后台或开源业务框架二次开发,核心数据模型和业务流程定制,避免被纯配置平台锁死,也避免从零造全部基础设施。

图3:评分维度把"感觉合适"变成可解释的技术判断。

四、MySQL表结构:选型单、评分项和决策快照

选型评估也应该落库。否则会议上说过的话、评估过的风险、最终推荐理由,很快就会变成口头记忆。

下面是一组最小表结构:

sql 复制代码
CREATE TABLE solution_selection (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  selection_no VARCHAR(64) NOT NULL,
  project_name VARCHAR(128) NOT NULL,
  company_id BIGINT NOT NULL,
  current_stage VARCHAR(32) NOT NULL,
  recommended_solution VARCHAR(32) NULL,
  decision_reason VARCHAR(1000) NULL,
  request_key VARCHAR(64) NOT NULL,
  create_by BIGINT NOT NULL,
  create_time DATETIME NOT NULL,
  update_time DATETIME NULL,
  UNIQUE KEY uk_selection_no (company_id, selection_no),
  UNIQUE KEY uk_selection_request (request_key),
  CHECK (current_stage IN ('DRAFT','EVALUATING','CONFIRMED','REJECTED')),
  CHECK (recommended_solution IS NULL OR recommended_solution IN
    ('LOW_CODE','OPEN_SOURCE_CUSTOM','FULL_CUSTOM'))
);

CREATE TABLE solution_selection_score (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  selection_id BIGINT NOT NULL,
  dimension_code VARCHAR(64) NOT NULL,
  dimension_name VARCHAR(128) NOT NULL,
  score INT NOT NULL,
  evidence VARCHAR(1000) NOT NULL,
  create_time DATETIME NOT NULL,
  UNIQUE KEY uk_selection_dimension (selection_id, dimension_code),
  CHECK (score BETWEEN 1 AND 5)
);

CREATE TABLE solution_selection_result (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  selection_id BIGINT NOT NULL,
  low_code_score DECIMAL(8,2) NOT NULL,
  open_source_custom_score DECIMAL(8,2) NOT NULL,
  full_custom_score DECIMAL(8,2) NOT NULL,
  recommended_solution VARCHAR(32) NOT NULL,
  reason_snapshot JSON NOT NULL,
  create_time DATETIME NOT NULL,
  UNIQUE KEY uk_selection_result (selection_id)
);

三张表分别保存不同事实。

solution_selection 保存评估单主信息和最终结论。solution_selection_score 保存每个维度的分数和证据。solution_selection_result 保存计算后的三种方案得分和决策快照。为什么要保存快照?因为未来需求变化后,评分可能会调整;保留当时的输入和输出,才能解释当时为什么推荐这个方案。

这里不建议把评分维度塞进一个 JSON 字段。JSON 对快速原型很方便,但后续要统计"哪些项目最后没有选低代码、原因是什么""权限复杂度高的项目是否更容易超预算"时,结构化评分项更容易查询。

图4:选型单、评分项和结果快照分开保存,便于复核和复盘。

五、Java实现:按维度计算推荐方案

下面给出一个简化的服务层实现。它不是为了把选型变成机械公式,而是让推荐过程有一致的计算口径。

java 复制代码
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import org.springframework.transaction.annotation.Transactional;

public class SolutionSelectionService {

    private final SelectionRepository selectionRepository;
    private final ScoreRepository scoreRepository;
    private final ResultRepository resultRepository;

    public SolutionSelectionService(SelectionRepository selectionRepository,
                                    ScoreRepository scoreRepository,
                                    ResultRepository resultRepository) {
        this.selectionRepository = selectionRepository;
        this.scoreRepository = scoreRepository;
        this.resultRepository = resultRepository;
    }

    @Transactional(rollbackFor = Exception.class)
    public SelectionResult confirm(SelectionCommand command) {
        Selection selection = selectionRepository.lockByNo(
                command.companyId(), command.selectionNo())
                .orElseThrow(() -> new BizException("评估单不存在"));

        if (!"EVALUATING".equals(selection.stage())) {
            throw new BizException("只有评估中的选型单才能确认:" + selection.stage());
        }

        List<DimensionScore> scores = scoreRepository.listBySelectionId(selection.id());
        ensureRequiredDimensions(scores);

        Recommendation recommendation = calculate(scores);
        resultRepository.upsert(selection.id(), recommendation, LocalDateTime.now());
        selectionRepository.confirm(selection.id(),
                recommendation.solution().name(),
                recommendation.reason(),
                command.requestKey(),
                LocalDateTime.now());

        return new SelectionResult(selection.selectionNo(),
                recommendation.solution().name(),
                recommendation.reason(),
                recommendation.scores());
    }

    private Recommendation calculate(List<DimensionScore> scores) {
        Map<Dimension, Integer> value = new EnumMap<>(Dimension.class);
        for (DimensionScore score : scores) {
            value.put(Dimension.valueOf(score.dimensionCode()), score.score());
        }

        int change = value.get(Dimension.REQUIREMENT_CHANGE);
        int model = value.get(Dimension.DATA_MODEL);
        int permission = value.get(Dimension.PERMISSION);
        int integration = value.get(Dimension.INTEGRATION);
        int control = value.get(Dimension.SOURCE_CONTROL);
        int timeBudget = value.get(Dimension.TIME_BUDGET);

        BigDecimal lowCode = scoreLowCode(change, model, permission, integration, control, timeBudget);
        BigDecimal openSource = scoreOpenSourceCustom(change, model, permission, integration, control, timeBudget);
        BigDecimal custom = scoreFullCustom(change, model, permission, integration, control, timeBudget);

        Solution solution = Solution.OPEN_SOURCE_CUSTOM;
        BigDecimal best = openSource;
        if (lowCode.compareTo(best) > 0) {
            solution = Solution.LOW_CODE;
            best = lowCode;
        }
        if (custom.compareTo(best) > 0) {
            solution = Solution.FULL_CUSTOM;
        }

        String reason = buildReason(solution, change, model, permission, integration, control, timeBudget);
        return new Recommendation(solution, reason,
                Map.of("LOW_CODE", lowCode, "OPEN_SOURCE_CUSTOM", openSource, "FULL_CUSTOM", custom));
    }

    private BigDecimal scoreLowCode(int change, int model, int permission, int integration,
                                    int control, int timeBudget) {
        int score = 80;
        score -= (change - 1) * 8;
        score -= (model - 1) * 10;
        score -= (permission - 1) * 6;
        score -= (integration - 1) * 5;
        score -= (control - 1) * 10;
        score += (5 - timeBudget) * 4;
        return BigDecimal.valueOf(score);
    }

    private BigDecimal scoreOpenSourceCustom(int change, int model, int permission, int integration,
                                             int control, int timeBudget) {
        int score = 70;
        score += Math.min(change, 4) * 4;
        score += Math.min(model, 4) * 5;
        score += Math.min(permission, 4) * 4;
        score += Math.min(integration, 4) * 3;
        score += control * 5;
        score -= Math.max(timeBudget - 3, 0) * 3;
        return BigDecimal.valueOf(score);
    }

    private BigDecimal scoreFullCustom(int change, int model, int permission, int integration,
                                       int control, int timeBudget) {
        int score = 60;
        score += change * 5;
        score += model * 6;
        score += permission * 5;
        score += integration * 4;
        score += control * 4;
        score -= Math.max(4 - timeBudget, 0) * 8;
        return BigDecimal.valueOf(score);
    }

    private void ensureRequiredDimensions(List<DimensionScore> scores) {
        if (scores.size() < Dimension.values().length) {
            throw new BizException("评估维度不完整,不能生成选型建议");
        }
    }
}

这段代码有几个刻意设计。

第一,低代码不是一票否决。时间压力大、需求稳定、模型简单时,低代码分数可能最高。第二,开源二开不是默认正确。若源码质量差、许可证不适合、团队没有二开能力,实际项目仍可能失败。第三,定制开发也不是越复杂越推荐。预算和周期不足时,全定制的风险会明显上升。

代码里的分数不是财务报价,而是技术适配度。真正项目中可以把权重配置化,例如医疗、制造、集团企业对审计、权限和集成的权重会不同;小团队内部工具则可能更重视上线速度。

六、预期输出和JUnit测试

固定案例输入分数如下:

text 复制代码
REQUIREMENT_CHANGE = 4
DATA_MODEL = 5
PERMISSION = 4
INTEGRATION = 3
SOURCE_CONTROL = 5
TIME_BUDGET = 3

预期输出:

text 复制代码
recommended_solution = OPEN_SOURCE_CUSTOM
reason 包含:需求变化快、数据模型复杂、需要源码和部署控制
LOW_CODE 分数低于 OPEN_SOURCE_CUSTOM
FULL_CUSTOM 分数接近但受 45 天上线窗口影响

JUnit 测试可以这样写:

java 复制代码
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class SolutionSelectionServiceTest {

    @Test
    void shouldRecommendOpenSourceCustomForAssetSystem() {
        Fixture fx = Fixture.assetSelection("SEL-20260920-004");
        fx.score("REQUIREMENT_CHANGE", 4, "三个月后新增折旧、盘点和权限");
        fx.score("DATA_MODEL", 5, "资产全生命周期需要历史和期间");
        fx.score("PERMISSION", 4, "三家公司十八个部门");
        fx.score("INTEGRATION", 3, "财务、二维码和消息提醒");
        fx.score("SOURCE_CONTROL", 5, "要求源码和部署可交接");
        fx.score("TIME_BUDGET", 3, "45天上线第一版");

        SelectionResult result = fx.service().confirm(
                new SelectionCommand(1L, "SEL-20260920-004", "REQ-04-001"));

        assertEquals("OPEN_SOURCE_CUSTOM", result.recommendedSolution());
        assertTrue(result.reason().contains("源码"));
        assertTrue(result.schemeScores().get("OPEN_SOURCE_CUSTOM")
                .compareTo(result.schemeScores().get("LOW_CODE")) > 0);
    }

    @Test
    void shouldRejectWhenRequiredDimensionMissing() {
        Fixture fx = Fixture.assetSelection("SEL-20260920-004");
        fx.score("DATA_MODEL", 5, "资产全生命周期复杂");

        BizException ex = assertThrows(BizException.class, () ->
                fx.service().confirm(new SelectionCommand(1L, "SEL-20260920-004", "REQ-04-002")));

        assertEquals("评估维度不完整,不能生成选型建议", ex.getMessage());
    }

    @Test
    void shouldPreferLowCodeWhenModelIsSimpleAndStable() {
        Fixture fx = Fixture.simpleSelection("SEL-20260920-005");
        fx.score("REQUIREMENT_CHANGE", 1, "字段稳定");
        fx.score("DATA_MODEL", 1, "单表登记");
        fx.score("PERMISSION", 1, "只有管理员维护");
        fx.score("INTEGRATION", 1, "不对接外部系统");
        fx.score("SOURCE_CONTROL", 1, "不要求源码交付");
        fx.score("TIME_BUDGET", 1, "一周内上线");

        SelectionResult result = fx.service().confirm(
                new SelectionCommand(1L, "SEL-20260920-005", "REQ-04-003"));

        assertEquals("LOW_CODE", result.recommendedSolution());
    }
}

这组测试不是为了证明某种方案永远正确,而是固定边界:复杂资产系统推荐开源二开,极简单登记工具可以推荐低代码,评分维度不完整时不能给出结论。

七、SQL验证:上线前怎么复核选型结论

第一,查评估单最终推荐:

sql 复制代码
SELECT selection_no, project_name, current_stage, recommended_solution
FROM solution_selection
WHERE company_id = 1
  AND selection_no = 'SEL-20260920-004';

预期结果:

text 复制代码
SEL-20260920-004 | 固定资产管理系统 | CONFIRMED | OPEN_SOURCE_CUSTOM

第二,查六个评分维度是否齐全:

sql 复制代码
SELECT dimension_code, score, evidence
FROM solution_selection_score
WHERE selection_id = 1001
ORDER BY dimension_code;

预期至少包含:

text 复制代码
DATA_MODEL          | 5 | 资产全生命周期需要历史和期间
INTEGRATION         | 3 | 财务、二维码和消息提醒
PERMISSION          | 4 | 三家公司十八个部门
REQUIREMENT_CHANGE  | 4 | 三个月后新增折旧、盘点和权限
SOURCE_CONTROL      | 5 | 要求源码和部署可交接
TIME_BUDGET         | 3 | 45天上线第一版

第三,查三种方案得分:

sql 复制代码
SELECT low_code_score,
       open_source_custom_score,
       full_custom_score,
       recommended_solution
FROM solution_selection_result
WHERE selection_id = 1001;

第四,查有没有已经确认但没有结果快照的评估单:

sql 复制代码
SELECT s.selection_no, s.recommended_solution
FROM solution_selection s
LEFT JOIN solution_selection_result r ON r.selection_id = s.id
WHERE s.current_stage = 'CONFIRMED'
  AND r.id IS NULL;

这几组 SQL 的目的,是防止选型会后只留下一句"建议开源二开"。真正可复核的结论,应该能看到评分输入、推荐结果和理由快照。

图5:评估单、评分项和结果快照都要能被 SQL 查出来。

八、异常边界和上线验收

选型评估不是写完一张表就结束,至少要检查这些边界:

异常 不能怎么做 应该怎么处理
评分维度缺失 不能生成推荐结论 阻断确认,提示缺少维度
证据为空 不能只填分数 要求填写原因或会议纪要
只比较报价 不能直接选最低价 同时比较源码、部署、权限、集成和维护责任
供应商不交源码 不能默认可二开 标记 SOURCE_CONTROL 高风险
开源许可证不清楚 不能进入生产决策 法务或技术负责人确认许可证边界
低代码平台锁定 不能忽略退出成本 评估数据导出、接口、私有化和二开能力
定制开发周期不足 不能承诺完整重做 拆分 MVP 和后续迭代边界

上线前建议按下面清单验收:

text 复制代码
1. 固定资产系统的第一版和三个月后需求都已写入评估输入。
2. 六个评分维度都有分数和证据,不允许空证据确认。
3. 推荐结论能解释为什么不是纯低代码,也不是直接全定制。
4. 已明确开源系统的许可证、源码交付、部署方式和升级责任。
5. 已明确哪些模块复用开源后台,哪些模块必须定制。
6. 已明确数据模型归属,资产卡、折旧、盘点、权限不依赖黑盒配置。
7. 已明确第一版 MVP 范围和后续迭代清单。
8. 已明确二开边界、验收标准和交接材料。
9. SQL 能查到评分项、推荐结果和理由快照。
10. 评估结论变更时,保留历史快照而不是覆盖旧记录。

九、小结和延伸阅读

低代码、开源系统、定制开发不是简单的优劣排序,而是针对具体业务复杂度、变化速度、权限边界、集成要求和交付责任做取舍。固定资产系统这种长期使用的企业管理场景,最怕只看第一版页面快不快,而忽略数据模型和后续二开。

本文给出的结论是:在本案例输入下,更适合选择 OPEN_SOURCE_CUSTOM,也就是基于成熟开源后台或开源业务框架做二次开发,同时把资产核心模型、权限边界、折旧盘点和交付文档掌握在自己可控范围内。若需求只是单表登记,低代码可以更合适;若客户有非常强的行业流程和预算周期,全定制也可能更合理。关键是选型结论要有证据,而不是靠会议上的一句"这个看起来快"。

延伸阅读:

  1. MySQL 8.0 Reference Manual:CREATE TABLE
  2. MySQL 8.0 Reference Manual:CHECK Constraints
  3. Spring Framework:Transaction Management
  4. Java SE 17:BigDecimal
  5. Open Source Initiative:Licenses
相关推荐
亥时科技2 小时前
无人机和监控视频,真能一次性打通吗?
开源·无人机·ai巡检
Casbin开源社区2 小时前
OpenAgent 详解:单二进制自托管 AI Agent 平台,30+ 模型接入、RAG 知识库、MCP 工具调用与 Casbin 工具权限
人工智能·golang·开源
tokenKe12 小时前
Tencent BrowserSkill :让 AI Agent 借用你的真实浏览器| 已登录浏览器与编码 Agent 之间的本地桥 |SSP
chrome·开源·github
Nicander13 小时前
我给 Excalidraw 做了一个本地工作区:DrawSpace
前端·开源
冬奇Lab14 小时前
一天一个开源项目(第223篇):TeamAI-CLI —— 腾讯开源的团队级 AI Agent 中间层,让每个人的 AI 能力变成团队共享能力
人工智能·开源·资讯
SL_staff15 小时前
从RBAC到场景化授权:《无忧·企业文档》三级权限模型的技术实践解析
java·开源·产品
SL_staff15 小时前
财务系统慎用低代码?从数据模型闭环看合规落地的技术实践
java·低代码·全栈
今夕资源网16 小时前
把 DeepSeek 网页版变成能操作本地项目的 AI Agent:Cuckoo Code 使用指南 GitHub开源
开源·github·deepseek
m4Rk_20 小时前
【论文阅读】Agent 记忆机制(75):TiMem——用时间记忆树实现长期记忆的层级巩固
论文阅读·人工智能·学习·开源·github