尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Python Flask+Vue企业CRM客户关系管理系统设计与实现解析

发布时间:2026/9/26 13:04:41

资讯中心
01
ARTICLE

Python Flask+Vue企业CRM客户关系管理系统设计与实现解析

Python Flask+Vue企业CRM客户关系管理系统设计与实现解析
项目标题: python基于flask 企业crm客户关系管理系统的设计与实现-vue pycharm django关键词: python, flask, crm, vue, django摘要描述: 基于Python Flask与Vue前后端分离架构的企业CRM客户关系管理系统覆盖数据库设计、JWT鉴权、数据权限隔离、接口开发、前端联调与生产部署的完整实现路径。1. 为什么这套CRM我选了FlaskVue而不是Django全家桶企业里做一套CRM系统最磨人的往往不是功能数量而是数据权限怎么分、客户怎么分配、跟进记录怎么做到不丢。这套基于Python Flask Vue前后端分离架构的企业客户关系管理系统我从数据库设计、后端接口、Vue页面到用PyCharm调试、最终部署上线完整走下来用了两周多时间。如果你正打算自己动手做一套CRM——无论这是毕业设计、企业内训项目还是给公司做的真正业务工具——这篇内容会把选型、建表、接口、前端联调、权限隔离、部署上线的关键路径一条条拆开讲清楚。先说个大方向问题标题里同时出现了Flask和Django很多刚接触Python的人会纠结到底用哪个。我的态度很明确——这套CRM我选Flask做主力Django只作为对照参考。原因不复杂CRM这种业务系统核心是几十个接口、若干张关联表、一套清晰的权限规则用Flask的轻量级特性足够覆盖而且代码量更少、可读性更强。Django自带Admin后台和ORM起步快但它在中小型前后端分离项目里带来的重量感和约束感往往超过它带来的便利。1.1 先说需求边界中小团队CRM真正需要什么很多项目一上来就设计一堆功能最后全变成摆设。我在做这套系统之前专门和实际使用CRM的销售、主管聊过一轮整理出来的真实需求大致是销售能录入自己的客户记录每次电话、拜访、报价的跟进内容销售只能看到自己的客户主管能看到本组成员客户管理员看全部能把客户按阶段推进比如初步接触→需求确认→方案报价→签约能从客户库里搜索历史记录知道上次聊到哪了有简单的数据统计看本月新增客户数和成交转化这些需求听起来不复杂但落地时的坑全在细节里——比如销售离职了客户归谁主管怎么接管组员客户跟进记录能不能被篡改。如果一开始没在表结构上留好字段后补极其痛苦。这也是为什么我把数据库设计放在整篇博文最前面讲。1.2 Flask与Django的选型权衡我把两个框架在这类项目里的真实差异整理了一张表供你参考对比维度FlaskDjango项目结构自由蓝图按模块拆固定app目录结构规范ORMSQLAlchemy灵活可控Django ORM与框架耦合深Admin后台默认没有需自建自带Admin改一改就能用适合场景API后端、微服务、前后端分离全栈快速开发、内容管理类学习曲线平缓半天能上手陡一些概念多代码可控性高坑要自己踩中很多坑框架帮你踩了在前后端分离架构下Flask只需要聚焦在返回JSON接口这一件事上模板、表单、Admin这些Django的强项反而用不上。而且Vue前端已经把交互都吃了后端越轻联调越省心。如果你的项目需要后端直接输出页面、还要在短期内完成整套后台管理系统那选Django更合适——但凡是打算把前端交给Vue、React这类框架的Flask是一个更顺手的选择。1.3 技术栈全貌与项目结构这套系统的完整技术栈是Python 3.10 Flask 2.3 SQLAlchemy 2.0 MySQL 8.0 Vue 3 Vite开发IDE用PyCharm和VS Code配合。目录结构分前后端两个大目录后端只负责API前端只负责渲染部署时由Nginx统一托管。crm-project/ ├── backend/ │ ├── app.py # Flask入口创建app注册蓝图 │ ├── config.py # 配置数据库连接、JWT密钥 │ ├── models/ # SQLAlchemy模型层 │ │ ├── user.py │ │ ├── role.py │ │ ├── customer.py │ │ └── follow_up.py │ ├── blueprints/ # 蓝图路由层 │ │ ├── auth.py │ │ ├── customer.py │ │ └── stats.py │ ├── utils/ # 工具类JWT校验装饰器、统一返回格式 │ └── requirements.txt └── frontend/ ├── src/ │ ├── api/ # axios请求封装 │ ├── views/ # 页面组件 │ ├── router/ # vue-router路由配置 │ └── store/ # pinia状态管理 └── package.json这种结构的好处是两个团队或你自己一个人身兼前后端可以并行开发——后端接口只要约定好返回格式前端可以用Mock数据先跑页面两边进度互不阻塞。后面我会按照这个结构一步步展开。2. 数据库建模客户、跟进、合同三张核心表怎么设计CRM系统说穿了就是围绕客户这个核心实体做增删改查、做记录流转、做数据分析。所以数据库设计的重要性排在接口开发之前。我建表的原则是能少则少但关键关联字段绝不省。少是为了降低业务理解成本不省是为了日后统计、权限隔离时不打补丁。2.1 用户、角色、部门三张基础表的关联在中小型CRM里用户的权限模型我建议做成部门 角色两级而不是复杂的RBAC矩阵——因为销售、主管、管理员三种角色基本够用。用户表和部门表、角色表的关系如下user表id、username、password_hash、real_name、dept_id、role_id、status、created_atdept表id、dept_name、parent_idrole表id、role_code、role_name关键点在于dept_id。做数据权限隔离时最自然的维度就是部门销售A在销售一组主管B负责销售一组那么主管B查客户列表时只需要一条SQL就能筛出dept_id等于本组的所有客户。如果把权限做成每个用户手工指定可见客户ID那管理成本会指数级上升凡是做过权限系统的人应该都有同感。密码字段一定要存哈希值用werkzeug.security.generate_password_hash()别明文存。我第一次做系统时图省事存了明文后来CTF比赛玩多了才意识到这有多危险——数据库一旦被拖库所有账号密码直接裸奔。2.2 客户主表与联系人表的字段取舍客户表和联系人表是CRM最核心的两张业务表。客户表记录公司或个人客户的主体信息联系人表记录客户的对接人——一个客户下可能有多位联系人这是实际业务中和销售对接时最常见的场景。我设计的客户表核心字段如下class Customer(db.Model): __tablename__ customer id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(120), nullableFalse, comment客户名称) industry db.Column(db.String(50), comment所属行业) phone db.Column(db.String(20), comment联系电话) level db.Column(db.SmallInteger, default0, comment客户等级: 0普通 1重点 2VIP) source db.Column(db.String(50), comment客户来源: 展会/转介绍/网站咨询) stage db.Column(db.String(20), default初步接触, comment商机阶段) owner_id db.Column(db.Integer, db.ForeignKey(user.id), comment负责人) dept_id db.Column(db.Integer, db.ForeignKey(dept.id), comment所属部门) created_at db.Column(db.DateTime, defaultdatetime.now) updated_at db.Column(db.DateTime, defaultdatetime.now, onupdatedatetime.now)注意这里我把owner_id和dept_id同时放进了客户表。owner_id用来做销售只能看到自己名下客户的精确控制dept_id用来做主管能看本部门所有客户的粗粒度范围。有人问为什么不通过join用户表再join部门表非要在客户表冗余一个dept_id——因为权限查询是高频操作每次跨三张表join对数据库来说是不必要的负担冗余一个部门ID在业务上完全可接受这也符合读写分离的思路——查询时会更快。联系人表相对简单id、customer_id、name、position、phone、wechat、is_primary。业务上要注意的点是一个客户至少要有一个主联系人在录入客户的表单里如果没填联系人后端要主动提示。我在前端验证规则里加了必填但后端接口也要再验证一次不能只依赖前端——这是前后端分离项目最基本的安全底线。2.3 跟进记录与商机漏斗的业务落库跟进记录表记录每一次销售动作它决定了CRM能不能帮你复盘这个客户为什么推进慢。字段设计上我特别加了next_follow_time这是很多初版CRM会漏掉的字段——没有下一次跟进时间销售就很容易把一个客户凉在池子里。class FollowUp(db.Model): __tablename__ follow_up id db.Column(db.Integer, primary_keyTrue) customer_id db.Column(db.Integer, db.ForeignKey(customer.id)) content db.Column(db.Text, nullableFalse, comment跟进内容) next_follow_time db.Column(db.DateTime, comment下次跟进时间) created_by db.Column(db.Integer, db.ForeignKey(user.id)) created_at db.Column(db.DateTime, defaultdatetime.now)商机阶段我直接用客户表里的stage字符串字段取值限定为初步接触、需求确认、方案报价、商务谈判、已签约、已流失。为什么不用数字状态因为我试过用0、1、2、3后期每次看数据都要在心里做一次映射太反人类字符串字段在业务系统里更直观。查询统计时按stage分组就能得到一个销售漏斗的近似分布。这里有一个设计心得跟进记录表天生只追加、不修改。一旦销售记录了跟进内容这条记录就应该是历史事实不允许被update和delete。我在后端只暴露了新增和查询接口刻意不写修改和删除接口——毕竟CRM是管理客户资产的系统审计追踪比灵活编辑的价值大得多。3. Flask后端从目录结构到接口落地的完整链路后端是整个CRM的大脑我把Flask后端的实现逻辑拆成四个关键环节入口与蓝图拆分、JWT鉴权、业务接口、列表与统计接口。每个环节都有值得展开的实操细节。3.1 用蓝图拆分模块别把所有路由堆在一个py里新手写Flask最容易犯的错就是把所有路由写进一个app.py写上几百行、十几个路由最后自己都找不到对应函数。用蓝图Blueprint按业务模块拆是正确姿势。入口文件只负责装配业务逻辑各回各家。# app.py from flask import Flask from flask_cors import CORS from blueprints.auth import auth_bp from blueprints.customer import customer_bp from blueprints.follow_up import follow_up_bp from blueprints.stats import stats_bp from models import db from config import Config def create_app(): app Flask(__name__) app.config.from_object(Config) CORS(app, resources{r/*: {origins: *}}) db.init_app(app) with app.app_context(): db.create_all() app.register_blueprint(auth_bp, url_prefix/api/auth) app.register_blueprint(customer_bp, url_prefix/api/customer) app.register_blueprint(follow_up_bp, url_prefix/api/follow_up) app.register_blueprint(stats_bp, url_prefix/api/stats) return app app create_app() if __name__ __main__: app.run(debugTrue, port5000)每个蓝图就是一个模块比如customer_bp里的路由都围绕客户资源。# blueprints/customer.py from flask import Blueprint, request, jsonify from models import db, Customer, FollowUp customer_bp Blueprint(customer, __name__) customer_bp.route(/list, methods[GET]) def list_customers(): ... customer_bp.route(/int:cid, methods[GET]) def get_customer(cid): ... customer_bp.route(/create, methods[POST]) def create_customer(): ... customer_bp.route(/int:cid/update, methods[POST]) def update_customer(cid): ...这样做的好处是以后加一个合同管理模块只需要复制一份蓝图模板改改路由前缀和模型就能在半小时内接入主项目完全不影响现有模块。3.2 基于JWT的登录鉴权与权限注解这套系统用JWT做接口鉴权而不是Flask自带的session。因为前端是分离的Vue应用Session依赖Cookie和浏览器状态在跨域场景下容易遇到麻烦JWT把状态放进Token里后端无状态更契合前后端分离的架构。登录接口逻辑auth_bp.route(/login, methods[POST]) def login(): data request.get_json() username data.get(username, ).strip() password data.get(password, ) if not username or not password: return jsonify({code: 400, msg: 用户名和密码不能为空}) user User.query.filter_by(usernameusername).first() if not user or not check_password_hash(user.password_hash, password): return jsonify({code: 401, msg: 用户名或密码错误}) if user.status ! 1: return jsonify({code: 403, msg: 账号已禁用}) token jwt.encode({ uid: user.id, role: user.role.role_code, dept_id: user.dept_id, exp: datetime.utcnow() timedelta(hours12) }, Config.SECRET_KEY, algorithmHS256) return jsonify({code: 200, msg: ok, token: token, user_info: user.to_dict()})JWT放在请求头Authorization: Bearer token里后端通过装饰器统一校验。我封装了一个token_required装饰器在视图函数执行前解析Token把用户ID和部门ID挂到request上from functools import wraps import jwt from flask import request, jsonify def token_required(f): wraps(f) def decorated(*args, **kwargs): auth request.headers.get(Authorization, ) if not auth.startswith(Bearer ): return jsonify({code: 401, msg: 未登录或登录过期}), 401 try: payload jwt.decode( auth.replace(Bearer , ), Config.SECRET_KEY, algorithms[HS256] ) request.current_user_id payload[uid] request.current_role payload[role] request.current_dept_id payload[dept_id] except jwt.ExpiredSignatureError: return jsonify({code: 401, msg: 登录过期请重新登录}), 401 except jwt.InvalidTokenError: return jsonify({code: 401, msg: 无效的登录凭证}), 401 return f(*args, **kwargs) return decoratedJWT有个经典的痛点就是Token签发后无法主动失效解决思路是短过期时间前端主动续期。我在前端axios拦截器里遇到401就跳登录页遇到Token还剩20分钟过期就带旧Token去调一个刷新接口拿新Token。这个机制让用户基本感觉不到登录过期。3.3 客户增删改查接口的完整实现客户接口是CRUD里最典型的样例我重点讲讲创建和查询两个接口因为增删改查的坑基本都集中在这两处。创建客户接口需要同时做参数校验、权限绑定、去重检查customer_bp.route(/create, methods[POST]) token_required def create_customer(): data request.get_json() name data.get(name, ).strip() if not name: return jsonify({code: 400, msg: 客户名称必填}) # 避免同一销售重复录入同名客户 exists Customer.query.filter_by( namename, owner_idrequest.current_user_id ).first() if exists: return jsonify({code: 400, msg: 已存在同名客户}) customer Customer( namename, industrydata.get(industry, ), phonedata.get(phone, ), leveldata.get(level, 0), sourcedata.get(source, ), stage初步接触, owner_idrequest.current_user_id, dept_idrequest.current_dept_id ) db.session.add(customer) db.session.commit() return jsonify({code: 200, msg: 创建成功, id: customer.id})注意owner_id和dept_id不能从前端传只能从Token里取。否则销售可以伪造一个dept_id把客户挂到别的部门名下换取权限这属于典型的越权。凡是涉及数据归属的字段都必须以后端鉴权信息为准这条规则适用于所有写操作。查询列表是CRM里最复杂的接口因为它要组合搜索、分页、排序和数据权限过滤。我给出完整的写法customer_bp.route(/list, methods[GET]) token_required def list_customers(): page request.args.get(page, 1, typeint) limit request.args.get(limit, 10, typeint) keyword request.args.get(keyword, ).strip() stage request.args.get(stage, ) level request.args.get(level, typeint) query Customer.query # 数据权限范围过滤 if request.current_role admin: pass # 管理员看全部 elif request.current_role manager: query query.filter(Customer.dept_id request.current_dept_id) else: query query.filter(Customer.owner_id request.current_user_id) # 关键字搜索 if keyword: query query.filter( db.or_(Customer.name.like(f%{keyword}%), Customer.phone.like(f%{keyword}%)) ) # 阶段筛选 if stage: query query.filter(Customer.stage stage) if level is not None: query query.filter(Customer.level level) total query.count() customers query.order_by(Customer.updated_at.desc()) \ .offset((page - 1) * limit) \ .limit(limit) \ .all() return jsonify({ code: 200, msg: ok, data: { total: total, items: [c.to_dict() for c in customers] } })关于查询性能的提醒count()和offset/limit在数据量超过几万条后性能会明显下滑优化思路是先用索引覆盖过滤条件name、phone、owner_id字段都建索引再把count改成子查询。对中小团队CRM这个量级其实碰不到瓶颈但知道优化方向还是重要的。3.4 指标统计接口销售漏斗与新增趋势统计接口是CRM里最有领导看得见价值的部分我做了两个核心统计一是按客户阶段分组的销售漏斗二是最近30天的新增客户趋势。# blueprints/stats.py stats_bp.route(/funnel, methods[GET]) token_required def funnel(): query Customer.query if request.current_role admin: pass elif request.current_role manager: query query.filter(Customer.dept_id request.current_dept_id) else: query query.filter(Customer.owner_id request.current_user_id) rows query.with_entities( Customer.stage, db.func.count(Customer.id).label(cnt) ).group_by(Customer.stage).all() stages [初步接触, 需求确认, 方案报价, 商务谈判, 已签约, 已流失] result {s: 0 for s in stages} for stage, cnt in rows: if stage in result: result[stage] cnt return jsonify({code: 200, msg: ok, data: result})这个接口的关键是前端不用再做二次聚合按返回里的key直接画柱状图或漏斗图就行。前后端分离项目有一个约定俗成的习惯能后端算好的统计就别丢给前端算这么做的理由很简单前端算错没人背锅后端算错还能靠单测兜底。4. Vue前端页面交互与Flask接口对接实战CRM的前端部分技术选型是Vue 3 Vite Vue Router Pinia Element Plus。Element Plus的表格、表单、弹窗组件能省下大量写UI的时间。这里分享几个关键的对接实现。4.1 项目初始化与PyCharm VS Code 双开环境创建前端项目用Vite是最省事的比Webpack配置轻太多。npm create vitelatest frontend -- --template vue cd frontend npm install npm install axios vue-router4 pinia element-plus我个人的开发方式是用PyCharm写Flask后端用VS Code写Vue前端两个IDE加一个终端窗口后端跑在5000端口前端Vite跑在5173端口。PyCharm里配置好Python解释器并把后端项目根目录标记为Sources Root这样import不会爆红。VS Code里装上Vue Language Features和ESLint写代码的体验最接近一线大厂配置。前后端联调必须先解决跨域问题。开发环境下我用Vite的proxy配置做代理把/api开头的请求转发到后端5000端口绕开浏览器的同源策略// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } })这样前端所有axios请求都写/api/xxx开发时走代理生产时由Nginx统一处理前端代码完全不用改动。4.2 axios请求封装与登录态管理axios封装的核心目的是统一处理Token注入、响应解包、异常拦截。我把response.data直接返回业务代码里就不需要每次写res.data.data了。// src/api/request.js import axios from axios import router from ../router/index import { ElMessage } from element-plus const service axios.create({ baseURL: /api, timeout: 15000 }) // 请求拦截器注入token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer token } return config }) // 响应拦截器处理错误状态 service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response) { const status error.response.status if (status 401) { ElMessage.error(登录已过期请重新登录) localStorage.removeItem(token) localStorage.removeItem(user_info) router.push(/login) } else { ElMessage.error(error.response.data.msg || 服务器错误) } } else { ElMessage.error(网络异常请检查后端服务) } return Promise.reject(error) } ) export default service这里有个客户端存储的细节我把用户角色、部门、姓名这些非敏感信息同时存了一份到localStorage这样前端做菜单权限和显示用户名时不用每次调接口但涉及具体数据范围的操作一律以后端返回的为主。前端存的东西只用于展示绝不用于权限判断。4.3 客户管理页面的核心逻辑客户管理页面是整个前端最核心的页面包含搜索栏、客户表格、新增/编辑弹窗、删除确认、跟进记录抽屉。表格用Element Plus的el-table搜索用el-form弹窗用el-dialog。表格配置的核心代码template div classcustomer-page el-form :inlinetrue :modelsearchForm classsearch-bar el-form-item label关键词 el-input v-modelsearchForm.keyword placeholder客户名称/电话 clearable / /el-form-item el-form-item label阶段 el-select v-modelsearchForm.stage clearable placeholder全部 el-option v-fors in stageOptions :keys :labels :values / /el-select /el-form-item el-form-item el-button typeprimary clickhandleSearch查询/el-button el-button clickhandleReset重置/el-button /el-form-item /el-form div classtable-bar el-button typeprimary clickopenCreateDialog新增客户/el-button /div el-table :datatableData v-loadingloading border stripe el-table-column propname label客户名称 min-width120 / el-table-column proplevel label客户等级 width90 template #default{ row } el-tag :typelevelTagType(row.level){{ levelText(row.level) }}/el-tag /template /el-table-column el-table-column propstage label商机阶段 width110 / el-table-column propphone label联系电话 width130 / el-table-column propowner_name label负责人 width100 / el-table-column propcreated_at label创建时间 width170 / el-table-column label操作 width240 fixedright template #default{ row } el-button link typeprimary clickopenFollowUp(row)跟进/el-button el-button link typeprimary clickopenEditDialog(row)编辑/el-button el-button link typedanger clickhandleDelete(row)删除/el-button /template /el-table-column /el-table el-pagination v-model:current-pagepage v-model:page-sizelimit :totaltotal current-changeloadData layouttotal, prev, pager, next / /div /template加载数据的方法const loadData async () { loading.value true try { const res await apiCustomerList({ page: page.value, limit: limit.value, keyword: searchForm.value.keyword, stage: searchForm.value.stage }) tableData.value res.data.items total.value res.data.total } finally { loading.value false } }这里要重点讲一个实践细节tableData中要显示负责人姓名但后端只存了owner_id。我的做法是在后端接口返回的to_dict()里把负责人姓名和部门名称一起查出来拼进字典前端拿到的就是可以直接渲染的完整数据。这就是为什么后端列表接口看起来比单纯ORM多了两步join——为了减少前端联调的往返次数这是实际做项目时一个很常见的取舍。4.4 数据看板用ECharts展示销售漏斗与新增趋势数据看板我选ECharts实现它功能全面、文档丰富、社区方案多。先安装npm install echarts销售漏斗图的ECharts配置import * as echarts from echarts import { onMounted } from vue import { apiGetFunnel } from ../api/customer onMounted(async () { const res await apiGetFunnel() const data res.data const chart echarts.init(document.getElementById(funnelChart)) chart.setOption({ title: { text: 销售漏斗按客户阶段 }, tooltip: { trigger: item }, series: [{ type: funnel, left: 10%, top: 50, bottom: 20, width: 80%, minSize: 20%, maxSize: 100%, sort: descending, gap: 4, label: { show: true, position: inside }, itemStyle: { borderColor: #fff, borderWidth: 0 }, data: Object.entries(data).map(([name, value]) ({ name, value })) }] }) })做数据看板的时候我踩过一个坑图表容器初始化时如果父容器还没完成布局ECharts拿到的宽高是0图表就绘不出来了。解决方法是用nextTick或者给chart的初始化延迟到页面mounted之后再或者直接给容器设置固定高度。我建议所有图表容器都直接设styleheight: 400px这类固定高度一劳永逸。5. 权限隔离销售只能看自己的客户主管看全组权限隔离是CRM和普通信息管理系统的最大区别。一个员工登录进去看不到别人的客户一个主管能看到自己部门全部一个管理员能看到全公司——这套规则直接决定CRM能不能真正被销售团队接纳也直接影响数据安全性。我分开谈后端和前端两层实现。5.1 角色和资源权限的层级设计我用三个角色覆盖典型场景角色权限范围典型操作admin全公司数据所有管理功能、删除客户、分配客户manager本部门数据查看组内客户、分配组内客户、查看组内统计sales本人名下数据录入客户、跟进、修改本人客户资源维度上每个客户、每条跟进记录都是资源。资源的归属通过owner_id和dept_id两个字段确定。后端接口的统一数据过滤逻辑是def filter_by_role(query): if request.current_role admin: return query if request.current_role manager: return query.filter(Customer.dept_id request.current_dept_id) return query.filter(Customer.owner_id request.current_user_id)这个函数在新建客户、查询列表、删除客户、统计接口里都会被调用保证任何入口都无法绕过权限过滤这是安全设计上纵深防御的思路。前端菜单再隐藏也是辅助真正的权限必须由后端斩钉截铁地执行。5.2 客户分配与接管的业务逻辑CRM系统里还有个高频操作是客户分配转移。销售离职、主管接手、新人入职都需要把客户转移给别人。这个接口我只开放给admin和manager角色管理员可以把任意客户转给任意销售主管只能把本部门客户转给本部门销售customer_bp.route(/transfer, methods[POST]) token_required def transfer_customer(): data request.get_json() cid data.get(customer_id) target_uid data.get(target_user_id) customer Customer.query.get(cid) if not customer: return jsonify({code: 404, msg: 客户不存在}) # 管理员可以不限部门主管只能操作本部门 if request.current_role manager: if customer.dept_id ! request.current_dept_id: return jsonify({code: 403, msg: 不能操作其他部门的客户}) target_user User.query.get(target_uid) if not target_user: return jsonify({code: 404, msg: 目标用户不存在}) if request.current_role manager and target_user.dept_id ! request.current_dept_id: return jsonify({code: 403, msg: 目标用户不在本部门}) customer.owner_id target_uid customer.dept_id target_user.dept_id db.session.commit() return jsonify({code: 200, msg: 转移成功})注意转移之后要同步更新dept_id——这是很多人容易漏掉的。owner_id改了但dept_id没改新负责人在本部门查不到这个客户这就是权限隔离的活账问题。5.3 前端菜单与路由守卫联动前端权限最简单有效的做法是路由配置里给每个页面标上允许的角色路由守卫统一校验。// router/index.js const routes [ { path: /login, component: () import(../views/Login.vue) }, { path: /dashboard, component: () import(../views/Dashboard.vue), meta: { roles: [admin, manager, sales] } }, { path: /customers, component: () import(../views/CustomerList.vue), meta: { roles: [admin, manager, sales] } }, { path: /stats, component: () import(../views/Stats.vue), meta: { roles: [admin, manager] } }, { path: /user-manage, component: () import(../views/UserManage.vue), meta: { roles: [admin] } }, ] router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.path ! /login !token) { next(/login) } else if (to.meta.roles to.meta.roles.indexOf(role) -1) { next(/403) } else { next() } })这套方案的好处是非常直观角色一变能进的页面和能看的菜单立即变化配置改动成本接近零。缺点是粒度不够细只能到页面级。如果业务要求某些按钮只有管理员能看到那就再用一个自定义指令v-permission控制按钮级显隐。但就大部分CRM场景来说页面级接口级双重控制已经足够安全可靠。6. 从PyCharm跑通到部署上线的关键节点开发阶段用Flask内置开发服务器没问题但上线部署是另一码事。这节我把从PyCharm配置到Nginx Gunicorn部署的完整链路走一遍顺便讲几个上线后必然会遇到的经典问题。6.1 开发阶段在PyCharm里的配置细节如果你用PyCharm跑Flask项目有几个配置细节直接影响开发效率解释器指向项目专用的virtualenv别用全局Python避免依赖冲突编辑Run Configuration时在Environment variables里把FLASK_ENVdevelopment配好开启热重载数据库连接建议直接用PyCharm的Database面板连接MySQL方便查看表结构和调试SQL我发现一个特别实用的技巧把SQLAlchemy的echoTrue临时打开让所有SQL语句打印到控制台排查ORM关联查询是否产生多余SQL时非常管用。上线前一定要关掉否则日志会被刷爆。6.2 生产部署Gunicorn Nginx Vue静态资源后端用Gunicorn作为WSGI容器运行Flask应用Nginx负责静态文件、反向代理和HTTPS终止。在项目根目录创建一个wsgi.pyfrom app import app if __name__ __main__: app.run()安装Gunicorn后启动gunicorn -w 4 -b 127.0.0.1:5000 wsgi:app这里的-w 4表示4个worker进程一般按服务器CPU核心数的2倍设置。127.0.0.1:5000是关键——Gunicorn只监听本机外部的访问统一走Nginx这样既安全又让Nginx做静态资源缓存和负载均衡。前端构建npm run build构建产物在dist/目录。Nginx配置示例server { listen 80; server_name your-domain.com; client_max_body_size 20m; # Vue静态资源 location / { root /opt/crm/frontend/dist; try_files $uri $uri/ /index.html; # 解决history路由404 } # Flask API反向代理 location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 图片等上传文件 location /uploads/ { alias /opt/crm/backend/uploads/; } }try_files $uri $uri/ /index.html这一行是Vue history模式路由的灵魂配置。如果没有它用户直接访问/customers时会得到404因为Nginx在磁盘上找不到对应的静态文件。有了它所有未知路径都会回退到index.html由Vue Router接管。6.3 上线后必然会遇到的三个经典问题问题一接口报415或跨域。上线后前端和后端在同一个域名下通常不会再有跨域问题。但如果前后端域名不一致必须在Flask端配好flask-cors的白名单不要用*——*等于开放所有外部请求配合登录Token一起用风险很高。线上环境应该写成具体的域名。问题二表格数据量过万后接口变慢。我遇到过最典型的慢SQL是客户列表分页查询里做了排序但没建索引。order_by(Customer.updated_at.desc())如果没在updated_at上建索引每页查询都会触发一次全表排序。解决方法是给高频过滤和排序字段建联合索引比如ALTER TABLE customer ADD INDEX idx_owner_updated (owner_id, updated_at);问题三静态文件404图片上传后显示不出来。这个问题多出在路径拼接上。后端保存上传文件时我建议统一在config.py里配一个UPLOAD_FOLDER绝对路径返回给前端的URL统一用/uploads/xxx.jpg前端直接用服务域名拼接。如果前端和后端不在同一台服务器则必须用对象存储或CDN不存在第二种高效方案。7. 你可能会踩到的三个隐蔽坑与我的优化心得开发这套CRM过程中有几个问题不是从报错信息里能直接看出来的需要靠经验判断。这部分内容偏手感但价值可能比前面的代码还大。7.1 数据权限查询在ORM里的N1陷阱用SQLAlchemy查询客户列表时如果to_dict()内部又触发了contact或owner的懒加载查询那么每条客户都会多执行一次SELECT。100条客户就是101条SQL数据库压力瞬间翻倍。解决方法是查询时就用joinedload把关联对象一次性加载from sqlalchemy.orm import joinedload customers query.options( joinedload(Customer.owner) ).offset(...).limit(...).all()排查N1的方法很简单把SQLAlchemy的echoTrue打开数一下查询条数如果列表接口的SQL条数明显大于页码数量基本就是N1了。这是个很常见的性能陷阱我把这个排查习惯写进了自己的开发checklist。7.2 管理员修改客户归属时的权限设计缺陷系统上线后管理层提了一个需求管理员A把所有离职销售的客户分配给新人。我一开始直接在transfer接口里放开给admin没考虑管理员本身也可能被分配客户。结果管理员不小心把自己名下客户转移给别人后自己反而看不到这个客户了。后来我在transfer接口里加了一条规则如果管理员的操作对象是自己必须二次确认前端弹窗提示。这个场景虽然不常见但权限系统的边界情况往往就在这里暴露设计问题。7.3 JWT过期时间太短导致销售频繁掉线一开始我把Token过期时间设成2小时结果销售用着用着就掉线体验很糟糕。后来调整为12小时并加了一个读操作刷新过期时间的策略——每次有效请求都把过期时间向后顺延30分钟类似滑动窗口。实现也不复杂在响应拦截器里判断exp剩余时间小于30分钟就静默调用刷新接口。这个优化落地后团队基本感觉不到登录过期这回事了除非真的隔了十几天没打开系统。最后分享一个我在这套系统里实践出来的小技巧前后端接口的字段命名一定要用蛇形命名snake_case而不是驼峰命名。虽然不是硬性规定但Python后端用蛇形、JavaScript前端用驼峰会让数据结构在两套体系里反复翻译平白多出映射代码很容易出错。不如统一成一种两边都省事我是直接让后端接口全部返回蛇形字段、前端JS也直接用蛇形变量名联调时改字段名的情况几乎再没出现过。这套Flask Vue的CRM架构整体上是一个小而全的解决方案从技术深度、业务贴合度、扩展空间三个维度看都是适合个人开发者或小团队落地的一套架子。真要往更重了做在这个基础上加WebSocket消息提醒、接企业微信、做销售外呼集成骨架都不用换扩展空间是足够的。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。