栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java > SpringBoot

SpringBoot技术栈搭建个人博客

SpringBoot 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

SpringBoot技术栈搭建个人博客

前言:在之前,我们已经完成了项目的基本准备,那么就可以开始后台开发了,突然又想到一个问题,就是准备的时候只是设计了前台的RESTful APIs,但是后台管理我们同样也是需要API的,那么就在这一篇里面一起实现了吧...

  • 前序文章链接:SpringBoot技术栈搭建个人博客【项目准备】:https://www.jianshu.com/p/0293368fe750

一些设计上的调整

在查了一些资料和吸收了一些评论给出良好的建议之后,我觉得有必要对一些设计进行一些调整:

  • 1)数据库:命名应该更加规范,比如表示分类最好用category而不是sort,表示评论最好用comment而不是message;

  • 2)RESful APIs:在准备着手开始写后台的时候就已经发现,本来想的是凡是以/api开头的都是暴露出来给前端用的,凡是以/admin开头的都是给后台使用的地址,但是意外的没有设计后天的API也把一些删除命令暴露给了前端,这就不好了重新设计设计;

  • 3)命名规范的问题:因为使用MyBatis逆向工程自动生成的时候,配置了一个useActualColumnNames使用表真正名称的东西,所以整得来生成POJO类基础字段有下划线,看着着实有点不爽,把它给干掉干掉...;

数据库调整

把字段规范了一下,并且删除了分类下是否有效的字段(感觉这种不经常变换的字段留着也没啥用干脆干掉..),所以调整为了下面这个样子(调整字段已标红):

然后重新使用生成器自动生成对应的文件,注意记得修改generatorConfig.xml文件中对应的数据库名称;

创建和修改时间的字段设置

通过查资料发现其实我们可以通过直接设置数据库来自动更新我们的modified_by字段,并且可以像设置初始值那样给create_by和modified_by两个字段以当前时间戳设置默认值,这里具体以tbl_article_info这张表为例:

CREATE TABLE `tbl_article_info` (  `id` bigint(40) NOT NULL AUTO_INCREMENT COMMENT '主键',  `title` varchar(50) NOT NULL DEFAULT '' COMMENT '文章标题',  `summary` varchar(300) NOT NULL DEFAULT '' COMMENT '文章简介,默认100个汉字以内',  `is_top` tinyint(1) NOT NULL DEFAULT '0' COMMENT '文章是否置顶,0为否,1为是',  `traffic` int(10) NOT NULL DEFAULT '0' COMMENT '文章访问量',  `create_by` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',  `modified_by` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATe CURRENT_TIMESTAMP COMMENT '修改日期',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

我们通过设置DEFAULT为CURRENT_TIMESTAMP,然后给modified_by字段多添加了一句ON UPDATE CURRENT_TIMESTAMP,这样它就会在更新的时候将该字段的值设置为更新时间,这样我们就不用在后台关心这两个值了,也少写了一些代码(其实是写代码的时候发现可以这样偷懒..hhh...);

RESTful APIs重新设计

我们需要把一些不能够暴露给前台的API收回,然后再设计一下后台的API,捣鼓了一下,最后大概是这个样子了:

后台Restful APIs:

前台开放RESful APIs:

这些API只是用来和前端交互的接口,另外一些关于日志啊之类的东西就直接在后台写就行了,OK,这样就爽多了,可以开始着手写代码了;

基本配置

随着配置内容的增多,我逐渐的想要放弃.yml的配置文件,主要的一点是这东西不好对内容进行分类(下图是简单配置了一些基本文件后的.yml和.properties文件的对比)..

最后还是用回.properties文件吧,不分类还是有点难受

编码设置

我们首先需要解决的是中文乱码的问题,对应GET请求,我们可以通过修改Tomcat的配置文件【server.xml】来把它默认的编码格式改为UTF-8,而对于POST请求,我们需要统一配置一个拦截器一样的东西把请求的编码统一改成UTF-8:

## ——————————编码设置——————————spring.http.encoding.charset=UTF-8spring.http.encoding.force=truespring.http.encoding.enabled=trueserver.tomcat.uri-encoding=UTF-8

但是这样设置之后,在后面的使用当中还是会发生提交表单时中文乱码的问题,在网上搜索了一下找到了解决方法,新建一个【config】包创建下面这样一个配置类:

@Configurationpublic class MyWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter {    @Bean
    public HttpMessageConverter responseBodyConverter() {
        StringHttpMessageConverter converter = new StringHttpMessageConverter(Charset.forName("UTF-8"));        return converter;
    }    @Override
    public void configureMessageConverters(List> converters) {        super.configureMessageConverters(converters);
        converters.add(responseBodyConverter());
    }    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(false);
    }
}
数据库及连接池配置

决定这一次试试Druid的监控功能,所以给一下数据库的配置:

## ——————————数据库访问配置——————————
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://127.0.0.1:3306/blog?characterEncoding=UTF-8
spring.datasource.username = root
spring.datasource.password = 123456

# 下面为连接池的补充设置,应用到上面所有数据源中
# 初始化大小,最小,最大
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=20
# 配置获取连接等待超时的时间
spring.datasource.druid.max-wait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
spring.datasource.druid.test-while-idle=truespring.datasource.druid.test-on-borrow=falsespring.datasource.druid.test-on-return=false# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.druid.pool-prepared-statements=truespring.datasource.druid.max-pool-prepared-statement-per-connection-size=20# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.druid.filters=stat,wall,log4j
日志配置

在SpringBoot中其实已经使用了Logback来作为默认的日志框架,这是log4j作者推出的新一代日志框架,它效率更高、能够适应诸多的运行环境,同时天然支持SLF4J,在SpringBoot中我们无需再添加额外的依赖就能使用,这是因为在spring-boot-starter-web包中已经有了该依赖了,所以我们只需要进行配置使用就好了

第一步:创建logback-spring.xml

当项目跑起来的时候,我们不可能还去看控制台的输出信息吧,所以我们需要把日志写到文件里面,在网上找到一个例子(链接:http://tengj.top/2017/04/05/springboot7/)


    logback
    
    
    
    
        
        
            %d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n
        
    

    
    
        
            ${log.path}/logback.%d{yyyy-MM-dd}.log
        
        
            %d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n
        
    

    
        
        
    

    
    

在Spring Boot中你只要按照规则组织文件名,就能够使得配置文件能够被正确加载,并且官方推荐优先使用带有-spring的文件名作为日志的配置(如上面使用的logback-spring.xml,而不是logback.xml),满足这样的命名规范并且保证文件在src/main/resources下就好了;

第二步:重启项目检查是否成功

我们定义的目录位置为/log/wmyskxz/,但是在项目的根目录下并没有发现这样的目录,反而是在当前盘符的根目录..不是很懂这个规则..总之是成功了的..

打开是密密麻麻一堆跟控制台一样的【info】级别的信息,因为这个系统本身就比较简单,所以就没有必要去搞什么文本切割之类的东西了,ok..日志算是配置完成;

实际测试了一下,上线之后肯定需要调整输出级别的,不然日志文件就会特别大...

拦截器配置

我们需要对地址进行拦截,对所有的/admin开头的地址请求进行拦截,因为这是后台管理的默认访问地址开头,这是必须进行验证之后才能访问的地址,正如上面的RESTful APIs,这里包含了一些增加/删除/更改/编辑一类的操作,而统统这些操作都是不能够开放给用户的操作,所以我们需要对这些地址进行拦截:

第一步:创建User实体类

做验证还是需要添加session,不然不好弄,所以我们还是得创建一个常规的实体:

public class User {    private String username;    private String password;    }
第二步:创建拦截器并继承HandlerInterceptor接口

在【interceptor】包下新建一个【BackInterceptor】类并继承HandlerInterceptor接口:

public class BackInterceptor implements HandlerInterceptor {    private static String username = "wmyskxz";    private static String password = "123456";    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        boolean flag = true;
        User user = (User) request.getSession().getAttribute("user");        if (null == user) {
            flag = false;
        } else {            // 对用户账号进行验证,是否正确
            if (user.getUsername().equals(username) && user.getPassword().equals(password)) {
                flag = true;
            } else {
                flag = false;
            }
        }        return flag;
    }
}

在拦截器中,我们从session中取出了user,并判断是否符合要求,这里我们直接写死了(并没有更改密码的需求,但需要加密),而且我们并没有做任何的跳转操作,原因很简单,根本就不需要跳转,因为访问后台的用户只有我一个人,所以只需要我知道正确的登录地址就可以了...

第三步:在配置类中复写addInterceptors方法

刚才我们在设置编码的时候自己创建了一个继承自WebMvcConfigurerAdapter的设置类,我们需要复写其中的addInterceptors方法来为我们的拦截器添加配置:

@Overridepublic void addInterceptors(InterceptorRegistry registry) {    // addPathPatterns 用于添加拦截规则
    // excludePathPatterns 用户排除拦截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/admin@Overridepublic void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/admin/login").setViewName("login.html");    super.addViewControllers(registry);
}
  • 注意:login.html记得要放在【templates】下才会生效哦...(我试过使用login绑定视图名不成功,只能写全了...)

访问日志记录

上面我们设置了访问限制的拦截器,对后台访问进行了限制,这是拦截器的好处,我们同样也使用拦截器对于访问数量进行一个统计

第一步:编写前台访问拦截器

对照着数据库的设计,我们需要保存的信息都从request对象中去获取,然后保存到数据库中即可,代码也很简单:

public class ForeInterceptor implements HandlerInterceptor {    @Autowired
    SysService sysService;    private SysLog sysLog = new SysLog();    private SysView sysView = new SysView();    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        // 访问者的IP
        String ip = request.getRemoteAddr();        // 访问地址
        String url = request.getRequestURL().toString();        //得到用户的浏览器名
        String userbrowser = BrowserUtil.getOsAndBrowserInfo(request);        // 给SysLog增加字段
        sysLog.setIp(StringUtils.isEmpty(ip) ? "0.0.0.0" : ip);
        sysLog.setOperateBy(StringUtils.isEmpty(userbrowser) ? "获取浏览器名失败" : userbrowser);
        sysLog.setOperateUrl(StringUtils.isEmpty(url) ? "获取URL失败" : url);        // 增加访问量
        sysView.setIp(StringUtils.isEmpty(ip) ? "0.0.0.0" : ip);
        sysService.addView(sysView);        return true;
    }    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();        // 保存日志信息
        sysLog.setRemark(method.getName());
        sysService.addLog(sysLog);
    }    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
  • 注意:但是需要注意的是测试的时候别把拦截器开了(主要是postHandle方法中中无法强转handler),不然不方便测试...

BrowserUtil是找的网上的一段代码,直接黏贴复制放【util】包下就可以了:

public class BrowserUtil {    
    public static String getOsAndBrowserInfo(HttpServletRequest request) {
        String browserDetails = request.getHeader("User-Agent");
        String userAgent = browserDetails;
        String user = userAgent.toLowerCase();

        String os = "";
        String browser = "";        //=================OS Info=======================
        if (userAgent.toLowerCase().indexOf("windows") >= 0) {
            os = "Windows";
        } else if (userAgent.toLowerCase().indexOf("mac") >= 0) {
            os = "Mac";
        } else if (userAgent.toLowerCase().indexOf("x11") >= 0) {
            os = "Unix";
        } else if (userAgent.toLowerCase().indexOf("android") >= 0) {
            os = "Android";
        } else if (userAgent.toLowerCase().indexOf("iphone") >= 0) {
            os = "IPhone";
        } else {
            os = "UnKnown, More-Info: " + userAgent;
        }        //===============Browser===========================
        if (user.contains("edge")) {
            browser = (userAgent.substring(userAgent.indexOf("Edge")).split(" ")[0]).replace("/", "-");
        } else if (user.contains("msie")) {
            String substring = userAgent.substring(userAgent.indexOf("MSIE")).split(";")[0];
            browser = substring.split(" ")[0].replace("MSIE", "IE") + "-" + substring.split(" ")[1];
        } else if (user.contains("safari") && user.contains("version")) {
            browser = (userAgent.substring(userAgent.indexOf("Safari")).split(" ")[0]).split("/")[0]
                    + "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1];
        } else if (user.contains("opr") || user.contains("opera")) {            if (user.contains("opera")) {
                browser = (userAgent.substring(userAgent.indexOf("Opera")).split(" ")[0]).split("/")[0]
                        + "-" + (userAgent.substring(userAgent.indexOf("Version")).split(" ")[0]).split("/")[1];
            } else if (user.contains("opr")) {
                browser = ((userAgent.substring(userAgent.indexOf("OPR")).split(" ")[0]).replace("/", "-"))
                        .replace("OPR", "Opera");
            }

        } else if (user.contains("chrome")) {
            browser = (userAgent.substring(userAgent.indexOf("Chrome")).split(" ")[0]).replace("/", "-");
        } else if ((user.indexOf("mozilla/7.0") > -1) || (user.indexOf("netscape6") != -1) ||
                (user.indexOf("mozilla/4.7") != -1) || (user.indexOf("mozilla/4.78") != -1) ||
                (user.indexOf("mozilla/4.08") != -1) || (user.indexOf("mozilla/3") != -1)) {
            browser = "Netscape-?";

        } else if (user.contains("firefox")) {
            browser = (userAgent.substring(userAgent.indexOf("Firefox")).split(" ")[0]).replace("/", "-");
        } else if (user.contains("rv")) {
            String IEVersion = (userAgent.substring(userAgent.indexOf("rv")).split(" ")[0]).replace("rv:", "-");
            browser = "IE" + IEVersion.substring(0, IEVersion.length() - 1);
        } else {
            browser = "UnKnown, More-Info: " + userAgent;
        }        return os + "-" + browser;
    }
}
第二步:设置拦截地址

还是在刚才的配置类中,新增这么一条:

@Overridepublic void addInterceptors(InterceptorRegistry registry) {    // addPathPatterns 用于添加拦截规则
    // excludePathPatterns 用户排除拦截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/adminpublic interface IService {    T selectByKey(Object key);    int save(T entity);    int delete(Object key);    int updateAll(T entity);    int updateNotNull(T entity);    List selectByExample(Object example);

}
第二步:实现通用接口类
public abstract class baseService implements IService {    @Autowired
    protected Mapper mapper;    public Mapper getMapper() {        return mapper;
    }    
    @Override
    public T selectByKey(Object key) {        return mapper.selectByPrimaryKey(key);
    }    
    @Override
    public int save(T entity) {        return mapper.insert(entity);
    }    
    @Override
    public int delete(Object key) {        return mapper.deleteByPrimaryKey(key);
    }    
    @Override
    public int updateAll(T entity) {        return mapper.updateByPrimaryKey(entity);
    }    
    @Override
    public int updateNotNull(T entity) {        return mapper.updateByPrimaryKeySelective(entity);
    }    
    @Override
    public List selectByExample(Object example) {        return mapper.selectByExample(example);
    }
}

至此呢,我们的通用接口就开发完成了

第三步:使用通用接口

编写好我们的通用接口之后,使用就变得很方便了,只需要继承相应的通用接口或者通用接口实现类,然后进行简单的封装就行了,下面以SortInfo为例:

public interface SortInfoService extends IService {
}
========================分割线========================@Servicepublic class SortInfoServiceImpl extends baseService implements SortInfoService {
}

对应到SortInfo的RESTful API设计,这样简单的继承就能够很好的支持,但是我们还是使用最原始的方式来创建吧...

Service接口申明

查了一些资料,问了一下实习公司的前辈老师,并且根据我们之前设计好的RESTful APIs,我们很有必要搞一个dto层用于前后端之间的数据交互,这一层主要是对数据库的数据进行一个封装整合,也方便前后端的数据交互,所以我们首先就需要分析在dto层中应该存在哪些数据:

DTO层开发

对应我们的业务逻辑和RESTful APIs,我大概弄了下面几个Dto:

① ArticleDto:

该Dto封装了文章的详细信息,对应RESTful API中的/api/article/{id}——通过文章ID获取文章信息

public class ArticleDto {    // tbl_article_info基础字段
    private Long id;    private String title;    private String summary;    private Boolean isTop;    private Integer traffic;    // tbl_article_content基础字段
    private Long articleContentId;    private String content;    // tbl_category_info基础字段
    private Long categoryId;    private String categoryName;    private Byte categoryNumber;    // tbl_article_category基础字段
    private Long articleCategoryId;    // tbl_article_picture基础字段
    private Long articlePictureId;    private String pictureUrl;    }

②ArticleCommentDto:

该Dto封装的事文章的评论信息,对应/api/comment/article/{id}——通过文章ID获取某一篇文章的全部评论信息

public class ArticleCommentDto {    // tbl_comment基础字段
    private Long id;                // 评论id
    private String content;         // 评论内容
    private String name;            // 用户自定义的显示名称
    private String email;    private String ip;    // tbl_article_comment基础字段
    private Long articleCommentId;  // tbl_article_comment主键
    private Long articleId;         // 文章ID

    }

③ArticleCategoryDto:

该Dto是封装了文章的一些分类信息,对应/admin/category/{id}——获取某一篇文章的分类信息

public class ArticleCategoryDto {    //  tbl_article_category表基础字段
    private Long id;            // tbl_article_category表主键
    private Long categoryId;    // 分类信息ID
    private Long articleId;     // 文章ID

    // tbl_category_info表基础字段
    private String name;        // 分类信息显示名称
    private Byte number;        // 该分类下对应的文章数量

    }

④ArticleWithPictureDto:

该Dto封装了文章用于显示的基本信息,对应所有的获取文章集合的RESful APIs

public class ArticleWithPictureDto {    // tbl_article_info基础字段
    private Long id;    private String title;    private String summary;    private Boolean isTop;    private Integer traffic;    // tbl_article_picture基础字段
    private Long articlePictureId;    private String pictureUrl;    }
Service接口开发

Service层其实就是对我们业务的一个封装,所以有了RESTful APIs文档,我们可以很轻易的写出对应的业务模块:

文章Service
public interface ArticleService {    void addArticle(ArticleDto articleDto);    void deleteArticleById(Long id);    void updateArticle(ArticleDto articleDto);    void updateArticleCategory(Long articleId, Long categoryId);    ArticleDto getoneById(Long id);    ArticlePicture getPictureByArticleId(Long id);    List listAll();    List listByCategoryId(Long id);    List listLastest();
}
分类Service
public interface CategoryService {    void addCategory(CategoryInfo categoryInfo);    void deleteCategoryById(Long id);    void updateCategory(CategoryInfo categoryInfo);    void updateArticleCategory(ArticleCategory articleCategory);    CategoryInfo getoneById(Long id);    List listAllCategory();    ArticleCategoryDto getCategoryByArticleId(Long id);
}
留言Service
public interface CommentService {    void addComment(Comment comment);    void addArticleComment(ArticleCommentDto articleCommentDto);    void deleteCommentById(Long id);    void deleteArticleCommentById(Long id);    List listAllComment();    List listAllArticleCommentById(Long id);
}
系统Service
public interface SysService {    void addLog(SysLog sysLog);    void addView(SysView sysView);    int getLogCount();    int getViewCount();    List listAllLog();    List listAllView();
}

Controller 层开发

Controller层简单理解的话,就是用来获取数据的,所以只要Service层开发好了Controller层就很容易,就不多说了,只是我们可以把一些公用的东西放到一个baseController中,比如引入Service:

public class baseController {    @Autowired
    ArticleService articleService;    @Autowired
    CommentService commentService;    @Autowired
    CategoryService categoryService;
}

然后前后台的控制器只需要继承该类就行了,这样的方式非常值得借鉴的,只是因为这个系统比较简单,所以这个baseController,我看过一些源码,可以在里面弄一个通用的用于返回数据的方法,比如分页数据/错误信息之类的;


记录坑1)MyBatis中Text类型的坑

按照《阿里手册》(简称)上所规范的那样,我把文章的content单独弄成了一张表并且将这个“可能很长”的字段的类型设置成了text类型,但是MyBatis逆向工程自动生成的时候,却把这个text类型的字段单独给列了出去,即在生成的xml中多出了一个,标识id为ResultMapWithBLOBs,MyBatis这样做可能的原因还是怕这个字段太长影响前面的字段查询吧,但是操作这样的LONGVARCHAR类型的字段MyBatis好像并没有集成很好,所以想要很好的操作还是需要给它弄成VARCHAR类型才行;

在generatorConfig.xml中配置生成字段的时候加上这样一句话就好了:

  
      
2)拦截器中Service注入为null的坑

在编写前台拦截器的时候,我使用@Autowired注解自动注入了SysService系统服务Service,但是却报nullpointer的错,发现是没有自动注入上,SysService为空..这是为什么呢?排除掉注解没有识别或者没有给Service添加上注解的可能性之后,我发现好像是拦截器拦截的时候Service并没有创建成功造成的,参考这篇文章:https://blog.csdn.net/slgxmh/article/details/51860278,成功解决问题:

@Beanpublic HandlerInterceptor getForeInterceptor() {    return new ForeInterceptor();
}@Overridepublic void addInterceptors(InterceptorRegistry registry) {    // addPathPatterns 用于添加拦截规则
    // excludePathPatterns 用户排除拦截
    registry.addInterceptor(new BackInterceptor()).addPathPatterns("/adminpublic class Markdown2HtmlUtil {    
    public static String markdown2html(String markdown) {
        MutableDataSet options = new MutableDataSet();
        options.setFrom(ParserEmulationProfile.MARKDOWN);
        options.set(Parser.EXTENSIONS, Arrays.asList(new Extension[]{TablesExtension.create()}));
        Parser parser = Parser.builder(options).build();
        HtmlRenderer renderer = HtmlRenderer.builder(options).build();

        Node document = parser.parse(markdown);        return renderer.render(document);
    }
}

使用也很简单,只需要在获取一篇文章的时候把ArticleDto里面的md源码转成html代码再返回给前台就好了:

@ApiOperation("通过文章ID获取文章信息")@GetMapping("article/{id}")public ArticleDto getArticleById(@PathVariable Long id) {
    ArticleDto articleDto = articleService.getoneById(id);
    articleDto.setContent(Markdown2HtmlUtil.markdown2html(articleDto.getContent()));    return articleDto;
}

样式之类的交给前台就好了,搞定...


简单总结

关于统计啊日志类的Controller还没有开发,RESful API也没有设计,这里就先发布文章了,因为好像时间有点紧,后台的页面暂时可能开发不完,准备直接开始前台页面显示的开发(主要是自己对前端不熟悉还要学习..),这里对后台进行一个简单的总结:

其实发现当数据库设计好了,RESful APIs设计好了之后,后台的任务变得非常明确,开发起来也就思路很清晰了,只是自己还是缺少一些必要的经验,如对一些通用方法的抽象/层与层之间数据交互的典型设计之类的东西,特别是一些安全方面的东西,网上的资料也比较少一些,也是自己需要学习的地方;

原文出处

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/235617.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号