Nccloud1909集成方案说明

限于喜欢 提交于 2020-10-22 01:42:16

Nccloud单点手册 wangmo 2020年07月01日

本方案适用于version:Nccloud1909+,不支持1903,1903有其他实现方案;

 

功能支持:单点登陆,单点审批,单点联查

单点登陆

1:业务系统向Nccloud注册accesstoken

请求参数:usercode=wangmo
模拟地址:http://localhost/service/registAccessToken?usercode=wangmo
成功响应:{"status":"1","result":{"accesstoken":"20011b791553a8338e4b9a7c8936111c"}}
失败响应:{"status":"0","result":"nccloud系统中没有usercode对应的用户,请联系管理员同步人员并生成用户!"}

 

2:业务系统携带Token访问Nccloud登陆页面

请求地址:http://localhost/nccloud/resources/uap/rbac/thirdpartylogin/main/index.html?accesstoken=20011b791553a8338e4b9a7c8936111c
成功响应:将直接转到首页http://localhost/nccloud/resources/workbench/public/common/main/index.html
失败响应:未找到第三方系统 ...

 

单点审批

1:前提条件

待办任务已经发送到第三方系统 task = sm_msg_approve.pk_message
select * from sm_msg_approve where PK_MESSAGE='1001ZZ10000000007JMA'

 

2:第三方系统请求注册Token和获取任务模拟审批中心的地址

请求参数:usercode=wangmo&task=1001ZZ10000000007JMA
模拟地址:http://localhost/service/registAccessToken?usercode=wangmo&task=1001ZZ10000000007JMA
描述:通过usercode和task确定用户应用页面模板等信息,用于模拟审批中心地址
成功响应:
{
    "status":"1",
    "result":
    {
        "accesstoken":"2cd633f56a5a8422740f48b37a9d07a3",
  "redirect":"ifr=%252Fuap%252Fmsgcenter%252Fmessage%252FapproveDetail%252Findex.html%2523%252F%253Fpk_message%253D1001ZZ10000000007JMA%2526pageMsgType%253Dapprove%2526checknote%253D批准%2526p%253DHTNZ-Cxx-99%2526c%253D81H10200A%2526n%253D合同拟制审批"
    }
}
失败响应:
{"status":"0","result":"不存在的流程任务Task:1001ZZ10000000007JMA1"}

3:携带Token和redirect请求中转页面

请求参数:accesstoken=2cd633f56a5a8422740f48b37a9d07a3&type=approve&redirect=data.result.redirectURL
参数说明:accesstoken 用于登陆 type 用于判断是否(单点登陆,单点联查,单点审批) redirect 注册时返回的地址
模拟地址:http://localhost/nccloud/resources/uap/rbac/thirdpartylogin/main/index.html?accesstoken=2cd633f56a5a8422740f48b37a9d07a3&type=approve&redirect_uri=ifr=%252Fuap%252Fmsgcenter%252Fmessage%252FapproveDetail%252Findex.html%2523%252F%253Fpk_message%253D1001ZZ10000000007JMA%2526pageMsgType%253Dapprove%2526checknote%253D%E5%8D%95%E7%82%B9%E5%AE%A1%E6%89%B9%2526c%253D81H10200A
成功响应:跳转到审批界面
失败响应:请发布场景应用 ...等

 

单点联查

1:前提条件

单据信息已经发送到第三方系统
主要信息 billid  billtype

2:业务系统向Nccloud注册accesstoken

请求参数:usercode=wangmo
模拟地址:http://localhost/service/registAccessToken?usercode=wangmo
成功响应:{"status":"1","result":{"accesstoken":"20011b791553a8338e4b9a7c8936111c"}}
失败响应:{"status":"0","result":"nccloud系统中没有usercode对应的用户,请联系管理员同步人员并生成用户!"}

3:携带Token和billid billtype请求中转页面

请求参数:accesstoken=2cd633f56a5a8422740f48b37a9d07a3&type=link&billid=billid&billtype=billtype
参数说明:accesstoken 用于登陆 type 用于判断是否(单点登陆,单点联查,单点审批) billid billtype单据信息
模拟地址:http://localhost/nccloud/resources/uap/rbac/thirdpartylogin/main/index.html?accesstoken=2cd633f56a5a8422740f48b37a9d07a3&type=link&billid=1001ZZ10000000007JLY&billtype=HTNZ
成功响应:跳转到单据卡片界面浏览态
失败响应:没有应用权限 ...等
ncc_for_uap_debug: 单据联查数据权限sql=select distinct ht_contract_h.pk_contract_h from  ht_contract_h left join ht_contract_history on ht_contract_h.pk_contract_h = ht_contract_history.pk_contract_h left join ht_contract_review on ht_contract_h.pk_contract_h = ht_contract_review.pk_contract_h left join ht_contract_measure on ht_contract_h.pk_contract_h = ht_contract_measure.pk_contract_h left join ht_contract_exe on ht_contract_h.pk_contract_h = ht_contract_exe.pk_contract_h left join ht_contract_materiel on ht_contract_h.pk_contract_h = ht_contract_materiel.pk_contract_h where 1=1  and ht_contract_h.pk_org='GLOBLE00000000000000'  and ht_contract_h.dr=0 and ht_contract_h.pk_contract_h='1001ZZ10000000007JLY' 
ncc_for_uap_debug:该单据无小应用81H10200的数据权限,或组织权限,请刷新后重试 appcode:81H10200 pk:1001ZZ10000000007JLY


解决方案:开发配置 应用管理 设置应用适用类型全局改为业务单元

 

 

代码支持

package ncc.sso.bs;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.alibaba.fastjson.JSONObject;

import nc.bs.framework.adaptor.IHttpServletAdaptor;
import nc.bs.framework.common.InvocationInfoProxy;
import nc.bs.framework.common.NCLocator;
import nc.bs.logging.Logger;
import nc.itf.uap.rbac.IUserManageQuery;
import nc.sso.bs.ISSOService;
import nc.sso.bs.RandomUtils;
import nc.sso.vo.SSOAuthenConfVO;
import nc.vo.pub.BusinessException;
import nc.vo.sm.UserVO;
import nccloud.message.itf.IMessageQueryService;
import nccloud.putitf.riart.billtype.IBillRelatedAppFinder;
import nccloud.riart.pubimpl.billtype.BillRelatedAppFactory;

/***
 * @discription Ncc端单点登录注册类
 * @author  wangmo
 * @date    2020年06月10日
 * @UPM  <component accessProtected="false"  name="registAccessToken" remote="false" singleton="true" tx="NONE"><implementation>ncc.sso.bs.ThirdPartyAccessTokenServlet</implementation></component>
 */
public class ThirdPartyAccessTokenServlet extends HttpServlet implements IHttpServletAdaptor {
    
    /**
     * 部分固定参数
     */
    private final static String Dsname  = "design";
    private final static String GroupId = "0001X1100000000003V9";
    private final static String Language = "simpchn";
    private final static String BizCenterCode = "develop";
    
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)  throws ServletException, IOException {
        doAction(req, resp);
    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doAction(req, resp);
    }

    public void doAction(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
        try {
            response.setContentType("application/json;charset=utf-8");
            //TODO 此处省略系统之间的请求加密及安全校验过程 ; 
            //securityValidate(request);
            environmentProxy(request);
            UserVO securityUser = validateUser(request.getParameter("usercode"));
            //NCLocator.getInstance().lookup(ISecurityTokenCallback.class).token("NCSystem".getBytes(), "pfxx".getBytes());
            String access_token = registerSSOToken(securityUser);
            //@2.1判task的有效性
            String taskId = request.getParameter("task");
            if(!isEmpty(taskId)){
                IMessageQueryService workListQuery = NCLocator.getInstance().lookup(nccloud.message.itf.IApproveMessageQueryService.class);
                JSONObject obj = workListQuery.queryNCMessageByPk(taskId, securityUser.getCuserid());
                if(obj == null) {
                    throw new BusinessException("不存在的流程任务Task:"+taskId);
                }
                IBillRelatedAppFinder appFinder = BillRelatedAppFactory.getInstance().createAppFinder(null);
                Map<String, String> appInfo = appFinder.getTargetURL("", obj.getString("billtype"));
                //TODO 获取对应节点信息 
                //走1级地址 注册Token时取地址
                StringBuilder sb = new StringBuilder();
                sb.append("ifr=%252Fuap%252Fmsgcenter%252Fmessage%252FapproveDetail%252Findex.html%2523%252F%253Fpk_message%253D");
                sb.append(obj.getString("pk_message"));  //1001ZZ10000000009WIL
                sb.append("%2526pageMsgType%253Dapprove");
                sb.append("%2526checknote%253D批准");
                sb.append("%2526p%253D");
                sb.append(appInfo.get("pagecode"));
                sb.append("%2526c%253D");
                sb.append(appInfo.get("appcode"));
                sb.append("%2526n%253D");
                sb.append(appInfo.get("pagename"));
                response.getWriter().write("{\"status\":\"1\",\"result\":{\"accesstoken\":\""+access_token+"\",\"redirect\":\""+sb.toString()+"\"}}");
            }else {
                response.getWriter().write("{\"status\":\"1\",\"result\":{\"accesstoken\":\""+access_token+"\"}}");
            }
        } catch (Exception e) {
            response.getWriter().write("{\"status\":\"0\",\"result\":\""+e.getMessage()+"\"}");
            Logger.error(e);
        }finally {
            response.getWriter().close();
        }
    }

    /**
     * <p>Title: registerSSOToken</p>  
     * <p>Description: 注册Token</p>  
     * @param securityUser
     * @param access_token
     * @throws BusinessException
     */
    private String registerSSOToken(UserVO securityUser) throws BusinessException {
        InvocationInfoProxy.getInstance().setUserId(securityUser.getCuserid());
        String access_token = RandomUtils.genRandomNumber(0, "");
        NCCSSORegInfo ssoInfo = new NCCSSORegInfo();
        ssoInfo.setUsercode(securityUser.getUser_code());
        ssoInfo.setLangCode(Language);
        ssoInfo.setBusiCenterCode(BizCenterCode==null?"01":BizCenterCode);
        ssoInfo.setAccess_token(access_token);
        INCCSSOService lookup = NCLocator.getInstance().lookup(INCCSSOService.class);
        lookup.registerSSOInfo(ssoInfo);
        return access_token;
    }

    /**
     * <p>Title: EnvironmentProxy</p>  
     * <p>Description: EnvironmentProxy </p>  
     * @param   request
     * @throws  Exception
     */
    private void environmentProxy(HttpServletRequest request) throws Exception {
        InvocationInfoProxy.getInstance().setGroupId(GroupId);
        InvocationInfoProxy.getInstance().setUserDataSource(Dsname);
        InvocationInfoProxy.getInstance().setBizCenterCode(BizCenterCode);
    }

    /**
     * <p>Title: validateUser</p>  
     * <p>Description: 校验用户</p>  
     * @param   usercode
     * @return  UserVO
     * @throws  Exception
     */
    private UserVO validateUser(String usercode) throws Exception {
        UserVO ncuser  = null;
        if (isEmpty(usercode)) {
            throw new RuntimeException("未传入必要参数人员编码 usercode");
        }else{
            ncuser = NCLocator.getInstance().lookup(IUserManageQuery.class).findUserByCode(usercode, InvocationInfoProxy.getInstance().getUserDataSource());
            if(ncuser == null){
                throw new RuntimeException("nccloud系统中没有usercode对应的用户,请联系管理员同步人员并生成用户!");
            }
        }
        return ncuser;
    }

    /**
     * <p>Title: securityValidate</p>  
     * <p>Description: IP白名单过滤</p>  
     * @param   request
     * @throws  Exception
     */
    protected void securityValidate(HttpServletRequest request) throws Exception {
        boolean flag = true;
        String host = request.getRemoteHost();
        ISSOService service = NCLocator.getInstance().lookup(ISSOService.class);
        List<SSOAuthenConfVO> confList = service.getAuthenConfList();
        String[] addrs = confList.get(0).getListValueMap().get("IPAddress");
        for (String ip : addrs) {
            flag = (ip.trim().equals(host)) ? false : flag;
        }
        if (flag) {
            throw new ServletException("非法IP,无法注册ssoKey,请联系管理员!" + host);
        }
    }
    
    /**
     * <p>Title: isEmpty</p>  
     * <p>Description: assert null</p>  
     * @param   str
     * @return  
     */
    private boolean isEmpty(String str) {
        return str == null || str.isEmpty();
    }
    
}

 

代码支持

说明:    获取轻量端 待办 预警 工作流 代码
        // Request  参数按需定制
        JSONObject params = new JSONObject();
        params.put("msgType", "approve");
        IMessageQueryService workListQuery = NCLocator.getInstance().lookup(nccloud.message.itf.IApproveMessageQueryService.class);
        List<JSONObject> NCworkList = workListQuery.queryNCMessages(params);
        params.put("msgType", "notice");
        IMessageQueryService noticeQuery = NCLocator.getInstance().lookup(nccloud.message.itf.INoticeMessageQueryService.class);
        List<JSONObject> NCnotice = noticeQuery.queryNCMessages(params);
        params.put("msgType", "prealert");
        IMessageQueryService prealertQuery = NCLocator.getInstance().lookup(nccloud.message.itf.IPrealertMessageQueryService.class);
        List<JSONObject> NCprealert = prealertQuery.queryNCMessages(params);
        
        params.clear();
        //组合
        params.put("approve", NCworkList);
        params.put("notice", NCnotice);
        params.put("prealert", NCprealert);
        //组合完成 HttpClient发送到第三方

 

前端代码支持

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { createPage, toast, cacheTools, ajax, Cipher ,base, viewModel ,pageTo} from 'nc-lightapp-front';
import './index.less';
import RSAUtils from "../../login/rsa/security.js";
const { NCSelect } = base;
const NCOption = NCSelect.NCOption;
const { setGlobalStorage, getGlobalStorage, removeGlobalStorage } = viewModel;
const { opaqueEncrypt, opaqueDecrypt} = Cipher;

class ThirdPartyLogin extends Component {
    //modify by wangmo 20200610
    constructor(props) {
        super(props);
        let userAgent = navigator.userAgent;
        //判断是否IE浏览器
        let url = window.location.href;
        let redirect = url.substring(url.indexOf("redirect")+9,window.location.href.length); //modify by wangmo
        if(userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1){
            try {
                let shellOBJ = new ActiveXObject("WScript.Shell");
                shellOBJ.Run("cmd.exe /c start chrome "+url,0,true);
            } catch (error) {
                toast({content:'建议使用Chrome谷歌浏览器68+,以获得最佳体验,请安装Google浏览器!',color:'danger'});
                console.log(error);
                window.open(window.location.origin+"/nccloud/resources/download/Chrome64-20200623.exe")
            }
        }
        this.state = {
            loginprops:{},
            accesstoken:props.getUrlParam('accesstoken'),
            billtype:props.getUrlParam('billtype'),
            billid:props.getUrlParam('billid'),
            baseIndex: window.location.origin,   //add by wangmo 2020/06/12
            connectType:props.getUrlParam('type'),
            redirect:redirect,
            busicenter:'',
            langcode:'',
            usercode:'',
            json:{},
            inlt:null
        }
    }
    //扩展处理重定向地址
    componentWillMount() {
        //处理时区
        String.prototype.insert = function (index, item) {
            var temp = [];
            for (var i = 0; i < this.length; i++) {
                temp.push(this[i]);
            }
            temp.splice(index, 0, item);
            return temp.join("")
        }; 
        //处理多语
        let callback = (json, status, inlt) => { // json 多语json格式参数; status: 是否请求到json多语,可用来判断多语资源是否请求到并进行一些别的操作; inlt: 可用来进行占位符的一些操作
            if (status) {
                this.setState({ json, inlt });// 保存json和inlt到页面state中并刷新页面
            } else {
                console.log(未加载到多语资源);   // 未请求到多语资源的后续操作/* 国际化处理: 未加载到多语资源,未加载到多语资源*/
            }
        }
        this.props.MultiInit.getMultiLang({ 'moduleId': '1018-ssologin', 'domainName': 'uap', callback });
    }

    componentDidMount() {
        this.onSubmit();
    }

    initOption = (data) =>{
        let body=[];
        data.map((option,i) => (
            body.push(<NCOption value={option.code}>{option.name}</NCOption>)
        ));
        return body;
    }

    handleChange = (value) => {
        this.setState({
            busicenter:value
        });
    };

    /**
     * 判断浏览器类型
     * add by wangmo 20200610
     */
    userAgent = () =>{
        var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串
        //判断是否Opera浏览器
        if (userAgent.indexOf("Opera") > -1) {
            return "Opera";
        }
        //判断是否Firefox浏览器
        if (userAgent.indexOf("Firefox") > -1) {
            return "FF";
        }
        //判断是否chorme浏览器
        if (userAgent.indexOf("Chrome") > -1) {
            return "Chrome";
        }
        //判断是否Safari浏览器
        if (userAgent.indexOf("Safari") > -1) {
            return "Safari";
        }
        //判断是否IE浏览器
        if (
            userAgent.indexOf("compatible") > -1 &&
            userAgent.indexOf("MSIE") > -1 &&
            !isOpera
        ) {
            return "IE";
        }
        //判断是否Edge浏览器
        if (userAgent.indexOf("Trident") > -1) {
            return "Edge";
        }
    }

    //add by wangmo 20200610
    getTimeZone = () => {
        let d = new Date();
        let random = Math.floor(Math.random()*100);
        let y = d.getFullYear();
        let m = (d.getMonth()+1) < 10 ? '0'+(d.getMonth()+1) : d.getMonth()+1 ;
        let day = d.getDate()<10?'0'+d.getDate():d.getDate();
        let h = d.getHours()<10?'0'+d.getHours():d.getHours();
        let i = d.getMinutes()<10?'0'+d.getMinutes():d.getMinutes();
        let s = d.getSeconds()<10?'0'+d.getSeconds():d.getSeconds();
        return ''+y+m+day+h+i+s+random;
    }

    //add by wangmo 20200610
    doRedirect = (res) =>{
        switch(this.state.connectType){
            case 'link':
                pageTo.openAppByBilltype({
                    billpk: this.state.billid,  //单据pk
                    billtype: this.state.billtype //单据类型
                });
                let childUrl = window.location.href.split("#")[1];
                childUrl && (window.location.href = `${this.state.baseIndex}/workbench/public/common/main/index.html#${window.location.href.split("#")[1]}`);
                break;
            case 'approve':
                //1级地址 单点审批
                let kv = new Date();
                let key = this.getTimeZone();
                sessionStorage.setItem('NCCAPPURL', '{"'+key+'":"'+this.state.redirect+'"}');
                window.location.href = this.state.baseIndex+'/nccloud/resources/workbench/public/common/main/index.html#/ifr?page='+key;
                //2级地址 单点审批
                //url: '/nccloud/ssologin/base/indexQuery.do',
                break;
            default:
                window.location.href = this.state.baseIndex+'/'+res.data.index;
        }
    }

    onSubmit = ()=>{
        try{
            cacheTools.clear();
        }catch(e){
        }
        var that = this;
        ajax({
            url: '/nccloud/riart/login/init.do',
            success: (res)=> {
                if (res.success) {
                    function uuidv4() {
                        return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                            var r = (Math.random() * 16) | 0,
                                v = c == 'x' ? r : (r & 0x3) | 0x8;
                            return v.toString(16);
                        });
                    }
                    let key = RSAUtils.getKeyPair(res.data.exponent, '', res.data.modulus);
                    let aeskeyCahe = uuidv4();
                    // 由于同一个浏览器  只有一个用户
                    let cowboy = getGlobalStorage('localStorage','cowboy');
                    aeskeyCahe = cowboy ? opaqueDecrypt(cowboy) : aeskeyCahe;
                    let aeskey = RSAUtils.encryptedString(key, aeskeyCahe);
                    // 重置 aes 请求 --bbqin
                    localStorage.removeItem('rockin');
                    localStorage.removeItem('rockinlog');
                    ajax({
                        url: '/nccloud/baseapp/thirdPartyLogin/thirdPartyVerfiy.do',
                        data:{
                            aeskey:aeskey,
                            firstlogin:"Y",
                            token:this.state.accesstoken,
                            connecttype:this.state.connecttype,
                            timezone: new Date().toString().substring(25,33).insert(6,":")
                        }, 
                        success: (res) => {
                            if (res.success) {
                                // 发一个同步ajax
                                // 如果浏览器器中出现了两个用户  以第一个用户的为主  保持同一个浏览器中唯一
                                setGlobalStorage('localStorage','cowboy', cowboy || opaqueEncrypt(aeskeyCahe)); // 当前的
                                ajax({
                                    type: 'post',
                                    url: '/nccloud/platform/aes/switch.do',
                                    data: { 
                                        sysParamJson: {
                                            busiaction: '查询请求aes加密开关' 
                                        }
                                    },
                                    async: false,
                                    success: function(asd) { // aesSwitchData
                                        asd = typeof asd === 'string' ? JSON.parse(asd) : asd;
                                        if (asd.data) {
                                            if (asd.data.success || asd.success) {
                                                if (asd.data) {
                                                    setGlobalStorage('localStorage','rockin', true);
                                                    setGlobalStorage('localStorage','rockinlog',167);
                                                } 
                                                else {
                                                    setGlobalStorage('localStorage','rockin', false);
                                                    setGlobalStorage('localStorage','rockinlog',171);
                                                }
                                            }
                                        }
                                    },
                                    error: function(res) {
                                        setGlobalStorage('localStorage','rockinlog',176);
                                    }
                                });
                                let rslCode = res.data.rslCode;
                                if(rslCode=="0" || rslCode=="5" ){//登录成功 or 强制登陆 modify by wangmo
                                        console.log(res);
                                        that.doRedirect(res);
                                }
                                if(rslCode=="6"){//token错误或者过期
                                    toast({content:res.data.rslMsg,color:'danger'});
                                }
                                if(rslCode=="7"){//没有可登录的系统
                                    toast({content:res.data.rslMsg,color:'danger'});
                                }
                                if(rslCode=="8"){//多账套
                                    that.setState({
                                        usercode:res.data.usercode,
                                        langcode:res.data.langcode,
                                        busicenter:res.data.rslMsg[0].code
                                    });
                                    that.props.modal.show('busi',{
                                        content:<div>
                                                    <NCSelect 
                                                        defaultValue={res.data.rslMsg[0].code} 
                                                        onChange={that.handleChange.bind(this)}
                                                        style={{ width: 200, marginRight: 2 }}>
                                                        {that.initOption(res.data.rslMsg)}
                                                    </NCSelect>
                                                </div>,
                                        hideRightBtn:true,
                                        beSureBtnClick: () => {
                                            ajax({
                                                url: '/nccloud/baseapp/thirdPartyLogin/thirdPartyVerfiy.do',
                                                data:{
                                                    firstlogin:'N',
                                                    usercode:that.state.usercode,
                                                    langcode:that.state.langcode,
                                                    busicenter:that.state.busicenter,
                                                    timezone: new Date().toString().substring(25,33).insert(6,":"),
                                                },  
                                                success: function (res) {
                                                    //window.location = that.state.redirect_uri; modify by wangmo
                                                    window.location.href = that.state.baseIndex+'/'+res.data.index;
                                                }
                                            })
                                        }
                                    })
                                }
                                if(rslCode=="-1"){//登录异常
                                    toast({content:res.data.rslMsg,color:'danger'});
                                }  
                            }
                        },
                        error:function(res){
                        }
                    })
                }
            },
            error:function(res){
            }
        });
    }

    render() {
        let { modal, data } = this.props;
        let { createModal } = modal;
        this.loginprops = data;
        return (
            <div id= "login_div" className="content-right">
                {createModal('busi', {
                    title:this.state.json['1880000025-000002']/* 国际化处理: 请选择登录账套*/
                })}
            </div>
        );
    }
}
// 重置 aes 请求 --bbqin
localStorage.removeItem('rockin');
localStorage.removeItem('rockinlog');

export default ThirdPartyLogin = createPage({

})(ThirdPartyLogin);
ReactDOM.render(<ThirdPartyLogin />, document.querySelector('#app'));

wangmob@yonyou.com
用友网络科技有限公司四川分公司

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!