开源中国源码分析(一)

开源中国源码地址:http://git.oschina.net/oschina/android-app

咨讯详情页面分析

布局页面文件fragment_general_news_detail.xml布局文件中包含上滑评论条隐藏,下滑评论条显示

相关推荐、相关评论布局是通过继承LinearLayout再填充marge布局页面

相关推荐布局文件lay_detail_about_layout.xml,看运行效果貌似是ListView,

其实是LineartLayout,由DetailAboutView类的如下代码实现

详情页面文件NewsDetailActivity.java其继承于DetailAvtivity.java,DetailActivity.java的布局文件中帧布局父容器包含一个帧布局和一个自定义的EmptyLayout。基类中onCreate()方法中的initWindow()、initWidget()、initData();getContentView()、getImageLoader()

public abstract class DetailActivity<Data, DataView extends DetailContract.View> extends BaseBaceActivity implements DetailContract.Operator<Data, DataView> 有2个泛型的属性

private Data mData; 

private DataView mView;

数据的加载是在NewsDetailActivity.java文件中,数据的展示是在NewsDetailFragment.java文件中

在DetailFragment.java的如下代码关联了数据的获取和数据的展示

public void onAttach(Context context) {

this.mOperator = (Operator) context;

this.mOperator.setDataView((DataView) this);

super.onAttach(context);

}


public interface DetailContract 、 public interface NewsDetailContract 同时被NewsDetaiActivity 和 NewsDetailFragment实现

数据在NewsDetailActivity.java文件中请求成功后才会通过handleView()方法填充NewsDetailFragment.java文件


NewsDetailFragment.java


CommentsView.java



对于评论中的他人跟随的评论CommentsUtil.java

综合 -> 资讯 -> 博客详情页面 底部评论工具条上滑消失,下滑出现的实现方式:

1. 根布局是CoordinatorLayout

2. 上面滑动布局的根布局是NestedScrollView

3. 在底部评论工具条的容器布局中添加一个属性app:layout_behavior="FloatingHideDownBehavior类的全路径"


public class FloatingAutoHideDownBehavior extends CoordinatorLayout.Behavior<View> {
    private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
    private boolean mIsAnimatingOut = false;   // 动画已经显示标识
    private boolean mIsScrollToBottom = false;

    public FloatingAutoHideDownBehavior(Context context, AttributeSet attrs) {
        super();
    }

    /**

    * onStartNestedScroll返回true时被调用

    * 由上往下滑动dy<0并且滑动的力度越大dy越小;由下往上滑动dy>0并且滑动的力度越大dy越大。

    */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);


        // 手指下滑dy < 0     工具条隐藏时 translationY = child.height
        if (!mIsScrollToBottom) {
            float mPreTranslationY = dy + child.getTranslationY();
            if (mPreTranslationY <= 0) {
                child.setTranslationY(0);
                mIsAnimatingOut = true;
            }
            if (mPreTranslationY >= child.getHeight()) {
                child.setTranslationY(child.getHeight());
                mIsAnimatingOut = false;
            }
            if (mPreTranslationY > 0 && mPreTranslationY < child.getHeight()) {
                child.setTranslationY(mPreTranslationY);
                mIsAnimatingOut = dy > 0;
            }
        }
    }

    /**

    *  判断是否是所依赖的view。例如:return dependency instanceof ListView;

  * 判断child依赖的dependency是否是ListView 只有返回true时才执行onDependentViewChanged方法

    */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, final View child, View dependency) {
        if (child != null && dependency != null && dependency instanceof NestedScrollView) {
            NestedScrollView s = (NestedScrollView) dependency;
            if (s.getChildCount() > 0) {
                // 嵌套布局底部的控件有个底部工具条控件高度的paddingBottom
                View view = s.getChildAt(s.getChildCount() - 1);
                view.setPadding(view.getPaddingLeft(),
                        view.getPaddingTop(),
                        view.getPaddingRight(),
                        view.getPaddingBottom() + child.getHeight());
            }


            s.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
                @Override
                public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                    if (v.getChildCount() > 0) {
                        // Grab the last child placed in the ScrollView, we need it to determinate the bottom position.
                        View view = v.getChildAt(v.getChildCount() - 1);
                        // Calculate the scrolldiff
                        int diff = (view.getBottom() - (v.getHeight() + scrollY));
                        // if diff is zero, then the bottom has been reached
                        Log.d("@@", "view.bottom = " + view.getBottom() + " , scrollY = " + scrollY);
                        if (diff == 0) {   // 滑动到底部时,显示工具条
                            // notify that we have reached the bottom
                            animateIn(child);
                            mIsScrollToBottom = true;
                        } else {
                            mIsScrollToBottom = false;
                        }
                    }
                }
            });
        }
        return super.layoutDependsOn(parent, child, dependency);
    }

    /**

    *  只要滑动就会被调用,注意被依赖的view需要设置setNestedScrollingEnabled(true)可以判断滑动方向,比   * 如:(nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 判断滑动方向是否为垂直方向

    */
    @Override
    public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final View child,
                                       final View directTargetChild, final View target, final int nestedScrollAxes) {
        // 滑动时隐藏软键盘
        TDevice.hideSoftKeyboard(coordinatorLayout);


        // Ensure we react to vertical scrolling 判断是否是垂直滑动方向
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL
                || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }


    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
        super.onStopNestedScroll(coordinatorLayout, child, target);
        // 完全显示或者完全隐藏 则返回
        if (child.getTranslationY() == 0 || child.getTranslationY() == child.getHeight()) return;

        if (mIsAnimatingOut) {
            animateOut(child);
        } else {
            animateIn(child);
        }
    }


    /***
     * 隐藏底部工具条
     * @param button
     */
    private void animateOut(final View button) {
        button.animate()
                .translationY(button.getHeight())
                .setInterpolator(INTERPOLATOR)
                .setDuration(200)
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        button.setTranslationY(button.getHeight());
                    }
                });
    }

    /**
     * 显示底部工具条
     * @param button
     */
    private void animateIn(final View button) {
        button.animate()
                .translationY(0)
                .setInterpolator(INTERPOLATOR)
                .setDuration(200)
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        button.setTranslationY(0);
                    }
                });
    }

    /**
     * 点击内容栏唤起底部操作区域
     * @param coordinatorLayout 外部CoordinatorLayout
     * @param contentView       滚动区域
     * @param bottomView        滚动时隐藏底部区域
     */
    public static void showBottomLayout(CoordinatorLayout coordinatorLayout, View contentView, final View bottomView) {
        bottomView.animate()
                .translationY(0)
                .setInterpolator(INTERPOLATOR)
                .setDuration(200)
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        bottomView.setTranslationY(0);
                    }
                });
    }
}

咨讯详情导航栏右上角带有数字图标的点击事件执行方法

public void scrollToComment() {
        NestedScrollView nestedScrollView = mScrollView;
        View target = mScrollTargetView;
        if (nestedScrollView != null && target != null) {
            // 默认scrollY是0, 当手指上滑的过程中scrollY变大
            int curY = nestedScrollView.getScrollY();
            int targetY = target.getTop();
            Log.e("TAG", "curY:" + curY + " targetY:" + targetY + " mScrollYPoint:" + mScrollYPoint);
            if (targetY > 0) { // 具有评论
                if (curY == targetY && targetY == mScrollYPoint) {
                    nestedScrollView.fullScroll(View.FOCUS_UP);
                } else {
                    if (mScrollYPoint == -1) {
                        nestedScrollView.smoothScrollTo(0, targetY);
                        mScrollYPoint = curY;
                        return;
                    }
                    if (curY > targetY) {
                        // 当前在评论之后
                        if (mScrollYPoint < targetY) {
                            nestedScrollView.smoothScrollTo(0, mScrollYPoint);
                        } else {
                            nestedScrollView.fullScroll(View.FOCUS_UP);
                        }
                        mScrollYPoint = curY;
                    } else {
                        // 当前在评论之前
                        nestedScrollView.smoothScrollTo(0, mScrollYPoint);
                        if (mScrollYPoint < curY) {
                            mScrollYPoint = -1;
                        } else {
                            mScrollYPoint = 0;
                        }
                    }
                }
            } else { // 没有评论时的逻辑
                if (mScrollYPoint == -1) {
                    nestedScrollView.fullScroll(View.FOCUS_DOWN); // 滑动到底部,并且记录当前滑动的距离
                    mScrollYPoint = curY;
                } else {
                    nestedScrollView.smoothScrollTo(0, mScrollYPoint); // 定位到先前的位置
                    mScrollYPoint = -1;
                }
            }
        }
    }


活动详情EventFragment.java

底部收藏工具条的收藏按钮的背景点击特效实现

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape><solid android:color="#eeeeee"/></shape>
    </item>
    <item>
        <shape><solid android:color="@color/white"/></shape>
    </item>
</selector>

对应网页内容的显示使用了自定义的OWebView加载字符串


BlogFragment.java如下效果的实现方式,给按钮添加个背景,按钮布局是作为GridView的itemView,再将GridView作为ListView的头布局


<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_activated="false">
        <shape>
            <corners android:radius="2dp" />
            <solid android:color="@color/ques_bt_bg" />
            <stroke android:width="1px" android:color="@android:color/transparent" />
        </shape>
    </item>
    <item android:state_activated="true">
        <shape>
            <corners android:radius="2dp" />
            <solid android:color="@color/ques_bt_bg" />
            <stroke android:width="1px" android:color="@color/ques_bt_text_color_light" />
        </shape>
    </item>
</selector>


就拿BlogFragment.java来说综合->博客页面数据的加载过程

重写了基类onCreateView()的initWidget()在调用了super.initWidget()方法初始化了父类中ListView、SuperRefreshLayout、EmptyLayout之后就开始初始化顶部Tab标签的GridView

接着执行initData(),调用了super.initData()在父类的initData()中初始化了从子类获取到的适配器对象设置给ListView,初始化网络回调处理的对象,然后开启线程加载数据,首先读取缓存数据如果有缓存数据就显示缓存数据然后执行onRefreshing(),否则设置其他条件后开始执行onRefreshing(), 在onRefreshing()方法中先设置刷新标识位,然后调用子类重写的requestData()方法请求数据,携带父类中初始化过的网络回调处理的对象

    protected void setListData(ResultBean<PageBean<T>> resultBean) {
        //is refresh
        mBean.setNextPageToken(resultBean.getResult().getNextPageToken());
        if (mIsRefresh) { // 首次加载或者下拉刷新执行逻辑
            // cache the time
            mTime = resultBean.getTime();
            mBean.setItems(resultBean.getResult().getItems());
            mAdapter.clear();
            mAdapter.addItem(mBean.getItems());
            mBean.setPrevPageToken(resultBean.getResult().getPrevPageToken());
            /**
             * 默认加载更多由多个条件决定,mCanLoadMore默认为false,外部可控制,如果有数据则置为true,否则不具备加载更多功能
             * 如果在有数据的情况下,断掉网络,上拉到底部会显示"网络错误",如果再链接网络继续上拉会更新底部布局控件显示
             **/
            mRefreshLayout.setCanLoadMore();
            AppOperator.runOnThread(new Runnable() {
                @Override
                public void run() {
                    CacheManager.saveObject(getActivity(), mBean, CACHE_NAME);
                }
            });
        } else { // 加载更多逻辑
            mAdapter.addItem(resultBean.getResult().getItems());
        }
        if (resultBean.getResult().getItems().size() < 20) {
            setFooterType(TYPE_NO_MORE);
            // mRefreshLayout.setNoMoreData();
        }
        if (mAdapter.getDatas().size() > 0) {
            mErrorLayout.setErrorType(EmptyLayout.HIDE_LAYOUT);
            mRefreshLayout.setVisibility(View.VISIBLE);
        } else {
            mErrorLayout.setErrorType(EmptyLayout.NODATA);
        }
    }

ItemView开始的一个图标的实现

        String text = "";
        SpannableStringBuilder spannable = new SpannableStringBuilder(text);
        if (item.isOriginal()) {
            spannable.append("[icon] ");
            Drawable originate = mCallback.getContext().getResources().getDrawable(R.mipmap.ic_label_originate);
            if (originate != null) {
                originate.setBounds(0, 0, originate.getIntrinsicWidth(), originate.getIntrinsicHeight());
            }
            ImageSpan imageSpan = new ImageSpan(originate, ImageSpan.ALIGN_BOTTOM);
            spannable.setSpan(imageSpan, 0, 6, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        }


        if (item.isRecommend()) {
            spannable.append("[icon] ");
            Drawable recommend = mCallback.getContext().getResources().getDrawable(R.mipmap.ic_label_recommend);
            if (recommend != null) {
                recommend.setBounds(0, 0, recommend.getIntrinsicWidth(), recommend.getIntrinsicHeight());
            }
            ImageSpan imageSpan = new ImageSpan(recommend, ImageSpan.ALIGN_BOTTOM);
            if (item.isOriginal()) {
                spannable.setSpan(imageSpan, 7, 13, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
            } else {
                spannable.setSpan(imageSpan, 0, 6, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
            }
        }
        title.setText(spannable.append(item.getTitle()));


动弹Tab

TweetFragment.java


图片点击事件的监听抽象类,可以实现传递其他参数

private abstract class OnTweetImageClickListener implements View.OnClickListener {
        @Override
        public void onClick(View v) {
            onClick(v, Integer.parseInt(v.getTag(R.id.iv_tweet_face).toString()),
                    Integer.parseInt(v.getTag(R.id.iv_tweet_image).toString()));
        }


        public abstract void onClick(View v, int position, int imagePosition);
    }

    可能有多个图片的布局容器使用的是自定义控件的FlowLayout

        FlowLayout flowLayout = vh.getView(R.id.fl_image);
        flowLayout.removeAllViews();
        if (images != null && images.length > 0) {
            flowLayout.setVisibility(View.VISIBLE);
            LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams((int) Ui.dipToPx(mCallback.getContext().getResources(), 64)
                    , (int) Ui.dipToPx(mCallback.getContext().getResources(), 64));
            for (int i = 0; i < images.length; i++) {
                ImageView imageView = new ImageView(mCallback.getContext());
                imageView.setLayoutParams(layoutParams);
                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
                imageView.setTag(R.id.iv_tweet_image, i);
                imageView.setOnClickListener(imageClickListener);

                String path = images[i].getThumb();
                DrawableRequestBuilder builder = getImgLoader().load(path)
                        .centerCrop()
                        .placeholder(R.color.grey_200)
                        .error(R.mipmap.ic_default_image_error);
                if (path.toLowerCase().endsWith("gif"))
                    builder = builder.diskCacheStrategy(DiskCacheStrategy.SOURCE);
                builder.into(imageView);
                flowLayout.addView(imageView);
            }
        } else {
            flowLayout.setVisibility(View.GONE);
        }

具体细节查看TweetAdapter.java文件,图片预览细节ImageGalleryActivity.java文件

    /**
     * 以友好的方式显示时间
     *
     * @param sdate
     * @return
     */
    public static String friendly_time(String sdate) {
        Date time = null;


        if (TimeZoneUtil.isInEasternEightZones())
            time = toDate(sdate);
        else
            time = TimeZoneUtil.transformTime(toDate(sdate),
                    TimeZone.getTimeZone("GMT+08"), TimeZone.getDefault());


        if (time == null) {
            return "Unknown";
        }
        String ftime = "";
        Calendar cal = Calendar.getInstance();


        // 判断是否是同一天
        String curDate = dateFormater2.get().format(cal.getTime());
        String paramDate = dateFormater2.get().format(time);
        if (curDate.equals(paramDate)) {
            int hour = (int) ((cal.getTimeInMillis() - time.getTime()) / 3600000);
            if (hour == 0)
                ftime = Math.max(
                        (cal.getTimeInMillis() - time.getTime()) / 60000, 1)
                        + "分钟前";
            else
                ftime = hour + "小时前";
            return ftime;
        }


        long lt = time.getTime() / 86400000;
        long ct = cal.getTimeInMillis() / 86400000;
        int days = (int) (ct - lt);
        if (days == 0) {
            int hour = (int) ((cal.getTimeInMillis() - time.getTime()) / 3600000);
            if (hour == 0)
                ftime = Math.max(
                        (cal.getTimeInMillis() - time.getTime()) / 60000, 1)
                        + "分钟前";
            else
                ftime = hour + "小时前";
        } else if (days == 1) {
            ftime = "昨天";
        } else if (days == 2) {
            ftime = "前天 ";
        } else if (days > 2 && days < 31) {
            ftime = days + "天前";
        } else if (days >= 31 && days <= 2 * 31) {
            ftime = "一个月前";
        } else if (days > 2 * 31 && days <= 3 * 31) {
            ftime = "2个月前";
        } else if (days > 3 * 31 && days <= 4 * 31) {
            ftime = "3个月前";
        } else {
            ftime = dateFormater2.get().format(time);
        }
        return ftime;
    }


下拉刷新上拉加载更多控件RecyclerRefreshLayout.java


TabLayout+ViewPager是一个Fragment【TweetDetailViewPagerFragment】填充了详情页的一个帧布局,ViewPager中是2个Fragment,上滑布局后TabLayot的tab会固定在顶部,同样是使用了协调布局的一个属性+自定义的一个类, 底部的发表评论布局是在KeyboardInputDelegation.java类中填充的


public class TweetDetailActivity extends BaseBackActivity implements TweetDetailContract.Operator中initWidget()方法中有如下片段,此处拿到了Framgent的属性赋值给了Activity中的属性了,当用户删除信息后需要更新Fragment中TabLayout中Tab的数据

        TweetDetailViewPagerFragment mPagerFrag = TweetDetailViewPagerFragment.instantiate(this);
        mCmnViewImp = mPagerFrag.getCommentViewHandler();
        mThumbupViewImp = mPagerFrag.getThumbupViewHandler();
        mAgencyViewImp = mPagerFrag.getAgencyViewHandler();
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.fragment_container, mPagerFrag)
                .commit();


public class TweetDetailViewPagerFragment extends Fragment implements TweetDetailContract.ICmnView, TweetDetailContract.IThumbupView, TweetDetailContract.IAgencyView {

private ViewPager mViewPager;

        private TabLayout mTabLayout;
   
    protected FragmentStatePagerAdapter mAdapter;
   
    private TweetDetailContract.ICmnView mCmnViewImp;
   
    private TweetDetailContract.IThumbupView mThumbupViewImp;
   
    private TweetDetailContract.Operator mOperator;

}

mOperator的实例化

    public void onAttach(Activity activity) {
        super.onAttach(activity);
        mOperator = (TweetDetailContract.Operator) activity;
    }


mCmnViewImp

mThumbupViewImp

接口对象的实例化

onViewCreated() {

if (mAdapter == null){
final ListTweetLikeUsersFragment mCmnFrag;
mThumbupViewImp = mCmnFrag = ListTweetLikeUsersFragment.instantiate(mOperator, this);

    final ListTweetCommentFragment mThumbupFrag;
    mCmnViewImp = mThumbupFrag = ListTweetCommentFragment.instantiate(mOperator, this);
}  

}


点赞列表页面

public class ListTweetLikeUsersFragment extends BaseRecyclerViewFragment<TweetLike> implements TweetDetailContract.IThumbupView {   

    private TweetDetailContract.Operator mOperator;
    private TweetDetailContract.IAgencyView mAgencyView; 


    /** 两个接口对象的实例化 */

    public static ListTweetLikeUsersFragment instantiate(TweetDetailContract.Operator operator, TweetDetailContract.IAgencyView mAgencyView) {
        ListTweetLikeUsersFragment fragment = new ListTweetLikeUsersFragment();
        fragment.mOperator = operator;
        fragment.mAgencyView = mAgencyView;
        return fragment;
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        mOperator = (TweetDetailContract.Operator) activity;
    }  

}

在initViewget()方法中

    protected void initWidget(View root) {
        super.initWidget(root);
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
                    mOperator.onScroll();
                }
            }
        });
    }

调用的就是activity中的onScroll()方法

当请求数据成功的时候机会调用mAgencyView.resetCmnCount(mAdapter.getCount());其实就是回调ViewPager所在类TweetDetailViewPagerFragment的方法更新TabLayout中对应Tab的显示



## 项目说明 FlyCms 是一个类似知乎以问答为基础的完全开源的JAVA语言开发的社交网络建站程序,基于 Spring Boot+Bootstrap3+MyBatis+MYSQL+Solr+Ehcache 应用架构,专注于社区内容的整理、归类和检索,它集合了问答,digg,wiki 等多个程序的优点,帮助用户轻松搭建专业的知识库和在线问答社区。 业务模块包括:权限管理,会员管理,角色管理,定时任务管理(调度管理),问答管理,文章管理,分享管理,短信接口管理和邮件系统发送(注册、找回密码、邮件订阅),跨域登录,消息推送,全文检索、前端国际化等等众多模块等您自己来体验! ##### 开放源码,便捷开发 基于Spring Boot版本开发,利用成熟开发技术,开放系统源码和开发文档,让二次开发更容易。 ##### 弹性设计,任性定制 UI设计简约又灵活, Bootstrap定制模板让用户自由发挥,让自己的社区更具独特个性。 ##### 管理后台,轻松掌控 管理后台功能实用简约,只需轻点功能开关,即可完成系统配置,属于你的知识社区一挥而就。 ##### 轻快智能,更懂社交 FlyCms比论坛更开放,比资讯网站更了解用户,以用户为中心,更懂社交,更注重内容的分析和分享。 ##### 用戶互助,精准推送 用户之间相互解答,分享产品使用心得,只向用户推送自己关心的消息,扩大价值,压缩干扰。 > 使用说明:请保留页面底部的 *powered by 28844. com* 感谢支持 在线地址: [28844.com](http://www.28844.com) | [文档地址](https://www.28844.com/) ## 技术栈 - JDK8 - Spring-Boot - MyBatis - Freemarker - MySQL - Bootstrap3 - Solr - Quartz - i18n - Ehcache ## 特性 - 社区兼容性(IE9+) - 页面自适应布局 - 本地登录,手机注册, 邮件注册 - 登录时有验证码,尝试登录次数的限制 - 使用 `Spring-Boot` 开发 - MyBatis操作数据库 - 目前sql语句只支持MySQL,如果要切换数据库,需要手动修改代码 - Solr提供搜索和前台部分页面列表查询和分页 ## 网站功能 ##### 前台功能 - 问答栏目,用户可发布奖励积分邀请回答问题 - 答案发布管理统计等等; - 文章栏目,专家专栏,后台设置专家组可发布专业性文章资讯 - 文章栏目可分类,文章栏目目前可无限极分类 - 分享栏目,用户可分享发布如百度网盘资源地址,电影地址等等,赚取积分 - 各个栏目话题聚合内容,话题可编辑删除,审核; - 用户个人首页,可查看自己关注的用户和自己发布的内容 - 用户内容收藏 - 用户积分管理充值 - 用户绑定手机号码 - 用户绑定邮箱地址 - 用户密码找回 - 用户手机号码注册,需后台绑定运营商API; - 用户邀请注册奖励 ##### 网站后台 - 网站运营基本信息编辑 - 用户注册相关信息管理 - 用户手机运营商信息管理(目前只支持阿里大鱼) - 网站发送邮箱绑定,邮箱模板设置 - 会员信息管理、审核 - 问答管理 - 答案管理 - 文章管理、文章分类 - 话题管理 - 分享管理 - 友情链接管理 - 积分奖励规则管理 - 违禁关键词、敏感词过滤设置 - 增加定时任务管理,通过后台设置启动、关闭,执行任务日志 #### 二次开发准备 1. 开发环境要在IDE里装上lombok插件,否则编译器会报错 2. 程序部署建议使用Maven的assembly插件实现自定义打包方式 3. 项目已经接入了solr,程序安装需要先运行solr,solr已调试好,可直接启动,使用方法可访问官网查看; 4. 项目用到了阿里大于jar包,Maven时无法使用,可以下载doc/dysmsapi.zip,然后放本地的Maven的.m2\repository\com\alibaba\aliyun\目录下 5. 搜索和首页列表后期大部分前台列表准备都使用solr,所以要先下载目录下的solr,然后运行后启动程序,不然报错! #### solr原来的目录里上传的时候jar文件无法上传,压缩后提示上传文件不能大于10MB ## 如果需要的话进群空间下载,QQ群:`211378508` ## Windows下solr启动 - 比如solr放E:/盘下面,首先打开CMD窗口 - `e: `回车 - `cd e:/solr/bin` 回车 - `solr start -p 8983` 回车,等待启动成功
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值