资讯详情

资讯详情

百科系统开发实战:Spring Boot+MySQL构建仿百度百科全栈项目

简介这套高度模仿百度百科的百科建站系统面向站长、开发者和知识管理爱好者用于快速搭建在线词库平台复刻百度百科的浏览、搜索、词条编辑与审核体验解决自主建站时内容管理不规范、审核机制缺失等问题。资源以开源的HDWiki系统为核心将类百科的界面布局、多用户贡献机制、权威性审核流程以及分类检索模块整合在一个约1.95MB的压缩包内。包内主要包含安装说明类文本、程序源码文件以及使用与二次开发文档可指导用户完成环境部署、数据库配置、站点初始化等操作并帮助排查常见安装问题。已有384人学习下载非常适合初学者按照说明从零构建百科网站也适合中级开发者研究其目录结构、模板标签和扩展机制进一步定制词条分类、编辑权限和搜索功能搭建面向个人、团队或学校的知识共享平台。1. 照抄页面只要三天真正被卡住的是数据模型和搜索排序把百度百科的首页、词条页、搜索页截图摆在一起第一反应通常是这不就是个带搜索框的内容管理系统吗实际动手做这个超级模仿项目时才发现UI 模仿是整条链路里最不值钱的部分。真正卡住人的是词条数据怎么建模、正文大字段放哪、搜索怎么模拟原版名称精确命中排最前的排序以及多人编辑时怎么防止词条互相覆盖。这类百科系统前台仿搜索、详情、分类导航后台管发布、编辑与版本回滚适合想完整走一遍全栈的开发者拿去当课程设计或简历项目。下面按我自己的落地路径讲先定架构和库表再写搜索与编辑链路最后是五个绕不开的坑。2. 先定技术栈和模块边界百科系统不是一堆页面堆出来的好多人一上来就照着百度百科写页面结果写了两周发现搜索、分类、编辑历史全都接不上只能推倒重来。我习惯先用一小时把技术栈和模块边界画清楚。百科系统的本质是多对多关系很重的内容平台——分类挂词条、标签挂词条、词条挂多版本选型时最怕的就是把骨架搭窄后面每加一个功能都要动表结构。2.1 技术选型为什么我选 Spring Boot MySQL 而不是纯前端方案纯静态页面方案做仿站能看浏览器里点着挺像那么回事但撑不住编辑留痕、搜索排序、分类聚合这三件事。它们都需要服务端参与前端静态页只能做到看起来像做不到用起来像。我用的组合是 Spring Boot 做后端、Thymeleaf 做服务端渲染、MyBatis 操作 MySQL缓存按需加 Redis。选这个组合有三个理由第一百科页面本身是服务端渲染为主搜索结果和词条详情都是整页刷新用 Thymeleaf 模仿起来天然贴近第二Java 技术栈的课程设计和简历项目认可度最高遇到问题能搜到的资料也最多第三MyBatis 写复杂排序 SQL 很直接搜索排序这种逻辑放在 XML 里比写在 ORM 注解里好维护。方案适合场景短板Spring Boot Thymeleaf课程设计、简历项目页面交互不如 SPA 顺滑Vue/React 后端 API想顺便练前后端分离两套工程周期长一倍Flask/Django 快速版单独练 Python 技术栈和主流 Java 岗位要求错位如果你想同时练 Vue把 Thymeleaf 换成 RESTful API Vue 脚手架完全可行本文后半部分的搜索接口和编辑接口设计照用就行只是多一步 JSON 序列化。我的建议是别在选型上花超过半天百科系统的难点从来不在框架而在数据模型和事务边界。2.2 模块划分前台、管理端与编辑流三条线百科系统的页面不算多首页、搜索页、分类导航、词条详情、编辑页再加一个后台管理页。但页面少不代表结构简单。我习惯把整个工程拆成三条线前台展示线负责能搜、能看管理线负责词条上下架和分类维护编辑流单独拎出来因为它涉及版本表和内容表两次写入必须在 service 层用事务包住放 controller 里会导致事务失效。目录结构按这个思路组织baike-system/ ├── pom.xml └── src/main/ ├── java/com/example/baike/ │ ├── controller/ # 页面入口和 API │ │ ├── PortalController.java # 首页、搜索、分类 │ │ └── EntryController.java # 词条详情、编辑、回滚 │ ├── service/ # 业务逻辑编辑事务、版本管理 │ ├── mapper/ # MyBatis 数据访问接口 │ ├── entity/ # 表和实体映射 │ └── config/ # 静态资源映射、编码拦截器 └── resources/ ├── mapper/ # MyBatis XML 查询搜索排序在这里 ├── templates/ # Thymeleaf 页面 │ ├── index.html # 首页 │ ├── search.html # 搜索结果页 │ └── entry/ │ ├── detail.html │ └── edit.html └── application.yml这个划分遵循一个原则页面入口只管参数和视图业务全在 service。你后面做版本回滚、做缓存清理都在 service 层收口controller 永远瘦瘦的。别小看这条线我见过不少项目把业务逻辑写进 controller最后加一个回滚功能要改五个页面入口。2.3 最小骨架application.yml 里三个必调的配置初始化项目后先不要急着写页面把配置调对能省掉后面一整天的排查时间。我每次建这类项目application.yml 里必调三个地方server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/baike_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 改成你自己的密码 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false redis: host: localhost port: 6379 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true第一个必调是数据源 URL 里的characterEncodingutf8mb4数据库侧字符集和 JDBC 侧字符集不一致是最常见的中文查不出来来源这个坑后面会专门展开。第二个是thymeleaf.cache: false开发期改模板不用重启改完刷新浏览器就能看到效果上线前记得改回 true不然流量一大模板编译会拖垮性能。第三个是 MyBatis 的map-underscore-to-camel-case打开后库表里的update_time自动映射成实体里的updateTime省掉一堆手写 ResultMap 的琐碎工作。启动类也别忘加扫描注解SpringBootApplication MapperScan(com.example.baike.mapper) public class BaikeApplication { public static void main(String[] args) { SpringApplication.run(BaikeApplication.class, args); } }MapperScan的作用是让 MyBatis 自动注册所有 Mapper 接口少了它每个 Mapper 都得手动加Mapper注解漏一个启动就报错。到这里骨架就立住了下一步是设计数据库这是整个项目的地基。3. 数据库设计词条、内容、版本三张表撑起整个百科百科系统的库表设计是所有后续功能的底板。我见过很多翻车现场都是因为把所有东西塞进一张大表正文、标签、分类、阅读量全堆在 entry 表里改着改着连索引都加不进去。我的做法是拆成三张核心表和两张关联表entry 主表、entry_content 内容表、entry_revision 版本表外挂 category 分类树和 tag 标签表。下面逐个说清楚为什么这么拆。3.1 词条主表和内容表拆开正文大字段为什么单独放词条列表页只需要词条名、摘要、分类、浏览量和封面图而正文是 LONGTEXT 级别的富文本 HTML一条词条正文轻松几十 KB。如果正文和主表放同一张表列表查询也得把这些大字段捞出来虽然 InnoDB 按行读取但行太大直接拖累缓冲池命中率行数一多性能就崩。拆开后主表行短小普通查询快得很正文只有进详情页才查这是内容型系统的通用分法。建表语句如下-- 词条主表只放稳定、短小的信息 CREATE TABLE entry ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 词条名唯一, summary TEXT COMMENT 词条摘要列表页和搜索页展示, category_id BIGINT NOT NULL COMMENT 所属分类, view_count INT DEFAULT 0 COMMENT 浏览量, status TINYINT DEFAULT 1 COMMENT 1-已发布 0-草稿, cover_image VARCHAR(255) COMMENT 封面图地址, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_entry_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT词条主表; -- 词条内容表正文大字段单独放 CREATE TABLE entry_content ( id BIGINT PRIMARY KEY AUTO_INCREMENT, entry_id BIGINT NOT NULL COMMENT 关联词条主表, content LONGTEXT COMMENT 词条正文 HTML, source VARCHAR(255) COMMENT 参考资料来源, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_entry_id (entry_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT词条内容表;两个细节值得注意。第一name加了唯一索引uk_entry_name这是防止同名词条重复插入的第一道防线后面并发章节还会用到它。第二summary字段不能省它承担列表页和搜索页的摘要展示如果没有这个字段每次列表页都要去 content 表里截取正文性能差一大截。3.2 分类和标签两套挂载方式不能混成一锅分类是树结构比如自然科学 → 物理 → 力学一个词条只能挂一个主分类标签是平铺的一个词条可以挂多个比如相对论可以同时挂物理爱因斯坦时间三个标签。把这两者混在一张表里分类页 URL 做不出来树遍历也做不了。分开设计-- 分类表parent_id 实现树形结构 CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名, parent_id BIGINT DEFAULT 0 COMMENT 父分类 ID0 表示顶级, sort_order INT DEFAULT 0 COMMENT 同层排序越小越靠前, KEY idx_parent (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT分类表; -- 标签表扁平结构 CREATE TABLE tag ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE COMMENT 标签名 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT标签表; -- 词条与标签关联表联合主键防重复挂载 CREATE TABLE entry_tag ( entry_id BIGINT NOT NULL, tag_id BIGINT NOT NULL, PRIMARY KEY (entry_id, tag_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT词条标签关联表;分类树的便利在于前台分类导航页只需要parent_id 0查顶级分类再按父 ID 逐层展开天然支持无限层级。关联表用联合主键(entry_id, tag_id)同一个标签重复挂到同一个词条会直接报主键冲突从数据库层面堵住脏数据。后面做相关词条推荐也是靠这张关联表做标签重叠度计算它是省不掉的。3.3 版本表编辑留痕是百科类系统的底线百科类产品最重要的信任点是这个词条改没改过、改了什么地方、能不能回滚。没有版本表一次手滑的编辑就可能把整条词条内容毁掉且无法恢复。entry_revision 表的核心设计是每次编辑都生成一份正文快照revision 从 1 开始自增回滚就是取出指定版本的 content 覆盖回当前内容。CREATE TABLE entry_revision ( id BIGINT PRIMARY KEY AUTO_INCREMENT, entry_id BIGINT NOT NULL COMMENT 归属词条, revision INT NOT NULL COMMENT 版本号从 1 开始递增, content LONGTEXT COMMENT 该版本的正文快照, editor VARCHAR(50) COMMENT 编辑者账号, edit_reason VARCHAR(200) COMMENT 编辑摘要如修正错别字, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_entry_revision (entry_id, revision) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT词条版本表;(entry_id, revision)联合唯一索引保证同一个词条下版本号不会重复。查询某词条所有历史版本就是WHERE entry_id ? ORDER BY revision DESC。这里有个设计习惯版本表只存正文快照不存词条名和摘要因为名称和摘要的变更频率远低于正文而且主表里有 update_time 可以追溯真要完整审计可以再扩展但作为课程设计和简历项目快照到正文这一层足够撑起回滚功能。4. 复刻百科核心体验搜索命中、词条渲染与编辑发布库表稳定之后剩下的就是三个核心链路搜索怎么命中、详情页怎么渲染、编辑怎么落库。这三个链路单独看不难连起来跑通才体会到模仿的味道——搜索排序要像原版那样把精确命中放最前详情页 URL 要像原版那样用词条名而不是数字 ID编辑要能留痕能回滚。4.1 搜索接口名称优先加摘要兜底的排序 SQL打开百度百科搜索框输入力学排在前面的一定是词条名精确叫力学的结果然后是力学XXX这种前缀命中的再往后才是正文或摘要里提到力学的。模拟这套排序核心是 ORDER BY 里的 CASE WHEN 打分select idsearchByKeyword resultTypecom.example.baike.vo.EntryVO SELECT e.id, e.name, e.summary, e.view_count, c.name AS categoryName FROM entry e LEFT JOIN category c ON e.category_id c.id WHERE e.status 1 AND ( e.name LIKE CONCAT(%, #{keyword}, %) OR e.summary LIKE CONCAT(%, #{keyword}, %)) ORDER BY CASE WHEN e.name #{keyword} THEN 0 WHEN e.name LIKE CONCAT(#{keyword}, %) THEN 1 ELSE 2 END, e.view_count DESC LIMIT #{offset}, #{pageSize} /select同样处理前缀匹配时e.name LIKE CONCAT(#{keyword}, %)不会命中词条名带空格或标点的场景但作为第一版已经够用。三个关键点第一模糊匹配用CONCAT(%, #{keyword}, %)不要直接写%${keyword}%后者存在 SQL 注入风险第二CASE 打分把精确匹配定为 0、前缀匹配定为 1、其余定为 2这就是像不像原版的核心第三同层级内用view_count DESC做热度排序浏览量高的词条排前面这和原版热门优先的体验一致。对应的 Mapper 接口Mapper public interface EntryMapper { ListEntryVO searchByKeyword(Param(keyword) String keyword, Param(offset) int offset, Param(pageSize) int pageSize); }分页参数直接用LIMIT #{offset}, #{pageSize}前端搜索页每页 20 条第 N 页的偏移量就是(N - 1) * 20。这套 SQL 在数据量几万条时性能没有问题但深翻页的坑会在避坑章展开。4.2 词条详情页目录式 URL 与模板渲染百度百科的词条地址是/item/词条名这种目录式风格而不是/detail?id123。模仿做法是用GetMapping(/entry/{name})直接把词条名放进路径GetMapping(/entry/{name}) public String detail(PathVariable String name, Model model) { EntryVO entry entryService.getByName(name); if (entry null) { return error/404; } // 浏览量 1 放异步或队列别放在主链路里同步执行 entryService.incrementViewAsync(entry.getId()); model.addAttribute(entry, entry); return entry/detail; }URL 上的中文词条名浏览器请求时自动做百分号编码Controller 拿到的name已经是解码后的中文不需要额外处理。真正要注意的是词条名里如果含/字符路径解析会出问题我一般在上传词条时就把/替换成全角或过滤掉。详情页模板里最关键的渲染点是正文!-- 正文是编辑器生成的富文本 HTML必须用 utext 输出 -- div classentry-content th:utext${entry.content}/divThymeleaf 里th:text会转义 HTML 标签th:utext才原样输出。正文必须用 utext否则p、img全变成纯文本显示在页面上但用了 utext 就意味着服务端必须做好 XSS 过滤否则就是给攻击者留后门这个坑在避坑章详细说。词条名、摘要这些普通文本字段一律用th:text让模板自动转义。4.3 编辑发布链路事务里先写版本再更新正文百科系统的编辑不是改一行 UPDATE 那么简单它牵扯版本表、内容表、主表三处变更。我用的核心链路是先插版本快照再更新正文整个操作包在一个事务里Transactional(rollbackFor Exception.class) public void publishEdit(EntryEditForm form) { // 1. 按词条名拿主记录不存在直接抛业务异常 Entry entry entryMapper.selectByName(form.getName()); if (entry null) { throw new BizException(词条不存在请先创建); } // 2. 先插入一条新的版本快照revision 在旧版本号基础上 1 EntryRevision revision new EntryRevision(); revision.setEntryId(entry.getId()); revision.setRevision(entry.getRevision() 1); revision.setContent(form.getContent()); revision.setEditor(form.getEditor()); revision.setEditReason(form.getEditReason()); revisionMapper.insert(revision); // 3. 再更新内容表和主表的版本号 entryContentMapper.updateContent(entry.getId(), form.getContent()); entryMapper.updateRevision(entry.getId(), entry.getRevision() 1); }注意Transactional必须写rollbackFor Exception.class因为 Spring 默认只对 RuntimeException 回滚业务异常类 Exception 默认不回滚忘了这个参数等于事务白开。顺序上为什么要先插版本再更新正文如果先更新正文再写版本快照第二步失败时事务虽然整体回滚但日志里排查时会看到版本号和内容对不上先插版本正文更新失败时版本快照也一起回滚两边始终一致。这个链路保证了每个已发布版本都有对应的正文快照回滚功能就是从这里来的。5. 百科系统避坑清单五个绕不开的坑下面这五条是我和周围同事做同类项目时真实踩过的每一条都按现象、原因、解决写你排查时可以直接照着定位。5.1 中文搜索返回空字符集没对齐现象词条表里用客户端工具看中文一切正常但在搜索框输入中文关键词查出来的结果永远为空或者干脆报Illegal mix of collations异常。原因这是百科系统最典型的字符集问题一般出在三个地方不一致——建库时用的默认字符集不是 utf8mb4、JDBC 连接 URL 没指定字符集、前端页面请求编码不对。查一下定位SHOW CREATE TABLE entry; SHOW VARIABLES LIKE character_set_server;如果character_set_server显示 latin1 或 utf8非 utf8mb4那建表时继承的字符集就有问题。utf8 在 MySQL 里最多存 3 字节生僻字和 emoji 会直接写入失败或变成问号。解决建库时强制指定DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ciJDBC URL 加上characterEncodingutf8mb4同时给项目加一个编码过滤器强制请求和响应都是 UTF-8。三个位置对齐后中文搜索基本不再翻车。血泪经验是客户端工具看着对不代表连接层对一定要在应用日志里打印实际执行的 SQL 去核对。5.2 词条被静默覆盖并发编辑没有防线现象两个人同时编辑同一个词条后提交的人把前一个人的改动整个覆盖掉了且没有任何提示等发现时数据已经丢了。原因编辑接口的 SQL 是无条件 UPDATE不校验版本号同时词条表如果没有唯一索引还可能插入两条同名词条搜索时出现诡异重复。这是我初版代码最典型的翻车点——功能能跑但经不起两个人同时操作。解决第一道防线是entry表的uk_entry_name唯一索引从数据库层堵住同名词条插入第二道防线是乐观锁编辑提交时带上页面里隐藏的当前版本号UPDATE entry SET summary #{summary}, revision revision 1 WHERE id #{id} AND revision #{expectRevision};执行后看返回的影响行数如果是 0说明这期间词条被别人改过直接提示词条已被其他人修改请刷新后重试。这就是后端处理并发最经典的乐观锁写法实现成本低效果立竿见影。5.3 正文被脚本注入百科类系统的 XSS 重灾区现象后台编辑器里粘贴了一段带script的内容保存后在词条详情页弹出广告窗或者页面被完整跳转到别的地址。原因富文本编辑器提交的是 HTML服务端没做任何过滤直接入库详情页为了显示富文本用了th:utext脚本就原样执行了。XSS 在百科类系统里尤其危险因为词条是公开内容任何人编辑都可能带脚本。解决前端过滤不可靠攻击者直接 POST 就能绕过必须在服务端用白名单机制过滤。我用的方案是 Jsoup 的 Safelistpublic static String cleanContent(String html) { Safelist safelist Safelist.relaxed() .addTags(img, h1, h2, h3) .addAttributes(img, src, alt, width, height); return Jsoup.clean(html, safelist); }relaxed() 保留 p、br、strong、em 等常用标签我再补上图片和标题标签script、iframe、style 全被剔除。调用时机在保存编辑内容之前存进库里的已经是过滤后的安全 HTML。5.4 深翻页性能崩塌LIMIT 偏移的代价现象搜索结果翻到 100 页之后明显变慢接口耗时从 50 毫秒涨到 2 秒越往后越慢。原因LIMIT 200000, 20看着只取 20 条但 MySQL 要先扫描并丢弃前面 20 万行这部分全都在空转。数据量到十万级以后深翻页的消耗是线性的翻到底直接把数据库拖垮。解决搜索场景限制最大翻页深度比如 offset 超过某个阈值就不允许继续翻提示用户换更精确的关键词原版搜索引擎也是这么做的列表类场景改用游标分页记住上一页最后一条的 idSELECT ... WHERE id #{lastId} ORDER BY id ASC LIMIT #{pageSize}游标分页无论翻到多深扫描行数都恒定等于页大小这是深翻页的根治方案。但要注意它只能用于按 id 或时间排序的场景搜索场景的排序带 CASE 打分所以搜索页用限深度策略列表页用游标策略。5.5 上传图片 404本地路径没有映射到 URL现象后台编辑器上传图片成功返回的路径类似/upload/2025/xxx.jpg但在详情页打开图片链接是 404。原因图片文件确实写到了服务器磁盘但应用没有把磁盘路径映射成可访问的 URL。浏览器请求/upload/xxx.jpg时去 classpath 里找自然找不到。解决加一个静态资源映射配置把磁盘上传目录映射到 URL 路径Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // Windows 用 file:/D:/data/baike/upload/Linux 用 file:/data/baike/upload/ registry.addResourceHandler(/upload/**) .addResourceLocations(file:/data/baike/upload/); } }注意 Windows 和 Linux 的路径前缀写法不同Windows 要写file:/盘符:/路径/Linux 写file:/绝对路径/。这个本地映射只适合开发环境部署上线后图片应当走对象存储但作为百科系统的课程设计本地映射足够只要记住路径别写死在业务代码里统一收口在配置类。6. 把百科做活热榜聚合、相关推荐与缓存抖动6.1 首页热榜与相关词条两条能直接复用的查询首页要像原版那样有热门词条板块最简单可靠的做法不是每次实时查全表排序而是用一个定时任务每小时把浏览量前 20 的词条写入热榜表页面只查热榜表。相关词条推荐用标签重叠度算关联表在这里派上用场SELECT t2.entry_id, COUNT(*) AS overlap FROM entry_tag t1 JOIN entry_tag t2 ON t1.tag_id t2.tag_id WHERE t1.entry_id #{entryId} AND t2.entry_id ! #{entryId} GROUP BY t2.entry_id ORDER BY overlap DESC LIMIT 8;这条 SQL 的逻辑是找出和我共享标签最多的其他词条共享的标签越多相关性越高。复杂度不高但比随机的猜你喜欢有意义得多是相关推荐里性价比最高的入门写法。6.2 热词条缓存过期时间别设死值详情页是百科系统访问最集中的接口一定要加缓存。读多写少、数据一致性要求不高的词条正文非常适合 Redis 缓存。但缓存过期时间设成一个固定值比如统一的 30 分钟流量大时所有 key 同时过期数据库会被瞬间打穿这就是缓存雪崩。我的习惯是过期时间加随机抖动public EntryVO getByName(String name) { String key entry: name; String json redisTemplate.opsForValue().get(key); if (json ! null) { return JSON.parseObject(json, EntryVO.class); } EntryVO entry entryMapper.selectByName(name); if (entry ! null) { // 基础 30 分钟再加 1-10 分钟随机值避免同时失效 int expire 1800 ThreadLocalRandom.current().nextInt(600); redisTemplate.opsForValue().set(key, JSON.toJSONString(entry), expire, TimeUnit.SECONDS); } return entry; }编辑发布时记得删掉对应 key下次访问重新回源数据库即可代价几乎为零。验证这套做法很简单压测详情页接口对比开缓存前后的 QPS 和平均耗时能看到数量级的差距。做这类系统我的习惯是每加一个查询先看执行计划每上线一个页面先想这页被刷爆怎么办。百科这类系统最怕的不是功能少而是功能堆了很多却扛不住一轮访问。这套流程做完之后至少你能跟人说清楚每个表为什么这么拆、每次搜索为什么这么快、并发编辑为什么不会打架。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →