简介:本资源是一套面向前端开发初学者与毕业设计学生的CRM客户关系管理系统原型页面,聚焦于交互逻辑实现与界面体验优化,适用于Web前端课程设计、毕设原型开发及JavaScript实战练习。压缩包为RAR格式,大小10MB,虽未提供具体文件总数与类型明细,但根据描述可知其包含完整的HTML页面结构、集成JavaScript表单验证逻辑、基于jQuery的DOM操作与动态效果代码,以及采用Ajax技术实现的无刷新数据交互模块,覆盖用户注册、客户信息录入、订单状态查询等典型业务场景。已有301人学习下载,反映出该原型在教学实践中的实用价值。读者可直接获取一套结构清晰、注释充分、具备响应式设计的CRM前端原型工程,掌握表单实时校验、元素动态控制、异步通信及UI动效等核心技能,并可基于此快速拓展后端对接或迭代功能模块。
1. CRM客户关系管理系统原型:不是PPT画布,而是能跑通线索→商机→成交闭环的最小可验证体
很多团队一说“做CRM原型”,第一反应是打开Axure拖几个表单、画几条流程线,导出PDF发给业务方签字——结果上线前发现销售根本不会按你设计的“线索分配规则”手动点“转商机”,客服也拒绝在“服务满意度字段”里填数字。真正的CRM原型,必须是带真实数据流、有状态机驱动、能被销售拿去当天工作台用的轻量级系统。它不追求功能完整,但要求每个模块都经得起“今天下午就让A同学用它录3条新线索并走完首次跟进”的压力测试。本文讲的,就是如何用Python+Flask+SQLite在两天内搭出这样一个原型:支持线索录入、自动打标签、商机阶段推进、基础统计看板,所有代码可直接运行,数据库结构清晰可改,接口留好扩展缝。适合刚接手CRM需求的产品经理、想快速验证流程逻辑的开发工程师,以及需要向管理层证明“先跑通再扩功能”可行性的技术负责人。
2. 用Flask+SQLite三步搭起CRM核心骨架:从零初始化到首页可访问
CRM原型不是堆功能,而是建骨架。骨架稳了,后续加字段、改流程、接短信通知才不翻车。我们选Flask而非Django,是因为它不强制你接受ORM、Admin、用户认证整套方案——原型阶段,你只需要一个路由、一个模板、一个数据库连接。SQLite则彻底省掉部署数据库服务的步骤,.db文件即数据库,双击就能用DB Browser打开查数据,销售同事提“为什么这条线索没进我的池子”,你5秒内就能定位到leads表里assigned_to字段值是不是空。
2.1 初始化项目结构与依赖安装
新建目录crm-prototype,执行以下命令(注意:Python 3.8+):
mkdir crm-prototype && cd crm-prototype python -m venv venv source venv/bin/activate # Windows用 venv\Scripts\activate pip install flask flask-sqlalchemy python-dotenv提示:
flask-sqlalchemy用于简化数据库操作,但不启用其自动迁移功能(flask-migrate),原型阶段表结构变更是手动SQL更可控;python-dotenv用来管理配置,避免把DEBUG=True误提交到代码库。
2.2 定义核心数据模型:线索、商机、联系人三张表
在项目根目录创建models.py,定义三个实体。关键点在于状态字段用字符串而非外键——原型阶段业务规则未固化,硬编码status ENUM('new','contacted','qualified')反而限制迭代:
# models.py from flask_sqlalchemy import SQLAlchemy from datetime import datetime db = SQLAlchemy() class Lead(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) phone = db.Column(db.String(20)) email = db.Column(db.String(120)) source = db.Column(db.String(50), default='web_form') # 来源:web_form, wechat, call status = db.Column(db.String(20), default='new') # 状态:new, contacted, qualified, lost assigned_to = db.Column(db.String(50), default='') # 分配给谁(姓名或ID) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) class Opportunity(db.Model): id = db.Column(db.Integer, primary_key=True) lead_id = db.Column(db.Integer, db.ForeignKey('lead.id'), nullable=False) name = db.Column(db.String(100), nullable=False) stage = db.Column(db.String(30), default='prospecting') # 阶段:prospecting, proposal, negotiation, closed_won, closed_lost value = db.Column(db.Float, default=0.0) close_date = db.Column(db.Date) created_at = db.Column(db.DateTime, default=datetime.utcnow) class Contact(db.Model): id = db.Column(db.Integer, primary_key=True) lead_id = db.Column(db.Integer, db.ForeignKey('lead.id')) name = db.Column(db.String(100), nullable=False) position = db.Column(db.String(100)) phone = db.Column(db.String(20)) email = db.Column(db.String(120)) is_primary = db.Column(db.Boolean, default=False)逻辑说明:
Lead是源头,所有客户信息起点;Opportunity必须关联Lead,体现“线索升商机”强业务约束;Contact允许一对多(一个线索可能有多个对接人)。status和stage用字符串而非枚举类型,方便后期在前端下拉框里直接增删选项,无需改代码、重跑迁移。assigned_to存字符串而非用户ID,因原型阶段无统一用户中心,销售姓名(如“A同学”)比UUID更直观,业务方一眼能懂。
2.3 创建应用入口与数据库初始化脚本
创建app.py,这是整个原型的启动心脏:
# app.py from flask import Flask, render_template, request, redirect, url_for, flash from models import db, Lead, Opportunity, Contact import os def create_app(): app = Flask(__name__) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key-change-in-prod') app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///crm.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db.init_app(app) # 创建数据库表(仅首次运行) with app.app_context(): db.create_all() return app app = create_app() @app.route('/') def index(): leads = Lead.query.order_by(Lead.created_at.desc()).limit(10).all() return render_template('index.html', leads=leads) if __name__ == '__main__': app.run(debug=True)参数说明:
SQLALCHEMY_DATABASE_URI = 'sqlite:///crm.db':数据库文件名为crm.db,与代码同目录,无需额外安装数据库服务。db.create_all()放在with app.app_context():内,确保Flask应用上下文已加载,否则会报RuntimeError: Working outside of application context。debug=True仅限本地开发,上线前必须设为False并移除SECRET_KEY硬编码(用环境变量)。
运行python app.py,访问http://127.0.0.1:5000,若看到空白页(因index.html尚未创建),说明骨架已通电——下一步就是让页面活起来。
3. 实现线索全生命周期操作:从表单提交到状态流转的闭环逻辑
原型的价值,在于让业务动作可追踪。销售点“新建线索”,填完表单点提交,数据进库;再点“转商机”,自动生成商机记录并更新线索状态;最后在列表页看到“阶段”列从new变成qualified——这个闭环,就是业务方信任你的起点。本节实现从线索录入、状态更新、商机创建到列表展示的完整链路,所有操作均通过原生HTML表单+Flask路由完成,不引入前端框架,降低理解门槛。
3.1 构建线索录入表单与后端接收逻辑
在项目根目录创建templates文件夹,新建templates/lead_form.html:
<!-- templates/lead_form.html --> <!DOCTYPE html> <html> <head><title>新建线索</title></head> <body> <h2>新建线索</h2> <form method="POST"> <label>姓名:<input type="text" name="name" required></label><br><br> <label>电话:<input type="text" name="phone"></label><br><br> <label>邮箱:<input type="email" name="email"></label><br><br> <label>来源: <select name="source"> <option value="web_form">网站表单</option> <option value="wechat">微信咨询</option> <option value="call">电话呼入</option> </select> </label><br><br> <label>分配给:<input type="text" name="assigned_to" placeholder="例如:A同学"></label><br><br> <button type="submit">保存线索</button> </form> </body> </html>在app.py中添加对应路由(放在@app.route('/')下方):
@app.route('/leads/new', methods=['GET', 'POST']) def new_lead(): if request.method == 'POST': lead = Lead( name=request.form['name'], phone=request.form.get('phone', ''), email=request.form.get('email', ''), source=request.form['source'], assigned_to=request.form.get('assigned_to', '') ) db.session.add(lead) db.session.commit() flash(f'线索 "{lead.name}" 创建成功!', 'success') return redirect(url_for('index')) return render_template('lead_form.html')逻辑说明:
request.form.get('field', '')用get而非[],避免字段缺失时报KeyError;空字符串作为默认值,比None更易在数据库中显示和查询。flash()函数需在index.html中渲染提示消息,稍后补充;redirect(url_for('index'))防止用户刷新页面时重复提交表单(PRG模式)。- 此处未做手机号/邮箱格式校验,因原型阶段优先保证流程跑通,校验逻辑可后续用JavaScript或Flask-WTF补上。
3.2 实现线索状态更新与商机创建联动
销售跟进线索后,需标记状态并可能创建商机。我们在线索列表页(index.html)为每条线索添加操作按钮,并在app.py中新增两个路由:
# app.py 中追加 @app.route('/leads/<int:lead_id>/update_status', methods=['POST']) def update_lead_status(lead_id): lead = Lead.query.get_or_404(lead_id) new_status = request.form['status'] lead.status = new_status lead.updated_at = datetime.utcnow() # 若状态变为 qualified,自动创建商机(最小闭环) if new_status == 'qualified': opp = Opportunity( lead_id=lead_id, name=f"{lead.name}的商机", stage='prospecting' ) db.session.add(opp) db.session.commit() flash(f'线索 "{lead.name}" 状态已更新为 {new_status}', 'info') return redirect(url_for('index')) @app.route('/opportunities/<int:opp_id>/update_stage', methods=['POST']) def update_opportunity_stage(opp_id): opp = Opportunity.query.get_or_404(opp_id) new_stage = request.form['stage'] opp.stage = new_stage opp.updated_at = datetime.utcnow() db.session.commit() flash(f'商机阶段已更新为 {new_stage}', 'info') return redirect(url_for('index'))对应templates/index.html(需先创建):
<!-- templates/index.html --> <!DOCTYPE html> <html> <head><title>CRM原型首页</title></head> <body> <h1>CRM客户关系管理系统原型</h1> <!-- Flash消息显示区 --> {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} <div style="color: {% if category == 'error' %}red{% elif category == 'success' %}green{% else %}blue{% endif %};"> {{ message }} </div> {% endfor %} {% endif %} {% endwith %} <a href="{{ url_for('new_lead') }}">+ 新建线索</a><br><br> <h2>最新线索(10条)</h2> <table border="1" class="dataframe"> <thead> <tr><th>姓名</th><th>电话</th><th>来源</th><th>状态</th><th>分配给</th><th>操作</th></tr> </thead> <tbody> {% for lead in leads %} <tr> <td>{{ lead.name }}</td> <td>{{ lead.phone }}</td> <td>{{ lead.source }}</td> <td>{{ lead.status }}</td> <td>{{ lead.assigned_to }}</td> <td> <form method="POST" action="{{ url_for('update_lead_status', lead_id=lead.id) }}" style="display:inline;"> <select name="status"> <option value="new" {% if lead.status == 'new' %}selected{% endif %}>新线索</option> <option value="contacted" {% if lead.status == 'contacted' %}selected{% endif %}>已联系</option> <option value="qualified" {% if lead.status == 'qualified' %}selected{% endif %}>已确认商机</option> <option value="lost" {% if lead.status == 'lost' %}selected{% endif %}>已流失</option> </select> <button type="submit">更新</button> </form> </td> </tr> {% endfor %} </tbody> </table> <h2>商机列表</h2> <table border="1" class="dataframe"> <thead> <tr><th>商机名称</th><th>关联线索</th><th>阶段</th><th>操作</th></tr> </thead> <tbody> {% for opp in opportunities %} <tr> <td>{{ opp.name }}</td> <td>{{ opp.lead.name }}</td> <td>{{ opp.stage }}</td> <td> <form method="POST" action="{{ url_for('update_opportunity_stage', opp_id=opp.id) }}" style="display:inline;"> <select name="stage"> <option value="prospecting" {% if opp.stage == 'prospecting' %}selected{% endif %}>初步接触</option> <option value="proposal" {% if opp.stage == 'proposal' %}selected{% endif %}>方案提交</option> <option value="negotiation" {% if opp.stage == 'negotiation' %}selected{% endif %}>谈判中</option> <option value="closed_won" {% if opp.stage == 'closed_won' %}selected{% endif %}>已成交</option> <option value="closed_lost" {% if opp.stage == 'closed_lost' %}selected{% endif %}>已失败</option> </select> <button type="submit">更新阶段</button> </form> </td> </tr> {% endfor %} </tbody> </table> </body> </html>关键细节:
url_for('update_lead_status', lead_id=lead.id)生成动态URL,比硬写/leads/123/update_status更安全,路由名改了模板自动适配。- 表单
style="display:inline;"让下拉框和按钮在同一行,UI更紧凑;selected属性根据当前状态高亮选项,提升操作反馈。 - 商机列表中
{{ opp.lead.name }}能直接访问关联线索,得益于SQLAlchemy的relationship(需在models.py中补上,见下文)。
注意:上面模板中用了
opportunities变量,需在index()路由中查询并传入。修改app.py中的index函数:@app.route('/') def index(): leads = Lead.query.order_by(Lead.created_at.desc()).limit(10).all() opportunities = Opportunity.query.join(Lead).order_by(Opportunity.created_at.desc()).limit(10).all() return render_template('index.html', leads=leads, opportunities=opportunities)
3.3 补全模型关系与数据库查询优化
当前Opportunity模型缺少对Lead的反向引用,导致模板中opp.lead.name报错。在models.py中Opportunity类下方添加关系声明:
# models.py 中追加 class Opportunity(db.Model): # ... 原有字段 ... lead = db.relationship('Lead', backref=db.backref('opportunities', lazy=True))同时,为提升查询效率(避免N+1问题),在index()路由中使用join一次性查出线索和商机,而非循环查:
# app.py 中 index() 函数内 opportunities = Opportunity.query\ .join(Lead, Opportunity.lead_id == Lead.id)\ .add_columns(Lead.name.label('lead_name'))\ .order_by(Opportunity.created_at.desc())\ .limit(10).all()然后在模板中改为{{ lead_name }}。此写法明确告知SQLAlchemy要JOIN,生成的SQL只有一条,而非对每条商机再查一次线索。
4. 避坑:原型阶段最常踩的5个深坑及血泪解决方案
做CRM原型不是写Hello World,业务逻辑嵌套、状态流转约束、数据一致性要求极高。我曾在某高校实验室带学生做模拟项目X时,连续三天卡在同一个问题上:销售反馈“线索转商机后,列表里还是显示new”。排查发现是db.session.commit()调用位置错误。以下是我在多个CRM原型项目中踩过的真坑,按发生频率排序,每条都附可复现的代码片段和修复命令。
4.1 现象:线索状态更新后页面不刷新,仍显示旧值
原因:Flask默认不缓存POST请求,但浏览器可能对GET请求(如/)启用了强缓存,导致重定向后看到的是旧页面快照。
解决:在app.py顶部添加响应头,禁用页面缓存(仅开发期):
from functools import wraps from flask import make_response def no_cache(f): @wraps(f) def decorated_function(*args, **kwargs): response = make_response(f(*args, **kwargs)) response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' return response return decorated_function # 在 @app.route('/') 上方添加装饰器 @app.route('/') @no_cache def index(): # ... 原有代码4.2 现象:assigned_to字段存了空格,导致销售查不到自己名下的线索
原因:用户输入" A同学 ",前后空格未清理,WHERE assigned_to = 'A同学'匹配失败。
解决:在new_lead和update_lead_status路由中,对字符串字段做strip():
# 替换 app.py 中 lead = Lead(...) 的赋值部分 lead = Lead( name=request.form['name'].strip(), phone=request.form.get('phone', '').strip(), email=request.form.get('email', '').strip(), source=request.form['source'].strip(), assigned_to=request.form.get('assigned_to', '').strip() )4.3 现象:商机创建后,线索状态未自动更新为qualified
原因:update_lead_status路由中,if new_status == 'qualified':判断后创建商机,但忘记提交事务,db.session.add(opp)后无db.session.commit(),商机记录未落库。
解决:将db.session.commit()移到所有操作之后,确保线索和商机原子性提交:
# 修改 update_lead_status 路由末尾 db.session.commit() # 此行必须在 if 块之外,且在所有 add() 之后4.4 现象:datetime.utcnow()在不同服务器时区下返回时间不一致,导致“今日线索”统计不准
原因:utcnow()返回UTC时间,但销售看报表习惯用本地时间(如东八区),WHERE created_at > '2024-06-01'实际查的是UTC时间。
解决:原型阶段统一用UTC,但显示时转换。在模板中用Jinja2过滤器(需先注册):
# app.py 中 create_app() 内添加 @app.template_filter('localtime') def localtime(dt): from datetime import timedelta return dt + timedelta(hours=8) # 东八区偏移模板中使用:{{ lead.created_at | localtime }}。
4.5 现象:SQLite数据库被多个请求同时写入,报database is locked错误
原因:SQLite是文件数据库,高并发写入时锁表,而原型常开多个浏览器标签测试。
解决:增加重试机制。在app.py顶部添加:
import time from sqlalchemy.exc import OperationalError def safe_commit(): for i in range(3): try: db.session.commit() return True except OperationalError: time.sleep(0.1 * (2 ** i)) # 指数退避 return False # 在所有 commit() 处替换为 safe_commit()然后将所有db.session.commit()替换为safe_commit()。
5. 加入自动化标签与基础统计看板:让原型从“能用”升级为“有用”
原型做完线索录入和状态流转,只是完成了“能用”。真正让业务方眼前一亮的,是系统能主动帮他们发现问题——比如自动给来自“微信咨询”的线索打上high_priority标签,或在首页显示“本周新线索:23条,已转化商机:8个”。本节不引入复杂BI工具,用纯Python+Jinja2实现两件事:基于规则的线索自动打标,以及首页嵌入的轻量级统计卡片。所有逻辑都在app.py内完成,无需额外服务。
5.1 实现基于规则的线索自动打标:用字典代替硬编码if-else
标签(Tag)是CRM的灵魂,它让销售能快速筛选高价值线索。原型阶段不搞机器学习,用确定性规则:
- 邮箱含
@company.com→ 标签enterprise - 电话以
138开头 → 标签vip_mobile - 来源为
wechat→ 标签high_touch
在models.py中为Lead添加tags字段(字符串,逗号分隔,便于原型期快速增删):
# models.py 中 Lead 类内追加 tags = db.Column(db.String(200), default='')在new_lead路由中,提交前插入打标逻辑:
# app.py 中 new_lead() 函数内,db.session.add(lead) 之前添加 def apply_tags(lead): tags = [] if '@company.com' in (lead.email or ''): tags.append('enterprise') if lead.phone and lead.phone.startswith('138'): tags.append('vip_mobile') if lead.source == 'wechat': tags.append('high_touch') lead.tags = ','.join(tags) # 在 lead = Lead(...) 之后,db.session.add(lead) 之前调用 apply_tags(lead)逻辑说明:
tags用逗号分隔而非JSON,因SQLite无原生JSON支持,字符串更易用SQL查询(如WHERE tags LIKE '%enterprise%')。- 规则函数
apply_tags()独立封装,未来可轻松替换为调用外部规则引擎API。 - 不在数据库层面建
tags关联表,因原型阶段标签数量<10个,冗余存储比JOIN更高效。
5.2 构建首页统计看板:5个SQL查询撑起业务仪表盘
销售总监第一次打开CRM,最想看的不是列表,而是“我团队干得怎么样”。我们在index()路由中增加5个统计查询,全部用原生SQL(绕过ORM性能损耗),结果以字典形式传给模板:
# app.py 中 index() 函数内追加 stats = {} # 1. 今日新线索 result = db.session.execute("SELECT COUNT(*) FROM lead WHERE DATE(created_at) = DATE('now')") stats['today_leads'] = result.scalar() # 2. 本周转化率(线索→商机) result = db.session.execute(""" SELECT COUNT(l.id) as total_leads, COUNT(o.id) as opp_count FROM lead l LEFT JOIN opportunity o ON l.id = o.lead_id WHERE l.created_at >= DATE('now', '-7 days') """) row = result.fetchone() stats['week_conversion'] = f"{(row.opp_count / row.total_leads * 100):.1f}%" if row.total_leads else "0%" # 3. 各阶段商机数 result = db.session.execute(""" SELECT stage, COUNT(*) as cnt FROM opportunity GROUP BY stage """) stats['stage_breakdown'] = {row.stage: row.cnt for row in result} # 4. 高优先级线索数(含high_priority标签) result = db.session.execute("SELECT COUNT(*) FROM lead WHERE tags LIKE '%high_touch%'") stats['high_touch_leads'] = result.scalar() # 5. 待跟进线索(状态为new或contacted) result = db.session.execute("SELECT COUNT(*) FROM lead WHERE status IN ('new', 'contacted')") stats['pending_followup'] = result.scalar()在templates/index.html中,于<h1>下方插入统计卡片:
<!-- templates/index.html 中 <h1> 后添加 --> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin: 20px 0;"> <div style="border: 1px solid #ddd; padding: 12px; text-align: center;"> <div style="font-size: 18px; font-weight: bold; color: #2c3e50;">{{ stats.today_leads }}</div> <div>今日新线索</div> </div> <div style="border: 1px solid #ddd; padding: 12px; text-align: center;"> <div style="font-size: 18px; font-weight: bold; color: #27ae60;">{{ stats.week_conversion }}</div> <div>本周转化率</div> </div> <div style="border: 1px solid #ddd; padding: 12px; text-align: center;"> <div style="font-size: 18px; font-weight: bold; color: #e67e22;">{{ stats.high_touch_leads }}</div> <div>高触达线索</div> </div> <div style="border: 1px solid #ddd; padding: 12px; text-align: center;"> <div style="font-size: 18px; font-weight: bold; color: #e74c3c;">{{ stats.pending_followup }}</div> <div>待跟进线索</div> </div> </div> <!-- 阶段分布用表格 --> <h3>商机阶段分布</h3> <table border="1" style="width:100%; text-align:center;"> <tr><th>阶段</th><th>数量</th></tr> {% for stage, cnt in stats.stage_breakdown.items() %} <tr><td>{{ stage }}</td><td>{{ cnt }}</td></tr> {% endfor %} </table>提示:SQLite的
DATE('now')和DATE('now', '-7 days')是跨平台安全的日期函数,无需担心MySQL或PostgreSQL语法差异。原型阶段用SQLite,上线时换数据库只需改SQLALCHEMY_DATABASE_URI,统计SQL几乎不用动。
5.3 进阶技巧:用CSV导出功能让销售自己分析数据
业务方常提:“能不能把线索导出成Excel?”原型阶段不必集成Excel库,用CSV即可。在app.py中添加导出路由:
@app.route('/leads/export') def export_leads(): import csv from io import StringIO from flask import Response output = StringIO() writer = csv.writer(output) writer.writerow(['ID', '姓名', '电话', '邮箱', '来源', '状态', '分配给', '标签', '创建时间']) leads = Lead.query.all() for lead in leads: writer.writerow([ lead.id, lead.name, lead.phone, lead.email, lead.source, lead.status, lead.assigned_to, lead.tags, lead.created_at.strftime('%Y-%m-%d %H:%M:%S') ]) output.seek(0) return Response( output.getvalue(), mimetype="text/csv", headers={"Content-disposition": "attachment; filename=crm_leads_export.csv"} )在index.html中加导出按钮:
<a href="{{ url_for('export_leads') }}" style="margin-left:10px;">📥 导出线索CSV</a>此功能价值极大:销售下载CSV后,可用Excel透视表自己分析“哪个来源的线索转化率最高”,比等你开发报表更快。这也是让业务方从“被动使用者”变成“主动参与者”的关键一步。
6. 我的原型交付 checklist:上线前必做的7件事与一条后悔药
交付CRM原型不是代码git push就结束。我带过的每个模拟项目X,上线演示前夜都会逐项核对这份checklist。它不追求完美,但确保“不丢人”——当销售总监问“这条线索为什么没进我的池子”,你能30秒内打开crm.db,定位到assigned_to字段,而不是手忙脚乱查日志。以下是我压箱底的7条,按执行顺序排列,最后一条是真正的“后悔药”。
6.1 数据库备份与初始数据注入
原型跑通后,立刻备份crm.db为crm_demo.db,并注入10条模拟数据(含不同状态、来源、标签),确保每次git clone后python app.py能直接看到真实场景:
# 备份 cp crm.db crm_demo.db # 创建 init_data.py(运行一次) from app import create_app from models import db, Lead app = create_app() with app.app_context(): db.create_all() # 插入10条模拟数据... db.session.commit()为什么重要?避免演示时因空库导致“首页一片空白”,业务方第一印象直接崩塌。
6.2 关闭调试模式与设置密钥
app.py中debug=True必须改为False,且SECRET_KEY不能硬编码:
# app.py app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key-change-in-prod') # 启动时设置环境变量 # export SECRET_KEY="a-very-secure-random-string"玄学警告:曾有项目因
SECRET_KEY未设,flash()消息在生产环境完全不显示,销售以为系统坏了。
6.3 验证所有表单提交的错误处理
手动在表单中留空必填字段、输入超长字符串、提交特殊字符(如<script>),确认页面不崩溃,且有友好提示。Flask默认500错误页太丑,加一个简单错误页:
@app.errorhandler(404) def not_found(e): return render_template('error.html', error="页面未找到"), 404 @app.errorhandler(500) def server_error(e): return render_template('error.html', error="系统内部错误,请稍后重试"), 5006.4 检查URL路由是否全部可访问
用curl -I http://127.0.0.1:5000/xxx测试所有路由(/,/leads/new,/leads/1/update_status,/leads/export),确认返回200或302。漏掉一个路由,演示时点不动按钮就是灾难。
6.5 测试移动端适配
用Chrome DevTools切到iPhone X尺寸,打开首页。表单字段是否可点击?按钮是否够大?表格是否横向滚动?原型阶段不追求响应式设计,但至少保证销售用手机能录线索。
6.6 文档化“三句话说明”
写一个README.md,只包含三句话:
- “这是一个CRM客户关系管理系统原型,支持线索录入、状态更新、商机创建和基础统计。”
- “运行方式:
pip install -r requirements.txt && python app.py,访问http://127.0.0.1:5000。” - “默认管理员无密码,所有操作均可直接使用;数据库文件为
crm.db,双击可用DB Browser打开。”
——业务方不需要知道Flask是什么,他们只需要知道“怎么用”。
6.7 最后一条:后悔药——一键回滚到初始状态
在app.py中加一个隐藏路由(仅本地开发用),点击即清空所有数据,重置为初始10条:
@app.route('/reset-db') def reset_db(): if not app.debug: return "仅开发环境可用", 403 db.drop_all() db.create_all() # 重新注入初始数据... return redirect(url_for('index'))演示中途发现数据错乱?点一下/reset-db,3秒回到干净状态。这招救过我三次——一次是销售误操作把所有线索状态改成lost,一次是测试时数据库锁死,还有一次是……算了,不说了。
希望帮到你。
本文还有配套的精品资源,点击获取