iBatis開發(fā)詳解(5)---分頁(Struts、freeMark)

2018-10-13 11:45 更新

    之前介紹過基于Hibernate分頁的原理和設計,這里我們所用的分頁都是物理分頁技術,不是JS實現(xiàn)的頁面分頁技術,是在SQL語句上執(zhí)行的分頁,可以獲取結果集數(shù)量固定的列表,執(zhí)行效率很高。下面來看看iBatis中如何設計分頁,本文基于Struts2,Spring3來整合,因為暫時Spring不支持MyBatis3(可以選用MyBatis官方的MyBatis-Spring插件來實現(xiàn),配有中文文檔,很好理解),我們這里仍然以iBatis2作為載體來介紹。 


    首先就是搭建開發(fā)環(huán)境,這里可以說也是對Struts2,Spring3和iBatis2進行了簡單的整合,大家也可以來參考。項目的結構如下,使用Maven創(chuàng)建的web項目: 


   添加必要的依賴,因為整合了Struts2和Spring,依賴就比較多了,如下: 

  首先來配置一下Struts2,這個就比較簡單了,相信大家都不陌生。在web.xml中:

<!-- Struts2的過濾器 -->  
<filter>  
    <filter-name>struts2</filter-name>  
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>  
</filter>  
<filter-mapping>  
    <filter-name>struts2</filter-name>  
    <url-pattern>*.action</url-pattern>  
</filter-mapping>  
    然后是struts.xml,配置Struts相關的內容,這里我們配置freemarker為默認的結果類型,然后配置一個測試的Action,因為和Spring進行了集成,所以Action具體的配置放到Spring中來進行,如下即可: 
<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE struts PUBLIC  
    "-//Apache Software Foundation//DTD Struts Configuration 2.1.7//EN"  
    "http://struts.apache.org/dtds/struts-2.1.7.dtd">  
<struts>  
    <package name="ibatis-paging" extends="struts-default"  
        namespace="/">  
        <result-types>  
            <result-type name="freemarker"  
        class="org.apache.struts2.views.freemarker.FreemarkerResult"  
                default="true" />  
        </result-types>  
        <action name="user_*" class="userAction" method="{1}">  
            <result name="list">user_list.ftl</result>  
        </action>  
    </package>  
</struts>  
  對Freemarker做一個簡單的設置,卸載freeemarer.properties文件中即可,這里我們主要是引用了一個宏文件,就是分頁宏的配置,如下:
template_update_delay=5  
default_encoding=UTF-8  
url_escaping_charset=UTF-8  
number_format=0.#  
date_format=yyyy-MM-dd  
time_format=HH:mm:ss  
datetime_format=yyyy-MM-dd HH:mm:ss  
boolean_format=true,false  
whitespace_stripping=true  
tag_syntax=auto_detect  
auto_import=/Freemarker/page_macro.ftl as p  
    Log4J的配置這里不再貼出代碼,大家可以去下載源碼,一看就明白了,之后我們配置Spring,在resources文件夾下創(chuàng)建spring子目錄,里面放置Spring的配置文件,在web.xml中如下設置即可加載Spring的配置文件: 
<context-param>  
    <param-name>contextConfigLocation</param-name>  
    <param-value>classpath:spring/*.xml</param-value>  
</context-param>  
<!-- Spring加載配置文件來初始化IoC容器 -->  
<listener>        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  
</listener> 
   Spring中主要配置數(shù)據(jù)源,iBatis的SqlMapClient和SqlMapClientTemplate,事務處理還有Action和Service的管理,其實內容大家也都很熟悉了,比較簡單:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"  
        destroy-method="close">  
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />  
        <property name="url" value="jdbc:mysql://localhost:3306/test" />  
        <property name="username" value="root" />  
        <property name="password" value="123" />  
        <property name="maxActive" value="100" />  
        <property name="maxIdle" value="50" />  
        <property name="maxWait" value="100" />  
        <property name="defaultAutoCommit" value="true" />  
    </bean>  
    <!-- 創(chuàng)建JdbcTemplate -->  
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">  
        <property name="dataSource" ref="dataSource" />  
    </bean>  
    <!-- 創(chuàng)建sqlMapClient -->  
    <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">  
        <property name="configLocation" value="classpath:sqlMapConfig.xml" />  
        <property name="dataSource" ref="dataSource" />  
    </bean>  
    <!-- 創(chuàng)建sqlMapClientTemplate -->  
    <bean id="sqlMapClientTemplate" class="org.springframework.orm.ibatis.SqlMapClientTemplate">  
        <constructor-arg>  
            <ref bean="sqlMapClient" />  
        </constructor-arg>  
    </bean>  
    <!-- 事務管理器 -->  
    <bean id="transactionManager"  
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
        <property name="dataSource" ref="dataSource" />  
    </bean>  
    <!-- 配置事務處理通知 -->  
    <tx:advice id="txAdvice" transaction-manager="transactionManager">  
        <tx:attributes>  
            <tx:method name="get*" read-only="true" />  
            <tx:method name="add*" rollback-for="Exception" />  
            <tx:method name="addOrUpdate*" rollback-for="Exception" />  
            <tx:method name="del*" rollback-for="Exception" />  
            <tx:method name="update*" rollback-for="Exception" />  
        </tx:attributes>  
    </tx:advice>  
    <aop:config proxy-target-class="true">  
        <aop:pointcut id="serviceMethod"  
            expression="execution(* org.ourpioneer.service.*Service.*(..))" />  
        <aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethod" />  
    </aop:config>  
  之后對Service和Action進行配置:
<bean id="baseService" class="org.ourpioneer.service.BaseService">  
    <property name="jdbcTemplate" ref="jdbcTemplate"></property>  
</bean>  
  
<bean id="userService" class="org.ourpioneer.service.UserService"  
    parent="baseService">  
    <property name="sqlMapClientTemplate" ref="sqlMapClientTemplate" />  
</bean>  
<bean id="userAction" class="org.ourpioneer.action.UserAction">  
    <property name="userService" ref="userService" />  
</bean>  
 下面來看一下iBatis的配置,在配置SqlMapClient的時候,加入了iBatis的配置文件,我們來看看sqlMapConfig.xml如何來設置: 
<?xml version="1.0" encoding="UTF-8" ?>   
<!DOCTYPE sqlMapConfig   
    PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"   
    "http://www.ibatis.com/dtd/sql-map-config-2.dtd">  
<sqlMapConfig>  
    <settings cacheModelsEnabled="true" enhancementEnabled="true"  
        lazyLoadingEnabled="true" errorTracingEnabled="true" maxRequests="32"  
        maxSessions="10" maxTransactions="5" />  
    <sqlMap resource="sqlMaps/user.xml" />  
</sqlMapConfig>  
    其實內容也很簡單,就是設置一下必要的信息,其中的含義可以參考之前寫過的對iBatis的介紹的相關文章,最后不要忘了加入sqlMaps配置文件即可,這里我們就一個user.xml文件,為了測試,也就是一條查詢,針對這個查詢進行分頁操作: 
<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd">  
<sqlMap>  
    <typeAlias alias="parameterMap" type="org.pioneer.bean.ParameterMap" />  
    <select id="selectAllUsers" resultClass="java.util.HashMap">  
        select * from user  
    </select>  
</sqlMap>  
   ParameterMap在之前的介紹中也多次出現(xiàn),這里我們也再來看下: 
package org.ourpioneer.bean;  
import java.util.HashMap;  
public class ParameterMap extends HashMap {  
    public ParameterMap(Object... parameters) {  
        for (int i = 0; i < parameters.length - 1; i += 2) {  
            super.put(parameters[i], parameters[i + 1]);  
        }  
    }  
}  
  其實就是擴展了一下HashMap類,來進行參數(shù)的放置,注意參數(shù)類型是可變參數(shù)的形式,也就是名-值對的形式出現(xiàn)的,不過本例中沒有使用它。下面就是分頁類的設計了: 
package org.ourpioneer.bean;  
  
import java.util.HashMap;  
import java.util.List;  
  
import org.springframework.jdbc.core.JdbcTemplate;  
import org.springframework.orm.ibatis.SqlMapClientTemplate;  
  
import com.ibatis.sqlmap.engine.impl.SqlMapClientImpl;  
import com.ibatis.sqlmap.engine.mapping.sql.Sql;  
import com.ibatis.sqlmap.engine.mapping.statement.MappedStatement;  
import com.ibatis.sqlmap.engine.scope.SessionScope;  
import com.ibatis.sqlmap.engine.scope.StatementScope;  
  
/** 
 * iBatis分頁類 
 *  
 * @author Nanlei 
 *  
 */  
public class PagingList {  
    private int rowCount = 0; // 記錄總數(shù)  
    private int pageCount = 1; // 分頁總數(shù)  
    private int pageSize = 10; // 每頁記錄數(shù)  
    private int pageNum = 1; // 當前頁數(shù)  
    private int startIndex = 1; // 起始記錄數(shù)  
    private int endIndex = 1; // 結束記錄數(shù)  
    private List list;// 記錄列表  
  
    /** 
     * 構造方法,進行分頁 
     *  
     * @param statementName 
     *            iBatis中語句的ID 
     * @param parameterObject 
     *            SQL語句參數(shù) 
     * @param pageNum 
     *            起始頁數(shù) 
     * @param pageSize 
     *            每頁大小 
     * @param sqlMapClientTemplate 
     *            iBatis的sqlMapClientTemplate對象 
     */  
    public PagingList(String statementName, Object parameterObject,  
            int pageNum, int pageSize,  
            SqlMapClientTemplate sqlMapClientTemplate, JdbcTemplate jdbcTemplate) {  
        preProcessParams(pageNum, pageSize);  
        execute(statementName, parameterObject, pageNum, pageSize,  
                sqlMapClientTemplate, jdbcTemplate);  
    }  
  
    /** 
     * 構造方法,進行分頁 
     *  
     * @param statementName 
     *            iBatis中語句的ID 
     * @param pageNum 
     *            起始頁數(shù) 
     * @param pageSize 
     *            每頁大小 
     * @param sqlMapClientTemplate 
     *            iBatis的sqlMapClientTemplate對象 
     */  
    public PagingList(String statementName, int pageNum, int pageSize,  
            SqlMapClientTemplate sqlMapClientTemplate, JdbcTemplate jdbcTemplate) {  
        preProcessParams(pageNum, pageSize);  
        execute(statementName, pageNum, pageSize, sqlMapClientTemplate,  
                jdbcTemplate);  
    }  
  
    /** 
     * 執(zhí)行方法 
     *  
     * @param statementName 
     * @param parameterObject 
     * @param pageNum 
     * @param pageSize 
     * @param sqlMapClientTemplate 
     */  
    public void execute(String statementName, Object parameterObject,  
            int pageNum, int pageSize,  
            SqlMapClientTemplate sqlMapClientTemplate, JdbcTemplate jdbcTemplate) {  
        // 計算記錄總數(shù)  
        this.rowCount = jdbcTemplate.queryForInt(  
                getCountSql(getSrcSql(statementName, parameterObject,  
                        sqlMapClientTemplate)), ((HashMap) parameterObject)  
                        .values().toArray());  
        System.out.println(rowCount);  
        // 計算分頁數(shù)及起止記錄  
        countPage();  
        // 獲取分頁列表  
        this.list = sqlMapClientTemplate.queryForList(statementName,  
                parameterObject, (pageNum - 1) * pageSize, pageSize);  
    }  
  
    /** 
     * 執(zhí)行方法 
     *  
     * @param statementName 
     * @param pageNum 
     * @param pageSize 
     * @param sqlMapClientTemplate 
     */  
    public void execute(String statementName, int pageNum, int pageSize,  
            SqlMapClientTemplate sqlMapClientTemplate, JdbcTemplate jdbcTemplate) {  
        // 計算記錄總數(shù)  
        this.rowCount = jdbcTemplate.queryForInt(getCountSql(getSrcSql(  
                statementName, null, sqlMapClientTemplate)));  
        System.out.println(rowCount);  
        // 計算分頁數(shù)及起止記錄  
        countPage();  
        // 獲取分頁列表  
        this.list = sqlMapClientTemplate.queryForList(statementName,  
                (pageNum - 1) * pageSize, pageSize);  
    }  
  
    /** 
     * 預處理SQL語句和頁面參數(shù) 
     */  
    private void preProcessParams(int pageNum, int pageSize) {  
        if (pageNum > 0) {  
            this.pageNum = pageNum;  
        }  
        if (pageSize > 0) {  
            this.pageSize = pageSize;  
        }  
        if (pageSize > 1000) {  
            this.pageSize = 1000;  
        }  
    }  
  
    /** 
     * 計算分頁數(shù)及起止記錄 
     */  
    private void countPage() {  
        // 計算分頁總數(shù)  
        if ((rowCount % pageSize) == 0) {  
            pageCount = rowCount / pageSize;  
        } else {  
            pageCount = rowCount / pageSize + 1;  
        }  
        if (pageCount == 0) {  
            pageCount = 1;  
        }  
        // 判斷pageNum是否過界  
        if (pageNum > pageCount && rowCount != 0) {  
            pageNum = pageCount;  
        }  
        // 計算起止記錄  
        startIndex = (pageNum - 1) * pageSize + 1;  
        endIndex = (pageNum) * pageSize;  
    }  
  
    /** 
     * 獲得對象列表 
     */  
    public List getList() {  
        return list;  
    }  
  
    /* 獲得起始記錄數(shù) */  
    public int getStartIndex() {  
        return startIndex;  
    }  
  
    public Integer getStartIndexInteger() {  
        return new Integer(startIndex);  
    }  
  
    /* 獲得結束記錄數(shù) */  
    public int getEndIndex() {  
        return endIndex;  
    }  
  
    public Integer getEndIndexInteger() {  
        return new Integer(endIndex);  
    }  
  
    /* 獲得分頁其它信息 */  
    public int getPageCount() {  
        return pageCount;  
    }  
  
    public int getPageNum() {  
        return pageNum;  
    }  
  
    public int getPageSize() {  
        return pageSize;  
    }  
  
    public int getRowCount() {  
        return rowCount;  
    }  
  
    private String getSrcSql(String statementName, Object parameterObject,  
            SqlMapClientTemplate sqlMapClientTemplate) {  
        SqlMapClientImpl sqlMapClientImpl = (SqlMapClientImpl) sqlMapClientTemplate  
                .getSqlMapClient();  
        MappedStatement mappedStatement = sqlMapClientImpl  
                .getMappedStatement(statementName);  
        Sql sql = mappedStatement.getSql();  
        StatementScope statementScope = new StatementScope(new SessionScope());  
        String srcSql = sql.getSql(statementScope, parameterObject);  
        return srcSql;  
    }  
  
    private String getCountSql(String srcSql) {  
        return "SELECT COUNT(*) FROM ( " + srcSql + " ) CTBL_";  
    }  
  
}  
  寫好分頁類,還要和框架進行集成,那么我們可以抽象出Service的基類,在業(yè)務邏輯層中調用它來獲取分頁信息: 
package org.ourpioneer.service;  
import org.ourpioneer.bean.PagingList;  
import org.springframework.orm.ibatis.SqlMapClientTemplate;  
import com.opensymphony.xwork2.ActionContext;  
import com.opensymphony.xwork2.util.ValueStack;  
public class BaseService {  
    private JdbcTemplate jdbcTemplate;  
  
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {  
        this.jdbcTemplate = jdbcTemplate;  
    }  
  
    /** 
     * 獲取ValueStack 
     *  
     * @return ValueStack對象 
     */  
    public ValueStack getValueStack() {  
        return ActionContext.getContext().getValueStack();  
    }  
  
    /** 
     * 獲取分頁的List 
     *  
     * @param statementName 
     * @param sqlMapClientTemplate 
     * @return 
     */  
    public PagingList getPagingList(String statementName,  
            SqlMapClientTemplate sqlMapClientTemplate) {  
        int pageNum = ((Integer) getValueStack().findValue("pageNum"))  
                .intValue();  
        int pageSize = ((Integer) getValueStack().findValue("pageSize"))  
                .intValue();  
        return new PagingList(statementName, pageNum, pageSize,  
                sqlMapClientTemplate, jdbcTemplate);  
    }  
  
    /** 
     * 獲取分頁的List 
     *  
     * @param statementName 
     * @param parameterObject 
     * @param sqlMapClientTemplate 
     * @return 
     */  
    public PagingList getPagingList(String statementName,  
            Object parameterObject, SqlMapClientTemplate sqlMapClientTemplate) {  
        int pageNum = ((Integer) getValueStack().findValue("pageNum"))  
                .intValue();  
        int pageSize = ((Integer) getValueStack().findValue("pageSize"))  
                .intValue();  
        return new PagingList(statementName, parameterObject, pageNum,  
                pageSize, sqlMapClientTemplate, jdbcTemplate);  
    }  
}  
    兩個構造方法我們都使用了,也就是一個帶參數(shù),一個不帶參數(shù)。下面來看抽象出的Action基類,主要是處理頁面?zhèn)魅氲姆猪搮?shù)的處理:
package org.ourpioneer.action;  
import java.util.Map;  
import javax.servlet.http.HttpServletRequest;  
import org.apache.struts2.ServletActionContext;  
import org.ourpioneer.util.QueryUtil;  
import com.opensymphony.xwork2.ActionContext;  
import com.opensymphony.xwork2.ActionSupport;  
public class BaseAction extends ActionSupport {  
    @Override  
    public String execute() throws Exception {  
        return SUCCESS;  
    }  
    public Map<String, Object> getParameters() {  
        return ActionContext.getContext().getParameters();  
    }  
    public HttpServletRequest getRequest() {  
        return ServletActionContext.getRequest();  
    }  
    /* 分頁信息 */  
    protected int pageNum = 1;  
    protected int pageSize = 10;  
    public int getPageNum() {  
        return pageNum;  
    }  
    public void setPageNum(int pageNum) {  
        this.pageNum = pageNum;  
    }  
    public int getPageSize() {  
        return pageSize;  
    }  
    public void setPageSize(int pageSize) {  
        this.pageSize = pageSize;  
    }  
    public int getMaxPageSize() {  
        return 1000;  
    }  
    public int getDefaultPageSize() {  
        return 10;  
    }  
    // 頁面解析分頁信息使用的方法  
    public String getQueryStringWithoutPageNum() {  
        Map<String, Object> m = getParameters();  
        m.remove("pageNum");  
        return QueryUtil.getQueryString(m);  
    }  
    public String getFullUrlWithoutPageNum() {  
        return getRequest().getServletPath() + "?"  
                + getQueryStringWithoutPageNum();  
    }  
    public String getQueryStringWithoutPageInfo() {  
        Map<String, Object> m = getParameters();  
        m.remove("pageNum");  
        m.remove("pageSize");  
        return QueryUtil.getQueryString(m);  
    }  
    public String getFullUrlWithoutPageInfo() {  
        return getRequest().getServletPath() + "?"  
                + getQueryStringWithoutPageInfo();  
    }  
}  
   這里為了演示,我們將分頁的信息都直接定義死了,大家可以根據(jù)需要來修改,其中處理信息的QueryUtil大家可以直接參考源代碼,這里不做說明了,下面是UserAction處理代碼的編寫: 
package org.ourpioneer.action;  
import org.ourpioneer.bean.PagingList;  
import org.ourpioneer.service.UserService;  
public class UserAction extends BaseAction {  
    private UserService userService;  
    public PagingList userList;  
    public void setUserService(UserService userService) {  
        this.userService = userService;  
    }  
    public PagingList getUserList() {  
        return userList;  
    }  
    public String list() {  
        userList = userService.getAllUsers();  
        return "list";  
    }  
}  
   根據(jù)前面的配置,我們也不難寫出代碼,下面就是視圖處理了,我們使用了Freemarker進行解析,也編寫了FreeMarker的分頁宏: 
<#-- 處理分頁參數(shù) -->  
<#function getPageUrl pageNum>  
<#local pageUrl=base+fullUrlWithoutPageInfo>  
<#if pageUrl?ends_with("?")>  
<#return pageUrl + "pageSize=" + pageSize + "&pageNum=" + pageNum>  
<#else>  
<#return pageUrl + "&pageSize=" + pageSize + "&pageNum=" + pageNum>  
</#if>  
</#function>  
<#-- 全部或分頁顯示 -->  
<#function getPageUrlResize size>  
<#local pageUrl=base+fullUrlWithoutPageInfo>  
<#if pageUrl?ends_with("?")>  
<#return pageUrl + "pageNum=1&pageSize=" + size>  
<#else>  
<#return pageUrl + "&pageNum=1&pageSize=" + size>  
</#if>  
</#function>  
  
<#-- 分頁信息 -->  
<#macro paging pagingList>  
<#local pageCount=pagingList.pageCount>  
<#local rowCount=pagingList.rowCount>  
<#local pageNum=pagingList.pageNum>  
<#local pageSize=pagingList.pageSize>  
<#if rowCount == 0>  
    <#if useFlag?exists>  
        <div style="border:1px solid #666;padding:2 5 2 5;background:#efefef;color:#333">沒有相關記錄</div>  
    <#else>  
        <#assign useFlag = 1>  
    </#if>  
<#else>  
<table>  
    <tr>  
        <td style="line-height:150%">共 ${rowCount} 條記錄 ${pageCount} 頁   
        <#if pageCount gt 1 && pageSize!=maxPageSize>  
            <span class="selectedPage" style="padding:2px 3px 0 3px"><a class="page" href="${getPageUrlResize(maxPageSize)}">全部顯示</a></span>  
        <#elseif pageSize==maxPageSize>  
            <span class="selectedPage" style="padding:2px 3px 0 3px"><a class="page" href="${getPageUrlResize(defaultPageSize)}">分頁顯示</a></span>  
        </#if>  
        <#if (pageCount <= 11)>  
            <#local startPage = 1>  
            <#local endPage = pageCount>  
        <#elseif (pageNum + 5 > pageCount)>  
            <#local startPage = pageCount - 10>  
            <#local endPage = pageCount>  
        <#elseif (pageNum - 5 < 1)>  
            <#local startPage = 1>  
            <#local endPage = 11>  
        <#else>  
            <#local startPage = pageNum - 5>  
            <#local endPage = pageNum + 5>  
        </#if>  
        <#if (pageCount > 1)>  
            <#if (pageNum != 1)>  
                <#if (pageCount > 11)>  
                    <a class="page" href="${getPageUrl(1)}" style="font-family:Webdings" title="首頁">9</a>  
                </#if>  
                <a class="page" href="${getPageUrl(pageNum-1)}" style="font-family:Webdings" title="上頁">3</a>  
            <#else>  
                <#if (pageCount > 11)>  
                    <span style="font-family:Webdings;color:#999">9</span>  
                </#if>  
                <span style="font-family:Webdings;color:#999">3</span>  
            </#if>  
            <#list startPage..endPage as x>  
                <#if x=pageNum>  
                    <span class="selectedPage">${x}</span>  
                <#else>  
                    <span class="noSelectedPage"><a class="page" href="${getPageUrl(x)}">${x}</a></span>  
                </#if>  
            </#list>  
            <#if (pageCount != pageNum)>  
                <a class="page" href="${getPageUrl(pageNum+1)}" style="font-family:Webdings" title="下頁">4</a>  
                <#if (pageCount > 11)>  
                    <a class="page" href="${getPageUrl(pageCount)}" style="font-family:Webdings" title="尾頁">:</a>  
                </#if>  
            <#else>  
                <span style="font-family:Webdings;color:#999">4</span>  
                <#if (pageCount > 11)>  
                    <span style="font-family:Webdings;color:#999">:</span>  
                </#if>  
            </#if>  
        </#if>  
        </td>  
    </tr>  
</table>  
</#if>  
</#macro>  
 之后,我們來運行項目:

 可以通過點擊全部顯示和頁面來查看分頁效果。 
 本文系作者本人的實踐探索,方案可能不是最佳實踐,希望和大家交流溝通。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號