ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python与Vue3的智慧物业报修系统开发实战

基于Python与Vue3的智慧物业报修系统开发实战 做智慧物业的项目我第一个想到的场景不是大屏上的智慧驾驶舱而是小区业主群里那条被刷掉的报修消息我家水管漏了谁知道物业电话多少报修这件小事才是物业和业主每天都要面对的切肤之痛。我也是因为接手了一个真实的物业公司需求才开始动手做这套基于Python和Vue3的智慧物业报修服务系统。后端用的Python生态FastAPI SQLAlchemy前端用的Vue3全家桶Vite Pinia Element Plus前后端分离覆盖业主报修、物业派单、工人接单、维修进度、完工评价这条完整链路。这篇文章不是教科书式的项目介绍而是把我从需求梳理、表结构设计、状态机落地到前后端联调、上线部署的完整过程摊开来讲顺便把那些文档里不会告诉你的坑都标出来。适合手里有类似场景、想用Python配Vue3快速落地一个管理系统的开发者。1. 物业报修这件事的水有多深为什么需要一套系统1.1 传统报修的三个典型困境先说需求背景。我接触的这家物业公司管理着三个小区报修渠道有400电话、业主微信群、前台登记三种。听起来渠道丰富实际上每个渠道都是信息孤岛。电话报修靠客服手写工单微信群里的报修信息会被聊天记录冲掉前台登记的纸质单子动不动就找不到了。最崩溃的是月末对账的时候纸质工单、微信截图、电话录音全部得手动汇总有几张单子维修工人明明做了但业主不认账物业也拿不出完整的记录链。另一个痛点是派单全靠人工。客服拿着纸质工单跑到维修班维修班长再翻通讯录打电话遇到修理工在别的楼栋干活可能要到下午才能看到这张单子。业主等着急了就再打电话催客服只能说已经记录了师傅尽快过去中间的过程完全不可控。第三个痛点是没有任何量化数据。哪个工种报修最多哪个楼栋故障最频繁平均响应时间是多少维修满意度怎么样这些全凭印象。物业经理想跟业委会汇报工作拿不出数据只能被业主代表教育。1.2 这个系统能解决什么面向谁、服务谁这套系统解决的就是上面三个问题。业主端能提交报修单、拍照上传、实时查看进度、完工后评价物业端能接单、派工、跟踪状态、看数据看板维修工人端能收到分配的任务、更新维修状态、填写耗材和工时管理员后台负责用户管理、工单统计、满意度分析。整个系统有两个入口业主通过手机浏览器或小程序入口进入物业和维修工人通过Web管理后台操作。技术上都跑在同一个后端服务上只是前端路由和权限控制不同。做这类系统最忌讳一上来就写代码。我拿到需求的第一天先画了三张图报修业务流程图、角色权限矩阵、数据实体关系图。画完之后发现看似简单的报修业务核心其实是一个状态机——报修单从创建到归档经历哪些状态、哪些状态之间可以跳转、每个跳转由谁触发这几个问题不搞清楚后面写接口十有八九要返工。2. 技术选型逻辑Python后端配Vue3前端好在哪2.1 后端为什么选Python从Flask到FastAPI的取舍后端选Python基本没什么争议这个项目规模不大不小正是Python的主场。但在框架上我纠结了一下Django、Flask、FastAPI三选一。先说我为什么不选Django。Django的ORM、Admin、Migration确实强大但它的自动Admin后台和permission体系在这个项目里反而显得重。报修系统的权限逻辑是业主、客服、维修工、管理员四种角色Django自带的User-Group-Permission模型要绕一圈才能适配而且Django项目结构一旦生成很多约定俗成的写法会限制我做接口版的风格。另一个原因是这个系统前端完全独立后端只提供APIDjango的模板系统根本用不上。Flask我用了很多年轻、灵活、生态成熟但做API服务有个问题它没有原生的请求参数校验和API文档生成。报修单有十来个字段每个都要手写校验逻辑校验代码比业务代码还长。Flask-RESTful能补一部分但那个序列化模型写起来还是啰嗦。FastAPI是最终选择。它基于Pydantic请求参数校验直接在模型类里声明类型不对直接返回422错误省掉了一大堆if判断。它自动生成Swagger文档前端联调的时候打开/docs就能看到所有接口的参数和示例省了写接口文档的时间。它还原生支持async后面如果要做WebSocket实时通知FastAPI原生就是亲儿子。我在这个项目里用FastAPI SQLAlchemy 2.0 MySQL没有上Django那种重型全家桶整个后端的依赖管理很干净。2.2 前端为什么选Vue3Composition API与工程化优势前端选Vue3也是综合考虑的结果。项目里需要大量表单交互和动态组件Vue3的Composition API在逻辑复用上比Options API顺手得多。比如报修表单的故障类型联动常见故障描述这个功能在Options API里需要把相关的data、methods、watch散落在三个区块里而在Composition API里可以把这段逻辑完整封装成一个useFaultType.ts哪个组件要用直接调用。Vue3还解决了两个Vue2时代困扰我的问题。第一个是响应式性能Vue3用Proxy替代了Object.defineProperty表格里渲染几百条工单记录也不会卡。第二个是TypeScript友好虽然这个项目我用JavaScript居多但组件Props的类型校验用defineProps配合TS语法在未来要迁移时成本低。如果你跟我一样在Vue2里用惯了options写法看到Vue3的setup函数可能觉得别扭但适应之后最大的感受是逻辑不再需要绕了。2.3 关于Vue2与Vue3差异的快速上手参考很多从Vue2转过来的同事问我Vue3到底改了什么。我一般用三个最直接的差异总结对比项Vue2Vue3数据响应Object.defineProperty监听属性Proxy代理整个对象逻辑复用mixin、$emit、busComposition API、hook函数多根节点不支持支持Fragment多根节点状态管理VuexPiniaAPI更简洁异步渲染不支持Suspense内置Suspense组件实际项目里最大的感知差异在Composition API和Options API的选择上。我的建议是简单组件继续用Options写法没毛病但涉及跨组件逻辑复用、复杂表单联动、定时轮询这类场景直接用Composition API。3. 动手前的规划数据模型与接口边界3.1 核心数据表一张报修单从出生到归档都经历什么数据库设计是这个项目最关键的一步没有之一。我画的实体关系里核心是repair_order这张表围绕它展开的所有外围表都要服务于报修单的完整生命周期。repair_order表的主要字段我列一下你感受一下信息密度字段类型说明idbigint主键order_novarchar(32)报修单号年月日随机数生成owner_idbigint业主用户IDrepair_typeint故障类型1水电/2门禁/3电梯/4公共设施/5其他descriptiontext问题描述addressvarchar(128)具体房号门牌imagesjson上传的图片URL列表statusint状态0待接单、1已接单、2维修中、3待验收、4已完成、5已评价、6已取消priorityint紧急程度1低/2中/3高assignee_idbigint当前维修工人IDcreate_timedatetime创建时间accept_timedatetime接单时间finish_timedatetime完工时间close_timedatetime完成并关闭时间围绕主表还有user表业主和管理员/维修工通过role字段区分、repair_attachment表专门存图片虽然我在主表里预留了images字段实际项目里我改用附件表来管理原因后面说、evaluation表评价评分、评价内容、评价时间、repair_log表状态变更履历这个表非常重要运营上溯源的唯一依据。特别要说的是repair_log这张表。很多同学做系统只关心业务表当前状态忽略了操作审计。结果就是业主打电话说我家报修怎么被取消了的时候你根本查不出来是谁、什么时候、因为什么取消的。加了repair_log每次状态变更插入一条记录任何工单状态变更都能追溯到具体操作人和时间。3.2 接口清单设计先定义契约再写业务在写数据库迁移脚本之前我先把接口清单列出来了。前后端分离开发最怕的是前端等后端、后端等前端。先定好接口契约两边并行开发才能快。接口清单按角色划分公共接口短信验证码登录、微信授权登录、刷新token业主端创建报修单、查看我的报修列表、查看报修详情、取消报修、评价报修客服/管理员端待接单列表、派单给维修工、查看所有报修状态、关闭超时单维修工端我的工单列表、接单/开始维修/完成任务、填写维修反馈统计接口报修数量趋势、维修工工时排行、满意度分布每个接口我都用FastAPI的response_model预先定义了返回结构。统一用{code, message, data}的封装code为0表示成功非0为业务错误码。这个模式虽然简单但前后端沟通成本极低。3.3 数据库设计避坑状态字段和图片附件别按直觉来两个经验教训值得单独说。第一是状态的枚举值。我见过很多项目把状态做成字符串比如pending、accepted看起来直观但数据库存字符串有隐患排序不对、查找效率略低、还有拼写错误风险。用int枚举值在Python代码里定义一个常量类驻留前端通过接口拿到映射关系。后面写到状态机的时候再展开。第二是图片存储。我原方案只想在repair_order表放一个images JSON字段后来发现维修过程可能多次上传照片报修时传一次、维修完成再传一次每次上传还涉及图片压缩、缩略图生成、权限校验单独拆一张repair_attachment表字段id, order_id, uploader_id, url, thumbnail_url, create_time用order_id关联查询也方便也不会让repair_order表字段无限膨胀。MySQL的JSON字段不是不能用但多对多的关系场景下关联表永远比JSON字段好维护。4. Python后端实现报修工单状态机的落地4.1 项目初始化与环境准备环境准备这部分我简单说说那些容易卡壳的细节。Python环境我用的是3.10Windows和Linux都跑过没有版本兼容问题。如果刚接触Python建议直接去python.org下载安装包安装时勾选Add Python to PATH避免后面在命令行里找不到python。创建虚拟环境python -m venv venv venv\Scripts\activate # Windows source venv/bin/activate # Linux/Mac安装依赖pip install fastapi uvicorn[standard] sqlalchemy pymysql python-jose[cryptography] passlib[bcrypt] python-multipart我用的ORM是SQLAlchemy 2.0声明模型的方式和1.x差异挺大建议新项目直接用2.0的Mapped和mapped_column风格写起来更简洁类型提示也更完善。4.2 状态机设计从待接单到已评价的合法流转状态机是整个系统的核心。我把它定义为7个状态0 待接单 - 1 已接单 - 2 维修中 - 3 待验收 - 4 已完成 - 5 已评价 -------------- 0 待接单 - 6 已取消业主主动取消或超时自动取消有很多同学会把状态设计得太开比如已接单直接能跳到已完成中间漏了维修中和待验收。这样的后果是维修工可能接单后根本没干活过了半小时直接点完成业主收到通知说修好了结果回家一看水管还在漏水。状态机的价值就是强制流程完整。在代码里我用一个字典表示合法的状态迁移REPAIR_STATUS_TRANSITIONS { 0: {1, 6}, # 待接单 - 已接单 / 已取消 1: {2, 6}, # 已接单 - 维修中 / 已取消管理员可强制取消 2: {3}, # 维修中 - 待验收 3: {4}, # 待验收 - 已完成业主确认或管理员自动确认 4: {5}, # 已完成 - 已评价 # 已评价和已取消是终态不再有出边 }每次状态更新接口都先校验迁移合法性然后更新状态、插入repair_logdef transition_repair_status(db: Session, order_id: int, target_status: int, operator_id: int): order db.get(RepairOrder, order_id) if order is None: raise BusinessException(报修单不存在) allowed REPAIR_STATUS_TRANSITIONS.get(order.status, set()) if target_status not in allowed: raise BusinessException(f非法的状态迁移: {order.status} - {target_status}) order.status target_status log RepairLog( order_idorder.id, operator_idoperator_id, from_statusorder.status, to_statustarget_status, remark ) db.add(log) db.commit()这个函数是所有状态变更的入口所有的写接口都必须走它不允许直接update status字段。我把它放在独立的service层调用方传参数即可杜绝了业务代码里到处DB.update的混乱局面。4.3 核心接口代码创建报修单与维修派工创建报修单接口是业主端最核心的入口。用FastAPI的Pydantic模型做参数校验class RepairOrderCreate(BaseModel): repair_type: int Field(..., ge0, le5, description故障类型) description: str Field(..., min_length2, max_length500) address: str Field(..., min_length2, max_length128) priority: int Field(1, ge1, le3) images: list[str] []构造函数里生成单号用时间戳加随机数的方式保证并发下不重复def generate_order_no() - str: return datetime.now().strftime(%Y%m%d%H%M%S) str(random.randint(1000, 9999))派单接口的逻辑就需要一点业务判断了。维修工分成水电工、油漆工、安装工等不同工种报修的repair_type要和维修工的skill_type匹配。我维护了一张worker_skill表保养维修工和技能的关系。派单时先找到对应技能的在线维修工再按当前待办单量最少的优先分配算是简单的负载均衡。def auto_assign(db: Session, order_id: int) - int | None: order db.get(RepairOrder, order_id) workers ( db.query(User) .filter(User.role worker, User.skill_type order.repair_type, User.status 1) .all() ) if not workers: return None # 统计每个维修工当前未完成工单数选最少的 best_worker min( workers, keylambda w: db.query(RepairOrder) .filter(RepairOrder.assignee_id w.id, RepairOrder.status.in_([0, 1, 2])) .count() ) return best_worker.id这里有个小细节维修工在忙状态为维修中不代表不能分配新单只是优先级低。如果只分配给空闲维修工高峰期大量报修单会堆积在队列里没人接。分配策略要综合考虑技能匹配、待办数量、是否是紧急单三个因素优先级高的紧急单可以强制插队。4.4 文件上传维修照片怎么处理才不炸磁盘报修单要传图片维修完成要传图片文件上传这种功能看着简单处理不好全是坑。我用python-multipart接收文件保存到本地uploads目录同时生成压缩后的缩略图。图片不能原图直接存手机拍一张照片动辄3-8MB如果报修单传三张1000张工单就是几十GB的磁盘占用。我用的方案原图存一份用PILpillow等比压缩到最长边1280px质量85存一份缩略图原图保留7天缩略图永久保留数据库里只存相对路径不存绝对路径方便后续迁移到OSS存储from PIL import Image def save_upload_image(upload: UploadFile) - str: # 生成唯一文件名 ext Path(upload.filename).suffix.lower() or .jpg filename f{uuid4().hex}{ext} upload_dir Path(uploads) / datetime.now().strftime(%Y%m%d) upload_dir.mkdir(parentsTrue, exist_okTrue) file_path upload_dir / filename with file_path.open(wb) as f: shutil.copyfileobj(upload.file, f) # 生成缩略图 img Image.open(file_path) img.thumbnail((320, 320)) thumb_path upload_dir / fthumb_{filename} img.save(thumb_path, quality80) return f{datetime.now().strftime(%Y%m%d)}/{filename}这里有个隐藏坑Pillow打开图片的时候如果遇到用户传了带EXIF方向的手机照片图片可能会旋转。我花了两个小时才定位到是EXIF信息的问题后来在保存前用ImageOps.exif_transpose处理了一下。5. Vue3前端实现报修页面的组件化与状态管理5.1 工程搭建Vite Pinia Element Plus前端我用Vite搭建工程命令很简单npm create vuelatest在选项里选择Router、PiniaJavaScript版本。UI组件库选了Element Plus它跟Vue3的契合度最好表格、表单、日期选择器、上传组件都齐全少写很多造轮子的代码。项目结构里我把pages按角色分包owner/、worker/、admin/每个包下放对应角色的页面组件公共组件放components/API请求封装在api/目录。Element Plus引入方式我建议用按需导入。全量引入虽然省事但打包体积会大不少。我是通过unplugin-vue-components插件实现自动按需导入的安装配置一次后直接写ElButton就能在编译时自动引入组件和样式省心。5.2 用Composition API组织报修表单报修表单的复杂度不在字段多而在联动。故障类型选水电常见问题下拉框要换成水龙头漏水/马桶堵塞/电路跳闸选电梯问题是电梯运行异响/困人/按键失灵。我写了一个useFaultType hook把这套逻辑封装起来import { ref, computed } from vue const faultOptions { 1: [水龙头漏水, 下水道堵塞, 电路跳闸, 插座损坏], 2: [门禁卡失灵, 门禁系统报警], 3: [电梯运行异响, 电梯按钮失灵], 4: [路灯不亮, 公共设施破损], 5: [其他] } export function useFaultType() { const repairType ref(1) const faultDetail ref() const detailOptions computed(() faultOptions[repairType.value] || []) const onTypeChange () { faultDetail.value } return { repairType, faultDetail, detailOptions, onTypeChange } }页面组件里调用const { repairType, faultDetail, detailOptions, onTypeChange } useFaultType()这个Hook在业主提交报修页和后台手动录单弹窗两处复用维护一份逻辑。Composition API的核心优势在这里就体现出来了如果是Vue2这个联动逻辑得在mounted、watch、methods、data四处写还要考虑多组件复用的话可能要用mixin混入命名冲突风险很大。Composition API一个函数搞定。5.3 工单列表与状态标签的渲染策略工单列表是这个系统曝光量最大的界面物业客服每天盯着它工作。我用Element Plus的el-table渲染状态列通过一个映射函数显示不同颜色的标签const statusMap { 0: { text: 待接单, type: warning }, 1: { text: 已接单, type: primary }, 2: { text: 维修中, type: info }, 3: { text: 待验收, type: danger }, 4: { text: 已完成, type: success }, 5: { text: 已评价, type: success }, 6: { text: 已取消, type: info } }状态标签渲染在el-table里可以直接用template插槽el-table-column label状态 width100 template #default{ row } el-tag :typestatusMap[row.status].type {{ statusMap[row.status].text }} /el-tag /template /el-table-column前端状态枚举必须和后端保持同步。我在前端建了一个constants.js文件后端状态转移时同步更新这个文件避免出现后端返回3待验收前端不知道3是什么状态界面直接显示空白的尴尬。这个文件我会配合后端接口动态拉取详情见后面联调部分。列表数据量大了之后前端要做分页。我用el-pagination组件后端接口用limit和offset参数分页一次只取20条。很多初学者喜欢把全量数据拉下来前端筛选几百条没问题上万条就卡死了。接口分页是硬约束。5.4 工单详情抽屉与评价流程工单详情我用了el-drawer抽屉组件右侧滑出展示报修单的完整信息、图片列表、操作记录时间线、状态流转日志。业主的评价流程是工单状态变为待验收后业主确认无误点击确认完成然后弹出评价弹窗选择满意度1-5星填写评价内容提交后状态变为已评价。评价提交的接口我特意加了校验只有状态为4已完成的工单才能评价如果重复评价后端返回业务错误码。前端弹出提示避免用户重复提交。这个流程的体验细节是评价弹窗底部默认勾选匿名评价因为有些业主担心给差评后被物业区别对待。匿名评价在数据库里单独存一个anonymous字段列表展示时如果是匿名评价人显示为匿名业主。这个需求是物业公司经理提出来的真实业务场景里很常见做系统时不要凭想象设计功能。6. 打通前后端联调中的跨域、鉴权与刷新问题6.1 跨域配置为什么总是Access-Control-Allow-Origin报错前后端分离开发前端跑在5173端口Vite默认后端跑在8000端口第一次联调必遇到跨域问题。浏览器报错信息通常带Access-Control-Allow-Origin但没有这个响应头。FastAPI解决跨域用CORSMiddlewarefrom fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins[http://localhost:5173], allow_credentialsTrue, allow_methods[*], allow_headers[*], )这里有个非常关键的配置错误如果allow_origins写的是[*]同时allow_credentials又设成True浏览器会直接报错因为带cookie的跨域请求不允许通配origin。开发阶段我直接写上前端地址生产环境下是同一个域名的不同路径通过Nginx转发这个问题就不存在了。6.2 登录态与token刷新别把服务器当磁盘登录鉴权我用JWT。用户登录后后端签发一个access_token有效期2小时和一个refresh_token有效期7天。前端拿到token后存在Pinia里并同步到localStorage刷新页面后能恢复登录态。axios拦截器是前端鉴权的核心。我在请求拦截器里加Authorization头在响应拦截器里处理token过期import axios from axios import { useAuthStore } from /stores/auth const request axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000 }) request.interceptors.request.use((config) { const authStore useAuthStore() if (authStore.accessToken) { config.headers.Authorization Bearer ${authStore.accessToken} } return config }) request.interceptors.response.use( (response) response.data, async (error) { const authStore useAuthStore() if (error.response?.status 401) { // 尝试用refreshToken刷新 const ok await authStore.refreshToken() if (ok) { return request(error.config) } authStore.logout() window.location.href /login } return Promise.reject(error) } )需要注意refreshToken的接口不应该和普通接口一样挂在同一个axios实例上否则token过期后refresh接口本身也会被拦截器拦截形成死循环。我把refreshToken接口单独用一个不带拦截器的axios实例调用。后端签发JWT时我用了python-jose库from jose import jwt SECRET_KEY your-secret-key def create_token(user_id: int, role: str, expires_minutes: int) - str: payload { sub: str(user_id), role: role, exp: datetime.now(timezone.utc) timedelta(minutesexpires_minutes) } return jwt.encode(payload, SECRET_KEY, algorithmHS256)实际部署时SECRET_KEY绝不允许写死在后端代码里我用环境变量注入docker-compose里通过env文件管理。6.3 WebSocket推送维修进度如何实时通知业主报修系统里业主最关心的是进度变化。最low的方案是前端定时轮询每5秒调一次详情接口数据能拿到但很浪费服务器资源而且有明显的延迟感。我用FastAPI的WebSocket做了实时推送。维修工点击开始维修后端立刻把状态变更推送给对应业主from fastapi import WebSocket class ConnectionManager: def __init__(self): self.active_connections: dict[int, WebSocket] {} async def connect(self, user_id: int, ws: WebSocket): await ws.accept() self.active_connections[user_id] ws async def send_to_user(self, user_id: int, message: dict): ws self.active_connections.get(user_id) if ws: await ws.send_json(message)后端状态变更时调用await manager.send_to_user(order.owner_id, { type: repair_status_update, order_id: order.id, status: order.status, message: 维修工已接单 })前端在用户登录后建立WebSocket连接接收消息后更新当前页面的工单状态const ws new WebSocket(${import.meta.env.VITE_WS_BASE_URL}/ws/${userId}) ws.onmessage (event) { const data JSON.parse(event.data) if (data.type repair_status_update) { toast(您的报修单${data.message}) refreshOrderDetail(data.order_id) } }WebSocket在生产环境需要处理断线重连。我用了心跳检测机制前端每30秒发一个ping后端回pong如果连续三次没收到pong就重连。这个机制跑了两个月稳定性很好。7. 上线后的复盘性能、安全与运营数据7.1 数据库索引与慢查询上线前最容易被忽略的是数据库索引。报修系统最频繁的查询是业主查自己的报修列表WHERE owner_id ? ORDER BY create_time DESC、维修工查待办列表WHERE assignee_id ? AND status IN (...))、管理员按时间范围统计。这三个查询对应的索引ALTER TABLE repair_order ADD INDEX idx_owner_create (owner_id, create_time); ALTER TABLE repair_order ADD INDEX idx_assignee_status (assignee_id, status); ALTER TABLE repair_order ADD INDEX idx_create_time (create_time);没有索引之前数据量到5万条时查询延迟从几十毫秒飙升到1秒多。加了联合索引后稳定在100毫秒以内。还有个慢查询的隐藏源是状态统计接口。管理员首页要显示待接单数、维修中数、今日完成数、平均响应时间如果每个数字单独count一次这接口要跑5次全表扫描。我改成一个SQL聚合一次取全部数据result ( db.query( func.count().filter(RepairOrder.status 0).label(pending_count), func.count().filter(RepairOrder.status 2).label(repairing_count), func.count().filter(RepairOrder.status 4).label(completed_today_count), ) .select_from(RepairOrder) .one() )7.2 接口安全越权测试、xss与文件上传限制报修系统涉及个人信息和物业管理安全测试我做了三轮。最值得说的是越权漏洞。第一版详情接口我只校验了是否登录没有校验这条工单是不是这个业主的。这意味着任何一个登录用户只要遍历order_id就能查看别人的报修记录和家庭住址。修复方案是在SQL查询层加权限条件def get_order_detail(db: Session, order_id: int, user: User): order db.get(RepairOrder, order_id) if order is None: raise BusinessException(工单不存在) # 业主只能看自己的单 if user.role owner and order.owner_id ! user.id: raise PermissionError(无权查看该工单) # 维修工只能看分配给自己的单 if user.role worker and order.assignee_id ! user.id: raise PermissionError(无权查看该工单) return order这个校验逻辑必须放在后端业务层不能只依赖前端按钮隐藏。因为API是可以被直接调用的绕过前端界面不是难事。文件上传部分我用FastAPI的UploadFile接收文件后第一件事是校验扩展名和大小。扩展名白名单只允许jpg、jpeg、png、webp大小限制在10MB以内。还要检查文件的MIME类型但这里有个坑攻击者可以伪装Content-Type所以可靠的做法是用python-magic库读取文件真实的MIME类型再校验防止伪装成图片的可执行文件传上来。前端Vue3里也要注意XSS问题使用v-text插值而不是v-html渲染用户提交的描述内容。Element Plus的组件默认已经做了转义但如果你在template里用了v-html就要谨慎用户提交的故障描述字段如果包含
返回列表