Skip to content

系统架构设计文档

文档基础信息

  • 适用项目:grove/reed系统
  • 核心用途:明确grove/reed系统整体架构设计方案、技术选型、模块划分、交互逻辑及部署架构,为项目开发、测试、运维提供顶层设计依据,保障系统具备高可用、高并发、易扩展、易维护的特性,贴合业务长期发展需求。

架构设计总览

核心设计原则

  • 分层解耦:系统分层清晰,各层级职责单一,降低模块耦合度,便于独立开发、测试与维护
  • 高可用性:规避单点故障,支持故障快速自愈,保障系统7×24小时稳定运行
  • 可扩展性:支持横向扩容、功能模块化插拔,适配业务规模与需求迭代
  • 性能最优:优化数据流转、接口调用逻辑,减少冗余操作,提升系统响应速度
  • 安全可控:构建全链路安全防护体系,保障数据安全、访问安全、运行安全

整体架构模式

本系统采用前后端分离+多层架构设计模式,结合微服务/单体应用(按需选型)架构,拆分业务模块、数据模块、运维模块,实现请求接入、业务处理、数据存储、运维管控的全流程闭环,兼顾开发效率与系统性能。

技术架构选型

结合项目业务需求、性能要求与团队技术栈,选用成熟稳定、生态完善的技术体系,核心技术选型如下,统一版本避免兼容性问题:

后端技术栈

  • 核心框架:SpringBoot
  • ORM框架:自定义简易版ORM
  • API规范:RESTfulAPI
  • JSON组件:Fastjson2
  • 工具类:Lombok

依赖版本开发阶段不定期更新到最新

数据存储层

  • 关系型数据库:PostgreSQL
  • 文件存储:本地存储

中间件与运维工具

  • 反向代理/负载均衡:Nginx
  • 服务器系统:Linux+Docker
  • 项目管理:Git、Maven

系统分层架构设计

系统自上而下分为四层,各层级单向依赖,禁止跨层调用,明确职责边界,保障架构规整性:

接入层(前端展示层)

负责用户交互与页面渲染,承接用户操作请求,完成页面展示、数据校验、请求转发,适配PC端、移动端等多终端,通过RESTfulAPI与后端交互,实现前后端数据互通。

网关层(请求接入层)

统一请求入口,负责请求路由、负载均衡、权限校验、限流熔断、日志记录,拦截非法请求,实现请求流量管控,减轻后端业务层压力,保障后端服务安全稳定。

业务逻辑层

系统核心层,拆分基础通用模块与核心业务模块,处理业务逻辑、接口实现、数据校验、事务管控,调用数据层接口完成数据读写,封装业务能力供接入层调用,是系统功能实现的核心载体。

数据持久层

负责数据的存储、读取与维护,对接各类数据库,完成数据增删改查操作,优化数据查询逻辑,保障数据一致性、完整性,屏蔽数据存储细节,为业务层提供数据支撑。

时序图

功能模块架构划分

按照业务归属与功能特性,将系统拆分为三大核心模块组,各模块独立开发、低耦合高内聚,可独立迭代维护,具体划分如下:

基础支撑模块

  • bean:bean/factory组件。
  • boot:启动模块,配置文件等。
  • console:控制台模块,自动crud功能。
  • controller:控制器组件。
  • crypto:加解密组件。
  • dao:DAO封装组件,包含数据库连接,数据表维护,简易ORM封装。
  • icon:Icon模块,可动态替换前端icon。
  • langpack:多语言模块,用于增减语言资源。
  • message:消息模块。
  • office:Office相关封装。
  • scheduling:定时器组件。
  • setting:设置/配置模块。
  • user:用户模块。
  • util:工具包组件。

核心业务模块

  • ai:AI模块。
  • boot:启动模块,配置文件等。
  • coin:虚拟币/钱包模块。
  • guidline:玩法说明模块。
  • h5app:社区app参数模块。
  • luckywheel:幸运转盘模块。
  • message:消息模块。
  • payment:c2c支付模块。
  • player:玩家信息模块。
  • prizewheel:转盘模块。
  • reedenveloperrain:红包雨模块。
  • redpacketrain:红包雨模块。
  • silentlyrsync:数据抓取/同步模块。
  • tenant:租户模块。

数据库脚本

系统每次启动时会检测各模块下的以下脚本文件,并自动执行脚本创建和修改:

  • create.sql用于保存创建脚本,及初始化数据,表结构存在后就不会再执行此文件
  • alter.sql用于保存修改脚本,如果执行create.sql时已存在alter.sql,则里面的脚本将被自动标记为已执行,防止重复执行

所有数据库的变动都必须更新到这两个文件中,以确保所有变更都正常在开发环境、测试环境中正确运行,防止更新到正式环境后出现表结构不同步问题。

持久化操作

grove-dao提供了

  • crud工具用于基础的增删改查操作,简化orm映射及防止sql注入攻击
  • sql工具用于完全的sql操作,但建议优先使用crud
  • 原则上不允许使用多表关联查询,若确实需要请与技术负责人讨论

多语言配置

  1. 每个模块可在langpack.{lang}.jsonlangpack/{lang}.json中配置资源
  2. 前端langpacks/{lang}.json可以配置资源
  3. 项目运行后,管理员可进入后台【设置】|【语言包】修改资源

资源使用优先级:3>2>1

基础增删改查

  • 可在模块下配置crud.jsonconsole后台可自动处理基础增删改查操作,减少重复代码
  • crud.json中配置的参数校验规则,会同时在前端和api进行校验

启动服务

  • 运行bin/boot.sh

模块创建工具

修改参数,运行后可自动生成基础的模块文件。

java
package org.example.javatest;

import java.io.File;
import java.nio.file.Files;

public class Module {
    private static final String WORK = "/home/developer/work/";

    public static void main(String[] args) throws Throwable {
        String project = "reed";
        String name = "Notification"; // 主模块
        String sub = "Message"; // 子模块,可空
        String[][] properties = new String[][] {
                { "appId", "String", "App ID" },
                { "platform", "String", "平台" },
                { "token", "String", "设备Token" },
                { "user", "String", "用户ID" },
                { "session", "String", "Session ID" },
                { "title", "String", "标题" },
                { "body", "String", "内容" },
                { "sound", "String", "声音" },
                { "status", "int", "状态:0-待发;1-成功;2-失败" },
                { "time", "Timestamp", "时间" },
        };
        String[] path = path(project, name, sub);
        model(project, path[0], name, sub, properties);
        service(project, path[0], name, sub);
        create(path[1], name, sub, properties);
        langpack(path[1], properties);
        crud(path[1], name, sub, properties);
        if (sub.isBlank()) {
            pom(project, name);
        }
    }

    private static String[] path(String project, String name, String sub) {
        String[] path = new String[] {
                String.format("%s%s/%s-%s/src/main/java/io/delta/%s/%s/%s", WORK, project, project, name.toLowerCase(),
                        project, name.toLowerCase(), sub.toLowerCase()),
                String.format("%s%s/%s-%s/src/main/resources/io/delta/%s/%s/%s", WORK, project, project,
                        name.toLowerCase(), project, name.toLowerCase(), sub.toLowerCase()) };

        for (int i = 0; i < path.length; i++) {
            if (!path[i].endsWith("/"))
                path[i] += "/";
            boolean ok = new File(path[i]).mkdirs();
            if (!ok) {
                System.out.println("创建目录失败:" + path[i]);
            }
        }

        return path;
    }

    private static void model(String project, String path, String name, String sub, String[][] properties)
            throws Throwable {
        boolean date = false;
        boolean time = false;
        for (String[] property : properties) {
            if ("Date".equals(property[1]))
                date = true;
            else if ("Timestamp".equals(property[1]))
                time = true;
        }
        StringBuilder sb = new StringBuilder(String.format("""
                package io.delta.%s.%s%s;

                import jakarta.persistence.Table;
                import io.delta.grove.dao.orm.ModelSupport;
                import org.springframework.beans.factory.config.BeanDefinition;
                import org.springframework.context.annotation.Scope;
                import org.springframework.stereotype.Component;
                """, project, name.toLowerCase(), sub.isBlank() ? "" : ("." + sub.toLowerCase())));
        if (date || time) {
            sb.append('\n');
            if (date)
                sb.append("import java.sql.Date;\n");
            if (time)
                sb.append("import java.sql.Timestamp;\n");
        }
        sb.append(String.format("""
                import lombok.Getter;
                import lombok.Setter;

                @Getter
                @Setter
                @Component(%sModel.NAME)
                @Scope(BeanDefinition.SCOPE_PROTOTYPE)
                @Table(name = "t%s%s")
                public class %sModel extends ModelSupport {
                    static final String NAME = "%s.%s%s";
                """, sub.isBlank() ? name : sub, format(name, '_'), sub.isBlank() ? "" : format(sub, '_'),
                sub.isBlank() ? name : sub,
                project, format(name, '-').substring(1), sub.isBlank() ? "" : ("." + format(sub, '-').substring(1))));
        if (properties.length > 0) {
            sb.append("\n");
            for (String[] property : properties) {
                sb.append(String.format("    private %s %s; // %s\n", property[1].replaceAll("Id|Text", "String"),
                        property[0], property[2]));
            }
        }

        sb.append("}");
        Files.writeString(new File(path + (sub.isBlank() ? name : sub) + "Model.java").toPath(), sb.toString().trim());
    }

    private static void service(String project, String path, String name, String sub) throws Throwable {
        String className = sub.isBlank() ? name : sub;
        String string = String.format("""
                package io.delta.%s.%s%s;

                public interface %sService {
                }
                """, project, name.toLowerCase(), sub.isBlank() ? "" : ("." + sub.toLowerCase()), className);
        Files.writeString(new File(path + (sub.isBlank() ? name : sub) + "Service.java").toPath(), string);

        string = String.format("""
                package io.delta.%s.%s%s;

                import org.springframework.web.bind.annotation.RequestMapping;
                import org.springframework.web.bind.annotation.RestController;

                @RestController(%sModel.NAME + ".service")
                @RequestMapping("/%s%s/")
                public class %sServiceImpl implements %sService {
                }
                """, project, name.toLowerCase(), sub.isBlank() ? "" : ("." + sub.toLowerCase()), className,
                format(name, '-').substring(1),
                sub.isBlank() ? "" : ("/" + format(sub, '-').substring(1)), className, className);
        Files.writeString(new File(path + (sub.isBlank() ? name : sub) + "ServiceImpl.java").toPath(), string);
    }

    private static void create(String path, String name, String sub, String[][] properties) throws Throwable {
        StringBuilder sb = new StringBuilder()
                .append(String.format("CREATE TABLE t%s%s (\n    c_id CHAR(32) PRIMARY KEY NOT NULL",
                        format(name, '_'), sub.isBlank() ? "" : format(sub, '_')));
        for (String[] property : properties) {
            sb.append(String.format(",\n    c_%s %s", format(property[0], '_'), switch (property[1]) {
                case "Id" -> "CHAR(32)";
                case "String" -> "VARCHAR(255)";
                case "long" -> "BIGINT";
                case "Timestamp" -> "TIMESTAMP WITH TIME ZONE";
                default -> property[1].toUpperCase();
            }));
        }
        sb.append("\n);");
        Files.writeString(new File(path + "create.sql").toPath(), sb.toString().trim());
    }

    private static void langpack(String path, String[][] properties) throws Throwable {
        StringBuilder sb = new StringBuilder().append("{");
        for (String[] property : properties) {
            if (sb.length() > 1) {
                sb.append(',');
            }
            sb.append(String.format("\n    \"%s\": \"%s\"", property[0], property[2]));
        }
        sb.append("\n}");
        Files.writeString(new File(path + "langpack.zh.json").toPath(), sb.toString().trim());
    }

    private static void crud(String path, String name, String sub, String[][] properties) throws Throwable {
        StringBuilder sb = new StringBuilder().append(String.format("""
                {
                  "mapping": "/%s%s/",
                  "properties": [
                """.trim(), format(name, '-').substring(1),
                sub.isBlank() ? "" : ("/" + format(sub, '-').substring(1))));
        for (int i = 0; i < properties.length; i++) {
            if (i > 0)
                sb.append(',');
            switch (properties[i][1]) {
                case "Text" ->
                    sb.append(String.format("\n    {\n      \"name\": \"%s\",\n      \"type\": \"textarea\"\n    }",
                            properties[i][0]));
                case "Date" ->
                    sb.append(String.format("\n    {\n      \"name\": \"%s\",\n      \"type\": \"date\"\n    }",
                            properties[i][0]));
                case "Timestamp" ->
                    sb.append(String.format("\n    {\n      \"name\": \"%s\",\n      \"type\": \"datetime\"\n    }",
                            properties[i][0]));
                default -> sb.append(String.format("\n    {\n      \"name\": \"%s\"\n    }", properties[i][0]));
            }
        }
        sb.append("""
                 \s
                  ],
                  "query": {
                    "toolbar": [
                      {
                        "type": "create"
                      }
                    ],
                    "actions": [
                      {
                        "type": "modify"
                      },
                      {
                        "type": "delete"
                      }
                    ]
                  }
                }
                """);
        Files.writeString(new File(path + "crud.json").toPath(), sb.toString().trim());
    }

    private static String format(String name, char ch) {
        StringBuilder sb = new StringBuilder();
        for (char c : name.toCharArray()) {
            if (Character.isUpperCase(c)) {
                sb.append(ch).append(Character.toLowerCase(c));
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    private static void pom(String project, String name) throws Throwable {
        name = name.toLowerCase();
        String xml = """
                <?xml version="1.0" encoding="UTF-8"?>
                <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
                    <modelVersion>4.0.0</modelVersion>
                    <parent>
                        <artifactId>%s</artifactId>
                        <groupId>io.delta.%s</groupId>
                        <version>1.0</version>
                    </parent>

                    <artifactId>%s-%s</artifactId>
                    <version>1.0</version>

                    <name>%s-%s</name>
                    <url>http://www.example.com</url>

                    <properties>
                        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                    </properties>

                    <dependencies>
                        <dependency>
                            <groupId>io.delta.grove</groupId>
                            <artifactId>grove-controller</artifactId>
                            <version>1.0</version>
                        </dependency>
                    </dependencies>
                </project>
                """;
        xml = String.format(xml, project, project, project, name, project, name);
        Files.writeString(
                new File(String.format("%s%s/%s-%s/pom.xml", WORK, project, project, name)).toPath(),
                xml.trim());
    }
}

部署架构设计

多环境部署架构

遵循环境隔离原则,分为开发、测试、生产三大环境,各环境独立部署、配置分离,避免相互干扰,适配不同阶段项目运作需求,具体部署规范贴合部署流程文档要求。

集群与扩容设计

  • 生产环境采用集群部署模式,后端服务多实例部署,结合Nginx实现负载均衡,规避单点故障
  • 支持横向扩容,用户量增长、业务并发提升时,可快速新增服务实例,提升系统承载能力

网络架构规划

内外网隔离部署,外网仅开放80、443端口供用户访问;配置防火墙、安全组规则,拦截恶意网络请求,筑牢网络安全防线。

非功能架构设计

性能设计

  • 热点数据存入缓存,降低数据库查询压力,核心接口响应时长控制在3秒内
  • 优化SQL语句、建立合理索引,避免慢查询,提升数据读写效率
  • 接口限流、异步处理,应对高并发场景,防止系统拥堵崩溃

安全设计

  • 身份认证:采用Token令牌认证,登录超时自动失效,防范非法登录
  • 数据安全:敏感数据加密存储、传输,隐私信息脱敏展示,防止数据泄露
  • 防护机制:加入SQL注入、XSS攻击、CSRF攻击防护,拦截恶意请求

可维护性设计

  • 统一日志规范,全链路日志追踪,便于问题排查定位
  • 代码遵循标准化规范,注释完善,便于后续迭代维护
  • 支持配置热更新,无需重启服务即可修改核心配置

架构风险与应对措施

  • 单点故障风险:核心服务集群部署、主备切换,避免单点失效导致系统瘫痪
  • 数据丢失风险:定期数据库备份、日志备份,支持故障后快速数据恢复
  • 性能瓶颈风险:提前压测,优化架构与代码,预留扩容空间,应对流量峰值
  • 安全攻击风险:多层安全防护,定期漏洞扫描,及时修复安全隐患