API参考 - 筋斗云服务端JAVA版

最后更新:2020-11-04

%var env.isTestMode

是否为测试模式。

%var env.debugLevel

0-9间的调试等级。

%var env.appName

@var env.appType

@key getAppType ()

获取appName, appType

%var env.baseDir

应用程序的主目录,用于写文件。默认为 {user.home}/jd-data/{project}.

在CentOS下运行tomcat时,往往默认位置为 /usr/share/tomcat/jd-data/{project}
如果在web.properties中设置了

baseDir=.

表示以项目部署目录为基本目录。

%var env.props

1 应用入口 JDEnv

2 数据库连接

3 应用程序选项

@key web.properties

@key jdcloud-config

配置选项。从WEB-INF/web.properties中加载,也可以在env.onApiInit回调函数中修改。

1 应用入口 JDEnv

作为WebApi应用程序入口。
所有的接口实现都应与该类在同一个包中。

2 数据库连接

示例:连接mysql, 使用库mysql-connector-java-5.1.34.jar

P_DBTYPE=mysql
P_DB_DRIVER=com.mysql.jdbc.Driver
P_DB=jdbc:mysql://localhost:3306/jdcloud?characterEncoding=utf8
P_DBCRED=demo:demo123

示例:连接sqlserver(mssql),使用库sqljdbc42.jar

P_DBTYPE=mssql
P_DB_DRIVER=com.microsoft.sqlserver.jdbc.SQLServerDriver
P_DB=jdbc:sqlserver://localhost:1433;instanceName=MSSQL1;databaseName=jdcloud;integratedSecurity=false;
P_DBCRED=sa:demo123

示例:连接mysql, 且使用连接池,在web.properties中设置:

P_DBTYPE=mysql
P_DB_DRIVER=DataSource
P_DB=jdbc/jdcloud

在web.xml的<web-app>标签中增加:

<resource-ref> 
  <res-ref-name>jdbc/jdcloud</res-ref-name>  
  <res-type>javax.sql.DataSource</res-type>  
  <res-auth>Container</res-auth> 
</resource-ref>

在<Context>标签中增加Resource定义:

<Resource name="jdbc/jdcloud" auth="Container" driverClassName="com.mysql.jdbc.Driver" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" 
  maxActive="100" username="demo" password="demo123" type="javax.sql.DataSource" url="jdbc:mysql://server-pc:3306/jdcloud?characterEncoding=utf8"/> 

3 应用程序选项

%fn callSvc(ac)

@fn callSvc (ac, param, postParam, opt={backupEnv, isCleanCall, asAdmin})

调用接口,获得返回值。
如果指定了非空的param, postParam参数,则会并入当前环境的GET, POST参数中。
通过opt参数可调整行为。

@param opt.backupEnv 如果为true, 调用完成后恢复原先的GET, POST参数等。

@param opt.isCleanCall 如果为true,不使用原先环境,只用param, postParam作为GET/POST环境。

@param opt.asAdmin TODO: 以超级管理员权限调用。

JsObject rv = (JsObject)callSvc("User.get");

它不额外处理事务、不写ApiLog。

%fn env.onCreateApi() -> String[]

对于函数型调用,返回接口实现类(应继承JDApiBase)列表。默认为"Global":

return new String[] { "Global" };

也可以从多个类加载,常用于添加插件,如:

return new String[] { "Global", "JDLogin", "JDUpload" };

%fn env.onCreateAC(table) -> String[]

对于对象型调用,根据对象名(table)返回一个类名数组,用于绑定权限与AC类。注意类名不带包名。
默认逻辑作为示例:

    if (hasPerm(AUTH_USER)) {
        return new String[] { "AC1_" + table, "AC_" + table };
    }
    else if (hasPerm(AUTH_EMP)) {
        return new String[] { "AC2_" + table };
    }
    else if (hasPerm(AUTH_ADMIN)) {
        return new String[] { "AC0_" + table, "AccessControl" };
    }
    return new String[] {"AC_" + table};

它表示:

%fn env.onGetPerms() -> perms/i

返回权限集合。一般根据session来设置。默认检查uid, empId, adminId三个session变量,如果存在则认为具有用户、员工、超级管理员登录权限。

    int perms = 0;
    if (_SESSION("uid") != null) {
        perms |= AUTH_USER;
    }
    else if (_SESSION("empId") != null) {
        perms |= AUTH_EMP;
    }
    else if (_SESSION("adminId") != null) {
        perms |= AUTH_ADMIN;
    }
    return perms;

%fn env.onApiInit()

API调用前的回调函数。例如设置选项、检查客户版本等。
示例:关闭ApiLog

this.props.setProperty("enableApiLog", "0");

%fn env.onDbconn()

连接数据库后回调,用于设置连接选项,或切换数据库。示例:

@Override
protected void onDbconn() throws Exception {
    String project = this.ctx.getContextPath();
    if (project.indexOf("-hg") > 0) {
        this.conn.setCatalog("pdi_hg");
    }
}

@var JDEnvBase.skipLogCnt

如果想忽略输出一条SQL日志,可以在调用SQL查询前设置skipLogCnt,如:

++ env.skipLogCnt; // 若要忽略两条就用 env.skipLogCnt+=2;
execOne(...);

@see queryAll execOne dbconn

%fn queryOne(sql, assoc?=false)

执行查询语句,只返回一行数据,如果行中只有一列,则直接返回该列数值。
如果查询不到,返回false,可以用Objects.equals(rv, false)来判断。注意返回值可能为null,不要用rv.equals(false)判断。

示例:查询用户姓名与电话,默认返回值数组(JsArray):

Object rv = queryOne("SELECT name,phone FROM User WHERE id=" + id);
if (Objects.equals(rv, false))
    throw new MyException(E_PARAM, "bad user id");
JsArray row = (JsArray)rv;
// row = ["John", "13712345678"]

指定参数assoc=true时,返回关联数组:

Object rv = queryOne("SELECT name,phone FROM User WHERE id=" + id, true);
if (Objects.equals(rv, false))
    throw new MyException(E_PARAM, "bad user id");
JsObject row = (JsObject)rv;
// row = {"name": "John",  "phone":"13712345678"}

当查询结果只有一列且参数assoc=false时,直接返回该数值。

Object phone = queryOne("SELECT phone FROM User WHERE id="+id);
if (Objects.equals(phone, false))
    throw new MyException(E_PARAM, "bad user id");
// phone = "13712345678"

@see queryAll

%fn execOne(sql, getInsertId?=false)

@param getInsertId ?=false 取INSERT语句执行后得到的id. 仅用于INSERT语句。

执行SQL语句,如INSERT, UPDATE等。执行SELECT语句请使用queryOne/queryAll.

String token = (String)mparam("token");
execOne("UPDATE Cinf SET appleDeviceToken=" . Q(token));

注意:在拼接SQL语句时,对于传入的String类型参数,应使用Q函数进行转义,避免SQL注入攻击。

对于INSERT语句,当设置参数getInsertId=true时, 可返回新加入数据行的id. 例:

String sql = String.format("INSERT INTO Hongbao (userId, createTm, src, expireTm, vdays) VALUES (%s, '%s', '%s', '%s', %s)",
    userId, createTm, src, expireTm, vdays);
int hongbaoId = execOne(sql, true);

@fn dbInsert(table, kv) -> newId

字段值可以是数值、日期、字符串等类型的变量。
如果值为null或空串会被忽略。
e.g.

int orderId = dbInsert("Ordr", new JsObject(
    "tm", new Date(), // 支持Date类型
    "tm1", dbExpr("now()"), // 使用dbExpr直接提供SQL表达式
    "amount", 100,
    "dscr", null // null字段会被忽略
));

@fn dbUpdate(table, kv, id_or_cond?) -> cnt

@param id_or_cond 查询条件,如果是数值比如100或"100",则当作条件"id=100"处理;否则直接作为查询表达式,比如"qty<0";如果未指定则无查询条件。

e.g.

// UPDATE Ordr SET ... WHERE id=100
int cnt = dbUpdate("Ordr", new JsObject(
    "amount", 30,
    "dscr", "test dscr",
    "tm", "null", // 用""或"null"对字段置空;用"empty"对字段置空串。
    "tm1", null // null会被忽略
), 100);

// UPDATE Ordr SET tm=now() WHERE tm IS NULL
int cnt = dbUpdate("Ordr", new JsObject(
    "tm", dbExpr("now()")  // 使用dbExpr,表示是SQL表达式
), "tm IS NULL);

@fn dbExpr($val)

用于在dbInsert/dbUpdate(插入或更新数据库)时,使用表达式:

int id = dbInsert("Ordr", asMap(
    "tm", dbExpr("now()") // 使用dbExpr直接提供SQL表达式
));

%fn logit(str, addHeader?=true, type?="trace")

记录日志到文件。type参数指定日志文件名,例如默认为trace,则日志写到trace.log。

%fn parseBoolean(val) -> boolVal

val支持多种类型。

示例:

boolean forTest = param("test/b", false); 
boolean isTestMode = parseBoolean(getenv("P_TEST_MODE", "0")); // 仅用作示例,可以直接用 env.isTestMode

%fn parseDate(str, onlyDatePart?=false) -> dateVal

支持以下日期格式:

"2010/1/1 10:10", "2011-2-1 8:8:8", "2010.3.4", "2011-02-01T10:10:10Z"

如果格式无法解析,返回null。
如果onlyDatePart=true,则只取日期部分,忽略时间部分。

%fn parseDate(str, fmt) -> dateVal

java.util.Date tm = parseDate("20180101121030", "yyyyMMddHHmmss");
if (tm == null) // fail to parse
    return;
String tmstr = date("yyyy-MM-dd HH:mm:ss", tm); // "2018-01-01 12:10:30"

如果格式无法解析,返回null。

%fn param(name, defVal?, col?, doHtmlEscape=true)

@param col null - (默认)先取URL参数(env._GET)再取POST参数(api._POST),"G" - 从env.GET中取; "P" - 从env.POST中取

获取名为name的参数。
name中可以指定类型,返回值根据类型确定。如果该参数未定义或是空串,直接返回缺省值defVal。

name中指定类型的方式如下:

示例:

Integer id = (Integer)param("id");
Integer svcId = (Integer)param("svcId/i", 99);
Boolean wantArray = (Boolean)param("wantArray/b", false);
Date startTm = (Date)param("startTm/dt", new Date());
List<Integer> idList = (List<Integer>)param("idList/i+");

List类型示例。假设参数"items"类型在文档中定义为

items
: list(id/Integer, qty/Double, dscr/String)

这样取该参数,返回值为JsArray类型,数组中每一项又是JsArray类型(下面用中括号表示JsArray):

JsArray items = (JsArray)param("items/i:n:s");
// 假设 items="100:1:洗车,101:1:打蜡"
// 返回 [ [ 100, 1.0, "洗车"], [101, 1.0, "打蜡"] ]

如果某列可缺省,用"?"表示,如 param("items/i:n?:s?") 来获取值 items=100:1,101::打蜡
得到

[ [ 100, 1.0, null], [101, null, "打蜡"] ]

TODO: 直接支持 param("items/(id,qty?/n,dscr?)"), 添加param_objarr函数,去掉parseList函数。上例将返回

[
    [ "id"=>100, "qty"=>1.0, dscr=>null],
    [ "id"=>101, "qty"=>null, dscr=>"打蜡"]
]

%fn mparam(name, coll?=null, htmlEscape?=true)

必填参数(mandatory param)

参考param函数,查看name如何支持各种类型。

示例:

String svcId = (String)mparam("svcId");
Integer svcId = (Integer)mparam("svcId/i");
List<Integer> itts = (List<Integer>)mparam("itts/i+");

name也可以是一个数组,表示至少有一个参数有值,这时返回每个参数的值。

JsArray rv = mparam(new String[] {"svcId/i", "itts/i+"}); // require one of the 2 params
Integer svcId = rv.get(0);
List<Integer> itts = rv.get(1);

%fn mparam(names, coll?=null)

几个参数,必填其一(mandatory param)
names是一个数组,表示至少有一个参数有值,返回JsArray,包含每个参数的值,其中有且只有一个非null。

JsArray rv = mparam(new String[] {"svcId/i", "itts/i+"}); // require one of the 2 params
Integer svcId = rv.get(0);
List<Integer> itts = rv.get(1); 
if (svcId != null) {
}
else if (itts != null) {
}

@fn header (key, val)

@fn header (key, val, true)

获取或设置header. 第三种形式表示追加header

%fn getSession(name) -> Object

@see setSession

@see unsetSession

@see destroySession

%fn setSession(name, value)

@see getSession

@see unsetSession

@see destroySession

%fn unsetSession(name)

@see setSession

@see getSession

@see destroySession

%fn destroySession()

@see getSession

@see setSession

@see unsetSession

@fn table2objarr

将table格式转为 objarr, 即前端的rs2Array, 如:

table2objarr(
    [
        "h"=>["id", "name"],
        "d"=>[ 
            [100,"A"], 
            [101,"B"]
           ] 
    ]
) -> [ ["id"=>100, "name"=>"A"], ["id"=>101, "name"=>"B"] ]

@fn varr2objarr(d, h)

将值数组类型 d (仅有值的二维数组, elem=[$col1, $col2] ) 转为对象数组objarr, elem={col1=>cell1, col2=>cell2})

例:

varr2objarr(
    [ [100, "A"], [101, "B"] ], 
    ["id", "name"] )
-> [ ["id"=>100, "name"=>"A"], ["id"=>101, "name"=>"B"] ]

@fn list2varr(ls, colSep=':', rowSep=',')

将字符串代表的压缩表("v1:v2:v3,...")转成值数组。

e.g.

$users = "101:andy,102:beddy";
$varr = list2varr($users);
// $varr = [["101", "andy"], ["102", "beddy"]];

$cmts = "101\thello\n102\tgood";
$varr = list2varr($cmts, "\t", "\n");
// $varr=[["101", "hello"], ["102", "good"]]

%fn getenv(name, defVal?)

取全局设置。

String cred = getenv("P_ADMIN_CRED");
boolean isTestMode = parseBoolean(getenv("P_TEST_MODE", "0")); // 仅用作示例,可以直接用 env.isTestMode
int debugLevel = Integer.parseInt(getenv("P_DEBUG", "0"));  // 仅用作示例,可以直接用 env.debugLevel

%fn myEncrypt(data, op, key?="jdcloud") -> String

对字符串data加密,生成base64编码的字符串。
或者反过来,对base64编辑的data解密返回原文。
出错返回null。

算法:DES加密。

@param op "E"-加密(encrypt); "D"-解密(decrypt)

%fn getCred(cred) -> [user, pwd]

cred为"{user}:{pwd}"格式,支持使用base64编码。
返回2元素字符数组,表示user和pwd。
出错时返回null。

示例:

String[] cred = getCred(getenv("P_ADMIN_CRED"));
if (cred == null) {
    // 未设置用户名密码
}
String user = cred[0], pwd = cred[1];

%fn hashPwd(pwd) -> pwd1

对密码进行加密处理。如果已加密过,则直接返回。

%fn getPath(path, withSep) -> path

用于获得以"/"结尾或不以"/"结尾的路径。

String path = getPath("dir1/dir2/", true); // "dir1/dir2/"
String path = getPath("dir1/dir2", true); // "dir1/dir2/"
String path = getPath("dir1/dir2", false); // "dir1/dir2"
String path = getPath("dir1/dir2/", false); // "dir1/dir2"

%fn getBaseUrl(wantHost) -> String

返回项目的URL,以"/"结尾。

String url = getBaseUrl(true); // "http://myserver/myapp/"
String url1 = getBaseUrl(false); // "/myapp/"

%fn exit()

立即返回,不再处理,不自动输出任何内容。
如果想输出返回数据,请自行用echo输出后再调用exit,或直接用DirectReturn(val)返回[0,val]标准格式.

@see DirectReturn

%fn tmCols(fieldName = "t0.tm")

为查询添加时间维度单位: y,q,m,w,d,wd,h (年,季度,月,周,日,周几,时)。

示例:

this.vcolDefs = asList(
    new VcolDef().res(tmCols())
);

this.vcolDefs = asList(
    new VcolDef().res(tmCols("log_cr.tm")).require("createTm")
);

%fn issetval(fieldName)

判断POST内容中是否对该字段设置值,示例:
(在onValidate回调中)

if (issetval("pwd")) {
    String pwd = (String)env._POST.get("pwd");
    env._POST.put("pwd", hashPwd(pwd));
}

@fn parseKvList(kvListStr, sep, sep2)

解析key-value列表字符串。如果出错抛出异常。
注意:sep, sep2为行、列分隔符的正则式。

示例:

Map<String, Object> map = parseKvList("CR:新创建;PA:已付款", ";", ":");
// map: {"CR": "新创建", "PA":"已付款"}

%fn httpCall(url, urlParams, postParams, opt)

postParams非空使用POST请求,否则使用GET请求。
postParams可以是字符串、map或list等数据结构。默认contentType为"x-www-form-urlencoded"格式。如果postParams为list等结构,则使用"json"格式。
如果要明确指定格式,可以设置opt.contentType参数,如

String rv = httpCall(baseUrl, urlParams, postParams, asMap("contentType", "application/json"));

e.g.

String baseUrl = "http://oliveche.com/echo.php";
// 常用asMap或new JsObject
Map<String, Object> urlParams = asMap("intval", 100, "floatval", 12.345, "strval", "hello");
JsObject postParams = new JsObject("postintval", 100, "poststrval", "中文");
String rv = httpCall(baseUrl, urlParams, postParams, null);

@var AccessControl.enumFields 枚举支持及自定义字段处理

@alias enumFields (fieldName, EnumFieldFn)

(版本v1.1)

格式为{field => map/fn(val, row) }

作为比onHandleRow/onAfterActions等更易用的工具,enumFields可对返回字段做修正。例如,想要对返回的status字段做修正,如"CR"显示为"Created",可设置:

Map<String, Object> map = asMap("CR","Created", "CA","Cancelled");
this.enumFields("status", map)

也可以设置为自定义函数,如:

this.enumFields("status", (v, row) -> {
    if (map.containsKey(v))
        return String.format("%s-%s", v, map.get(v));
    return v;
});

此外,枚举字段可直接由请求方通过res参数指定描述值,如:

Ordr.query(res="id, status =CR:Created;CA:Cancelled")
或指定alias:
Ordr.query(res="id 编号, status 状态=CR:Created;CA:Cancelled")

更多地,设置enumFields也支持逗号分隔的枚举列表,比如字段值为"CR,CA",实际可返回"Created,Cancelled"。

@var AccessControl.delField

如果设置该字段(例如设置为disableFlag字段),则把删除动作当作是设置该字段为1,且在查询接口中跟踪此字段增加过滤。
必须是flag字段(0/1值)。

示例:

// onInit中:
this.delField = "disableFlag"

@fn AccessControl::callSvc(tbl, ac, param=null, postParam=null)

直接调用指定类的接口,如内部直接调用"PdiRecord.query"方法:

// 假如当前是AC2权限,对应的AC类为AC2_PdiRecord:
AccessControl acObj = new AC2_PdiRecord();
acObj.env = env; // 别忘记指定env
acObj.callSvc("PdiRecord", "query");

这相当于调用callSvc("PdiRecord.query")
区别是,用本方法可自由指定任意AC类,无须根据当前权限自动匹配类。

例如,"PdiRecord.query"接口不对外开放,只对内开放,我们就可以只定义class PdiRecord extends AccessControl(无AC前缀,外界无法访问),在内部访问它的query接口:

AccessControl acObj = new PdiRecord();
acObj.env = env;
acObj.callSvc("PdiRecord", "query");

如果未指定param/postParam,则使用当前GET/POST环境参数执行,否则使用指定环境执行,并在执行后恢复当前环境。

也适用于AC类内的调用,这时可不传table,例如调用当前类的add接口:

Object rv = this.callSvc(null, "add", null, postParam);

示例:通过手机号发优惠券时,支持批量发量,用逗号分隔的多个手机号,接口:

手机号userPhone只有一个时:
Coupon.add()(userPhone, ...) -> id

如果userPhone包含多个手机号:(用逗号隔开,支持中文逗号,支持有空格)
Coupon.add()(userPhone, ...) -> {cnt, idList}

重载add接口,如果是批量添加则通过callSvc再调用add接口:

public Object api_add() {
    if (this._POST.containsKey("userPhone")) {
        String userPhone = (String)this._POST.get("userPhone");
        String[] arr = userPhone.split("(?U)[,,]"); // 支持中文逗号
        if (arr.length > 1) {
            List idList = new ArrayList();
            JsObject postParam = new JsObject();
            postParam.putAll(this._POST);
            for (String e: arr) {
                postParam.put("userPhone", e.trim());
                idList.add(this.callSvc(null, "add", null, postParam));
            }
            setRet(0, asMap(
                "cnt", idList.size(),
                "idList", idList
            );
            throw new DirectReturn();
        }
    }
    return super.api_add();
}

框架自带的批量添加接口api_batch也是类似调用。

@see callSvc

@see callSvcSafe

@fn AccessControl::checkSetFields(allowedFields)

"set"/"setIf"接口中,限定可设置的字段。
不可设置的字段用readonlyFields/readonlyFields2来设置。

e.g.
void onValidate()
{
if (this.ac.equals("set"))
checkSetFields(asList("status", "cmt"));
}

@fn AccessControl::api_setIf()

批量更新。

setIf接口会检测readonlyFields及readonlyFields2中定义的字段不可更新。
也可以直接用checkSetFields指定哪些字段允许更新。
返回更新记录数。
示例:

class AC2_Ordr extends AccessControl {
    @Override
    public Object api_setIf() throws Exception {
        checkAuth(App.PERM_MGR);
        this.checkSetFields(asList("dscr", "amount"));
        Object empId = getSession("empId");
        addCond("t0.empId=" + empId);
        // addJoin("...");
        return super.api_setIf();
    }
}

@fn AccessControl::api_delIf()

批量删除。返回删除记录数。
示例:

class AC2_Ordr extends AccessControl {
    @Override
    public Object api_delIf() throws Exception {
        checkAuth(App.PERM_MGR);
        return super.api_delIf();
    }
}

@fn AccessControl.getCondParam(paramName)

由于cond参数的特殊性,不宜用param("cond")来取,可以使用:

String cond = getCondParam("cond");

支持GET/POST中各有一个cond/gcond条件。而且支持其中含有">","<"等特殊字符。
没有cond则返回null

@fn AccessControl.queryRet(objArr, nextkey?, totalCnt?, fixedColCnt?=0)

处理objArr,按照fmt参数指定的格式返回,与query接口返回相同。例如,默认的h-d表格式, list格式,excel等。

@fn AccessControl.qsearch(fields, q)

模糊查询

示例接口:

Obj.query(q) -> 同query接口返回

查询匹配参数q的内容(比如查询name, label等字段)。
参数q是一个字符串,或多个以空格分隔的字符串。例如"aa bb"表示字段包含"aa"且包含"bb"。
每个字符串中可以用通配符"",如"a"表示以a开头,"a"表示以a结尾,而"a*"和"a"是效果相同的。

实现:

protected function onQuery() {
    this.qsearch(asList("name", "label", "content"), param("q"));
}

@fn AccessControl::addCond(cond, prepend=false)

@param prepend 为true时将条件排到前面。

调用多次addCond时,多个条件会依次用"AND"连接起来。

添加查询条件。
示例:假如设计有接口:

Ordr.query(q?) . tbl(..., payTm?)
参数:
q:: 查询条件,值为"paid"时,查询10天内已付款的订单。且结果会多返回payTm/付款时间字段。

实现时,在onQuery中检查参数"q"并定制查询条件:

protected void onQuery()
{
    // 限制只能看用户自己的订单
    uid = _SESSION["uid"];
    this.addCond("t0.userId=uid");

    q = param("q");
    if (isset(q) && q.equals("paid")) {
        validDate = date("Y-m-d", strtotime("-9 day"));
        this.addRes("olpay.tm payTm");
        this.addJoin("INNER JOIN OrderLog olpay ON olpay.orderId=t0.id");
        this.addCond("olpay.action='PA' AND olpay.tm>'validDate'");
    }
}

@see AccessControl::addRes

@see AccessControl::addJoin

@fn AccessControl::addJoin(joinCond)

添加Join条件.

@see AccessControl::addCond 其中有示例

@fn AccessControl::addVCol(col, ignoreError=false, alias=null)

根据列名找到vcolMap中的一项,添加到最终查询语句中.
vcolMap是分析vcolDef后的结果,每一列都对应一项;而在一项vcolDef中可以包含多列。

@param col 必须是一个英文词, 不允许"col as col1"形式; 该列必须在 vcolDefs 中已定义.

@param alias 列的别名。可以中文. 特殊字符"-"表示不加到最终res中(只添加join/cond等定义), 由addVColDef内部调用时使用.

@return Boolean T/F

用于AccessControl子类添加已在vcolDefs中定义的vcol. 一般应先考虑调用addRes(col)函数.

@see AccessControl::addRes

@fn AccessControl::isFileExport()

返回是否为导出文件请求。

@fn AccessControl::api_batchAdd()

批量添加(导入)。返回导入记录数cnt及编号列表idList

Obj.batchAdd(title?)(...) -> {cnt, @idList}

在一个事务中执行,一行出错后立即失败返回,该行前面已导入的内容也会被取消(回滚)。

支持三种方式上传:

  1. 直接在HTTP POST中传输内容,数据格式为:首行为标题行(即字段名列表),之后为实际数据行。
    行使用"\n"分隔, 列使用"\t"分隔.
    接口为:

    {Obj}.batchAdd(title?)(标题行,数据行)
    (Content-Type=text/plain)

前端JS调用示例:

var data = "name\taddr\n" + "门店1\t地址1\n门店2\t地址2\n";
callSvr("Store.batchAdd", function (ret) {
    app_alert("成功导入" + ret.cnt + "条数据!");
}, data, {contentType:"text/plain"});

或指定title参数:

var data = "门店名\t地址\n" + "门店1\t地址1\n门店2\t地址2\n";
callSvr("Store.batchAdd", {title: "name,addr"}, function (ret) {
    app_alert("成功导入" + ret.cnt + "条数据!");
}, data, {contentType:"text/plain"});

示例: 在chrome console中导入数据

callSvr("Vendor.batchAdd", {title: "-,name, tel, idCard, addr, email, legalAddr, weixin, qq, area, picId"}, $.noop, `编号 姓名  手机号码    身份证号    通讯地址    邮箱  户籍地址    微信号 QQ号 负责安装的区域 身份证图
112 郭志强 15384813214 150221199211215000  内蒙古呼和浩特赛罕区丰州路法院小区二号楼    815060695@qq.com    内蒙古包头市  15384813214 815060695   内蒙古 532
111 高长平 18375998418 500226198312065000  重庆市南岸区丁香路同景国际W组 1119780700@qq.com   荣昌  18375998418 1119780700  重庆  534
`, {contentType:"text/plain"});
  1. 标准csv/txt文件上传:

上传的文件首行当作标题列,如果这一行不是后台要求的标题名称,可通过URL参数title重新定义。
一般使用excel csv文件(编码一般为gbk),或txt文件(以"\t"分隔列)。
接口为:

{Obj}.batchAdd(title?)(csv/txt文件)
(Content-Type=multipart/form-data, 即html form默认传文件的格式)

后端处理时, 将自动判断文本编码(utf-8或gbk).

前端HTML:

<input type="file" name="f" accept=".csv,.txt">

前端JS示例:

var fd = new FormData();
fd.append("file", frm.f.files[0]);
callSvr("Store.batchAdd", {title: "name,addr"}, function (ret) {
    app_alert("成功导入" + ret.cnt + "条数据!");
}, fd);

或者使用curl等工具导入:
从excel中将数据全选复制到1.txt中(包含标题行,也可另存为csv格式文件),然后导入。
下面示例用curl工具调用VendorA.batchAdd导入:

#/bin/sh
baseUrl=http://localhost/p/anzhuang/api.php
param=title=name,phone,idCard,addr,email,legalAddr,weixin,qq,area
curl -v -F "file=@1.txt" "$baseUrl/VendorA.batchAdd?$param"

如果要调试(php/xdebug),可加URL参数XDEBUG_SESSION_START=1或Cookie中加XDEBUG_SESSION=1

  1. 传入对象数组
    格式为 {list: [...]}

    var data = {
    list: [
    {name: "郭志强", tel: "15384813214"},
    {name: "高长平", tel: "18375998418"}
    ]
    };
    callSvr("Store.batchAdd", function (ret) {
    app_alert("成功导入" + ret.cnt + "条数据!");
    }, data, {contentType:"application/json"});

@class BatchAddLogic

用于定制批量导入行为。
示例,实现接口:

Task.batchAdd(orderId, task1)(city, brand, vendorName, storeName)

其中vendorName和storeName字段需要通过查阅修正为vendorId和storeId字段。

class TaskBatchAddLogic extends BatchAddLogic
{
    protected $vendorCache = [];
    function __construct () {
        // 每个对象添加时都会用的字段,加在$this->params数组中
        $this->params["orderId"] = mparam("orderId", "G"); // mparam要求必须指定该字段
        $this->params["task1"] = param("task1", null, "G");
    }
    // $params为待添加数据,可在此修改,如用`$params["k1"]=val1`添加或更新字段,用unset($params["k1"])删除字段。
    // $row为原始行数据数组。
    function beforeAdd(&$params, $row) {
        // vendorName -> vendorId
        // 如果会大量重复查询vendorName,可以将结果加入cache来优化性能
        if (! $this->vendorCache)
            $this->vendorCache = new SimpleCache(); // name=>vendorId
        $vendorId = $this->vendorCache->get($params["vendorName"], function () use ($params) {
            $id = queryOne("SELECT id FROM Vendor", false, ["name" => $params["vendorName"]] );
            if (!$id) {
                // throw new MyException(E_PARAM, "请添加供应商", "供应商未注册: " . $params["vendorName"]);
                // 自动添加
                $id = callSvcInt("Vendor.add", null, [
                    "name" => $params["vendorName"],
                    "tel" => $params["vendorPhone"]
                ]);
            }
            return $id;
        });
        $params["vendorId"] = $vendorId;
        unset($params["vendorName"]);
        unset($params["vendorPhone"]);

        // storeName -> storeId 类似处理 ...
    }
    // 处理原始标题行数据, $row1是通过title参数传入的标题数组,可能为空
    function onGetTitleRow($row, $row1) {
    }
}

class AC2_Task extends AC0_Task
{
    function api_batchAdd() {
        $this->batchAddLogic = new TaskBatchAddLogic();
        return parent::api_batchAdd();
    }
}

@see api_batchAdd

Generated by jdcloud-gendoc