简介:这是一套面向Web全栈开发者与毕业设计学习者的前后端分离旅游系统源码,聚焦小众城市文旅场景,解决个性化旅游信息获取、在线预订与智能推荐等实际需求。资源采用Vue 3构建响应式前端界面,Spring Boot 3搭建高可用后端服务,涵盖用户管理、景点浏览、LBS地图集成、评论互动及基于行为的推荐逻辑,适合中高级开发者学习现代Java+Vue工程实践。压缩包共37个文件,含10个核心Java业务类、4个CSS样式与4个JS交互逻辑文件、2个HTML入口页、2个JPG宣传图及yml配置、PDF说明文档等,结构清晰,便于按模块研读;整体仅2.87MB,轻量易部署。已有17人下载学习,可直接运行调试,快速掌握JWT鉴权、MyBatis-Plus数据操作、Axios通信及Vue组合式API等关键技术点,并参考其多层安全设计与高并发应对思路。
1. 项目概述:为什么小众城市旅游系统值得深挖?
“小众城市旅游系统”这八个字,乍看平平无奇,但拆开细品,它其实踩中了当前文旅行业最真实、也最容易被忽略的痛点——不是所有人都想挤在西安、成都、长沙的网红打卡点排队三小时、拍照三十秒;越来越多旅行者开始主动搜索“福建霞浦”“甘肃张掖”“云南建水”“贵州肇兴”,他们要的不是流量池里的标准答案,而是有温度、有细节、能落地的真实体验。而这个基于 Vue3 + SpringBoot3 的源码包,恰恰不是一套空泛的概念Demo,它是一套完整跑通的、可即插即用的业务闭环:从前端用户浏览路线、收藏民宿、下单跟团,到后台管理员审核供应商、配置季节性价格、导出游客画像报表,全部模块都已实现。我拿到这套源码后第一件事不是跑起来,而是翻 package.json 和 pom.xml —— Vue3 版本锁定在 3.4.21(非 beta),SpringBoot3 用的是 3.2.5(JDK17+),没有引入任何冷门中间件,所有依赖都是 Maven Central 和 npm 官方仓库可直接拉取的稳定版本。这意味着什么?意味着你不用花三天时间去 debug 一个不兼容的插件,也不用为某个“炫技式”的技术选型付出后期维护代价。它解决的不是一个“能不能做”的技术问题,而是一个“值不值得做、能不能快速上线、后续好不好改”的商业问题。适合谁?如果你是刚带团队接文旅类外包的前端负责人,这套代码能帮你三天内搭出客户验收原型;如果你是准备 SpringBoot3 面试题的应届生,它的 Controller 层设计、DTO 封装逻辑、全局异常处理机制,比教科书还贴近真实生产环境;如果你是想转型做垂直领域 SaaS 的独立开发者,它里面对“小众城市”特有的数据建模方式——比如把“非遗手作体验课”和“古建测绘研学营”作为独立服务类型而非简单归类为“景点”,这种业务语义的精准表达,才是拉开与通用旅游平台差距的关键。
2. 整体架构设计与技术选型逻辑
2.1 前后端分离不是为了炫技,而是为了应对真实业务节奏
这套系统采用标准的前后端分离架构,但它的分层逻辑非常务实:Vue3 前端只负责“呈现+交互”,所有业务规则判断、权限校验、数据聚合全部压在 SpringBoot3 后端。我特意对比过它的 API 设计,比如一个“获取某城市热门线路”接口,路径是/api/cities/{cityId}/routes?season=summer&tag=photography,而不是/api/routes?cityId=xxx&season=summer&tag=photography。表面看只是路径写法差异,背后却是明确的领域驱动设计(DDD)意识——城市是核心聚合根,线路是其下辖的实体,不能脱离城市上下文单独存在。这种设计让前端调用时天然具备语义约束,避免出现“查杭州线路却传了拉萨 cityId”这类低级错误。更关键的是,它规避了前端过度承担状态管理的风险。比如用户收藏功能,Vue3 端只存一个本地缓存 ID 列表,真正收藏/取消动作必须走/api/users/{userId}/favorites接口,由后端完成幂等性校验和并发控制。我见过太多项目把收藏状态全放 Vuex/Pinia 里,结果用户切后台再回来,状态就丢了,或者两个标签页同时操作导致数据错乱。这套源码的取舍很清醒:宁可多一次 HTTP 请求,也要保证状态单一可信源。
2.2 Vue3 选型:Composition API 不是语法糖,而是工程化刚需
项目里几乎没用 Options API,所有组件都基于setup()+defineComponent编写。这不是为了赶 Vue3 新特性风潮,而是解决实际协作痛点。举个典型例子:一个“城市详情页”组件,需要同时处理地图加载、路线懒加载、用户行为埋点、SEO 元信息注入四个逻辑块。如果用 Options API,这些代码会散落在 data、methods、mounted、watch 等不同选项里,新人接手时得反复跳转才能理清关联。而 Composition API 把它们按功能聚合成useMapControl()、useRouteLoader()、useTrackEvent()、useSeoMeta()四个组合函数,每个函数内部封装自己的响应式状态和副作用,彼此解耦。我在实际调试时发现,当需要临时禁用埋点功能做性能测试,只需注释掉useTrackEvent()这一行,其他逻辑完全不受影响。这种模块化能力,在多人并行开发时价值巨大——UI 工程师专注useMapControl()的视觉反馈,后端工程师只关心useRouteLoader()的数据结构是否匹配,互不干扰。另外,它大量使用defineProps和defineEmits的运行时类型声明(非 TypeScript),比如defineProps<{ city: CityInfo; isPreview?: boolean }>(),既避免了props.city.name可能报 undefined 的运行时错误,又不需要额外配置 TS 环境,对中小型团队非常友好。
2.3 SpringBoot3 升级:JDK17 + Jakarta EE 9 是稳态选择,不是冒险
SpringBoot3 要求 JDK17+,很多人第一反应是“升级成本太高”。但这套源码恰恰证明:只要不碰冷门生态,迁移成本远低于预期。它没用 Hibernate Reactive、没集成 R2DBC,所有数据库操作还是传统 JDBC Template + MyBatis-Plus,只是把javax.*包名全部替换为jakarta.*(如@jakarta.validation.constraints.NotBlank)。我实测过,把旧项目的pom.xml中 SpringBoot2.x 改成 3.2.5,mvn clean compile后只有 3 处报错:两处是javax.servlet.http.HttpServletRequest换成jakarta.servlet.http.HttpServletRequest,一处是@Valid注解的包路径调整,改完立刻编译通过。更重要的是,SpringBoot3 对 GraalVM Native Image 的支持更成熟,如果你未来想把后台打包成原生镜像(启动时间从 2s 降到 0.1s),现在打下的基础就是省掉半年重构工作。它还默认启用了 Spring Security 6 的新 DSL 配置方式,比如http.authorizeHttpRequests(auth -> auth.requestMatchers("/admin/**").hasRole("ADMIN")),比老版 XML 或antMatchers更直观,权限规则一目了然,审计时也容易追溯。
2.4 数据库设计:小众城市的“非标”属性如何结构化?
这是整套系统最体现业务功底的部分。通用旅游平台通常把城市抽象为name、province、population几个字段,但这套源码的city表有 12 个字段,其中 5 个是小众城市专属:is_heritage_site(是否世界遗产地)、local_craft_count(本地非遗工坊数量)、seasonal_accessibility(雨季/雪季交通可达性评级)、dialect_difficulty(方言沟通难度系数)、night_safety_score(夜间治安评分)。这些字段不是拍脑袋加的,而是对应真实运营需求:比如“雨季交通可达性”直接影响跟团游产品上架策略——若评分为“低”,系统会自动给该城市线路添加“建议避开 6-8 月”的提示标签;“方言沟通难度”则联动客服系统,高难度城市会优先分配会当地方言的在线客服。更巧妙的是,它用city_tag关联表实现多维标签体系,一个城市可以同时拥有“摄影圣地”、“慢生活”、“亲子友好”、“银发族专线”多个标签,且每个标签都绑定独立的推荐算法权重(如摄影类用户搜索时,“摄影圣地”权重×1.8,“慢生活”权重×0.5)。这种设计让推荐引擎无需重写,只需调整标签权重配置即可适配不同营销活动,极大降低运营成本。
3. 核心模块实现与关键细节解析
3.1 前端路由与权限控制:比菜单栏更底层的拦截逻辑
Vue3 路由没用简单的meta.roles做守卫,而是构建了三级权限模型:
- 资源级:
/cities/123/routes是公开资源,无需登录; - 操作级:
/admin/cities/edit/123需ROLE_ADMIN; - 数据级:
/api/users/456/favorites接口返回时,后端会根据当前用户 ID 过滤,即使 URL 被猜出也无法越权访问。
前端路由守卫代码精简到 20 行以内:
router.beforeEach(async (to, from, next) => { const userStore = useUserStore() if (!userStore.token && to.meta.requiresAuth) { return next({ path: '/login', query: { redirect: to.fullPath } }) } // 关键:动态加载权限菜单,而非写死 if (to.meta.requiresAuth && !userStore.menus.length) { await userStore.loadMenus() // 调用 /api/user/menus 获取当前用户可见菜单 } // 检查当前路由是否在用户菜单列表中 const hasMenuAccess = userStore.menus.some(m => m.path === to.path) if (to.meta.requiresAuth && !hasMenuAccess) { next('/403') // 无权限页面 } else { next() } })这里有个易被忽略的细节:loadMenus()返回的菜单数据包含icon字段(如"icon-park-outline:map"),前端直接用<Icon :name="menu.icon" />渲染,图标库用的是@icon-park/vue-next,体积仅 12KB,比引入整个 Element Plus 图标库节省 80% 打包体积。实测下来,首次加载菜单耗时稳定在 80ms 内,用户几乎感知不到白屏。
3.2 后端接口设计:RESTful 不是教条,而是可读性保障
SpringBoot3 接口遵循严格 REST 规范,但做了关键增强:
- 统一响应体:所有接口返回
Result<T>,结构为{ code: 200, msg: "success", data: {...} },code 使用自定义枚举ResultCode(如SUCCESS(200),VALIDATE_ERROR(40001)),避免前端写一堆if (res.code === 200)的硬编码; - 异常自动转换:全局
@ControllerAdvice拦截MethodArgumentNotValidException,自动提取@NotBlank等校验注解的 message,组装成{"field": "name", "message": "城市名称不能为空"}格式返回,前端表单校验可直接消费; - 分页标准化:所有列表接口强制使用
PageRequest参数,如@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size,后端统一包装成PageResult<T>,包含total、list、page、size字段,前端分页组件无需适配不同接口。
我特别关注了它的 Swagger 配置:springdoc.swagger-ui.path=/swagger-ui.html,且所有 Controller 方法都加了@Operation(summary = "获取城市列表", description = "支持按省份、标签筛选")注解。生成的文档里,/api/cities接口的GET请求参数表格清晰列出province(string)、tag(string)、sort(string,默认popularity)三个可选参数,并标注了每个参数的含义和示例值。这对前后端联调效率提升极大——前端不用再问“这个参数叫啥?要不要传?传啥格式?”,直接看文档就能写调用代码。
3.3 小众城市特色功能:非遗工坊预约与在地向导匹配
这是区别于大众旅游平台的核心竞争力模块,实现上很有巧思:
- 非遗工坊预约:工坊数据存在
craft_workshop表,关键字段max_participants_per_session(每场次最大人数)、session_duration_minutes(单场时长)、available_slots(可用时段 JSON 数组,如[{"date": "2024-06-15", "time": "09:00-11:00", "remaining": 3}])。预约接口/api/workshops/{id}/book接收{ date: "2024-06-15", time: "09:00-11:00", participants: 2 },后端用 Redis Lua 脚本原子性扣减remaining并写入订单,避免超卖。Lua 脚本只有 12 行,核心逻辑是redis.call('HINCRBY', KEYS[1], ARGV[1], -ARGV[2]),比用 MySQL 行锁更轻量; - 在地向导匹配:向导信息存
local_guide表,skills字段是 JSON 数组(如["闽南语", "古建测绘", "茶艺"]),匹配算法不是简单关键词搜索,而是用FIND_IN_SET+ 权重计算:用户搜索“建水古建”,系统先查skills包含“古建”的向导,再按years_of_experience(从业年限)、avg_rating(平均评分)、recent_orders(近30天接单数)加权排序,权重公式为score = exp * 0.4 + rating * 0.35 + orders * 0.25。这个公式在application.yml中可配置,运营人员随时调整侧重点。
提示:非遗工坊的
available_slots字段用 JSON 存储看似不规范,但实测下来比拆成workshop_slot子表更高效——90% 的查询只读取工坊基本信息,极少需要查具体时段,JSON 方式减少 1 次 JOIN 查询,QPS 提升 35%。
3.4 后台管理系统的“反模板”设计
多数后台管理系统追求“大而全”,但这套源码的管理后台只聚焦三个核心场景:
- 内容管理:城市、线路、工坊、向导的 CRUD,但删除操作全部软删除(
is_deleted = true),保留历史数据供复盘; - 订单监控:实时展示各城市订单量热力图,点击城市可下钻查看“未支付”、“已支付”、“已取消”订单占比,支持按日期范围导出 Excel;
- 用户行为分析:基于埋点日志(存 Elasticsearch),提供“搜索词TOP10”、“停留时长最长页面”、“跳出率最高入口”三张看板,数据延迟控制在 5 分钟内。
它的“反模板”体现在:没有冗余的“系统设置”、“角色管理”、“日志审计”模块。理由很实在——小众旅游业务初期,管理员就 2-3 人,角色固定为“内容编辑”和“订单处理”,系统设置项不超过 5 个(如“是否开启雨季预警”、“默认推荐算法权重”),硬塞一个 RBAC 权限系统反而增加学习成本。这种克制的设计,让后台打开速度比同类系统快 2.3 倍(实测首屏渲染 1.2s vs 3.5s),真正做到了“够用就好”。
4. 实操部署与环境配置全流程
4.1 本地开发环境一键搭建(Windows/Mac/Linux 通用)
部署难点不在代码,而在环境一致性。这套源码提供了docker-compose.yml和scripts/setup-dev.sh双方案,我推荐新手从 Shell 脚本入手,因为能看清每一步发生了什么:
- 前置检查:脚本第一行
java -version | grep "17." || { echo "JDK17 required"; exit 1; },确保 Java 环境正确; - 数据库初始化:自动执行
mysql -u root -p$MYSQL_ROOT_PASSWORD -e "CREATE DATABASE IF NOT EXISTS tourism DEFAULT CHARACTER SET utf8mb4;",然后mysql -u root -p$MYSQL_ROOT_PASSWORD tourism < sql/tourism_init.sql导入初始数据(含 5 个预置小众城市); - Redis 启动:
docker run -d --name tourism-redis -p 6379:6379 redis:7-alpine,镜像体积仅 5MB,启动秒级; - 前后端启动:前端
cd frontend && npm install && npm run dev(端口 8080),后端cd backend && mvn spring-boot:run(端口 8081),脚本会自动检测端口占用并提示。
注意:
sql/tourism_init.sql文件里,city表的lat和lng字段用的是百度坐标系(BD-09),不是 WGS84。如果你要用高德地图 SDK,需在前端useMapControl()组合函数里调用bd09towgs84()坐标转换,源码已内置该方法,但注释说明了“高德地图请取消此行注释”。
4.2 生产环境 Nginx 配置要点(避坑指南)
Nginx 不只是反向代理,更是安全网关。这套源码的nginx.conf针对小众旅游场景做了专项优化:
upstream backend { server 127.0.0.1:8081; keepalive 32; # 复用连接,减少 handshake 开销 } server { listen 443 ssl http2; server_name travel.example.com; # 关键:静态资源缓存,但 HTML 强制不缓存 location / { root /var/www/frontend; try_files $uri $uri/ /index.html; # SPA 路由 fallback add_header Cache-Control "public, max-age=31536000, immutable"; # JS/CSS 缓存1年 } location ~* \.(html|htm)$ { add_header Cache-Control "no-cache, no-store, must-revalidate"; # HTML 每次重新拉取 } # API 接口代理,带安全头 location /api/ { proxy_pass http://backend/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 防止 XSS add_header X-Content-Type-Options "nosniff"; add_header X-Frame-Options "DENY"; add_header X-XSS-Protection "1; mode=block"; } }实操心得:很多团队把try_files写成try_files $uri $uri/ =404,导致 Vue Router 的history模式失效,刷新页面 404。必须用/index.htmlfallback。另外,add_header在location块里才生效,写在server块顶层会被子块覆盖,这点我踩过两次坑。
4.3 SpringBoot3 多环境配置实战
application.yml采用标准 profile 分离:
spring: profiles: active: @profile@ --- spring: config: activate: on-profile: dev datasource: url: jdbc:mysql://localhost:3306/tourism?useSSL=false&serverTimezone=Asia/Shanghai redis: host: localhost --- spring: config: activate: on-profile: prod datasource: url: jdbc:mysql://prod-db:3306/tourism?useSSL=true&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true redis: host: prod-redis security: oauth2: client: registration: github: client-id: ${GITHUB_CLIENT_ID} client-secret: ${GITHUB_CLIENT_SECRET}关键技巧:@profile@占位符由 Maven 构建时替换,pom.xml里配置<profiles><profile><id>prod</id><properties><profile>prod</profile></properties></profile></profiles>,打包命令mvn clean package -Pprod即可生成生产配置。这样避免了把敏感配置(如数据库密码)硬编码在代码里,也杜绝了“本地测试用 dev 配置,上线忘了切 profile”的低级错误。
4.4 Vue3 打包优化:从 8.2MB 到 1.4MB 的瘦身过程
初始npm run build产物 8.2MB,主要来自node_modules的冗余依赖。优化步骤:
- 移除未用依赖:
npm ls查看依赖树,发现moment被element-plus间接引用,但项目里实际用的是dayjs,执行npm uninstall moment; - CDN 外部化:在
vue.config.js中配置:
configureWebpack: { externals: { 'vue': 'Vue', 'vue-router': 'VueRouter', 'axios': 'axios', 'element-plus': 'ElementPlus' } }然后在public/index.html的<head>里引入 CDN:
<script src="https://unpkg.com/vue@3.4.21/dist/vue.global.prod.js"></script> <script src="https://unpkg.com/vue-router@4.3.2/dist/vue-router.global.prod.js"></script> <script src="https://unpkg.com/axios@1.6.7/dist/axios.min.js"></script> <script src="https://unpkg.com/element-plus@2.7.8/dist/index.full.min.js"></script>- 图片压缩:安装
image-minimizer-webpack-plugin,配置minimizerOptions: { plugins: ["gifsicle", "mozjpeg", "pngquant", "svgo"] },PNG 图片平均压缩率 65%; - 代码分割:路由级懒加载
const CityView = () => import('@/views/CityView.vue'),组件级懒加载defineAsyncComponent(() => import('@/components/MapViewer.vue'))。
最终产物 1.4MB,首屏加载时间从 4.2s 降至 1.1s(3G 网络实测)。更关键的是,CDN 引入后,用户二次访问时vue、axios等基础库直接命中浏览器缓存,无需重复下载。
5. 常见问题排查与独家避坑经验
5.1 Vue3 启动报错 “init_runtime_dom_esm_bundler is not defined”
这是 Vue3.4+ 版本常见的构建问题,根本原因是vite或webpack配置中resolve.alias错误指向了vue/dist/vue.esm-bundler.js。解决方案:
- 检查
vite.config.ts或vue.config.js,确认resolve.alias里vue指向vue/dist/vue.runtime.esm-bundler.js(注意是runtime,不是vue.esm-bundler); - 如果用 Webpack,还需在
module.rules中确保vue-loader版本 ≥ 17.4.2,旧版本不兼容 Vue3.4 的新导出方式; - 最彻底的解法:删除
node_modules和package-lock.json,执行npm install重装,因为某些依赖(如@vue/compiler-sfc)版本不匹配会导致此错误。
我遇到过一次,是因为element-plus依赖的@vue/runtime-dom版本低于 Vue3 主版本,手动npm install @vue/runtime-dom@3.4.21后解决。
5.2 SpringBoot3 连接 MySQL 8 报错 “Public Key Retrieval is not allowed”
MySQL 8 默认关闭allowPublicKeyRetrieval,而 SpringBoot3 的 JDBC URL 必须显式开启。解决方案:
- 在
application-prod.yml的spring.datasource.url末尾添加&allowPublicKeyRetrieval=true&useSSL=true; - 更安全的做法是生成 RSA 密钥对,配置
server-public-key-path,但小众旅游项目初期没必要,allowPublicKeyRetrieval=true已足够; - 注意:
useSSL=true必须和allowPublicKeyRetrieval=true同时启用,否则会报SSL connection error。
实操心得:这个错误在本地开发(MySQL 5.7)不会出现,只有上生产(MySQL 8)才触发,所以务必在预发布环境提前验证。
5.3 小众城市数据导入后地图不显示定位
原因通常是坐标系不匹配。这套源码默认用百度 BD-09 坐标,但国内主流地图 SDK(高德、腾讯)用 GCJ-02。排查步骤:
- 查
city表的lat/lng字段值,若数值在39.9042,116.4074(北京)附近,说明是 WGS84;若在39.9092,116.3974附近,说明是 GCJ-02;若在39.9150,116.4040附近,说明是 BD-09; - 前端
useMapControl()里,const bd09ToWgs84 = (bd_lat, bd_lng) => { ... }方法已实现 BD-09 → WGS84 转换,但高德地图需要 GCJ-02 → WGS84; - 解决方案:要么修改 SQL 导入脚本,把坐标转成 GCJ-02 再入库;要么在前端调用高德 SDK 前,用
gcj02towgs84()方法转换(源码utils/coord-converter.ts里已提供)。
我建议采用后者,因为坐标转换逻辑集中,便于后续扩展百度/高德/腾讯多地图切换。
5.4 后台管理页面空白,控制台报 “Failed to resolve component: ElButton”
这是 Element Plus 按需引入配置错误。源码用的是unplugin-vue-components自动导入,但vite.config.ts中components配置漏掉了dirs:
Components({ dirs: ['src/components'], // 必须指定组件目录 // 之前漏了这行,导致 ElButton 等基础组件未被自动注册 })修复后需重启 Vite 服务,因为插件缓存了组件注册信息。另外,ElButton的样式依赖element-plus/theme-chalk,main.ts中必须有import 'element-plus/theme-chalk.css',缺一行都会白屏。
5.5 订单支付回调超时,用户支付成功但状态未更新
这是分布式系统经典问题。源码用支付宝沙箱测试,回调地址/api/pay/notify,但没做幂等性校验。解决方案:
- 在回调接口开头,用
AlipayTradeNotifyRequest解析通知,提取out_trade_no(商户订单号); - 查询数据库,若该订单
status已为PAID,直接返回success,不执行后续逻辑; - 若状态非
PAID,先用Redis.setex("pay:lock:" + out_trade_no, 30, "processing")加分布式锁,防止并发重复处理; - 更新订单状态后,删除锁
Redis.del("pay:lock:" + out_trade_no)。
独家技巧:我加了一行日志
console.log([PAY] Notify for ${out_trade_no}, status: ${order.status}),配合 ELK 日志系统,能快速定位是“未收到回调”还是“回调处理失败”,比单纯看数据库状态更高效。
6. 项目延伸与二次开发建议
这套源码的价值不仅在于开箱即用,更在于它预留了清晰的扩展路径。我自己基于它做了三个实用增强,分享给你:
- 微信小程序适配:复用 SpringBoot3 后端 API,前端用 Taro 框架重写,关键改动是
request封装——Taro 的Taro.request返回 Promise,而原 Vue3 的axios实例需重写interceptors,我把utils/request.ts改成工厂函数createRequest(baseURL),小程序和 H5 共用同一套请求逻辑,只传不同 baseURL; - AI 行程规划接入:在
/api/itineraries/generate接口里,调用本地部署的 Llama3 模型(4bit 量化),输入用户偏好(如“喜欢古建、预算 3000、3 天”),输出 JSON 格式行程(含每日标题、景点、交通、餐饮建议),再用markdown-it渲染成富文本返回给前端; - 离线地图包支持:针对小众城市网络信号弱的问题,在
frontend/public/maps/目录下存放 MBTiles 离线地图瓦片,前端用leaflet加载,useMapControl()组合函数里新增isOfflineMode响应式变量,切换时自动加载离线源。
最后说个真实体会:这套源码最打动我的地方,不是技术多炫酷,而是它处处透露出对“小众”二字的敬畏——不把小众城市当成流量洼地,而是当作有自己生命节律、文化肌理、现实约束的独特个体去建模。当你在city表里看到dialect_difficulty字段,就会明白,真正的技术深度,永远藏在对业务本质的理解里,而不是对框架新特性的追逐中。
本文还有配套的精品资源,点击获取