package com.apobates.forum.thrones.controller;

import com.apobates.forum.attention.ImageDirectoryEnum;
import com.apobates.forum.attention.ImageStoreDomain;
import com.apobates.forum.attention.core.AvatarImagePathConvert;
import com.apobates.forum.core.service.TopicActionCollectionService;
import com.apobates.forum.event.elderly.ForumActionEnum;
import com.apobates.forum.event.elderly.IpLocation;
import com.apobates.forum.event.elderly.MemberActionDescriptor;
import com.apobates.forum.event.utils.IpMatchLocationResult;
import com.apobates.forum.member.MemberBaseProfile;
import com.apobates.forum.member.MemberProfileBean;
import com.apobates.forum.member.entity.Member;
import com.apobates.forum.member.entity.MemberGroupEnum;
import com.apobates.forum.member.entity.MemberOnline;
import com.apobates.forum.member.entity.MemberRoleEnum;
import com.apobates.forum.member.exception.MemberNamesExistException;
import com.apobates.forum.member.exception.MemberNamesProtectException;
import com.apobates.forum.member.service.MemberOnlineService;
import com.apobates.forum.member.service.MemberService;
import com.apobates.forum.member.storage.OnlineMemberStorage;
import com.apobates.forum.member.storage.core.MemberSessionBean;
import com.apobates.forum.thrones.controller.form.LoginForm;
import com.apobates.forum.thrones.controller.form.RegisteForm;
import com.apobates.forum.thrones.controller.helper.OnlineDescriptor;
import com.apobates.forum.thrones.exception.ResourceNotFoundException;
import com.apobates.forum.thrones.vo.MemberModel;
import com.apobates.forum.thrones.vo.TopicStatsCollect;
import com.apobates.forum.thrones.vo.TopicStatsCollect.TopicStatsCollectBuilder;
import com.apobates.forum.utils.lang.CommonBean;
import com.apobates.forum.utils.Commons;
import com.apobates.forum.utils.CookieUtils;
import com.apobates.forum.utils.DateTimeUtils;
import com.apobates.forum.utils.TipMessage;
import com.apobates.forum.utils.lang.EnumArchitecture;
import eu.bitwalker.useragentutils.UserAgent;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.support.RequestContextUtils;

/**
 * 会员控制器
 *
 * @author xiaofanku
 * @since 20200511
 */
@Controller
@RequestMapping(value = "/member")
public class MemberController {
    @Autowired
    private MemberService memberService;
    @Autowired
    private MemberOnlineService memberOnlineService;
    @Autowired
    private TopicActionCollectionService topicActionCollectionService;
    @Autowired
    private ServletContext servletContext;
    @Autowired
    private ImageStoreDomain imageStoreDomain;
    @Autowired
    private OnlineMemberStorage onlineMemberStorage;
    @Value("${site.defat.avtar}")
    private String avatarDefaultDirect;
    private final static Logger logger = LoggerFactory.getLogger(MemberController.class);
    
    // 会员主页
    @GetMapping(path = "/{id}.xhtml")
    @OnlineDescriptor(action=ForumActionEnum.COMMON_VISIT)
    public String home(
            @PathVariable("id") long memberId,
            @RequestParam(value = "token", required = false, defaultValue = "0") String token,
            MemberSessionBean mbean,
            HttpServletRequest request,
            Model model) {
        Member m = memberService.get(memberId).orElseThrow(()->new ResourceNotFoundException("目标丢失或暂时无法访问"));
        model.addAttribute("mine", (memberId == mbean.getMid()));
        model.addAttribute("token", token);
        //
        Map<String, String> data = new HashMap<>(getStats(memberId).toMap());
        //活跃记录
        Optional<MemberOnline> mos = memberOnlineService.get(memberId);
        if (mos.isPresent()) {
            data.put("activeDateTime", DateTimeUtils.formatClock(mos.get().getActiveDateTime()));
        } else {
            data.put("activeDateTime", "-");
        }
        //
        model.addAttribute("member", MemberBaseProfile.init(m).toMergeMap(data));
        return "default/member/index";
    }
    // 查看任意会员头像
    @GetMapping(path = "/avatar/{id}.png", produces = MediaType.IMAGE_PNG_VALUE)
    public ResponseEntity<byte[]> getMemberAvatar(
            @PathVariable("id") long memberId,
            HttpServletResponse response,
            Model model) {
        String rav;
        try{
            rav = AvatarImagePathConvert.getDefaultAvtarFilePath(memberService.getMemberAvatar(memberId).orElse("/empty.png"));
        }catch(ClassCastException e){ //class org.springframework.cache.support.NullValue cannot be cast to class java.lang.String (org.springframework.cache.support.NullValue is in unnamed module of loader org.apache.catalina.loader.ParallelWebappClassLoader @649268a9; java.lang.String is in module java.base of loader 'bootstrap')
            rav = "/empty.png";
        }
        try {
            InputStream in = servletContext.getResourceAsStream("/" + avatarDefaultDirect + rav);
            byte[] avtarBytes = IOUtils.toByteArray(in);
            final HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.IMAGE_PNG);
            headers.setContentLength(avtarBytes.length);
            return new ResponseEntity<>(avtarBytes, headers, HttpStatus.CREATED);
        } catch (IOException e) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return null;
        }
    }
    // 会员面板(页面顶部的)
    @GetMapping(path = "/panel", produces = "application/json;charset=UTF-8")
    @ResponseBody
    public String getMemberProfile(MemberSessionBean mbean, HttpServletRequest request, Model model) {
        try {
            String defAvatar = onlineMemberStorage.getMetaConfig().getSite()+"/"+avatarDefaultDirect+"/empty.png";
            Supplier<URL> def = () -> {
                try{
                    return new URL(defAvatar);
                }catch(MalformedURLException e){
                    throw new RuntimeException(e.getMessage());
                }
            };
            URL avatarUrl = decodeDefaultAvatarURL(mbean.getAvatar()).orElseGet(def);
            return mbean.clone(avatarUrl).toJson();
        } catch (NullPointerException | IOException e) {
            if (logger.isDebugEnabled()) {
                logger.debug("convert member avatar fail, reason:"+e.getMessage());
            }
        }
        return "{}";
    }
    // 快速登录
    @PostMapping(path = "/login/quick", produces = "application/json;charset=UTF-8")
    @ResponseBody
    @OnlineDescriptor(action=ForumActionEnum.MEMBER_LOGIN)
    public String quickLoginAction(
            @RequestParam("names") String names,
            @RequestParam("password") String password,
            @RequestParam(value = "token", required = false, defaultValue = "0") String token,
            HttpServletRequest request,
            HttpServletResponse response,
            HttpSession session,
            Model model) {
        MemberActionDescriptor mad = getInstance(request, token);
        Member member = null;
        try{
            member = memberService.signIn(names.trim(), password.trim(), mad).orElse(null);
        }catch(IllegalStateException e){}
        if (null != member) {
            MemberSessionBean mbean = new MemberSessionBean(member, mad.location().getIpAddr());
            onlineMemberStorage.store(mbean, request, response);
            return mbean.toJson();
        }
        return TipMessage.ofError("会员不存在或密码错误").toJsonString();
    }
    
    // 登录
    @GetMapping(path = "/login")
    @OnlineDescriptor(action=ForumActionEnum.MEMBER_LOGIN)
    public String loginForm(
            @RequestParam(value = "from", required = false, defaultValue = "") String fromPath,
            @RequestParam(value = "token", required = false, defaultValue = "0") String token,
            MemberSessionBean mbean,
            HttpServletRequest request,
            Model model) {
        if (mbean.isOnline()) {
            return "redirect:/";
        }
        LoginForm form = new LoginForm();
        String A = Commons.isNotBlank(fromPath) ? fromPath : Commons.getNativeURL(request.getHeader("referer"), onlineMemberStorage.getMetaConfig().getSite(), "/");
        form.setRedirect(A);
        form.setTries(1);
        form.setToken(token);
        model.addAttribute("form", form);
        return "default/member/login";
    }
    
    @PostMapping(path = "/login")
    @OnlineDescriptor(action=ForumActionEnum.MEMBER_LOGIN)
    public String loginAction(
            HttpServletRequest request,
            HttpServletResponse response,
            HttpSession session,
            @Valid @ModelAttribute("form") LoginForm form,
            BindingResult bindingResult,
            Model model) {
        if (bindingResult.hasErrors()) {
            form.setTries(form.getTries() + 1);
            model.addAttribute("form", form);
            return "default/member/login";
        }
        MemberActionDescriptor mad = getInstance(request, form.getToken());
        Member member = null;
        try{
            member=memberService.signIn(form.getNames(), form.getPswd(), mad).orElse(null);
        }catch(IllegalStateException e){}
        if (null == member) {
            form.setTries(form.getTries() + 1);
            model.addAttribute("form", form);
            model.addAttribute("errors", "会员不存在或密码错误");
            return "default/member/login";
        }
        MemberSessionBean mbean = new MemberSessionBean(member, mad.location().getIpAddr());
        onlineMemberStorage.store(mbean, request, response);
        // ----------------------------------------清除客户端缓存的cookie标记,重新拉取@20200502
        if (Commons.isNotBlank(form.getTrace())) {
            emptyTraceForCookie(form.getTrace(), "login", request, response);
        }
        if (Commons.isNotBlank(form.getRedirect())) {
            return "redirect:" + (forbidRTLoginDirectURL(form.getRedirect()) ? "/" : form.getRedirect()); //保证地址是内部可跳转
        }
        return "redirect:/member/" + mbean.getMid() + ".xhtml";
    }
    // 注册
    @GetMapping(path = "/register")
    @OnlineDescriptor(action=ForumActionEnum.MEMBER_REGISTER)
    public String registeForm(
            @RequestParam(value = "invite", required = false, defaultValue = "") String inviteCode,
            @RequestParam(value = "token", required = false, defaultValue = "0") String token,
            MemberSessionBean mbean,
            HttpServletRequest request,
            Model model) {
        //--------------------------------------------------
        if (mbean.isOnline()) { // (R->T->L)(firefox: /member/register)
            return "redirect:/";
        }
        //配合:MemberInviteCodeInterceptorAdapter,不然会丢失上次输入的值
        Map<String, ?> inputFlashMap = RequestContextUtils.getInputFlashMap(request);
        if(inputFlashMap == null || !inputFlashMap.containsKey("form")){
            RegisteForm form = new RegisteForm();
            form.setToken(token);
            model.addAttribute("form", form);
        }
        return "default/member/register";
    }
    @PostMapping(path = "/register")
    @OnlineDescriptor(action=ForumActionEnum.MEMBER_REGISTER)
    public String registeAction(
            HttpServletRequest request,
            HttpServletResponse response, 
            @Valid @ModelAttribute("form") RegisteForm form,
            BindingResult bindingResult,
            Model model) {
        if (bindingResult.hasErrors()) {
            model.addAttribute("form", form);
            return "default/member/register";
        }
        String mnames = Commons.getAlphaNumberCharacter(form.getNames());
        if(mnames.length()<5 || mnames.length()>20){
            model.addAttribute("errors", "帐号可用长度小于5或大于20");
            model.addAttribute("form", form);
            return "default/member/register";
        }
        String mnick = Commons.chineseStringLength(form.getNickname()) < 5 ? "Member#" + Commons.randomNumeric(4) : form.getNickname();
        try{
            if(memberService.signUp(mnames, 
                    form.getPswdConfirm(), 
                    request.getParameter(Commons.optional(()->request.getSession().getAttribute("ICInputKey"), "-")), 
                    mnick, 
                    getInstance(request, form.getToken()))
                    .isPresent()){
                return "redirect:/member/register/success.xhtml";
            }
        }catch(IllegalArgumentException | IllegalStateException e){
            if(logger.isDebugEnabled()){
                logger.debug(e.getMessage(), e);
            }
        }
        form.setNickname(mnick);
        form.setNames(mnames);
        model.addAttribute("form", form);
        model.addAttribute("errors", "会员注册失败");
        return "default/member/register";
    }
    //配合:Bootstrap validator使用@see:https://github.com/1000hz/bootstrap-validator@BS3
    //配合:parsley.js使用@see:http://parsleyjs.org/@BS4
    //检查会员帐号是否可用
    @GetMapping(path = "/detection/unique")
    public ResponseEntity<String> checkMemberNames(
            @RequestParam("names") String names,
            HttpServletRequest request,
            Model model) {
        String tipMessage = "会员帐号唯一性检测失败";
        boolean symbol = false;
        //
        try{
            boolean result = memberService.checkNamesUnique(names).orElse(false);
            if(result){
                tipMessage = "可以使用"; symbol = true;
            }
        }catch(MemberNamesProtectException | MemberNamesExistException e){
            tipMessage = e.getMessage();
        }
        //
        HttpHeaders responseHeaders = new HttpHeaders();
        responseHeaders.set("Content-Type", "text/plain; charset=UTF-8"); //Content-Type: text/plain;charset=ISO-8859-1
        HttpStatus hs = symbol ? HttpStatus.OK : HttpStatus.BAD_REQUEST;
        return new ResponseEntity<>(tipMessage, responseHeaders, hs);
    }
    //配合:Bootstrap validator使用@see:https://github.com/1000hz/bootstrap-validator@BS3
    //配合:parsley.js使用@see:http://parsleyjs.org/@BS4
    //检查注册邀请码是否可用
    @GetMapping(path = "/detection/invite")
    public ResponseEntity<String> checkMemberInviteCode(
            @RequestParam("code") String inviteCode,
            HttpServletRequest request,
            Model model){
        String tipMessage="验证码不可用";
        boolean symbol = false;
        //
        Map<String,String> result = memberService.getActiveInviteCode(inviteCode);
        if(!result.get("id").equals("0")){
            tipMessage = "可以使用"; symbol = true;
        }
        HttpHeaders responseHeaders = new HttpHeaders();
        responseHeaders.set("Content-Type", "text/plain; charset=UTF-8"); //Content-Type: text/plain;charset=ISO-8859-1
        HttpStatus hs = symbol ? HttpStatus.OK : HttpStatus.BAD_REQUEST;
        return new ResponseEntity<>(tipMessage, responseHeaders, hs);
    }
    //校验uid时的
    @GetMapping(path = "/detection/uid")
    @ResponseBody
    public TipMessage checkMemberUID(
            @RequestParam("value") String uidString,
            MemberSessionBean mbean,
            HttpServletRequest request,
            Model model) {
        long memberId= Commons.stringToLong(uidString.substring(1), 0L);
        if (memberId <= 0) {
            return TipMessage.ofError("会员不接受消息");
        }
        final Member m = memberService.get(memberId).orElse(null);
        if (null == m || m.getId() == mbean.getMid()) {
            return TipMessage.ofError("会员不存在");
        }
        return TipMessage.ofSuccess(m.getNickname());
    }
    // 退出系统
    @GetMapping(path = "/offline.xhtml")
    public String signout(
            @RequestParam(value = "token", required = false, defaultValue = "0") String token,
            @RequestParam(value = "trace", required = false, defaultValue = "") String cookieTraceKey,
            MemberSessionBean mbean,
            HttpServletRequest request,
            HttpServletResponse response,
            HttpSession session,
            Model model) {
        if (mbean.isOnline()) {
            memberService.signOut(mbean.getNames(), mbean.toMember(), getInstance(request, token));
            // ----------------------------------------移除Cookie
            onlineMemberStorage.delete(request, response);
            // ----------------------------------------清除客户端缓存的cookie标记,重新拉取@20200502
            if (Commons.isNotBlank(cookieTraceKey)) {
                emptyTraceForCookie(cookieTraceKey, "offline", request, response);
            }
        }
        return "default/member/logout";
    }
    
    @GetMapping(path = "/register/success.xhtml")
    public String registeSuccess(MemberSessionBean mbean, HttpServletRequest request, Model model) throws IllegalAccessException {
        //来源页判断 http Referer: site.domain{http://center.test.com}/member/register
        String reqRef = request.getHeader("referer");
        if (null == reqRef || !reqRef.equals(onlineMemberStorage.getMetaConfig().getSite() + "/member/register")) {
            throw new IllegalAccessException("非法的请求路径");
        }
        if (mbean.isOnline()) {
            return "redirect:/";
        }
        return "default/member/success";
    }
    
    @GetMapping(path = "/register/licenses.xhtml")
    public String registeLicenses() {
        return "default/member/licenses";
    }
    
    // 会员菜单(话题内容页中的弹出菜单)
    @GetMapping(path = "/profile.json", produces = "application/json;charset=UTF-8")
    @ResponseBody
    public MemberProfileBean getMembereProfile(
            @RequestParam("id") long memberId,
            HttpServletRequest request,
            Model model) {
        if(memberId <=0){
            return MemberProfileBean.guest();
        }
        Map<ForumActionEnum, Long> rawdata = topicActionCollectionService.statsMemberAllAction(memberId); //.groupMemberTopicAction(memberId, allowActionSet());
        return memberService.calcMemberProfileBean(memberId, rawdata).orElseGet(MemberProfileBean::guest);
    }
    
    //版块首页(/board/home)的社区标兵
    @GetMapping(path = "/model/json", produces = "application/json;charset=UTF-8")
    @ResponseBody
    public List<MemberModel> getTopicMemberModel(HttpServletRequest request, Model model) {
        Map<Long, Map<String, Long>> rs = topicActionCollectionService.orderMemberTopices(10);
        Function<Entry<Long, Map<String, Long>>, MemberModel> coverMapper = entry->{
            long mid = entry.getKey();
            return entry.getValue().entrySet().stream().map(tmp->new MemberModel(mid, tmp.getKey(), tmp.getValue())).findFirst().get();
        };
        Stream<MemberModel> data = rs.entrySet().stream().map(coverMapper);
        return data.sorted(Comparator.comparing(MemberModel::getValue).reversed()).collect(Collectors.toList());
    }
    
    //话题|版块配置中允许的会员组
    @GetMapping(path = "/group/json", produces = "application/json;charset=UTF-8")
    @ResponseBody
    public List<CommonBean> getAllowMemberGroup() {
        Map<Integer, String> memberGroupData = EnumArchitecture.getInstance(MemberGroupEnum.class);
        return memberGroupData.entrySet().stream().filter(r -> r.getKey() >= 0 && r.getKey() < 3).map(r -> new CommonBean(r.getKey(), r.getValue())).collect(Collectors.toList());
    }
    
    //话题|版块配置中允许的会员角色
    @GetMapping(path = "/role/json", produces = "application/json;charset=UTF-8")
    @ResponseBody
    public List<CommonBean> getAllowMemberRole() {
        Map<Integer, String> memberRoleData = EnumArchitecture.getInstance(MemberRoleEnum.class);
        return memberRoleData.entrySet().stream().map(r -> new CommonBean(r.getKey(), r.getValue())).collect(Collectors.toList());
    }
    
    //[版块列表]底部的会员统计汇总
    @GetMapping(path = "/stats/collect", produces = "application/json;charset=UTF-8")
    @ResponseBody
    public Map<String, String> getMemberStatsCollect(HttpServletRequest request, Model model) {
        Map<String, String> stats = new HashMap<>();
        try {
            stats.put("members", memberService.count() + ""); //注册人数
        } catch (NullPointerException e) {
            stats.put("members", "0");
        }
        try {
            String jsonString = memberService.getRecent(1).map(m -> new CommonBean(m.getId(), m.getNickname())).map(CommonBean::toJson).findAny().orElse("{}");
            stats.put("recentMember", jsonString); //最近注册的会员是谁?
        } catch (NullPointerException e) {
            stats.put("recentMember", "{}");
        }
        Long onlines = 0L;
        try {
            LocalDateTime finish = LocalDateTime.now();
            LocalDateTime start = DateTimeUtils.beforeMinuteForDate(finish, 30);
            onlines = memberOnlineService.count(start, finish); //这里不包含游客
        } catch (Exception e) {}
        stats.put("onlines", onlines + ""); //在线的总人数
        return stats;
    }
    
    // 快速登录的表单
    @GetMapping(path = "/login/form")
    public String getQuickLoginForm(
            @RequestParam(value = "token", required = false, defaultValue = "0") String token,
            MemberSessionBean mbean,
            HttpServletRequest request,
            Model model) {
        //---------------------------------------------是否来源自本站
        String requestRefString = request.getHeader("referer");
        if (null == requestRefString || !requestRefString.startsWith(onlineMemberStorage.getMetaConfig().getSite())) { // 只允许本站连接
            return "default/common/illegal_embed";
        }
        //---------------------------------------------
        if (mbean.isOnline()) {
            model.addAttribute("member", MemberBaseProfile.init(mbean.toMember()).toMergeMap(Map.ofEntries(Map.entry("status", mbean.getStatus().getTitle()))));
            return "default/member/member_info_embedded";
        }
        model.addAttribute("token", token);
        return "default/member/login_embedded";
    }
    private TopicStatsCollect getStats(long memberId) {
        TopicStatsCollectBuilder tscb = new TopicStatsCollectBuilder(memberId);
        Map<ForumActionEnum, Long> statsActions = topicActionCollectionService.statsMemberAction(memberId, List.of(ForumActionEnum.TOPIC_PUBLISH, ForumActionEnum.POSTS_REPLY, ForumActionEnum.TOPIC_LIKED, ForumActionEnum.TOPIC_FAVORITE));
        try {
            Long ts = statsActions.get(ForumActionEnum.TOPIC_PUBLISH);
            tscb.threads(Commons.optionalOrZero(ts));
        } catch (Exception e) {}
        try {
            Long rs = statsActions.get(ForumActionEnum.POSTS_REPLY);
            tscb.replies(Commons.optionalOrZero(rs));
        } catch (Exception e) {}
        //
        try {
            Long tls = statsActions.get(ForumActionEnum.TOPIC_LIKED);
            tscb.threadsLikes(Commons.optionalOrZero(tls));
        } catch (Exception e) {}
        try {
            Long fls = statsActions.get(ForumActionEnum.TOPIC_FAVORITE);
            tscb.threadsFavorites(Commons.optionalOrZero(fls));
        } catch (Exception e) {}
        return tscb.build();
    }
    /**
     * 清除客户端缓存的cookie标记,重新拉取一次@20200502<br/>
     * 需要js-cookie的服务器端依赖<br/>
     * [有重复]com.apobates.forum.trident.controller.admin.HomeController.emptyTraceForCookie 
     * {@link https://github.com/js-cookie/java-cookie}
     * {@link https://github.com/js-cookie/js-cookie/blob/master/SERVER_SIDE.md}
     *
     * @param cookieKey js-cookie设置的cookie key, 客户端缓存的cookie标记
     * @param source 来自哪个方法
     * @param request http请求对象
     * @param response http响应对象
     */
    private void emptyTraceForCookie(String cookieKey, String source, HttpServletRequest request, HttpServletResponse response) {
        logger.info(String.format("[HMP][%s]current destroy cookie by key:%s", source, cookieKey));
        CookieUtils.expireCookie(request, response, cookieKey);
    }
    //注册->提示->登录禁止跳转的地址
    private boolean forbidRTLoginDirectURL(String jumpUrl) {
        return Arrays.asList("/member/register", "/member/register/success.xhtml", "/member/login").contains(jumpUrl);
    }
    /**
     * 解码会员头像|头像是本地存储
     *
     * @param avatarURI DB中保存的编码后的头像地址,例:avtar://defat[|local|remot]/目录名/文件名
     * @return
     * @throws MalformedURLException
     */
    private Optional<URL> decodeDefaultAvatarURL(String avatarURI) throws MalformedURLException {
        if(Commons.isNotBlank(avatarURI)){
            return new AvatarImagePathConvert(imageStoreDomain, avatarDefaultDirect).decode(avatarURI, ImageDirectoryEnum.LOCAL, null);
        }
        return Optional.empty();
    }
    public static MemberActionDescriptor getInstance(HttpServletRequest request, String token) {
        final String referrer = request.getHeader("referer");
        final String ipAddr = Commons.getRequestIp(request);
        final String operateToken = Commons.isNotBlank(token) ? token : "-";
        final String agent = request.getHeader("User-Agent");
        //
        return new MemberActionDescriptor() {
            @Override
            public String getToken() {
                return operateToken;
            }
            
            @Override
            public IpLocation location() {
                return new IpMatchLocationResult(ipAddr);
            }
            
            @Override
            public String getReferrer() {
                return referrer;
            }
            
            @Override
            public String getDevice() {
                return UserAgent.parseUserAgentString(agent).getOperatingSystem().getName();
            }
            
            @Override
            public String getAgent() {
                return agent;
            }
        };
    }
}