做了这么多个后台管理系统,我越来越觉得,景区旅游类的管理项目是最适合用来完整走一遍Python + Vue3全栈流程的。业务不复杂,但该有的东西全都有:多角色权限、订单状态流转、图片上传、数据统计、前后端联调,每一步都能踩出实实在在的坑。这篇就围绕"基于Python的旅游景区管理系统 + Vue3前端"这个组合,把从设计到落地的完整思路和关键代码拆开来讲,给正在做毕设、接外包,或者纯粹想练手全栈的朋友一个能直接参考的样板。
1. 先从业务说起:景区管理系统到底管些什么
拿到这类需求,别急着写代码,先想清楚一个问题:这套系统是给谁用的。很多自学项目做成了"管理后台的Demo",增删改查都有,但交付的时候客户一问"我的门票核销怎么处理"就哑火了。旅游景区管理系统的核心,不是管理景区,而是管理"票"和"人的流动"。
1.1 两类使用者,决定了系统的功能边界
我把这类系统的用户分成两类,边界划得很清楚:
- 游客端:查景区介绍、浏览景点列表、购买门票、提交评价。游客不需要登录就能看大部分内容,但下单和评价必须登录。
- 管理端:维护景区和景点信息、配置票种和价格、查看和处理订单、上传景区图片、看销售统计。
这个边界直接影响后面的接口设计。游客端只暴露只读接口和下单接口,管理端接口全部要走后台权限校验,路由也分两套。千万别把管理员功能塞在同一个路由前缀下面,不然后面调权限的时候一定会乱。
1.2 核心业务流:从浏览到核销的全过程
一套景区系统最核心的业务链路是:游客浏览景区 → 选择票种下单 → 支付 → 到景区核销 → 游客评价。这条链路里最关键的是订单状态,我一般用四个状态来跟踪:
| 状态 | 含义 | 触发条件 |
|---|---|---|
| PENDING | 待支付 | 用户提交订单,还没支付 |
| PAID | 已支付 | 支付回调或模拟支付成功 |
| USED | 已使用(已核销) | 游客到现场,管理员扫描或手动核销 |
| REFUNDED | 已退款 | 用户申请退款或管理员操作 |
有人会问,为什么要单独一个"已使用"状态?因为很多景区的门票不是买完立刻用,而是可以指定日期游玩,甚至几天内有效。没有这个状态,管理员根本不知道哪些订单已经进场核销过了,对账的时候只能靠猜。
订单之外,景区系统还有一条内容维护线:景区基本信息(名称、简介、等级、开放时间)、景点列表(每个景点属于哪个景区、有什么介绍、配什么图)、票种(普通成人票、儿童票、套票、联票),这几块是纯后台维护、前台展示的内容型业务。做管理后台的时候,大部分时间其实都在处理这部分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:Python后端和Vue3前端是怎么配合的
技术选型这个环节,很多新手会纠结很久,我直接给结论:后端用Flask,前端用Vite + Vue3 + Element Plus + Pinia。这套组合在景区管理系统这个体量下,开发效率最高,也最容易招到能接手的人。
2.1 后端框架:Flask是常规选择,FastAPI要看情况
Python后端常被提到的就是Flask和FastAPI。我做过对比,两者在这个项目里差得不算太大,但选Flask的理由很实在:
| 对比项 | Flask | FastAPI |
|---|---|---|
| 上手门槛 | 低,文档和教程数量碾压级 | 稍高,需要理解Pydantic和异步 |
| 异步支持 | 3.x后原生支持,生态逐步跟上 | 天生异步,高并发优势明显 |
| API文档 | 需要装flask-restx或手写 | 自动生成Swagger文档 |
| 适合场景 | 中小型管理系统、传统Web项目 | 高并发API服务、微服务 |
| 周边生态 | 老牌,SQLAlchemy/Flask-CORS全都有 | 新,但SQLModel集成也很顺 |
景区管理系统属于典型的"并发不高但业务字段多、管理逻辑复杂"的项目,Flask + SQLAlchemy组合非常成熟,踩坑记录到处都是,真遇到问题一搜就有答案。FastAPI的优势主要体现在接口性能上,但这个系统最吃性能的是前端页面和大图片加载,不是后端接口本身。
如果以后想扩展成高并发服务,或者希望自动生成交互式API文档给前端同事看,再用FastAPI重写也不迟。对于现在这个需求,Flask省心。
2.2 前端工程:Vite + Vue3 + Element Plus + Pinia 的基本盘
前端选择了Vue3,这点很明确。Vue3的两个核心变化——Composition API和基于Proxy的响应式系统,对后台管理系统来说都是实打实的提升:逻辑复用不再靠Mixin,一个订单列表的查询、分页、防重复提交可以很自然地组织在一起。
配套选型:
- 构建工具:Vite。比Webpack快很多,尤其项目中期依赖变多之后,热更新依然能保持秒级。
- UI组件库:Element Plus。Vue3时代的后台管理系统,90%都会选它。表格、表单、弹窗、上传组件齐活,省掉自己写组件的时间。
- 状态管理:Pinia。比Vuex简单,模块化得很舒服,存用户信息、token、权限标记都方便。
- 路由:Vue Router 4,配合路由守卫做登录拦截。
- HTTP:axios,封装请求和响应拦截器。
这套组合就像一个标准工装,不是最惊艳的,但合身、耐穿、坏了好补。
2.3 环境配置里的三个经典坑
这个项目做下来,环境配置阶段最容易翻车的是三件事:
第一,Python官方库版本兼容。 比如安装numpy、sklearn这类库时,Python版本太新会找不到对应wheel包。景区管理系统后端用不到这些,但如果你要在系统里跑点数据分析(比如客流量预测),建一个虚拟环境并且固定Python版本很有必要。我用的是Python 3.10 + requirements.txt锁版本,避免"在我电脑上是好的"这种尴尬。
第二,Node.js版本和依赖安装。 Vue3 + Vite对Node版本有要求,太老的Node跑不起来。装依赖的时候如果遇到node-sass相关报错,说明在用老掉牙的依赖,换成sass或dart-sass就好。启动项目时保持npm和pnpm一致,别混用,不然后面lock文件冲突能折腾半天。
第三,VSCode的环境选择。 如果你在VSCode里开发Python后端,务必要先Ctrl+Shift+P选择Python解释器,指向虚拟环境里的那个。不然Flask模块明明装好了,F5调试还是报ModuleNotFoundError。我见过太多人卡在这一步,其实不是代码问题,是解释器没选对。
3. 数据库建模:景区订单系统怎么建表才不后悔
数据库设计是这个项目里最值得多花时间的地方。表建好了,后面接口和前端都会很顺;表设计不合理,写到订单统计的时候就会想骂人。我按"基础信息 → 交易核心 → 用户与内容"三个层次拆开讲。
3.1 景区、景点、票种:三张基础表各司其职
第一层是内容型基础表:景区表(scenic_area)和景点表(attraction)。
景区表的字段大概是这样:
python复制class ScenicArea(db.Model):
__tablename__ = 'scenic_area'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(100), nullable=False, comment='景区名称')
description = db.Column(db.Text, comment='景区简介')
level = db.Column(db.String(20), comment='A级等级,如5A、4A')
open_time = db.Column(db.String(30), comment='开放时间')
address = db.Column(db.String(200), comment='地址')
cover_image = db.Column(db.String(255), comment='封面图URL')
created_at = db.Column(db.DateTime, default=datetime.now)
景点表需要外键指向景区:scenic_id = db.Column(db.Integer, db.ForeignKey('scenic_area.id'))。一个景区下有多个景点,一对多关系。
这里有个容易忽略的设计点:景点可以有门票吗? 很多景区是"大门票+内部小景点单独收费"的模式。如果一开始就把门票绑在景区下,后面接这种需求得改表。我建议单独建票种表(ticket_type),用scenic_id关联景区,同时留一个可空的attraction_id,既能支持单纯的景区门票,也能支持景点单独售票,两不耽误。
票种表的字段要注意价格类型。价格字段绝对不能用Float,要用Decimal,不然计算金额时出现0.1 + 0.2 != 0.3这种问题,财务对不上账,锅全在你身上:
python复制class TicketType(db.Model):
__tablename__ = 'ticket_type'
id = db.Column(db.Integer, primary_key=True)
scenic_id = db.Column(db.Integer, db.ForeignKey('scenic_area.id'))
name = db.Column(db.String(50), comment='票种名称,如成人票/儿童票')
price = db.Column(db.Numeric(10, 2), comment='价格,保留两位小数')
stock = db.Column(db.Integer, default=0, comment='库存')
valid_days = db.Column(db.Integer, default=1, comment='有效期天数')
3.2 订单与订单明细:千万不要把所有信息堆在一张表里
订单设计是重头戏。很多新手会建一张order表,把票种名称、价格、数量、总价全塞进去。这样做短期内很方便,查订单一条SQL就出来了。但问题是:票种价格调整了,历史订单怎么办?订单表里存的"价格"变成了业务上说不清楚的数据——它到底是下单时的价格还是当前价格?
正确的做法是订单主表和订单明细表分离:
order_info:订单号、用户ID、总金额、状态、支付时间、核销时间。order_item:订单ID、票种ID、票种名称快照、单价快照、数量、小计。
关键就在这个"快照":下单那一刻,把票种名称和单价拷贝一份到明细表里。之后票种表随便改价,历史订单永远不受影响,报表统计也准确。这就是电商系统常用的"订单快照"思路。
订单号生成也有讲究,不要用自增ID直接当订单号给用户看。我用的是时间戳 + 随机数方案,比如data:YYYYMMDDHHMMSS + 6位随机数,既好看又能避免撞单。
订单状态流转我在1.2节说过了,代码里可以通过一个状态字段控制。后端每次更新状态时做一个合法状态校验,比如PAID不能跳回PENDING,REFUNDED不能再变成USED。这个校验逻辑虽然简单,但能挡住不少前端传参捣乱的异常情况。
3.3 用户、评价、管理员:权限与关联设计
用户表(user)存游客,字段包括手机号、密码哈希、昵称、头像。密码一定不要存明文,用werkzeug.security.generate_password_hash生成哈希,校验时用check_password_hash。这是基本功,但不妨碍每次都有人踩坑。
评价表(review)关联用户、景区和订单。这里有个关键点:评价要不要关联订单? 我建议要,而且检查逻辑是"该订单已使用才能评价"。不然就会出现系统里一堆没去过景区的"云游客"在刷好评,数据完全不可信。
管理员表(admin_user)可以单独建,也可以复用user表加role字段。我推荐单独建,因为管理员和游客的字段差异很大(游客有手机号但管理员不需要),硬凑一张表会导致很多字段空着,看着难受。
外键和索引的设计有一个容易被忽视的细节:订单表里对user_id和status建复合索引。后台管理页面最常见的一个操作就是"查某个用户的所有订单"和"按状态筛订单",没有索引的时候,订单量一上万,接口响应速度肉眼可见地变慢。
python复制class OrderInfo(db.Model):
__tablename__ = 'order_info'
id = db.Column(db.Integer, primary_key=True)
order_no = db.Column(db.String(32), unique=True, index=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True)
status = db.Column(db.String(20), default='PENDING', index=True)
total_amount = db.Column(db.Numeric(10, 2))
paid_at = db.Column(db.DateTime)
used_at = db.Column(db.DateTime)
created_at = db.Column(db.DateTime, default=datetime.now)
__table_args__ = (
db.Index('idx_user_status', 'user_id', 'status'),
)
4. 后端API实现:认证、权限与核心接口
数据库设计好了,后端接口就变成了"翻译"工作:把业务操作翻译成数据库操作。但翻译也有翻译的章法,尤其认证权限和查询分页这两个环节,直接就决定了项目能不能上线。
4.1 JWT认证:前后端分离项目的通行证
景区管理系统是前后端分离的,后端不可能靠Session记会话,因为浏览器和API服务不在同一个域下,Cookie处理麻烦。我用JWT(JSON Web Token)做认证。
流程很简单:
- 用户登录,后端校验账号密码,签发JWT返回给前端。
- 前端把token存在localStorage里(管理端)或Pinia里。
- 后续每个请求在Header里带上
Authorization: Bearer <token>。 - 后端提供一个装饰器,校验token并解析出用户ID。
Flask这边用flask_jwt_extended这个扩展最省事:
python复制from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
@app.route('/api/admin/login', methods=['POST'])
def admin_login():
data = request.get_json()
admin = AdminUser.query.filter_by(username=data.get('username')).first()
if not admin or not check_password_hash(admin.password_hash, data.get('password')):
return jsonify(code=400, message='用户名或密码错误')
token = create_access_token(identity=str(admin.id))
return jsonify(code=200, data={'token': token, 'name': admin.username})
@app.route('/api/admin/orders', methods=['GET'])
@jwt_required()
def admin_orders():
user_id = get_jwt_identity()
# 管理员操作逻辑
有个坑要说一下:JWT的identity参数建议传字符串。如果你传整数,后面从token里解析出来的是字符串,跟数据库ID比较的时候会莫名对不上,排查起来很恼火。提前转成字符串,后面少掉一根头发。
4.2 接口设计原则:路径清晰、状态码统一
API路径设计遵循一个简单原则:/api/开头,admin和client分开。
| 模块 | 接口路径 | 方法 | 说明 |
|---|---|---|---|
| 景区列表 | /api/client/scenic/list | GET | 游客端查景区,支持分页 |
| 景区详情 | /api/client/scenic/<id> | GET | 包括景点和票种 |
| 创建订单 | /api/client/order | POST | 游客下单,需登录 |
| 订单列表 | /api/client/order/list | GET | 游客查自己的订单 |
| 订单管理 | /api/admin/orders | GET | 管理员查所有订单 |
| 核销订单 | /api/admin/order/use | POST | 管理员操作核销 |
| 票种维护 | /api/admin/ticket-types | POST/GET/PUT/DELETE | 后台维护票种 |
统一响应格式也很重要。我习惯用{code: 200, message: 'success', data: {...}}包装所有接口。前端axios拦截器拿到响应后先看code,不等于200就直接弹错误提示,不用每个页面重复写错误分支。
有朋友问过,为什么不直接用HTTP状态码当业务状态码?因为业务错误太多样了——"库存不足""未登录""订单状态不对""参数缺失",如果用HTTP状态码映射,前端写判断会很绕。统一用业务状态码,前端只认两层:HTTP 200代表服务通了,业务code代表这次操作成没成。
4.3 查询接口的分页、搜索和排序
后台管理系统的接口,八成都是列表查询。列表查询看起来简单,但最容易写出性能垃圾的代码。核心就是:能数据库过滤的就别拿到Python里过滤。
比如订单管理页要支持"按状态筛选 + 按游客手机号搜索 + 按时间范围查询",用SQLAlchemy链式查询:
python复制@app.route('/api/admin/orders', methods=['GET'])
@jwt_required()
def list_orders():
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 10, type=int)
status = request.args.get('status', '', type=str)
keyword = request.args.get('keyword', '', type=str)
start_date = request.args.get('start_date', '', type=str)
end_date = request.args.get('end_date', '', type=str)
query = OrderInfo.query
if status:
query = query.filter(OrderInfo.status == status)
if keyword:
query = query.join(User, OrderInfo.user_id == User.id).filter(
db.or_(User.mobile.contains(keyword), OrderInfo.order_no.contains(keyword))
)
if start_date:
query = query.filter(OrderInfo.created_at >= start_date)
if end_date:
query = query.filter(OrderInfo.created_at <= end_date)
pagination = query.order_by(OrderInfo.created_at.desc()).paginate(
page=page, per_page=page_size, error_out=False
)
items = [format_order_full(o) for o in pagination.items]
return jsonify(code=200, data={
'list': items,
'total': pagination.total,
'page': page,
'page_size': page_size
})
分页参数用request.args.get直接带type=int做类型转换,前端传"abc"进来时自动变成1,不会把整个接口搞崩。这是Flask的隐形福利,很多教程不讲。
排序默认用created_at.desc(),新订单永远在最上面。后台管理员高频操作就是看新订单有没有进来,别默认按ID升序,那一页翻下来眼都花了。
5. Vue3前端实践:后台管理页面的真实写法
前端这边,我会重点讲两个大多数人写Vue3时会纠结的点:Composition API怎么组织代码,以及请求和路由守卫怎么设计。这两个点想通了,Element Plus的表格表单反而都是体力活。
5.1 Composition API:ref和reactive的取舍
Vue3发布之后,Composition API和Options API的选择一直有人问。我的态度很明确:新项目一律用<script setup> + Composition API,理由只有一条——同一个功能的代码会待在一起,而不是被拆散到data、methods、computed几个选项里。
比如订单列表页,在Options API里,你需要去data里找query对象、去methods里找loadData函数、去computed里找pagination计算属性,关系全靠心里默记。Composition API直接写到一起:
vue复制<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getAdminOrders, useOrder } from '@/api/order'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const queryForm = reactive({
page: 1,
page_size: 10,
status: '',
keyword: ''
})
async function loadData() {
loading.value = true
try {
const res = await getAdminOrders(queryForm)
tableData.value = res.data.list
total.value = res.data.total
} finally {
loading.value = false
}
}
function handleSearch() {
queryForm.page = 1
loadData()
}
onMounted(loadData)
</script>
这里有一个ref和reactive的选择心得:普通变量用ref,对象类型用reactive。很多人被"ref万能对象"的说法迷惑(热词里都有),结果数据全用ref,模板里还好,但代码里到处都是.value,看着累。我的约定是:
- 基本类型(字符串、数字、布尔)→
ref - 对象、数组(如表单对象、查询参数)→
reactive - 从接口拉回来的列表数据 →
ref(因为后面要整体重新赋值)
这个约定不绝对,但能让代码风格统一,团队协作时互相读代码的负担小很多。
5.2 axios封装与请求拦截:统一处理token和错误
axios不封装就裸用在页面里,每个页面都要重复写token注入和401跳转,代码能臭到没法闻。我封装一个request.js:
javascript复制import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
import { useUserStore } from '@/store/user'
const service = axios.create({
baseURL: '/api',
timeout: 10000
})
service.interceptors.request.use(config => {
const userStore = useUserStore()
if (userStore.token) {
config.headers['Authorization'] = 'Bearer ' + userStore.token
}
return config
})
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.message || '请求出错')
return Promise.reject(new Error(res.message))
}
return res
},
error => {
if (error.response && error.response.status === 401) {
const userStore = useUserStore()
userStore.clearToken()
router.push('/login')
}
ElMessage.error(error.message || '网络异常')
return Promise.reject(error)
}
)
export default service
这个封装解决了三个最痛的问题:token统一注入、业务状态码统一处理、登录过期统一跳转。后面的业务页面只需要写const res = await getAdminOrders(params),拿到的直接就是res.data.list,不需要每个页面再剥一次壳。
路由守卫长这样:
javascript复制router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.path !== '/login' && !userStore.token) {
next('/login')
} else {
next()
}
})
5.3 管理后台页面落地:布局、表格、表单、上传
后台管理的布局我直接复用Element Plus的el-container搭建:左侧菜单(el-menu),右侧是内容区(el-main)。菜单项加上景区管理、景点管理、票种管理、订单管理、评价管理、统计看板几个入口。
一个典型的管理页面,比如景区列表页,由三块拼起来:搜索条件区(el-form inline)、操作按钮区(新增/编辑/删除)、数据表格区(el-table)。如果你们项目要用Vue3 + TypeScript(像若依vue3 ts那样),表格和接口的类型定义会很啰嗦,但换来的是改动字段时有IDE提示。对于景区管理系统,我用JavaScript已经足够快,纯看个人取舍。
图片上传是景区系统的刚需。项目里用el-upload组件的自定义上传方式,把文件POST给后端的/api/admin/upload接口,拿到返回的URL存入表单:
vue复制<el-upload
:action="uploadUrl"
:headers="uploadHeaders"
:show-file-list="false"
name="file"
:on-success="handleUploadSuccess"
>
<img v-if="form.cover_image" :src="form.cover_image" class="cover-preview" />
<el-button v-else>上传封面图</el-button>
</el-upload>
这里有个很重要的点:组件的action属性会直接发请求,绕过了axios拦截器,所以上传时要单独传headers把token带上,不然后端会返回401。我在第一次做这个项目时就在这里愣了半天,打死想不通为什么上传接口报未登录,最后还是看network请求才发现请求头里根本没有Authorization。
6. 前后端联调与上线部署:最容易翻车的几个环节
前后端都写完了,联调阶段才是真正考验项目的地方。这个阶段的问题不是"功能有没有",而是"能不能在真实环境里跑起来"。我把踩过的坑集中说一下。
6.1 跨域问题:CORS和Nginx两条路
前后端分离项目第一个联调问题是跨域。开发环境我在Flask这边直接启用Flask-CORS:
python复制from flask_cors import CORS
CORS(app, resources={r"/api/*": {"origins": "*"}})
注意这里origins="*"只应该出现在开发环境。生产环境如果也用*,等于允许任何网站调用你的接口,别人写个恶意页面就能往你库里灌数据,风险很大。
生产环境我推荐用Nginx反向代理解决跨域,这比CORS更优雅,因为前端和后端在同一个域下,根本没有跨域问题。具体配置写在后面。
6.2 图片上传与静态资源路径:一套路径走到底
后端上传接口接收图片后,保存位置和访问URL要提前设计好。我的做法是:
python复制@app.route('/api/admin/upload', methods=['POST'])
@jwt_required()
def upload_image():
file = request.files.get('file')
if not file:
return jsonify(code=400, message='未接收到文件')
ext = os.path.splitext(file.filename)[1]
if ext.lower() not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
return jsonify(code=400, message='不支持的图片格式')
filename = datetime.now().strftime('%Y%m%d%H%M%S') + '_' + str(uuid.uuid4().hex[:8]) + ext
save_dir = os.path.join(app.config['UPLOAD_FOLDER'], datetime.now().strftime('%Y%m%d'))
os.makedirs(save_dir, exist_ok=True)
file.save(os.path.join(save_dir, filename))
url = f"/uploads/{datetime.now().strftime('%Y%m%d')}/{filename}"
return jsonify(code=200, data={'url': url})
文件名用时间戳加短uuid,避免不同用户上传了相同文件名互相覆盖,也避免直接用原始文件名造成的路径安全问题。按日期分目录存储,后面按月份清理旧图或者备份都方便。
开发环境里,我给Flask配了静态目录:
python复制app = Flask(__name__, static_folder='uploads', static_url_path='/uploads')
这样/uploads/xxx就能直接访问到图片。生产环境则用Nginx直接托管这个uploads目录,不让Flask处理文件IO,性能好很多。
6.3 上线部署:gunicorn + Nginx 的标准动作
部署方案我推荐这样一层层叠上去:
- 前端执行
npm run build,产物在dist目录,交给Nginx托管。 - 后端用gunicorn跑Flask,监听127.0.0.1:8000。
- Nginx配置:
/走前端静态文件,/api和/uploads反向代理到后端。
Nginx的关键配置片段:
nginx复制server {
listen 80;
server_name your-domain.com;
root /var/www/scenic/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /uploads/ {
alias /var/www/scenic/uploads/;
}
}
try_files $uri $uri/ /index.html这行是Vue Router的history模式必须的。不写这行,刷新页面的时候Nginx会去找/orders这个文件,找不到就404。很多前端部署的"白屏"问题,根子都在这里。
后端启动命令:
bash复制gunicorn -w 4 -b 127.0.0.1:8000 app:app
-w 4的意思是四个worker进程。景区管理系统这个体量,4个worker完全够用。想要更高并发,就在Nginx层加缓存,或者把静态资源交给CDN,而不是盲目加worker数量。
部署完成后,还有一道自查动作:用浏览器无痕窗口开一下系统,把游客下单、管理员核销这条主链路走一遍。联调阶段最容易漏的问题往往不在新功能,而在"我这个页面刷新一下token丢了没"、"上传的图片换个环境还能不能显示"这种看似小事的地方。我自己的习惯是,部署完成后的第一件事,就是打开管理端把所有涉及图片的页面都翻一遍,确认图片URL没有因为基础路径变化而全部打不开。
最后再说一个我个人的经验:做这类管理系统,前端代码量会比后端大不少,但真正的业务难点全在后端的数据建模和状态流转上。把订单状态、库存数量、价格快照这几个概念想清楚,前端就算写得粗糙一点,系统照样能用;反过来前端再花哨,数据库设计一塌糊涂,后面报表和数据维护能让人崩溃。所以,无论你怎么分配精力,请一定先把后端的地基打好,再谈界面美化。
