public class TaleConst {

    public static final String CLASSPATH = new File(AdminApiController.class.getResource("/").getPath()).getPath() + File.separatorChar;

    public static final String      REMEMBER_IN_COOKIE = "remember_me";
    public static final String      LOGIN_ERROR_COUNT  = "login_error_count";
    public static       String      LOGIN_SESSION_KEY  = "login_user";
    public static       String      REMEMBER_TOKEN     = "";
    public static       Environment OPTIONS            = Environment.of(new HashMap<>());
    public static       Boolean     INSTALLED          = false;
    public static       Boolean     ENABLED_CDN        = true;
    public static       Environment BCONF              = null;

    /**
     * 最大页码
     */
    public static final int MAX_PAGE = 100;

    /**
     * 最大获取文章条数
     */
    public static final int MAX_POSTS = 9999;

    /**
     * 文章最多可以输入的文字数
     */
    public static final int MAX_TEXT_COUNT = 200000;

    /**
     * 文章标题最多可以输入的文字个数
     */
    public static final int MAX_TITLE_COUNT = 200;

    /**
     * 插件菜单
     */
    public static final List<PluginMenu> PLUGIN_MENUS = new ArrayList<>();

    /**
     * 上传文件最大20M
     */
    public static Integer MAX_FILE_SIZE = 204800;

    /**
     * 要过滤的ip列表
     */
    public static final Set<String> BLOCK_IPS = new HashSet<>(16);

    public static final String SLUG_HOME        = "/";
    public static final String SLUG_ARCHIVES    = "archives";
    public static final String SLUG_CATEGRORIES = "categories";
    public static final String SLUG_TAGS        = "tags";

    /**
     * 静态资源URI
     */
    public static final String STATIC_URI = "/static";

    /**
     * 安装页面URI
     */
    public static final String INSTALL_URI = "/install";

    /**
     * 后台URI前缀
     */
    public static final String ADMIN_URI = "/admin";

    /**
     * 后台登录地址
     */
    public static final String LOGIN_URI = "/admin/login";

    /**
     * 插件菜单 Attribute Name
     */
    public static final String PLUGINS_MENU_NAME = "plugin_menus";

    public static final String ENV_SUPPORT_163_MUSIC = "app.support_163_music";
    public static final String ENV_SUPPORT_GIST      = "app.support_gist";
    public static final String MP3_PREFIX            = "[mp3:";
    public static final String MUSIC_IFRAME          = "<iframe frameborder=\"no\" border=\"0\" marginwidth=\"0\" marginheight=\"0\" width=350 height=106 src=\"//music.163.com/outchain/player?type=2&id=$1&auto=0&height=88\"></iframe>";
    public static final String MUSIC_REG_PATTERN     = "\\[mp3:(\\d+)\\]";
    public static final String GIST_PREFIX_URL       = "https://gist.github.com/";
    public static final String GIST_REG_PATTERN      = "&lt;script src=\"https://gist.github.com/(\\w+)/(\\w+)\\.js\">&lt;/script>";
    public static final String GIST_REPLATE_PATTERN  = "<script src=\"https://gist.github.com/$1/$2\\.js\"></script>";


    public static final String SQL_QUERY_METAS = "select a.*, count(b.cid) as count from t_metas a left join `t_relationships` b on a.mid = b.mid " +
            "where a.type = ? and a.name = ? group by a.mid";

    public static final String SQL_QUERY_ARTICLES = "select a.* from t_contents a left join t_relationships b on a.cid = b.cid " +
            "where b.mid = ? and a.status = 'publish' and a.type = 'post' order by a.created desc";

    public static final String COMMENT_APPROVED = "approved";
    public static final String COMMENT_NO_AUDIT = "no_audit";

    public static final String OPTION_CDN_URL             = "cdn_url";
    public static final String OPTION_SITE_THEME          = "site_theme";
    public static final String OPTION_ALLOW_INSTALL       = "allow_install";
    public static final String OPTION_ALLOW_COMMENT_AUDIT = "allow_comment_audit";
    public static final String OPTION_ALLOW_CLOUD_CDN     = "allow_cloud_CDN";
    public static final String OPTION_SAFE_REMEMBER_ME    = "safe_remember_me";

} public final class Commons {

    private static final String TEMPLATES = "/templates/";

    public static void setSiteService(SiteService ss) {
        Theme.setSiteService(ss);
    }

    /**
     * 判断分页中是否有数据
     *
     * @param paginator
     * @return
     */
    public static boolean is_empty(Page<?> paginator) {
        return null == paginator || BladeKit.isEmpty(paginator.getRows());
    }

    /**
     * 判断字符串不为空
     *
     * @param str
     * @return
     */
    public static boolean not_empty(String str) {
        return StringKit.isNotBlank(str);
    }

    /**
     * 返回网站首页链接，如：http://tale.biezhi.me
     *
     * @return
     */
    public static String site_url() {
        return site_url("");
    }

    /**
     * 返回当前主题名称
     *
     * @return
     */
    public static String site_theme() {
        return site_option("site_theme", "default");
    }

    /**
     * 返回网站链接下的全址
     *
     * @param sub 后面追加的地址
     * @return
     */
    public static String site_url(String sub) {
        return site_option("site_url") + sub;
    }

    /**
     * 网站标题
     *
     * @return
     */
    public static String site_title() {
        return site_option("site_title");
    }

    /**
     * 网站子标题
     *
     * @return
     */
    public static String site_subtitle() {
        return site_option("site_subtitle");
    }

    /**
     * 是否允许使用云公共静态资源
     *
     * @return
     */
    public static String allow_cloud_CDN() {
        return site_option("allow_cloud_CDN");
    }

    /**
     * 网站配置项
     *
     * @param key
     * @return
     */
    public static String site_option(String key) {
        return site_option(key, "");
    }

    /**
     * 网站配置项
     *
     * @param key
     * @param defalutValue 默认值
     * @return
     */
    public static String site_option(String key, String defalutValue) {
        if (StringKit.isBlank(key)) {
            return "";
        }
        return TaleConst.OPTIONS.get(key, defalutValue);
    }

    /**
     * 返回站点设置的描述信息
     *
     * @return
     */
    public static String site_description() {
        return site_option("site_description");
    }

    /**
     * 截取字符串
     *
     * @param str
     * @param len
     * @return
     */
    public static String substr(String str, int len) {
        if (str.length() > len) {
            return str.substring(0, len);
        }
        return str;
    }

    /**
     * 返回主题URL
     *
     * @return
     */
    public static String theme_url() {
        return Commons.site_url(TEMPLATES + BaseController.THEME);
    }

    /**
     * 返回主题下的文件路径
     *
     * @param sub
     * @return
     */
    public static String theme_url(String sub) {
        return Commons.site_url(TEMPLATES + BaseController.THEME + sub);
    }


    /**
     * 返回gravatar头像地址
     *
     * @param email
     * @return
     */
    public static String gravatar(String email) {
        String avatarUrl = "https://cn.gravatar.com/avatar";
        if (StringKit.isBlank(email)) {
            return avatarUrl;
        }
        String hash = EncryptKit.md5(email.trim().toLowerCase());
        return avatarUrl + "/" + hash;
    }

    /**
     * 格式化unix时间戳为日期
     *
     * @param unixTime
     * @return
     */
    public static String fmtdate(Integer unixTime) {
        return fmtdate(unixTime, "yyyy-MM-dd");
    }

    /**
     * 格式化日期
     *
     * @param date
     * @param fmt
     * @return
     */
    public static String fmtdate(Date date, String fmt) {
        return DateKit.toString(date, fmt);
    }

    /**
     * 格式化unix时间戳为日期
     *
     * @param unixTime
     * @param patten
     * @return
     */
    public static String fmtdate(Integer unixTime, String patten) {
        if (null != unixTime && StringKit.isNotBlank(patten)) {
            return DateKit.toString(unixTime, patten);
        }
        return "";
    }

    /**
     * 获取随机数
     *
     * @param max
     * @param str
     * @return
     */
    public static String random(int max, String str) {
        return UUID.random(1, max) + str;
    }

    /**
     * An :grinning:awesome :smiley:string &#128516;with a few :wink:emojis!
     * <p>
     * 这种格式的字符转换为emoji表情
     *
     * @param value
     * @return
     */
    public static String emoji(String value) {
        return EmojiParser.parseToUnicode(value);
    }

    private static final Pattern SRC_PATTERN = Pattern.compile("src\\s*=\\s*\'?\"?(.*?)(\'|\"|>|\\s+)");
    /**
     * 获取文章第一张图片
     *
     * @return
     */
    public static String show_thumb(String content) {
        content = TaleUtils.mdToHtml(content);
        if (content.contains("<img")) {
            String  img       = "";
            String  regEx_img = "<img.*src\\s*=\\s*(.*?)[^>]*?>";
            Pattern p_image   = Pattern.compile(regEx_img, Pattern.CASE_INSENSITIVE);
            Matcher m_image   = p_image.matcher(content);
            if (m_image.find()) {
                img = img + "," + m_image.group();
                // //匹配src
                Matcher m = SRC_PATTERN.matcher(img);
                if (m.find()) {
                    return m.group(1);
                }
            }
        }
        return "";
    }

} public interface ErrorCode {

    /**
     * 非法请求
     */
    String BAD_REQUEST = "BAD REQUEST";

} public interface Types {

    String TAG = "tag";
    String CATEGORY = "category";
    String ARTICLE = "post";
    String PUBLISH = "publish";
    String PAGE = "page";
    String DRAFT = "draft";
    String IMAGE = "image";
    String FILE = "file";
    String COMMENTS_FREQUENCY = "comments:frequency";

    String RECENT_ARTICLE = "recent_article";
    String RANDOM_ARTICLE = "random_article";

    String RECENT_META = "recent_meta";
    String RANDOM_META = "random_meta";

    String SYS_STATISTICS = "sys:statistics";

    String NEXT = "next";
    String PREV = "prev";

    /**
     * 附件存放的URL，默认为网站地址，如集成第三方则为第三方CDN域名
     */
    String ATTACH_URL = "attach_url";
    String CDN_URL = "cdn_url";

    /**
     * 网站要过滤，禁止访问的ip列表
     */
    String BLOCK_IPS = "site_block_ips";

} @Data
@Table(name = "t_comments", pk = "coid")
public class Comments extends Model {

    // comment表主键
    private Integer coid;

    // post表主键,关联字段
    private Integer cid;

    // 评论生成时的GMT unix时间戳
    private Integer created;

    // 评论作者
    private String author;

    // 评论所属用户id
    private Integer authorId;

    // 评论所属内容作者id
    private Integer ownerId;

    // 评论者邮件
    private String mail;

    // 评论者网址
    private String url;

    // 评论者ip地址
    private String ip;

    // 评论者客户端
    private String agent;

    // 评论内容
    private String content;

    // 评论类型
    private String type;

    // 评论状态
    private String status;

    // 父级评论
    private Integer parent;

} @Data
@Table(name = "t_contents", pk = "cid")
public class Contents extends Model {

    /**
     * 文章表主键
     */
    private Integer cid;

    /**
     * 文章标题
     */
    private String title;

    /**
     * 文章缩略名
     */
    private String slug;

    /**
     * 文章创建时间戳
     */
    private Integer created;

    /**
     * 文章修改时间戳
     */
    private Integer modified;

    /**
     * 文章内容
     */
    private String content;

    /**
     * 文章创建用户
     */
    private Integer authorId;

    /**
     * 文章点击次数
     */
    private Integer hits;

    /**
     * 文章类型： PAGE、POST
     */
    private String type;

    /**
     * 内容类型，markdown或者html
     */
    private String fmtType;

    /**
     * 文章缩略图
     */
    private String thumbImg;

    /**
     * 标签列表
     */
    private String tags;

    /**
     * 分类列表
     */
    private String categories;

    /**
     * 内容状态
     */
    private String status;

    /**
     * 内容所属评论数
     */
    private Integer commentsNum;

    /**
     * 是否允许评论
     */
    private Boolean allowComment;

    /**
     * 是否允许ping
     */
    private Boolean allowPing;

    /**
     * 允许出现在Feed中
     */
    private Boolean allowFeed;

    @Ignore
    private String url;
} @Bean
public class CommentsService {

    /**
     * 保存评论
     *
     * @param comments
     */
    public void saveComment(Comments comments) {
        comments.setAuthor(TaleUtils.cleanXSS(comments.getAuthor()));
        comments.setContent(TaleUtils.cleanXSS(comments.getContent()));

        comments.setAuthor(EmojiParser.parseToAliases(comments.getAuthor()));
        comments.setContent(EmojiParser.parseToAliases(comments.getContent()));

        Contents contents = select().from(Contents.class).byId(comments.getCid());
        if (null == contents) {
            throw new ValidatorException("不存在的文章");
        }
        try {
            comments.setOwnerId(contents.getAuthorId());
            comments.setAuthorId(null == comments.getAuthorId() ? 0 : comments.getAuthorId());
            comments.setCreated(DateKit.nowUnix());
            comments.setParent(null == comments.getCoid() ? 0 : comments.getCoid());
            comments.setCoid(null);
            comments.save();

            new Contents().set(Contents::getCommentsNum, contents.getCommentsNum() + 1).updateById(contents.getCid());
        } catch (Exception e) {
            throw e;
        }
    }

    /**
     * 删除评论，暂时没用
     *
     * @param coid
     * @param cid
     * @throws Exception
     */
    public void delete(Integer coid, Integer cid) {
        Anima.delete().from(Comments.class).deleteById(coid);

        Contents contents = select().from(Contents.class).byId(cid);
        if (null != contents && contents.getCommentsNum() > 0) {
            update().from(Contents.class).set(Contents::getCommentsNum, contents.getCommentsNum() - 1).updateById(cid);
        }
    }

    /**
     * 获取文章下的评论
     *
     * @param cid
     * @param page
     * @param limit
     * @return
     */
    public Page<Comment> getComments(Integer cid, int page, int limit) {
        if (null == cid) {
            return null;
        }

        Page<Comments> commentsPage = select().from(Comments.class)
                .where(Comments::getCid, cid).and(Comments::getParent, 0)
                .and(Comments::getStatus, COMMENT_APPROVED)
                .order(Comments::getCoid, OrderBy.DESC).page(page, limit);

        return commentsPage.map(this::apply);
    }

    /**
     * 获取文章下的评论统计
     *
     * @param cid 文章ID
     */
    public long getCommentCount(Integer cid) {
        if (null == cid) {
            return 0;
        }
        return select().from(Comments.class).where(Comments::getCid, cid).count();
    }

    /**
     * 获取该评论下的追加评论
     *
     * @param coid
     * @return
     */
    private void getChildren(List<Comments> list, Integer coid) {
        List<Comments> cms = select().from(Comments.class).where(Comments::getParent, coid).order(Comments::getCoid, OrderBy.ASC).all();
        if (null != cms) {
            list.addAll(cms);
            cms.forEach(c -> getChildren(list, c.getCoid()));
        }
    }

    private Comment apply(Comments parent) {
        Comment        comment  = new Comment(parent);
        List<Comments> children = new ArrayList<>();
        getChildren(children, comment.getCoid());
        comment.setChildren(children);
        if (BladeKit.isNotEmpty(children)) {
            comment.setLevels(1);
        }
        return comment;
    }

    public Page<Comments> findComments(CommentParam commentParam) {
        return select().from(Comments.class)
                .order(Comments::getCoid, OrderBy.DESC)
                .page(commentParam.getPage(), commentParam.getLimit());
    }

} @Bean
public class ContentsService {

    @Inject
    private MetasService metasService;

    /**
     * 根据id或slug获取文章
     *
     * @param id 唯一标识
     */
    public Contents getContents(String id) {
        Contents contents = null;
        if (StringKit.isNotBlank(id)) {
            if (StringKit.isNumber(id)) {
                contents = select().from(Contents.class).byId(id);
            } else {
                contents = select().from(Contents.class).where(Contents::getSlug, id).one();
            }
            if (null != contents) {
                return this.mapContent(contents);
            }
        }
        return contents;
    }

    /**
     * 发布文章
     *
     * @param contents 文章对象
     */
    public Integer publish(Contents contents) {
        if (null == contents.getAuthorId()) {
            throw new ValidatorException("请登录后发布文章");
        }

        contents.setContent(EmojiParser.parseToAliases(contents.getContent()));

        int time = DateKit.nowUnix();
        contents.setCreated(time);
        contents.setModified(time);
        contents.setHits(0);

        String tags       = contents.getTags();
        String categories = contents.getCategories();

        Integer cid = contents.save().asInt();

        metasService.saveMetas(cid, tags, Types.TAG);
        metasService.saveMetas(cid, categories, Types.CATEGORY);

        return cid;
    }

    /**
     * 编辑文章
     *
     * @param contents 文章对象
     */
    public void updateArticle(Contents contents) {
        contents.setCreated(contents.getCreated());
        contents.setModified(DateKit.nowUnix());
        contents.setContent(EmojiParser.parseToAliases(contents.getContent()));
        contents.setTags(contents.getTags() != null ? contents.getTags() : "");
        contents.setCategories(contents.getCategories() != null ? contents.getCategories() : "");

        Integer cid = contents.getCid();

        contents.updateById(cid);

        String tags       = contents.getTags();
        String categories = contents.getCategories();

        if (null != contents.getType() && !contents.getType().equals(Types.PAGE)) {
            Anima.delete().from(Relationships.class).where(Relationships::getCid, cid).execute();
        }

        metasService.saveMetas(cid, tags, Types.TAG);
        metasService.saveMetas(cid, categories, Types.CATEGORY);
    }

    /**
     * 根据文章id删除
     *
     * @param cid 文章id
     */
    public void delete(int cid) {
        Contents contents = this.getContents(cid + "");
        if (null != contents) {
            deleteById(Contents.class, cid);
            Anima.delete().from(Relationships.class).where(Relationships::getCid, cid).execute();
            Anima.delete().from(Comments.class).where(Comments::getCid, cid).execute();
        }
    }

    /**
     * 查询分类/标签下的文章归档
     *
     * @param mid   分类、标签id
     * @param page  页码
     * @param limit 每页条数
     * @return
     */
    public Page<Contents> getArticles(Integer mid, int page, int limit) {
        return select().bySQL(Contents.class, SQL_QUERY_ARTICLES, mid).page(page, limit);
    }

    public Page<Contents> findArticles(ArticleParam articleParam) {
        AnimaQuery<Contents> query = select().from(Contents.class).exclude(Contents::getContent);

        if (StringKit.isNotEmpty(articleParam.getStatus())) {
            query.and(Contents::getStatus, articleParam.getStatus());
        }

        if (StringKit.isNotEmpty(articleParam.getTitle())) {
            query.and(Contents::getTitle).like("%" + articleParam.getTitle() + "%");
        }

        if (StringKit.isNotEmpty(articleParam.getCategories())) {
            query.and(Contents::getCategories).like("%" + articleParam.getCategories() + "%");
        }

        query.and(Contents::getType, articleParam.getType());
        query.order(articleParam.getOrderBy());
        Page<Contents> articles = query.page(articleParam.getPage(), articleParam.getLimit());
        return articles.map(this::mapContent);
    }

    private Contents mapContent(Contents contents) {
        if (StringKit.isNotEmpty(contents.getSlug())) {
            String url = "/" + contents.getSlug();
            contents.setUrl(url.replaceAll("[/]+", "/"));
        } else {
            contents.setUrl("/article/" + contents.getCid());
        }
        String content = contents.getContent();
        if (StringKit.isNotEmpty(content)) {
            content = content.replaceAll("\\\\\"", "\\\"");
            contents.setContent(content);
        }
        return contents;
    }
} @Bean
public class SiteService {

    @Inject
    private CommentsService commentsService;

    public MapCache mapCache = new MapCache();

    /**
     * 初始化站点
     *
     * @param users 用户
     */
    public void initSite(Users users) {
        String pwd = EncryptKit.md5(users.getUsername() + users.getPassword());
        users.setPassword(pwd);
        users.setScreenName(users.getUsername());
        users.setCreated(DateKit.nowUnix());
        Integer uid = users.save().asInt();

        try {
            String cp   = SiteService.class.getClassLoader().getResource("").getPath();
            File   lock = new File(cp + "install.lock");
            lock.createNewFile();
            TaleConst.INSTALLED = Boolean.TRUE;
            new Logs("初始化站点", null, "", uid).save();
        } catch (Exception e) {
            throw new ValidatorException("初始化站点失败");
        }
    }

    /**
     * 最新收到的评论
     *
     * @param limit 评论数
     */
    public List<Comments> recentComments(int limit) {
        if (limit < 0 || limit > 10) {
            limit = 10;
        }

        Page<Comments> commentsPage = select().from(Comments.class)
                                              .where(Comments::getStatus, COMMENT_APPROVED)
                                              .order(Comments::getCreated, OrderBy.DESC)
                                              .page(1, limit);
        return commentsPage.getRows();
    }

    /**
     * 根据类型获取文章列表
     *
     * @param type 最新,随机
     * @param limit 获取条数
     */
    public List<Contents> getContens(String type, int limit) {

        if (limit < 0 || limit > 20) {
            limit = 10;
        }

        // 最新文章
        if (Types.RECENT_ARTICLE.equals(type)) {
            Page<Contents> contentsPage = select().from(Contents.class)
                                                  .where(Contents::getStatus, Types.PUBLISH)
                                                  .and(Contents::getType, Types.ARTICLE)
                                                  .order(Contents::getCreated, OrderBy.DESC)
                                                  .page(1, limit);

            return contentsPage.getRows();
        }

        // 随机文章
        if (Types.RANDOM_ARTICLE.equals(type)) {
            List<Integer> cids = select().bySQL(Integer.class,
                                                "select cid from t_contents where type = ? and status = ? order by random() * cid limit ?",
                                                Types.ARTICLE, Types.PUBLISH, limit).all();
            if (BladeKit.isNotEmpty(cids)) {
                return select().from(Contents.class).in(Contents::getCid, cids).all();
            }
        }
        return new ArrayList<>();
    }

    /**
     * 获取后台统计数据
     */
    public Statistics getStatistics() {

        Statistics statistics = mapCache.get(Types.SYS_STATISTICS);
        if (null != statistics) {
            return statistics;
        }

        statistics = new Statistics();

        long articles = select().from(Contents.class).where(Contents::getType, Types.ARTICLE)
                                .and(Contents::getStatus, Types.PUBLISH).count();
        long pages = select().from(Contents.class).where(Contents::getType, Types.PAGE)
                             .and(Contents::getStatus, Types.PUBLISH).count();
        long comments   = select().from(Comments.class).count();
        long attachs    = select().from(Attach.class).count();
        long tags       = select().from(Metas.class).where(Metas::getType, Types.TAG).count();
        long categories = select().from(Metas.class).where(Metas::getType, Types.CATEGORY).count();

        statistics.setArticles(articles);
        statistics.setPages(pages);
        statistics.setComments(comments);
        statistics.setAttachs(attachs);
        statistics.setTags(tags);
        statistics.setCategories(categories);

        mapCache.set(Types.SYS_STATISTICS, statistics);
        return statistics;
    }

    /**
     * 查询文章归档
     */
    public List<Archive> getArchives() {
        String sql =
            "select strftime('%Y年%m月', datetime(created, 'unixepoch') ) as date_str, count(*) as count  from t_contents "
                +
                "where type = 'post' and status = 'publish' group by date_str order by date_str desc";

        List<Archive> archives = select().bySQL(Archive.class, sql).all();
        if (null != archives) {
            return archives.stream()
                           .map(this::parseArchive)
                           .collect(Collectors.toList());
        }
        return new ArrayList<>(0);
    }

    private Archive parseArchive(Archive archive) {
        String dateStr = archive.getDateStr();
        Date   sd      = DateKit.toDate(dateStr + "01", "yyyy年MM月dd");
        archive.setDate(sd);
        int      start    = DateKit.toUnix(sd);
        Calendar calender = Calendar.getInstance();
        calender.setTime(sd);
        calender.add(Calendar.MONTH, 1);
        Date endSd = calender.getTime();
        int  end   = DateKit.toUnix(endSd) - 1;

        List<Contents> contents = select().from(Contents.class)
                                          .where(Contents::getType, Types.ARTICLE)
                                          .and(Contents::getStatus, Types.PUBLISH)
                                          .and(Contents::getCreated).gt(start)
                                          .and(Contents::getCreated).lt(end)
                                          .order(Contents::getCreated, OrderBy.DESC)
                                          .all();

        archive.setArticles(contents);
        return archive;
    }

    /**
     * 查询一条评论
     *
     * @param coid 评论主键
     */
    public Comments getComment(Integer coid) {
        if (null != coid) {
            return select().from(Comments.class).byId(coid);
        }
        return null;
    }

    /**
     * 系统备份
     */
    public BackResponse backup(String bkType, String bkPath, String fmt) throws Exception {
        BackResponse backResponse = new BackResponse();
        if ("attach".equals(bkType)) {
            if (StringKit.isBlank(bkPath)) {
                throw new ValidatorException("请输入备份文件存储路径");
            }
            if (!Files.isDirectory(Paths.get(bkPath))) {
                throw new ValidatorException("请输入一个存在的目录");
            }
            String bkAttachDir = CLASSPATH + "upload";
            String bkThemesDir = CLASSPATH + "templates/themes";

            String fname = DateKit.toString(new Date(), fmt) + "_" + StringKit.rand(5) + ".zip";

            String attachPath = bkPath + "/" + "attachs_" + fname;
            String themesPath = bkPath + "/" + "themes_" + fname;

            backResponse.setAttach_path(attachPath);
            backResponse.setTheme_path(themesPath);
        }
        // 备份数据库
        if ("db".equals(bkType)) {
            String filePath = "upload/" + DateKit.toString(new Date(), "yyyyMMddHHmmss") + "_"
                + StringKit.rand(8) + ".db";
            String cp = CLASSPATH + filePath;
            Files.createDirectory(Paths.get(cp));
            Files.copy(Paths.get(SqliteJdbc.DB_PATH), Paths.get(cp));
            backResponse.setSql_path("/" + filePath);
            // 10秒后删除备份文件
            new Timer().schedule(new TimerTask() {
                @Override
                public void run() {
                    new File(cp).delete();
                }
            }, 10 * 1000);
        }
        return backResponse;
    }

    /**
     * 获取分类/标签列表
     */
    public List<Metas> getMetas(String searchType, String type, int limit) {

        if (StringKit.isBlank(searchType) || StringKit.isBlank(type)) {
            return new ArrayList<>(0);
        }

        if (limit < 1 || limit > TaleConst.MAX_POSTS) {
            limit = 10;
        }

        // 获取最新的项目
        if (Types.RECENT_META.equals(searchType)) {
            String sql =
                "select a.*, count(b.cid) as count from t_metas a left join `t_relationships` b on a.mid = b.mid "
                    +
                    "where a.type = ? group by a.mid order by count desc, a.mid desc limit ?";

            return select().bySQL(Metas.class, sql, type, limit).all();
        }

        // 随机获取项目
        if (Types.RANDOM_META.equals(searchType)) {
            List<Integer> mids = select().bySQL(Integer.class,
                                                "select mid from t_metas where type = ? order by random() * mid limit ?",
                                                type, limit).all();
            if (BladeKit.isNotEmpty(mids)) {
                String in = TaleUtils.listToInSql(mids);
                String sql =
                    "select a.*, count(b.cid) as count from t_metas a left join `t_relationships` b on a.mid = b.mid "
                        +
                        "where a.mid in " + in + "group by a.mid order by count desc, a.mid desc";

                return select().bySQL(Metas.class, sql).all();
            }
        }
        return new ArrayList<>(0);
    }

    /**
     * 获取相邻的文章
     *
     * @param type 上一篇:prev | 下一篇:next
     * @param created 当前文章创建时间
     */
    public Contents getNhContent(String type, Integer created) {
        Contents contents = null;
        if (Types.NEXT.equals(type)) {
            contents = select().bySQL(Contents.class,
                                      "SELECT * FROM t_contents WHERE type = ? AND status = ? AND created > ? ORDER BY created ASC LIMIT 1",
                                      Types.ARTICLE, Types.PUBLISH, created).one();
        }
        if (Types.PREV.equals(type)) {
            contents = select().bySQL(Contents.class,
                                      "SELECT * FROM t_contents WHERE type = ? AND status = ? AND created < ? ORDER BY created DESC LIMIT 1",
                                      Types.ARTICLE, Types.PUBLISH, created).one();
        }
        return contents;
    }

    /**
     * 获取文章的评论
     *
     * @param cid 文章id
     * @param page 页码
     * @param limit 每页条数
     */
    public Page<Comment> getComments(Integer cid, int page, int limit) {
        return commentsService.getComments(cid, page, limit);
    }

    /**
     * 获取文章的评论总数
     *
     * @param cid 文章id
     */
    public long getCommentCount(Integer cid) {
        return commentsService.getCommentCount(cid);
    }

    /**
     * 清楚缓存
     *
     * @param key 缓存key
     */
    public void cleanCache(String key) {
        if (StringKit.isNotBlank(key)) {
            if ("*".equals(key)) {
                mapCache.clean();
            } else {
                mapCache.del(key);
            }
        }
    }

} public class CommonValidator {

    public static void valid(Comments param) {
        Validators.notEmpty().test(param.getAuthor()).throwIfInvalid("评论作者");
        Validators.lessThan(30).test(param.getAuthor()).throwIfInvalid("姓名");

        Validators.notEmpty().test(param.getMail()).throwIfInvalid("电子邮箱");
        Validators.isEmail().test(param.getMail()).throwIfInvalid("电子邮箱");

        if (StringKit.isNotBlank(param.getUrl())) {
            Validators.isURL().test(param.getUrl()).throwIfInvalid("网址");
        }

        Validators.notEmpty().test(param.getContent()).throwIfInvalid("内容");
        Validators.moreThan(5).test(param.getContent()).throwIfInvalid("内容");
        Validators.lessThan(2000).test(param.getContent()).throwIfInvalid("内容");

        Validators.notNull().test(param.getCid()).throwIfInvalid("评论文章");

    }

    public static void validAdmin(Comments param) {
        Validators.notNull().test(param.getCoid()).throwIfInvalid("评论ID");

        Validators.notEmpty().test(param.getContent()).throwIfInvalid("内容");
        Validators.moreThan(5).test(param.getContent()).throwIfInvalid("内容");
        Validators.lessThan(2000).test(param.getContent()).throwIfInvalid("内容");
    }

    public static void valid(Contents param) {
        Validators.notEmpty().test(param.getTitle()).throwIfInvalid("文章标题");
        Validators.lessThan(MAX_TITLE_COUNT).test(param.getTitle()).throwIfInvalid("文章标题");

        Validators.notEmpty().test(param.getContent()).throwIfInvalid("文章内容");
        Validators.lessThan(MAX_TEXT_COUNT).test(param.getContent()).throwIfInvalid("文章内容");

        if (StringKit.isNotEmpty(param.getSlug())) {
            if (param.getSlug().length() < 2) {
                throw new ValidatorException("路径太短了");
            }
            if (!TaleUtils.isPath(param.getSlug())) {
                throw new ValidatorException("您输入的路径不合法");
            }

            Contents temp = select().from(Contents.class).where(Contents::getType, param.getType()).and(Contents::getSlug, param.getSlug()).one();
            if (null != temp && !temp.getCid().equals(param.getCid())) {
                throw new ValidatorException("该路径已经存在，请重新输入");
            }
        } else {
            param.setSlug(null);
        }
    }

    public static void valid(LoginParam param) {
        Validators.notEmpty().test(param.getUsername()).throwIfInvalid("用户名");
        Validators.notEmpty().test(param.getPassword()).throwIfInvalid("登录密码");

        Validators.moreThan(4).test(param.getUsername()).throwIfInvalid("用户名");
        Validators.lessThan(20).test(param.getUsername()).throwIfInvalid("用户名");
        Validators.moreThan(6).test(param.getPassword()).throwIfInvalid("登录密码");
        Validators.lessThan(14).test(param.getPassword()).throwIfInvalid("登录密码");
    }

    public static void valid(InstallParam param) {
        Validators.notEmpty().test(param.getSiteTitle()).throwIfInvalid("网站标题");
        Validators.notEmpty().test(param.getSiteUrl()).throwIfInvalid("网站URL");
        Validators.notEmpty().test(param.getAdminUser()).throwIfInvalid("管理员账户");
        Validators.moreThan(4).test(param.getAdminUser()).throwIfInvalid("管理员账户");
        Validators.lessThan(20).test(param.getAdminUser()).throwIfInvalid("管理员账户");

        Validators.notEmpty().test(param.getAdminPwd()).throwIfInvalid("管理员密码");

        Validators.moreThan(6).test(param.getAdminPwd()).throwIfInvalid("请输入6-14位密码");
        Validators.lessThan(14).test(param.getAdminPwd()).throwIfInvalid("请输入6-14位密码");
        if (StringKit.isNotEmpty(param.getAdminEmail())) {
            Validators.isEmail().test(param.getAdminEmail()).throwIfInvalid("电子邮箱");
        }
    }

} public static final String COMMENT_APPROVED = "approved"; public static final String COMMENT_NO_AUDIT = "no_audit"; public static final String OPTION_ALLOW_COMMENT_AUDIT = "allow_comment_audit";
import com.blade.exception.ValidatorException;
import com.blade.ioc.annotation.Inject;
import com.blade.kit.StringKit;
import com.blade.mvc.annotation.*;
import com.blade.mvc.http.Request;
import com.blade.mvc.http.Response;
import com.blade.mvc.ui.RestResponse;
import com.tale.bootstrap.TaleConst;
import com.tale.extension.Commons;
import com.tale.model.dto.ErrorCode;
import com.tale.model.dto.Types;
import com.tale.model.entity.Comments;
import com.tale.model.entity.Contents;
import com.tale.service.CommentsService;
import com.tale.service.ContentsService;
import com.tale.service.SiteService;
import com.tale.validators.CommonValidator;
import lombok.extern.slf4j.Slf4j;
import java.net.URLEncoder;
import static com.tale.bootstrap.TaleConst.COMMENT_APPROVED;
import static com.tale.bootstrap.TaleConst.COMMENT_NO_AUDIT;
import static com.tale.bootstrap.TaleConst.OPTION_ALLOW_COMMENT_AUDIT;
package com.tale.controller;




/**
 * @author biezhi
 * @date 2018/6/4
 */
@Path
@Slf4j
public class ArticleController extends BaseController {

    @Inject
    private ContentsService contentsService;

    @Inject
    private CommentsService commentsService;

    @Inject
    private SiteService siteService;

    /**
     * 自定义页面
     */
    @GetRoute(value = {"/:cid", "/:cid.html"})
    public String page(@PathParam String cid, Request request) {
        Contents contents = contentsService.getContents(cid);
        if (null == contents) {
            return this.render_404();
        }
        if (contents.getAllowComment()) {
            int cp = request.queryInt("cp", 1);
            request.attribute("cp", cp);
        }
        request.attribute("article", contents);
        Contents temp = new Contents();
        temp.setHits(contents.getHits() + 1);
        temp.updateById(contents.getCid());
        if (Types.ARTICLE.equals(contents.getType())) {
            return this.render("post");
        }
        if (Types.PAGE.equals(contents.getType())) {
            return this.render("page");
        }
        return this.render_404();
    }

    /**
     * 文章页
     */
    @GetRoute(value = {"article/:cid", "article/:cid.html"})
    public String post(Request request, @PathParam String cid) {
        Contents contents = contentsService.getContents(cid);
        if (null == contents) {
            return this.render_404();
        }
        if (Types.DRAFT.equals(contents.getStatus())) {
            return this.render_404();
        }
        request.attribute("article", contents);
        request.attribute("is_post", true);
        if (contents.getAllowComment()) {
            int cp = request.queryInt("cp", 1);
            request.attribute("cp", cp);
        }
        Contents temp = new Contents();
        temp.setHits(contents.getHits() + 1);
        temp.updateById(contents.getCid());
        return this.render("post");
    }

    /**
     * 评论操作
     */
    @PostRoute(value = "comment")
    @JSON
    public RestResponse<?> comment(Request request, Response response,
                                   @HeaderParam String Referer, Comments comments) {

        if (StringKit.isBlank(Referer)) {
            return RestResponse.fail(ErrorCode.BAD_REQUEST);
        }

        if (!Referer.startsWith(Commons.site_url())) {
            return RestResponse.fail("非法评论来源");
        }

        CommonValidator.valid(comments);

        String  val   = request.address() + ":" + comments.getCid();
        Integer count = cache.hget(Types.COMMENTS_FREQUENCY, val);
        if (null != count && count > 0) {
            return RestResponse.fail("您发表评论太快了，请过会再试");
        }
        comments.setIp(request.address());
        comments.setAgent(request.userAgent());

        if (TaleConst.OPTIONS.getBoolean(OPTION_ALLOW_COMMENT_AUDIT, true)) {