Skip to content

时间处理

基本概念

AppCube系统中的时间处理比较复杂,尤其涉及跨多时区时。它的复杂性不在于接口的复杂程度,而是脚本的使用场景的多样性。AppCube有三种触发方式:

  1. 用户登录后,通过http请求直接或间接触发脚本(间接触发方式有:flow调用,触发器触发等)
  2. 定时任务直接或间接触发的脚本
  3. 事件直接或间接触发的脚本

这3种情况, 执行上下文中使用的时区是不同的:

第1种情况使用的登录用户的时区,第2种使用的时租户的组织时区,第3种情况使用的UTC时区。

AppCube系统中有4种时区:

  • 用户时区

用户时区

用户的默认设置的时区是Local,这个很容易误导大家,认为这个是浏览器所在操作系统的时区,实际上它是AppCube服务端的操作系统时区。建议大家修改为明确的时区:

用户时区

正常请求触发的脚本执行上下中的时区就是用户时区。

  • 租户组织时区

组织时区

租户的组织时区,定时任务触发的脚本执行上下文采用就是这种时区。

  • 服务端操作系统时区

AppCube的后端脚本引擎默认采这种时区。

  • 客户端操作系统时区(浏览器)

AppCube后端脚本引擎不使用这种时区。只有AppCube前端脚本采用这种时区。

明确了时区的相关信息后,我们再来了解一下AppCube中涉及的时间类型。

javascript的时间类型Date

我们首先得明确一下javascriptDate类型的定义:

Date创建一个 JavaScript Date 实例,该实例呈现时间中的某个时刻。Date 对象则基于 Unix Time Stamp,即自1970年1月1日(UTC)起经过的毫秒数。

从定义我们可以看出,Date对象本身表示是的:自1970年1月1日(UTC)起经过的毫秒数。没有时区的概念。

什么时候才有时区的概念。简单的来说:

就是要把一个字符串变成一个Date实例,或者从一个Date实例得到一个时间字符串时,时区的概念就显现出来。

ts
const date1 = new Date('December 17, 1995 03:24:00');
console.log(date1.toString());

const date2 = new Date('1995-12-17T03:24:00');
console.log(date2.toLocaleString('zh-CN', { timeZone: 'UTC' }));

AppCube的后端脚本引擎默认采用UTC时区。如new Date(...), JSON.parse, JSON.stringify函数,

AppCube后端脚本引擎中的console.log打印Date实例变量时,采用的是脚本执行上下文时区。

console.log输出的时间字符串之所以采用脚本执行上下文时区,是因为它不是javascript对标准库,而是AppCube的扩展实现, AppCube脚本引擎打印日志时使用了执行上下文的时区信息。

AppCube的时间类型DateDatetime

AppCube中的对象字段的类型支持DateDatetime类型,这两种类型在后端脚本引擎中均映射到javascriptDate类型。但这两种类型在时区的处理是有区别的:

  • 对象的Date类型表示年月日信息,没有时区的概念,客户端调用传送的日期字符串yyyy-MM-dd均被做作为UTC时区的字符串处理。

  • 对象的Datetime类型表示年月日时分秒信息,有时区概念,客户端调用传送的日期字符串yyyy-MM-dd HH:mm:ss会在不同的脚本触发流程中被视为不同的时区的字符串。

写示例脚本

我们以一个示例脚本来说明一下:

ts
export class Input {
    @action.param({ type: "Date" })
    inDate: Date;

    @action.param({ type: "Datetime" })
    inDatetime: Date;
}

export class Output {
    @action.param({ type: "Date" })
    outDate: Date;

    @action.param({ type: "Datetime" })
    outDatetime: Date;
}

export class DateDemo {
    @action.method({ input: "Input", output: "Output", description: "do a operation" })
    run(input: Input): Output {
        let output = new Output();

        console.log("input date = ", input.inDate);
        console.log("input datetime = ", input.inDatetime);

        output.outDate = new Date("2021-07-01 06:30:30");
        output.outDatetime = new Date("2021-07-01 06:30:30");

        console.log("output date = ", output.outDate);
        console.log("output datetime = ", output.outDatetime);

        return output;
    }
}

执行脚本

  • 请求参数
json
{
    "inDate": "2021-07-01 06:30:30",
    "inDatetime": "2021-07-01 06:30:30"    
}

Date类型的输入参数也可以输入带有时分秒的字符串,后端脚本执行时,会直接去掉。

  • 脚本运行输出日志
log
0728 11:10:25.314|debug|vm[18]>>> Build #AppCube Core 1.3.7 on amd64
Built on 2021-07-27 20:14:22  
Commit #4c0246172d
0728 11:10:25.314|debug|vm[18]>>> node:  2
0728 11:10:25.314|debug|vm[18]>>> script:  my__datedemo 1.0.1 DateDemo.run
0728 11:10:25.314|debug|vm[18]>>> locale:  zh_CN
0728 11:10:25.314|debug|vm[18]>>> timezone:  (GMT+08:00) China Standard Time (Asia/Shanghai)
0728 11:10:25.314|debug|vm[18]>>> input date =  2021-07-01T08:00:00+08:00 (my__datedemo.ts:22)
0728 11:10:25.314|debug|vm[18]>>> input datetime =  2021-07-01T06:30:30+08:00 (my__datedemo.ts:23)
0728 11:10:25.314|debug|vm[18]>>> output date =  2021-07-01T14:30:30+08:00 (my__datedemo.ts:28)
0728 11:10:25.314|debug|vm[18]>>> output datetime =  2021-07-01T14:30:30+08:00 (my__datedemo.ts:29)
  1. input date = 2021-07-01T08:00:00+08:00信息来看,输入参数的inDate的时分秒被截断成2021-07-01,而且被当作了UTC日期字符串来处理。

  2. input datetime = 2021-07-01T06:30:30+08:00信息来看,输入参数的inDatetime被当作了用户时区字符串:东八区字符串

  3. output date = 2021-07-01T14:30:30+08:00 信息来看,new Date("2021-07-01 06:30:30")把输入参数当作了UTC时间字符串,console.log打印转换成用户时区东八区字符串,加了8个小时。

  4. 从日志打印来看,AppCube后端脚本引擎中的console.log打印Date实例变量时,采用的是脚本执行上下文时区:即用户时区。

  • 返回信息
json
{
    "outDate": "2021-07-01 08:00:00",
    "outDatetime": "2021-07-01 14:30:30"
}

outDate的返回结果来看,时分秒被截断了。且转换成用户时区的字符串返回了。从严格意义上来说应该返回2021-07-01才对。但因为底层库json序列化时区分不了对象引擎的DateDatetime类型,所以统一返回带时分秒的格式字符串了。

outDatetime的返回结果来看,在new Date("2021-07-01 06:30:30")把输入参数当作了UTC时间字符串基础上,返回时转换成用户时区东八区,又增加了8个小时。

上面是这个脚本在用户登录后,请求执行脚本的时区处理情况。如果在事件触发,定时任务触发时,上面的结果又有不同的:脚本执行上下文中时区影响了时间字符串的处理。

date模块

为了处理上面这些繁杂的日期时间的时区处理,AppCube提供了date模块专门处理这些问题。

  • format
ts
function format(date: Date, layout: string, timezone?: TimeZones): string

将Date类型格式为字符串.如果没指定时区值timezone, 则按照如下原则获取时区信息:

  • 用户http请求触发的脚本中,使用用户时区.
  • 定时任务触发的脚本中,使用租户组织时区
  • 事件触发的脚本中,使用UTC时区
ts
import { getTimeZone } from 'context';
import { format } from 'date';

let result = format(new Date(), 'yyyy-MM-dd HH:mm:ss', getTimeZone());

上面例子中,timezone使用的是脚本执行上下文的时区:getTimeZone(), 与默认情况一致。

  • parse
ts
function parse(dateStr: string): Date

按照指定是格式化字符串,把日期字符串解析为Date类型变量。按照如下原则获取时区信息:

  • 用户http请求触发的脚本中,使用用户时区.

  • 定时任务触发的脚本中,使用租户时区.

  • 事件触发的脚本中,使用UTC时区

  • toDate

ts
function toDate(date: string, layout: string, timezone?: TimeZones): Date

按照指定是格式化字符串,把日期字符串解析为Date类型变量。如果没指定时区值timezone, 则按照如下原则获取时区信息:

  • 用户http请求触发的脚本中,使用用户时区.
  • 定时任务触发的脚本中,使用租户时区.
  • 事件触发的脚本中,使用UTC时区
ts
import { getOrganizationTimeZone } from 'context';
import { toDate } from 'date';

let date = toDate('2018-08-08 20:08:08', 'yyyy-MM-dd HH:mm:ss', getOrganizationTimeZone());

上面示例中采用租户组织时区,将一个时间字符串2018-08-08 20:08:08转换成一个javascriptDate类型实例。

统一处理时区的建议方案

有了上面的知识基础后,我们可以开始讨论,怎么让一个处理时间的脚本能够在不同的执行上下文一致的成功执行。整体上来说,有如下3种策略:

时间字符串带上时区信息

在事件触发与脚本http请求等输入输出参数的时间字符串中带上时区信息,如:

json
{
    "inDate": "2021-07-01T06:30:30+08:00",
    "inDatetime": "2021-07-01T06:30:30+08:00"    
}

AppCube处理时,会优先以时间字符串中的时区信息为准。但这个只能解决输入输出参数的问题,无法解决new Date等脚本标准库时区问题。并且很多对接的第三方系统可能不支持这种带时区的字符串格式。

判断不同的执行上下文,作特殊处理

context模块提供了如下接口:

ts
/**
 * 脚本执行触发请求类型
 */
export enum RequestType {
    /**
     * 登录用户的restful请求
     */
    User = 0,
    /**
     * 事件触发的请求
     */
    Event = 1,
    /**
     * 定时任务触发的请求
     */
    Task = 2
}

function getRequestType(): RequestType

基于脚本的执行上下文类型,代码对时间字符串作不同的特殊处理,示例结合第3种情况给出。

统一使用租户组织时区

下面我们使用date模块重新写前面的示例脚本:

ts
import * as date from 'date';
import * as context from 'context';

export class Input {
    @action.param({ type: "Date" })
    inDate: Date;

    @action.param({ type: "Datetime" })
    inDatetime: Date;
}

export class Output {
    @action.param({ type: "Date" })
    outDate: Date;

    @action.param({ type: "Datetime" })
    outDatetime: Date;
}


const dateLayout = 'yyyy-MM-dd HH:mm:ss';

export class DateDemo {
    @action.method({ input: "Input", output: "Output", description: "do a operation" })
    run(input: Input): Output {
        let output = new Output();

        console.log("input date = ", input.inDate);

        let inDatetime = context2OrganizationTime(input.inDatetime);
        console.log("input datetime = ", inDatetime);

        output.outDate = newOrganizationTime("2021-07-01 06:30:30");
        output.outDatetime = newOrganizationTime("2021-07-01 06:30:30");

        console.log("output date = ", output.outDate);
        console.log("output datetime = ", output.outDatetime);

        return output;
    }
}

function newOrganizationTime(dateStr: string): Date {
    return date.toDate(dateStr, dateLayout, context.getOrganizationTimeZone())
}

function context2OrganizationTime(d: Date): Date {
    // 校正时间输入参数,主要在事件触发的执行场景可能会有问题
    // 该判断处理实际不需要,但此处增加可以提升性能,减少时间对象创建数目
    if (context.getRequestType() != context.RequestType.Event) {
        return d;
    }

    let dateStr = date.format(d, dateLayout, context.getTimeZone());
    return date.toDate(dateStr, dateLayout, context.getOrganizationTimeZone());
}

相对于前面一版的代码,这版本的代码主要变化点如下:

  • 对于输入参数,调用context2OrganizationTime函数,将其从脚本执行上下文时区转换为租户组织时区

  • 使用date.toDate代替了new Date

这一版本的脚本代码,执行信息如下:

  • 请求参数
json
{
    "inDate": "2021-07-01 06:30:30",
    "inDatetime": "2021-07-01 06:30:30"    
}
  • 脚本运行输出日志
log
0728 14:39:33.924|debug|vm[22]>>> Build #AppCube Core 1.3.7 on amd64
Built on 2021-07-27 20:14:22  
Commit #4c0246172d
0728 14:39:33.924|debug|vm[22]>>> node:  2
0728 14:39:33.924|debug|vm[22]>>> script:  my__orgdatedemo 1.0.1 DateDemo.run
0728 14:39:33.924|debug|vm[22]>>> locale:  zh_CN
0728 14:39:33.924|debug|vm[22]>>> timezone:  (GMT+08:00) China Standard Time (Asia/Shanghai)
0728 14:39:33.925|debug|vm[22]>>> input date =  2021-07-01T08:00:00+08:00 (my__orgdatedemo.ts:28)
0728 14:39:33.925|debug|vm[22]>>> input datetime =  2021-07-01T06:30:30+08:00 (my__orgdatedemo.ts:31)
0728 14:39:33.925|debug|vm[22]>>> output date =  2021-07-01T06:30:30+08:00 (my__orgdatedemo.ts:36)
0728 14:39:33.925|debug|vm[22]>>> output datetime =  2021-07-01T06:30:30+08:00 (my__orgdatedemo.ts:37)
  • 返回信息
json
{
    "outDate": "2021-07-01 08:00:00",
    "outDatetime": "2021-07-01 06:30:30"
}

从日志打印信息:output datetime = 2021-07-01T06:30:30+08:00与返回值"outDatetime": "2021-07-01 06:30:30"信息来看,原来的new Date加8个小时的问题没有了。

从日志信息input datetime = 2021-07-01T06:30:30+08:00来看,把inDatetime转换租户组织时区,结果依然是正确的。这个无论脚本在哪种场景下运行,输出结果一致。

这种方案可行的前提是用户时区与租户组织时区要设置成一样。