关键词:附件上传,表单附件,服务集成,接口开发

O2OA允许用户使用接口来完成对流程、表单以及数据的相关操作。本文介绍如何使用接口来实现表单上传附件的功能。这样便能将流程附件的管理与其他业务进行整合,更方便业务的办理过程。


创建接口

在服务管理平台中创建一个接口,接口代码如下:

/*
* resources.getEntityManagerContainer() // 实体管理容器.
* resources.getContext() //上下文根.
* resources.getOrganization() //组织访问接口.
* requestText //请求内容.
* request //请求对象.
*/



/*
    传入的参数
    假设requestText = {
        "title" : "关于某某某的通知公告(标题,必填)",  //标题
        "from" : "办公室(来文单位,必填)",
        "to" : "qhsnynctrsc(OA的部门人事处ID,必填)",
        "date" : "2020-05-09(收文日期,选填)",
        "no" : "农123(字号,选填)",
        "key" : "nmt(文件加密私钥,选填,为空则认为没加密)",
        "contents" : [ {
            "filepath" : "http://xxxxx.com?file=aaaa.docx",
            "filename" : "aaaa.docx"
        } ],
        "slaves" : [ {
            "filepath" : "http://xxxxx.com?file=slaves.docx",
            "filename" : "slaves.docx"
        } ]
    }
*/
try{
    var result = {
       
    }
  
    print( "requestText="+requestText );
 
    var requestJson = JSON.parse(requestText);
    print( "type of requestJson = " + typeof( requestJson ));
    
    print( "type of requestJson = " + requestJson );
    if( typeof(requestJson) === "string" ){
        requestJson = JSON.parse(requestJson);
    }
 
    var workId = "b4cbb9a4-3410-45a4-9c0e-4dad0dcf94b4";  //流程文档的workId
    
    //上传附件-----------------------------------------------------------begin
    var token = getToken();  
    
    //print( "contents个数:" + requestJson.contents.length);
    //print( "slaves个数:" + requestJson.slaves.length);
    var contentsSite = "attachment"; //正文附件放置的附件区域
    var slavesSite = "attachment_1"; //普通附件放置的附件区域
    //处理contents
    var conArr = requestJson.contents;
    for(var i=0;i<conArr.length;i++){
        //print( "contents-filepath:" + conArr[i].filepath);
        //print( "contents-filename:" + conArr[i].filename);
        uploadAtt(workId,conArr[i].filepath,conArr[i].filename,token,contentsSite)
    }
    //处理slaves
    var slavesArr = requestJson.slaves;
    for(var j=0;j<slavesArr.length;j++){
        //print( "slaves-filepath:" + slavesArr[j].filepath);
        //print( "slaves-filename:" + slavesArr[j].filename);
        uploadAtt(workId,slavesArr[j].filepath,slavesArr[j].filename,token,slavesSite)
    }
    
    //上传附件-----------------------------------------------------------end
    
    result.state = "NMT0001";
    result.message = "成功";
    result.data = workId;
        
    
    
}catch(e){
    e.printStackTrace();
    result.state = "NMT0002";
    result.message = "失败";
    result.data = e.name + ": " + e.message
}

this.response.setBody(result,"application/json");


/*
    var workid = "b4cbb9a4-3410-45a4-9c0e-4dad0dcf94b4"; //workid:待办id
    var fileUrl = "http://127.0.0.1/x_desktop/js/base.js"; //附件下载链接
    var fileName = "base.js"; //附件名称
    var token = getToken();  
    site:附件放置区域
*/
function uploadAtt(workid,fileUrl,fileName,token,site){ //上传附件
    
    try {
        
        print("token=============================="+token);
        
        //实例化java类
        var LinkedHashMap = Java.type("java.util.LinkedHashMap");
        var HttpClientUtilsUpfile =  Java.type("com.z.custom.HttpClientUtilsUpfile"); 
    
        var headMap = new LinkedHashMap();
        headMap.put("x-token", token);
        headMap.put("accept", "*/*");
        headMap.put("connection", "Keep-Alive");
        headMap.put("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");

        var uploadParams = new LinkedHashMap();
        uploadParams.put("fileName", fileName); //附件名称
        uploadParams.put("site", site); //附件区域
        uploadParams.put("extraParam","");
        
          
        //上传附件接口,传入文档的workid   
        var strurl = "http://127.0.0.1:20020/x_processplatform_assemble_surface/jaxrs/attachment/upload/work/" + workid;

        /*
        *方法功能:java模拟表单附件上传
        *strurl : 上传附件接口地址
        *fileUrl : 附件下载地址
        *fileName :附件名称
        *uploadParams : 上传附件接口参数
        *headMap : 表单头参数
        */
        HttpClientUtilsUpfile.getInstance().uploadUrlFileImpl(strurl, fileUrl,fileName,"file", uploadParams, headMap);
        
  
    } catch (e) {
        e.printStackTrace();
        e.printStackTrace();
        result.state = "NMT0002";
        result.message = "失败";
        result.data = e.name + ": " + e.message
    }

}


function getToken(){ //获取token
    var HttpClientUtilsUpfile =  Java.type("com.z.custom.HttpClientUtilsUpfile");
    //使用token进行Sso登陆
    var path = "http://127.0.0.1:20020/x_organization_assemble_authentication/jaxrs/sso";
    var login_uid = "13379254582"; //用户简称
    var client = "wwx"  //SSO名称
    var sso_key = "987654321"; //SSO密钥
    
    //获取token
    var token = HttpClientUtilsUpfile.getInstance().getToken(path,client,login_uid,sso_key);
    return token;
}


HttpClientUtilsUpfile类代码

接口中引用了com.z.custom.HttpClientUtilsUpfile中的java代码去实现模拟表单上传附件,HttpClientUtilsUpfile类代码如下:


package com.z.custom;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.x.base.core.project.tools.Crypto;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.InputStreamBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import javax.crypto.CipherInputStream;

public class HttpClientUtilsUpfile {
    public static final int THREAD_POOL_SIZE = 5;

    public interface HttpClientDownLoadProgress {
        public void onProgress(int prgetInstanceogress);
    }

    private static HttpClientUtilsUpfile httpClientDownload;

    private ExecutorService downloadExcutorService;

    private HttpClientUtilsUpfile() {
        downloadExcutorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
    }

    public static HttpClientUtilsUpfile getInstance() {
        if (httpClientDownload == null) {
            httpClientDownload = new HttpClientUtilsUpfile();
        }
        return httpClientDownload;
    }

    /**
     * 下载文件
     *
     * @param url
     * @param filePath
     */
    public void download(final String url, final String filePath) { downloadExcutorService.execute(new Runnable() {
            public void run() {
                httpDownloadFile( url, filePath, null, null);
            }
        });
    }

    /**
     * 下载文件
     *
     * @param url
     * @param filePath
     * @param progress
     *
     */
    public void download(final String url, final String filePath, final HttpClientDownLoadProgress progress) {
        downloadExcutorService.execute(new Runnable() {
            public void run() {
                httpDownloadFile(url, filePath, progress, null);
            }
        });
    }

    /**
     *下载文件
     *
     * @param url
     * @param filePath
     */
    private void httpDownloadFile(String url, String filePath,
                                  HttpClientDownLoadProgress progress, Map<String, String> headMap) {
        CloseableHttpClient httpclient = HttpClients.createDefault();
        try {
            HttpGet httpGet = new HttpGet(url);
            setGetHead(httpGet, headMap);
            CloseableHttpResponse response1 = httpclient.execute(httpGet);
            try {
                HttpEntity httpEntity = response1.getEntity();
                long contentLength = httpEntity.getContentLength();
                InputStream is = httpEntity.getContent();

                ByteArrayOutputStream output = new ByteArrayOutputStream();
                byte[] buffer = new byte[4096];
                int r = 0;
                long totalRead = 0;
                while ((r = is.read(buffer)) > 0) {
                    output.write(buffer, 0, r);
                    totalRead += r;
                    if (progress != null) {
                        progress.onProgress((int) (totalRead * 100 / contentLength));
                    }
                }
                FileOutputStream fos = new FileOutputStream(filePath);
                output.writeTo(fos);
                output.flush();
                output.close();
                fos.close();
                EntityUtils.consume(httpEntity);
            } finally {
                response1.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                httpclient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 发送Get请求
     *
     * @param url
     * @return
     */
    public String httpGet(String url) {
        return httpGet(url, null);
    }

    /**
     * 发送Get请求
     *
     * @param url
     * @return
     */
    public String httpGet(String url, Map<String, String> headMap) {
        String responseContent = null;
        CloseableHttpClient httpclient = HttpClients.createDefault();
        try {
            HttpGet httpGet = new HttpGet(url);
            CloseableHttpResponse response1 = httpclient.execute(httpGet);
            setGetHead(httpGet, headMap);
            try {
                System.out.println(response1.getStatusLine());
                HttpEntity entity = response1.getEntity();
                responseContent = getRespString(entity);
                System.out.println("debug:" + responseContent);
                EntityUtils.consume(entity);
            } finally {
                response1.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                httpclient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return responseContent;
    }

    public String httpPost(String url, Map<String, String> paramsMap) {
        return httpPost(url, paramsMap, null);
    }

    /**
     * 发送Post请求
     *
     * @param url
     * @param paramsMap
     * @return
     */
    public String httpPost(String url, Map<String, String> paramsMap, Map<String, String> headMap) {
        String responseContent = null;
        CloseableHttpClient httpclient = HttpClients.createDefault();
        try {
            HttpPost httpPost = new HttpPost(url);
            setPostHead(httpPost, headMap);
            setPostParams(httpPost, paramsMap);
            CloseableHttpResponse response = httpclient.execute(httpPost);
            try {
                System.out.println(response.getStatusLine());
                HttpEntity entity = response.getEntity();
                responseContent = getRespString(entity);
                EntityUtils.consume(entity);
            } finally {
                response.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                httpclient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        System.out.println("responseContent = " + responseContent);
        return responseContent;
    }

    /**
     * 设置Post请求的HttpHeader
     *
     * @param httpPost
     * @param headMap
     */
    private void setPostHead(HttpPost httpPost, Map<String, String> headMap) {
        if (headMap != null && headMap.size() > 0) {
            Set<String> keySet = headMap.keySet();
            for (String key : keySet) {
                httpPost.addHeader(key, headMap.get(key));
            }
        }
    }

    /**
     * 设置Get请求的HttpHeader
     *
     * @param httpGet
     * @param headMap
     */
    private void setGetHead(HttpGet httpGet, Map<String, String> headMap) {
        if (headMap != null && headMap.size() > 0) {
            Set<String> keySet = headMap.keySet();
            for (String key : keySet) {
                httpGet.addHeader(key, headMap.get(key));
            }
        }
    }


    /**
     * 上传文件的实现方法
     *
     * @param serverUrl
     * @param localFilePath
     * @param serverFieldName
     * @param params
     * @return
     * @throws Exception
     */
    public String uploadFileImpl(String serverUrl, String localFilePath,String serverFieldName, Map<String, String> params, Map<String, String> paramshead)
            throws Exception {
        String respStr = null;
        CloseableHttpClient httpclient = HttpClients.createDefault();
        try {
            HttpPost httppost = new HttpPost(serverUrl);
            setPostHead(httppost, paramshead);
            FileBody binFileBody = new FileBody(new File(localFilePath));

            MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
            // add the file params
            multipartEntityBuilder.addPart(serverFieldName, binFileBody);
            setUploadParams(multipartEntityBuilder, params);
            HttpEntity reqEntity = multipartEntityBuilder.build();
            httppost.setEntity(reqEntity);
            CloseableHttpResponse response = httpclient.execute(httppost);
            try {
                HttpEntity resEntity = response.getEntity();
                respStr = getRespString(resEntity);
                EntityUtils.consume(resEntity);
            } finally {
                response.close();
            }
        } finally {
            httpclient.close();
        }
        System.out.println("resp=" + respStr);
        return respStr;
    }


    /**
     * 上传文件的实现方法
     *
     * @param serverUrl
     * @param urlFilePath
     * @param urlFileName
     * @param serverFieldName
     * @param params
     * @return
     * @throws Exception
     */
    public String uploadUrlFileImpl(String serverUrl, String urlFilePath,String urlFileName,String serverFieldName, Map<String, String> params, Map<String, String> paramshead)
            throws Exception {
        String respStr = null;
        CloseableHttpClient httpclient = HttpClients.createDefault();
        try {
            HttpPost httppost = new HttpPost(serverUrl);
            setPostHead(httppost, paramshead);
            FileBody binFileBody = new FileBody(new File(urlFilePath));
            MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
            // add the file params
            BufferedInputStream in = getFileInputStream(urlFilePath);
            InputStreamBody inputStreamBody = new InputStreamBody(in,urlFileName);
            multipartEntityBuilder.addPart(serverFieldName, inputStreamBody);
            setUploadParams(multipartEntityBuilder, params);
            HttpEntity reqEntity = multipartEntityBuilder.build();
            httppost.setEntity(reqEntity);

            CloseableHttpResponse response = httpclient.execute(httppost);
            try {
                System.out.println(response.getStatusLine());
                HttpEntity resEntity = response.getEntity();
                respStr = getRespString(resEntity);
                EntityUtils.consume(resEntity);
            } finally {
                response.close();
            }
        } finally {
            httpclient.close();
        }
        System.out.println("resp=" + respStr);
        return respStr;
    }

    /**
     * 上传文件的实现方法
     * 上传前 需要解密
     * @param serverUrl  上传附件服务
     * @param urlFilePath  附件URL
     * @param urlFileName  附件名称
     * @param serverFieldName
     * @param params  附件参数
     * @param paramshead  文件头
     * @param skey  密钥
     * @param siv  向量
     * @return
     * @throws Exception
     */
    public String uploadByteFileImpl(String serverUrl, String urlFilePath,String urlFileName,String serverFieldName, Map<String, String> params, Map<String, String> paramshead,String skey,String siv)
            throws Exception {
        String respStr = null;
        CloseableHttpClient httpclient = HttpClients.createDefault();
        try {
            HttpPost httppost = new HttpPost(serverUrl);
            setPostHead(httppost, paramshead);
            //ileBody binFileBody = new FileBody(new File(urlFilePath));
            MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
            // add the file params

            BufferedInputStream fis = getFileInputStream(URLDecoder.decode(urlFilePath, "UTF-8" ));;
            //解密
            CipherInputStream cis = AESFile.decryptedFile(fis,skey,siv);

            if(cis != null){
                //InputStream inputByteFile = new ByteArrayInputStream(byteFile);
                //BufferedInputStream in = new BufferedInputStream(inputByteFile);
                InputStreamBody inputStreamBody = new InputStreamBody(cis,urlFileName);
                multipartEntityBuilder.addPart(serverFieldName, inputStreamBody);
                setUploadParams(multipartEntityBuilder, params);
                HttpEntity reqEntity = multipartEntityBuilder.build();
                httppost.setEntity(reqEntity);

                CloseableHttpResponse response = httpclient.execute(httppost);
                try {
                    System.out.println(response.getStatusLine());
                    HttpEntity resEntity = response.getEntity();
                    respStr = getRespString(resEntity);
                    EntityUtils.consume(resEntity);
                } finally {
                    response.close();
                }
            }else{
                respStr = "解密失败";
            }

        } finally {
            httpclient.close();
        }
        System.out.println("resp=" + respStr);
        return respStr;
    }

    /**
     * 设置上传时的参数
     *
     * @param multipartEntityBuilder
     * @param params
     */
    private void setUploadParams(MultipartEntityBuilder multipartEntityBuilder,
                                 Map<String, String> params) {
        if (params != null && params.size() > 0) {
            Set<String> keys = params.keySet();
            for (String key : keys) {
                multipartEntityBuilder.addPart(key, new StringBody(params.get(key),ContentType.APPLICATION_JSON));

            }
        }
    }

    /**
     * 获取响应内容
     *
     * @param entity
     * @return
     * @throws Exception
     */
    private String getRespString( HttpEntity entity ) throws Exception {
        if (entity == null) {
            return null;
        }
        InputStream is = entity.getContent();
        StringBuffer strBuf = new StringBuffer();
        byte[] buffer = new byte[4096];
        int r = 0;
        while ((r = is.read(buffer)) > 0) {
            strBuf.append(new String(buffer, 0, r, "UTF-8"));
        }
        return strBuf.toString();
    }
    /**
     * 设置Post请求发送的参数
     *
     * @param httpPost
     * @param paramsMap
     * @throws Exception
     */
    private void setPostParams(HttpPost httpPost, Map<String, String> paramsMap)
            throws Exception {
        if (paramsMap != null && paramsMap.size() > 0) {
            List<NameValuePair> nvps = new ArrayList<NameValuePair>();
            Set<String> keySet = paramsMap.keySet();
            for (String key : keySet) {
                nvps.add(new BasicNameValuePair(key, paramsMap.get(key)));
            }
            httpPost.setEntity(new UrlEncodedFormEntity(nvps));
        }
    }

    /**
     * 发送Post请求
     * @param url
     * @param param
     * @return
     */
    public static String sendPost(String url, String param) {
        PrintWriter out = null;
        BufferedReader in = null;
        String result = "";
        try {
            URL realUrl = new URL(url);
            URLConnection conn = realUrl.openConnection();
            conn.setRequestProperty("accept", "*/*");
            conn.setRequestProperty("connection", "Keep-Alive");
            conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            conn.setDoOutput(true);
            conn.setDoInput(true);
            out = new PrintWriter(conn.getOutputStream());
            out.print(param);
            out.flush();
            in = new BufferedReader( new InputStreamReader(conn.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        } catch (Exception e) {
            System.out.println(" POST"+e);
            e.printStackTrace();
        }
        finally{
            try{
                if(out!=null){
                    out.close();
                }
                if(in!=null){
                    in.close();
                }
            }
            catch(IOException ex){
                ex.printStackTrace();
            }
        }
        System.out.println(result);
        return result;
    }

    /**
     * 根据ssoToken获取人员认证后的真实xtoken
     * @param url
     * @param client
     * @param login_uid
     * @param sso_key
     * @return
     */
    public String getToken(String url, String client, String login_uid, String sso_key ) {
        long time = new Date().getTime();
        String xtoken = null;

        try {
            xtoken = Crypto.encrypt( login_uid + "#" + time, sso_key );
            System.out.println(xtoken);
        } catch (Exception e1) {
            e1.printStackTrace();
        }
        String string = "{\"token\": "+xtoken+", \"client\": \""+ client +"\"}";
        String str = getInstance().sendPost( url,string );
        System.out.println("sso response: " + str );
        try {
            JsonElement jsonObj = new JsonParser().parse(str);
            if( jsonObj != null && jsonObj.getAsJsonObject().get("data") != null ){
                JsonObject data = jsonObj.getAsJsonObject().get("data").getAsJsonObject();
                System.out.println("getToken: " + data.get("token"));
                return data.get("token").getAsString();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private BufferedInputStream getFileInputStream(String urlPath) {
        BufferedInputStream bin=null;
        URL url;
        try {
            url = new URL(urlPath);
            URLConnection urlConnection = url.openConnection();
            HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
            httpURLConnection.setConnectTimeout(1000*5);
            httpURLConnection.setRequestMethod("GET");
            // 设置字符编码
            httpURLConnection.setRequestProperty("Charset", "UTF-8");
            httpURLConnection.connect();
            bin = new BufferedInputStream(httpURLConnection.getInputStream());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bin;
    }

    private InputStream getInputStream(String urlPath) {
        InputStream bin=null;
        URL url;
        try {
            url = new URL(urlPath);
            URLConnection urlConnection = url.openConnection();
            HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;
            httpURLConnection.setConnectTimeout(1000*5);
            httpURLConnection.setRequestMethod("GET");
            // 设置字符编码
            httpURLConnection.setRequestProperty("Charset", "UTF-8");
            httpURLConnection.connect();
            bin = httpURLConnection.getInputStream();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bin;
    }

    public static void main(String[] args) throws IOException, Exception {
        
    }
}


推荐文章:

数据中心-查询配置中日期格式jpql sql匹配写法
2021-02-07
在jpql中,对日期格式有特殊的写法,格式如下:Date-{d'yyyy-mm-dd'}-forexample:{d'2019-12-31'}Time-{t'h
开发知识-单个端口模式的Nginx和系统配置
2021-02-07
配置目的部分企事业单位外网地址不能开通太多的端口,我们使用单个端口,上下文根的方式配置访问地址。Nginx服务器域名:harbor.o2oa.net(172.1
系统配置-平台数据库配置信息样例
2021-11-25
@平台部署@O2OA@开源办公系统@数据库连接配置@数据库连接串@数据库配置O2OA开发平台支持大多数主流的数据库以及国产数据库,用户可以进行相应的第三方数据库
移动办公-移动端应用权限配置
2021-02-25
O2OA平台拥有配套的原生开发的安卓和IOS移动APP,开发者在拥有公网IP或者域名的服务器上可以轻松体验移动办公环境,并且不会产生任何费用。本篇主要介绍如何配
移动办公-将平台集成到阿里钉钉(DingTalk)
2021-02-25
O2OA平台拥有配套的原生开发的安卓和IOS移动APP,可以以微应用的方式集成到阿里钉钉,同步钉钉的企业通讯录作为本地组织人员架构,并且可以将待办等通知直接推送
系统架构-集群部署配置及操作说明
2021-02-26
O2OA平台使用分布式架构设计,提供灵活的扩展方案用于对服务器的负载能力进行扩展,保障系统的高可用性。本篇主要介绍如何部署O2OA服务器集群。
表单-数据模板的配置方法
2022-01-21
数据模板可以用来让用户在使用层,自动进行添加整个数据表格。具体的使用方法如下:1.从表单中拖动数据表格:组件的标识是:datatemplate2.在数据模板中制
会议申请使用手册
2021-02-19
O2OA会议申请是使用平台流程管理能力开发的一个申请流程。员工的会议申请通过审批之后,可以同步到会议管理系统,由会议管理系统进行展现,提醒和跟踪。可以清晰地在会
移动办公-将平台集成到企业微信(WeChat)
2021-11-16
@移动办公@O2OA微信办公@企业微信办公@微信办公@手机办公O2OA平台拥有配套的原生开发的安卓和IOS移动APP,可以以自建应用的方式集成到企业微信,同步企
脑图管理
2021-02-19
O2OA思维导图是表达发散性思维的有效图形思维工具,它简单却又很有效同时又很高效,是一种实用性的思维工具。思维导图运用图文并重的技巧,把各级主题的关系用相互隶属

results matching ""

    No results matching ""