旅游小程序源码四端权限与线路发布全链路落地指南
简介:这是一套基于Uniapp与ThinkPHP开发的旅游系统源码,面向旅游平台运营商、旅游机构及开发者,解决线路发布、在线订票、票务核销与分销推广等业务需求。系统覆盖消费者端与机构工作人员手机端、机构端PC、平台管理端PC四个端口,机构可发布旅游线路与景点项目,用户在线购票支持成人价与儿童价,工作人员可现场扫码核销,并内置二级分销功能。资源包共216个文件,以68个vue组件、21个js脚本、13个scss样式、18个json配置为主,另含png图片、md说明文档及后台与数据库压缩包,整体约20.84MB,前端基于Uniapp可编译到多端,后端由ThinkPHP支撑。已有230人学习。通过搭建教程与前后端源码,读者可快速理解系统架构、完成安装配置,并参考现有模块进行二次开发或部署上线。
1. 旅游小程序源码落地:四端角色怎么分、机构发布线路这条链路怎么跑通
拿到一套「旅游小程序源码」,真正决定它能不能上线的,不是首页好不好看,而是四端角色有没有分清楚。消费者端(手机端)负责浏览线路、下单、看订单;机构工作人员(手机端)负责核销、带团、现场改状态;机构端(PC)负责发布旅游线路、维护景点项目、管价格库存;平台管理端(PC)负责审核机构、抽佣、看全局数据。标题里说的「机构可以发布旅游线路、景点项目」,本质是机构端 PC 写数据、消费者端小程序读数据、平台端管数据这条闭环。
这套东西适合谁?一是想快速搭一套区域旅游平台的小团队,二是手里有旅行社资源、想把线下线路搬到线上的运营方,三是接私活要做多角色 SaaS 的开发者。它不适合只想做个单店展示页的人——四端权限体系对单店是负担。下面按「先立骨架、再通链路、最后避坑」的顺序拆,每一步都给到能抄的参数和命令。
2. 四端权限模型与数据库表设计:先把角色和线路表定死
2.1 为什么四端不能共用一套用户表
很多翻车案例的起点,是把消费者、机构员工、机构管理员、平台管理员塞进一张user表,靠role字段区分。短期能跑,一旦机构要自己加员工、平台要封禁某个机构,权限判断就会变成一堆if role in (...)的玄学代码。常见做法是拆成四张主体表加一张统一账号表:
| 表名 | 归属端 | 关键字段 | 说明 |
|---|---|---|---|
sys_account | 全端 | id, phone, password, type | type: 1消费者 2机构员工 3机构管理员 4平台管理员 |
consumer | 消费者端 | account_id, nickname, avatar | 小程序登录主体 |
org_staff | 机构工作人员端 | account_id, org_id, post | 带团、核销的人 |
org | 机构端 PC | id, name, audit_status, commission_rate | 机构主体,含审核状态 |
platform_admin | 平台管理端 | account_id, level | 平台运营 |
这样设计的好处是:机构员工离职,只动org_staff,不影响账号;平台封机构,改org.audit_status即可,消费者端查询时统一过滤。type字段只用于登录路由,不用于业务权限判断,业务权限一律查具体主体表。
2.2 线路与景点项目的表结构
标题里「发布旅游线路、景点项目」对应两张核心业务表。线路是售卖单元,景点项目是线路的组成节点,两者是一对多。
-- 旅游线路主表 CREATE TABLE `tour_route` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `org_id` BIGINT NOT NULL COMMENT '所属机构', `title` VARCHAR(120) NOT NULL COMMENT '线路名称', `days` TINYINT NOT NULL DEFAULT 1 COMMENT '行程天数', `price` DECIMAL(10,2) NOT NULL COMMENT '成人价', `child_price` DECIMAL(10,2) DEFAULT 0 COMMENT '儿童价', `stock` INT NOT NULL DEFAULT 0 COMMENT '库存', `audit_status` TINYINT DEFAULT 0 COMMENT '0待审 1通过 2驳回', `status` TINYINT DEFAULT 1 COMMENT '1上架 0下架', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, KEY `idx_org` (`org_id`), KEY `idx_audit` (`audit_status`, `status`) ) COMMENT='旅游线路'; -- 景点项目表(线路节点) CREATE TABLE `route_spot` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `route_id` BIGINT NOT NULL, `day_index` TINYINT NOT NULL COMMENT '第几天', `spot_name` VARCHAR(120) NOT NULL, `stay_minutes` INT DEFAULT 60 COMMENT '停留分钟', `ticket_included` TINYINT DEFAULT 1 COMMENT '是否含门票', `sort` INT DEFAULT 0, KEY `idx_route` (`route_id`, `day_index`) ) COMMENT='线路景点项目';参数说明:audit_status和status必须分开,审核通过不等于上架,机构可以主动下架;stock放在线路主表而不是景点表,因为售卖粒度是整条线路;day_index用数字而不是日期,方便机构复制线路时改天数。索引idx_audit是给消费者端列表页用的,查询条件固定是「审核通过且上架」,这个联合索引能避免全表扫描。
2.3 机构发布线路的最小接口
机构端 PC 发布线路,本质是一次「主表 + 子表」的事务写入。下面用 Python 伪代码示意,重点看事务边界和审核状态初始化。
def publish_route(org_id, payload): # payload 含 route 基础信息和 spots 列表 with db.transaction(): route_id = db.insert("tour_route", { "org_id": org_id, "title": payload["title"], "days": payload["days"], "price": payload["price"], "stock": payload["stock"], "audit_status": 0, # 新发布一律待审 "status": 0 # 审核通过前不允许上架 }) for idx, spot in enumerate(payload["spots"]): db.insert("route_spot", { "route_id": route_id, "day_index": spot["day_index"], "spot_name": spot["spot_name"], "stay_minutes": spot.get("stay_minutes", 60), "sort": idx }) return route_id逻辑说明:audit_status=0和status=0是硬编码的,不接受前端传值,否则机构可以绕过审核直接上架。spots的sort用列表顺序生成,避免前端传重复值。事务保证线路和景点要么都写成功,要么都回滚,防止出现「有线路没景点」的脏数据。参数上stay_minutes给默认值 60,是因为机构录入时经常漏填,给个合理默认比报错体验好。
3. 消费者端小程序与机构工作人员端:线路展示、下单、核销怎么串
3.1 消费者端线路列表的查询与缓存
消费者端首页要展示线路列表,查询条件固定是「审核通过 + 上架 + 有库存」,按创建时间倒序。这个查询在高峰期会非常频繁,直接打数据库容易顶不住。
SELECT id, title, days, price, stock FROM tour_route WHERE audit_status = 1 AND status = 1 AND stock > 0 ORDER BY created_at DESC LIMIT 20;常见做法是在这层加 Redis 缓存,key 用route:list:{page},过期时间设 60 秒。为什么是 60 秒而不是更长?因为机构改价、下架后,消费者看到旧数据会引发投诉,60 秒是体验和数据一致性的折中。缓存失效策略用「主动删 + 过期兜底」:机构端修改线路时,直接删掉route:list:*相关 key,下次查询自然回源。
提示:库存
stock不要放进缓存做扣减判断,缓存只用于展示。真实扣库存必须走数据库行锁或 Redis 原子操作,否则超卖是迟早的事。
3.2 下单与库存扣减的原子操作
下单是消费者端最核心也最容易出事的环节。库存扣减必须原子,常见做法是用 SQL 的条件更新:
UPDATE tour_route SET stock = stock - 1 WHERE id = ? AND stock >= 1 AND status = 1 AND audit_status = 1;执行后判断影响行数,如果为 0,说明库存不足或线路已下架,直接返回失败。这个写法比「先查再扣」安全,因为WHERE stock >= 1和SET stock = stock - 1在同一条语句里,数据库保证原子性。参数上id是线路 ID,?占位符防注入。下单成功后写入订单表,订单状态初始为「待支付」,并设置 15 分钟超时未支付自动取消,取消时把库存加回去。
3.3 机构工作人员端的核销流程
机构工作人员手机端的核心动作是核销。消费者到店或上车时,工作人员扫码或输入订单号,系统校验订单状态并标记已核销。
def verify_order(staff_id, order_no): staff = db.query_one("SELECT org_id FROM org_staff WHERE account_id=%s", staff_id) order = db.query_one( "SELECT id, org_id, status FROM tour_order WHERE order_no=%s", order_no) if not order: return {"code": 404, "msg": "订单不存在"} if order["org_id"] != staff["org_id"]: return {"code": 403, "msg": "无权核销他机构订单"} if order["status"] != "paid": return {"code": 400, "msg": "订单状态不可核销"} db.update("tour_order", {"status": "verified", "verify_at": now()}, where={"id": order["id"]}) return {"code": 0, "msg": "核销成功"}逻辑说明:第一步查员工所属机构,第二步查订单,第三步做机构归属校验——这是防止 A 机构员工核销 B 机构订单的关键。status != "paid"拦截了未支付和已核销的重复操作。参数上order_no用业务单号而不是主键,避免暴露自增 ID。核销时间verify_at必须记录,后续对账和纠纷处理都靠它。
4. 机构端 PC 与平台管理端:审核、抽佣、数据看板怎么落地
4.1 平台审核机构与线路的两级审核
平台管理端 PC 要做两级审核:先审机构资质,再审机构发布的线路。机构注册后org.audit_status=0,平台审核通过置 1,机构才能登录机构端发布线路。线路发布后tour_route.audit_status=0,平台审核通过置 1,消费者端才能看到。
-- 平台审核线路 UPDATE tour_route SET audit_status = 1, status = 1 WHERE id = ? AND audit_status = 0;注意这里status也一起置 1,是因为审核通过后默认上架,机构可以后续手动下架。WHERE audit_status = 0防止重复审核。驳回时置audit_status=2,并要求填写驳回原因,存到单独的审核日志表,方便机构查看和申诉。
4.2 抽佣计算与结算单生成
平台靠抽佣盈利,org.commission_rate存机构费率,比如 0.1 表示 10%。订单核销后,按订单金额计算平台佣金和机构应结金额。
def settle_order(order_id): order = db.query_one("SELECT * FROM tour_order WHERE id=%s", order_id) org = db.query_one("SELECT commission_rate FROM org WHERE id=%s", order["org_id"]) commission = round(order["amount"] * org["commission_rate"], 2) settle_amount = round(order["amount"] - commission, 2) db.insert("settle_record", { "order_id": order_id, "org_id": order["org_id"], "amount": order["amount"], "commission": commission, "settle_amount": settle_amount, "settle_status": 0 # 0待结算 1已结算 })参数说明:round(..., 2)保留两位小数,避免浮点误差累积;commission_rate从机构表实时读取,不写死在代码里,方便平台调费率;settle_status区分待结算和已结算,平台财务按周期批量结算。这里没有直接改订单表,而是生成独立的结算记录,保证订单和结算解耦,对账时以结算记录为准。
4.3 平台数据看板的关键指标
平台管理端首页看板通常看四个数:今日订单数、今日 GMV、待审线路数、活跃机构数。这些指标不要实时算,用定时任务每 5 分钟聚合一次写入统计表。
INSERT INTO stat_daily (stat_date, order_cnt, gmv, pending_audit, active_org) SELECT CURDATE(), COUNT(*), SUM(amount), (SELECT COUNT(*) FROM tour_route WHERE audit_status=0), (SELECT COUNT(DISTINCT org_id) FROM tour_order WHERE created_at >= CURDATE()) FROM tour_order WHERE created_at >= CURDATE() ON DUPLICATE KEY UPDATE order_cnt = VALUES(order_cnt), gmv = VALUES(gmv);逻辑说明:ON DUPLICATE KEY UPDATE保证同一天重复执行不会产生多条记录,stat_date做唯一键。子查询算待审线路和活跃机构,虽然有点重,但 5 分钟跑一次可以接受。参数上CURDATE()取当天零点,避免用NOW()导致跨天数据错乱。
5. 旅游小程序源码落地避坑:五个血泪翻车现场
5.1 库存超卖:先查后扣的经典翻车
现象:活动线路明明库存 100,卖出 130 单,机构到店发现接待不了。原因:下单逻辑写成「先 SELECT 查库存,再 UPDATE 扣减」,两个请求并发时都查到 100,都扣成功。解决:改成UPDATE ... WHERE stock >= 1的条件更新,判断影响行数,为 0 直接失败。这是最没有后悔药的一类 bug,上线前必须压测。
5.2 机构越权:员工核销了别家订单
现象:A 机构员工能核销 B 机构订单,导致 B 机构订单被误标已核销。原因:核销接口只校验了订单存在和状态,没校验订单org_id和员工org_id是否一致。解决:核销前强制比对机构归属,不一致直接返回 403,并记录操作日志。多机构平台里,任何跨机构的数据操作都要加归属校验。
5.3 审核状态与上架状态混用
现象:平台审核通过后线路自动上架,机构想先改价再上架,结果消费者已经能下单,价格还是旧的。原因:审核逻辑里把audit_status和status一起置 1,没给机构留缓冲。解决:审核通过只置audit_status=1,status由机构自己控制,默认 0,机构确认无误后手动上架。这个改动很小,但能避免大量价格纠纷。
5.4 小程序端缓存导致下架线路仍可下单
现象:机构下架线路后,消费者端列表还显示,点进去还能下单。原因:列表走了 Redis 缓存,下架时没清缓存,且下单接口没校验status。解决:双保险——机构下架时主动删缓存 key,下单 SQL 里带上status=1 AND audit_status=1条件。缓存可以脏,但下单校验不能省。
5.5 结算金额浮点误差累积
现象:平台月底对账,佣金总额和订单佣金之和对不上,差几毛钱。原因:佣金计算用浮点数,多笔累加后误差放大。解决:金额统一用「分」存整数,或者用DECIMAL类型并在每笔计算时round到两位。结算记录生成时立即落库,不要等对账时再算。这类问题不致命,但会让财务和机构扯皮。
6. 从能跑到能卖:线路复制、库存预警与灰度上架三个进阶技巧
6.1 线路复制:机构最高频的操作
机构发布新线路时,80% 的内容和旧线路重复。做一个「复制线路」功能,能极大提升机构端使用率。实现上就是读原线路和景点,插入新记录,title加「副本」后缀,audit_status重置为 0。
def copy_route(route_id, org_id): src = db.query_one("SELECT * FROM tour_route WHERE id=%s AND org_id=%s", route_id, org_id) if not src: raise Exception("线路不存在或无权复制") new_id = db.insert("tour_route", { "org_id": org_id, "title": src["title"] + "-副本", "days": src["days"], "price": src["price"], "stock": 0, # 库存清零,强制机构重新设置 "audit_status": 0, "status": 0 }) spots = db.query("SELECT * FROM route_spot WHERE route_id=%s ORDER BY sort", route_id) for s in spots: db.insert("route_spot", {**s, "id": None, "route_id": new_id}) return new_id关键参数:stock强制清零,避免机构复制后忘记改库存导致超卖;audit_status和status都重置,复制出来的线路必须重新审核。{**s, "id": None}是去掉原主键,让数据库重新生成。
6.2 库存预警:别等卖完了才知道
给机构端加一个库存预警,当stock低于阈值时在机构端首页提示。阈值可以按线路设置,默认 5。
SELECT id, title, stock FROM tour_route WHERE org_id = ? AND status = 1 AND stock <= 5 ORDER BY stock ASC;这个查询走idx_org索引,机构端每次登录时跑一次,结果缓存在会话里。机构看到预警后可以及时补库存或下架,避免消费者下单后无法接待。阈值不要设太高,否则预警太多机构会麻木。
6.3 灰度上架:新线路先小范围验证
新线路直接全量上架有风险,可以做个灰度:先只对部分消费者可见,观察转化和投诉,再全量。实现上给tour_route加gray_status字段,消费者端查询时按用户 ID 哈希取模决定是否展示。
SELECT id, title, price FROM tour_route WHERE audit_status = 1 AND status = 1 AND stock > 0 AND (gray_status = 0 OR (gray_status = 1 AND MOD(?, 10) < 3));参数?是消费者 ID,MOD(?, 10) < 3表示 30% 用户可见。灰度期建议 3 到 7 天,观察退款率和投诉率,正常后再把gray_status置 0 全量。这个技巧在旅游这种重决策品类里特别有用,因为一条线路的口碑崩了,修复成本很高。
我自己做这类多端系统,最大的习惯是:任何涉及钱和库存的操作,先问一句「并发下会不会错」,再动手写代码。审核状态和上架状态永远分开,机构归属校验永远不省。这套旅游小程序源码的四端骨架搭对了,后面加功能就是填空;骨架搭错,每加一个功能都在还债。希望帮到你。
本文还有配套的精品资源,点击获取