740 lines
25 KiB
TypeScript
740 lines
25 KiB
TypeScript
import {
|
||
pgTable,
|
||
timestamp,
|
||
varchar,
|
||
integer,
|
||
text,
|
||
index,
|
||
serial,
|
||
jsonb,
|
||
uniqueIndex,
|
||
} from 'drizzle-orm/pg-core'
|
||
import { user } from './auth-schema.ts'
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 充电桩 / Charge Point
|
||
// OCPP 1.6-J Section 4.2 BootNotification
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const chargePoint = pgTable('charge_point', {
|
||
/** 内部 UUID 主键 */
|
||
id: varchar('id').primaryKey(),
|
||
/**
|
||
* 充电桩唯一标识符(chargeBoxIdentity)
|
||
* TLS/WebSocket 连接路径中的最后一段,CiString255Type
|
||
*/
|
||
chargePointIdentifier: varchar('charge_point_identifier', {
|
||
length: 255,
|
||
})
|
||
.unique()
|
||
.notNull(),
|
||
/** BootNotification.req: chargePointSerialNumber, CiString25Type */
|
||
chargePointSerialNumber: varchar('charge_point_serial_number', {
|
||
length: 25,
|
||
}),
|
||
/** BootNotification.req: chargePointModel, CiString20Type (required) */
|
||
chargePointModel: varchar('charge_point_model', { length: 20 }).notNull(),
|
||
/** BootNotification.req: chargePointVendor, CiString20Type (required) */
|
||
chargePointVendor: varchar('charge_point_vendor', { length: 20 }).notNull(),
|
||
/** BootNotification.req: firmwareVersion, CiString50Type */
|
||
firmwareVersion: varchar('firmware_version', { length: 50 }),
|
||
/** BootNotification.req: iccid (SIM card), CiString20Type */
|
||
iccid: varchar('iccid', { length: 20 }),
|
||
/** BootNotification.req: imsi, CiString20Type */
|
||
imsi: varchar('imsi', { length: 20 }),
|
||
/** BootNotification.req: meterSerialNumber, CiString25Type */
|
||
meterSerialNumber: varchar('meter_serial_number', { length: 25 }),
|
||
/** BootNotification.req: meterType, CiString25Type */
|
||
meterType: varchar('meter_type', { length: 25 }),
|
||
/**
|
||
* BootNotification.conf: status
|
||
* Accepted = 正常运行,Pending = 等待配置,Rejected = 拒绝(不提供服务)
|
||
*/
|
||
registrationStatus: varchar('registration_status', {
|
||
enum: ['Accepted', 'Pending', 'Rejected'],
|
||
})
|
||
.notNull()
|
||
.default('Pending'),
|
||
/**
|
||
* BootNotification.conf: heartbeatInterval(秒)
|
||
* CSMS 通知充电桩应以此间隔发送心跳
|
||
*/
|
||
heartbeatInterval: integer('heartbeat_interval').default(60),
|
||
/** 最后一次收到 Heartbeat.req 的时间(UTC) */
|
||
lastHeartbeatAt: timestamp('last_heartbeat_at', { withTimezone: true }),
|
||
/** 最后一次收到 BootNotification.req 的时间(UTC) */
|
||
lastBootNotificationAt: timestamp('last_boot_notification_at', {
|
||
withTimezone: true,
|
||
}),
|
||
/**
|
||
* 电价(单位:分/kWh,即 0.01 CNY/kWh)
|
||
* 交易结束时按实际用电量从储值卡扣费:fee = ceil(energyWh * feePerKwh / 1000)
|
||
* 默认为 0,即不计费
|
||
*/
|
||
feePerKwh: integer('fee_per_kwh').notNull().default(0),
|
||
createdAt: timestamp('created_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow(),
|
||
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow()
|
||
.$onUpdate(() => new Date()),
|
||
})
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 连接器 / Connector
|
||
// OCPP 1.6-J Section 4.6 StatusNotification
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* 连接器 ID 规范(OCPP 1.6-J Section 2.2):
|
||
* - connectorId = 0:主控制器(StatusNotification 中用于报告整桩状态)
|
||
* - connectorId ≥ 1:实际插口,从 1 开始按顺序编号,不能跳号
|
||
*/
|
||
export const connector = pgTable(
|
||
'connector',
|
||
{
|
||
id: varchar('id').primaryKey(),
|
||
chargePointId: varchar('charge_point_id')
|
||
.notNull()
|
||
.references(() => chargePoint.id, { onDelete: 'cascade' }),
|
||
/** OCPP connectorId(0 = 主控制器,≥1 = 实际插口) */
|
||
connectorId: integer('connector_id').notNull(),
|
||
/**
|
||
* 当前状态 — OCPP 1.6-J Section 7.7 ChargePointStatus
|
||
* Available / Preparing / Charging / SuspendedEVSE / SuspendedEV /
|
||
* Finishing / Reserved / Unavailable / Faulted
|
||
*/
|
||
status: varchar('status', {
|
||
enum: [
|
||
'Available',
|
||
'Preparing',
|
||
'Charging',
|
||
'SuspendedEVSE',
|
||
'SuspendedEV',
|
||
'Finishing',
|
||
'Reserved',
|
||
'Unavailable',
|
||
'Faulted',
|
||
],
|
||
})
|
||
.notNull()
|
||
.default('Unavailable'),
|
||
/** 错误码 — OCPP 1.6-J Section 7.6 ChargePointErrorCode */
|
||
errorCode: varchar('error_code', {
|
||
enum: [
|
||
'NoError',
|
||
'ConnectorLockFailure',
|
||
'EVCommunicationError',
|
||
'GroundFailure',
|
||
'HighTemperature',
|
||
'InternalError',
|
||
'LocalListConflict',
|
||
'OtherError',
|
||
'OverCurrentFailure',
|
||
'OverVoltage',
|
||
'PowerMeterFailure',
|
||
'PowerSwitchFailure',
|
||
'ReaderFailure',
|
||
'ResetFailure',
|
||
'UnderVoltage',
|
||
'WeakSignal',
|
||
],
|
||
})
|
||
.notNull()
|
||
.default('NoError'),
|
||
/** StatusNotification.req: info, CiString50Type */
|
||
info: varchar('info', { length: 50 }),
|
||
/** StatusNotification.req: vendorId, CiString255Type */
|
||
vendorId: varchar('vendor_id', { length: 255 }),
|
||
/** StatusNotification.req: vendorErrorCode, CiString50Type */
|
||
vendorErrorCode: varchar('vendor_error_code', { length: 50 }),
|
||
/** StatusNotification.req: timestamp(充电桩报告的状态时间,UTC) */
|
||
lastStatusAt: timestamp('last_status_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow(),
|
||
createdAt: timestamp('created_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow(),
|
||
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow()
|
||
.$onUpdate(() => new Date()),
|
||
},
|
||
(table) => ({
|
||
chargePointIdIdx: index('idx_connector_charge_point_id').on(
|
||
table.chargePointId
|
||
),
|
||
/** 同一充电桩内 connectorId 唯一 */
|
||
chargePointConnectorUniq: uniqueIndex(
|
||
'idx_connector_charge_point_connector'
|
||
).on(table.chargePointId, table.connectorId),
|
||
})
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 连接器状态历史 / Connector Status History
|
||
// 完整记录每条 StatusNotification.req,用于审计与历史回溯
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const connectorStatusHistory = pgTable(
|
||
'connector_status_history',
|
||
{
|
||
id: varchar('id').primaryKey(),
|
||
connectorId: varchar('connector_id')
|
||
.notNull()
|
||
.references(() => connector.id, { onDelete: 'cascade' }),
|
||
/** OCPP connectorId(冗余存储,方便直接查询) */
|
||
connectorNumber: integer('connector_number').notNull(),
|
||
status: varchar('status', {
|
||
enum: [
|
||
'Available',
|
||
'Preparing',
|
||
'Charging',
|
||
'SuspendedEVSE',
|
||
'SuspendedEV',
|
||
'Finishing',
|
||
'Reserved',
|
||
'Unavailable',
|
||
'Faulted',
|
||
],
|
||
}).notNull(),
|
||
errorCode: varchar('error_code', {
|
||
enum: [
|
||
'NoError',
|
||
'ConnectorLockFailure',
|
||
'EVCommunicationError',
|
||
'GroundFailure',
|
||
'HighTemperature',
|
||
'InternalError',
|
||
'LocalListConflict',
|
||
'OtherError',
|
||
'OverCurrentFailure',
|
||
'OverVoltage',
|
||
'PowerMeterFailure',
|
||
'PowerSwitchFailure',
|
||
'ReaderFailure',
|
||
'ResetFailure',
|
||
'UnderVoltage',
|
||
'WeakSignal',
|
||
],
|
||
}).notNull(),
|
||
info: varchar('info', { length: 50 }),
|
||
vendorId: varchar('vendor_id', { length: 255 }),
|
||
vendorErrorCode: varchar('vendor_error_code', { length: 50 }),
|
||
/** StatusNotification.req: timestamp(充电桩上报的状态时间) */
|
||
statusTimestamp: timestamp('status_timestamp', { withTimezone: true }),
|
||
/** CSMS 收到消息的时间 */
|
||
receivedAt: timestamp('received_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow(),
|
||
},
|
||
(table) => ({
|
||
connectorIdIdx: index('idx_status_history_connector_id').on(
|
||
table.connectorId
|
||
),
|
||
statusTimestampIdx: index('idx_status_history_timestamp').on(
|
||
table.statusTimestamp
|
||
),
|
||
receivedAtIdx: index('idx_status_history_received_at').on(
|
||
table.receivedAt
|
||
),
|
||
})
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 认证标签 / IdTag
|
||
// OCPP 1.6-J Section 4.1 Authorize / Section 7.15 IdTagInfo
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const idTag = pgTable('id_tag', {
|
||
/**
|
||
* RFID 卡号或 App 生成的令牌,CiString20Type
|
||
* 对应 OCPP Authorize.req.idTag
|
||
*/
|
||
idTag: varchar('id_tag', { length: 20 }).primaryKey(),
|
||
/**
|
||
* 父标签(用于分组授权),CiString20Type
|
||
* 当 parentIdTag 在本地列表被接受时,子标签同样被接受
|
||
*/
|
||
parentIdTag: varchar('parent_id_tag', { length: 20 }),
|
||
/**
|
||
* 标签当前状态 — OCPP 1.6-J Section 7.14 AuthorizationStatus
|
||
* Accepted / Blocked / Expired / Invalid / ConcurrentTx
|
||
*/
|
||
status: varchar('status', {
|
||
enum: ['Accepted', 'Blocked', 'Expired', 'Invalid', 'ConcurrentTx'],
|
||
})
|
||
.notNull()
|
||
.default('Accepted'),
|
||
/**
|
||
* 过期时间(UTC)
|
||
* 若设置,CSMS 在 Authorize.conf 中会返回此字段供充电桩离线缓存使用
|
||
*/
|
||
expiryDate: timestamp('expiry_date', { withTimezone: true }),
|
||
/**
|
||
* 关联的平台用户(可选)
|
||
* 允许将 RFID 卡与注册用户绑定,支持 Web/App 远程查询充电记录
|
||
*/
|
||
userId: text('user_id').references(() => user.id, { onDelete: 'set null' }),
|
||
/**
|
||
* 储值卡余额(单位:分)
|
||
* 以整数存储,1 分 = 0.01 CNY,前端显示时除以 100
|
||
*/
|
||
balance: integer('balance').notNull().default(0), /**
|
||
* 卡面内容排列方式
|
||
*/
|
||
cardLayout: varchar('card_layout', { enum: ['center', 'around'] }).default('around'),
|
||
/**
|
||
* 卡底装饰风格
|
||
* 对应 faces/ 目录中已注册的卡面组件
|
||
*/
|
||
cardSkin: varchar('card_skin', { enum: ['line', 'circles', 'glow', 'vip', 'redeye'] }).default('circles'), createdAt: timestamp('created_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow(),
|
||
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow()
|
||
.$onUpdate(() => new Date()),
|
||
})
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 充电事务 / Transaction
|
||
// OCPP 1.6-J Section 4.3 StartTransaction / Section 4.4 StopTransaction
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const transaction = pgTable(
|
||
'transaction',
|
||
{
|
||
/**
|
||
* OCPP transactionId(整型,自增)
|
||
* OCPP 1.6-J Section 4.3: "The transaction id MAY be a number ... it MUST be unique."
|
||
* 使用 serial 保证全局唯一且自动递增
|
||
*/
|
||
id: serial('id').primaryKey(),
|
||
chargePointId: varchar('charge_point_id')
|
||
.notNull()
|
||
.references(() => chargePoint.id, { onDelete: 'cascade' }),
|
||
connectorId: varchar('connector_id')
|
||
.notNull()
|
||
.references(() => connector.id, { onDelete: 'cascade' }),
|
||
/** OCPP connectorId(冗余存储,方便直接查询) */
|
||
connectorNumber: integer('connector_number').notNull(),
|
||
/**
|
||
* 发起充电的 idTag,CiString20Type
|
||
* StartTransaction.req.idTag
|
||
*/
|
||
idTag: varchar('id_tag', { length: 20 }).notNull(),
|
||
/**
|
||
* CSMS 对 idTag 的鉴权结果(StartTransaction.conf 中返回)
|
||
* OCPP 1.6-J Section 7.14 AuthorizationStatus
|
||
*/
|
||
idTagStatus: varchar('id_tag_status', {
|
||
enum: ['Accepted', 'Blocked', 'Expired', 'Invalid', 'ConcurrentTx'],
|
||
}),
|
||
/**
|
||
* 充电开始时间(充电桩本地时间,UTC)
|
||
* StartTransaction.req.timestamp
|
||
*/
|
||
startTimestamp: timestamp('start_timestamp', {
|
||
withTimezone: true,
|
||
}).notNull(),
|
||
/**
|
||
* 开始时的电表读数(Wh)
|
||
* StartTransaction.req.meterStart
|
||
*/
|
||
startMeterValue: integer('start_meter_value').notNull(),
|
||
/**
|
||
* 停止充电的 idTag(可能与 idTag 不同,如工作人员强制停止)
|
||
* StopTransaction.req.idTag(optional)
|
||
*/
|
||
stopIdTag: varchar('stop_id_tag', { length: 20 }),
|
||
/**
|
||
* 充电结束时间(UTC)
|
||
* StopTransaction.req.timestamp
|
||
*/
|
||
stopTimestamp: timestamp('stop_timestamp', { withTimezone: true }),
|
||
/**
|
||
* 结束时的电表读数(Wh)
|
||
* StopTransaction.req.meterStop
|
||
*/
|
||
stopMeterValue: integer('stop_meter_value'),
|
||
/**
|
||
* 停止原因 — OCPP 1.6-J Section 7.20 Reason
|
||
* EmergencyStop / EVDisconnected / HardReset / Local / Other /
|
||
* PowerLoss / Reboot / Remote / SoftReset / UnlockCommand / DeAuthorized
|
||
*/
|
||
stopReason: varchar('stop_reason', {
|
||
enum: [
|
||
'EmergencyStop',
|
||
'EVDisconnected',
|
||
'HardReset',
|
||
'Local',
|
||
'Other',
|
||
'PowerLoss',
|
||
'Reboot',
|
||
'Remote',
|
||
'SoftReset',
|
||
'UnlockCommand',
|
||
'DeAuthorized',
|
||
],
|
||
}),
|
||
/**
|
||
* 本次充电扣费金额(单位:分)
|
||
* 由 StopTransaction 处理时根据实际用电量和充电桩电价计算写入
|
||
* null 表示未计费(如免费充电桩或交易异常终止)
|
||
*/
|
||
chargeAmount: integer('charge_amount'),
|
||
/**
|
||
* 关联的预约 ID(若本次充电由预约触发)
|
||
* StartTransaction.req.reservationId(optional)
|
||
*/
|
||
reservationId: integer('reservation_id'),
|
||
createdAt: timestamp('created_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow(),
|
||
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow()
|
||
.$onUpdate(() => new Date()),
|
||
},
|
||
(table) => ({
|
||
chargePointIdIdx: index('idx_transaction_charge_point_id').on(
|
||
table.chargePointId
|
||
),
|
||
connectorIdIdx: index('idx_transaction_connector_id').on(
|
||
table.connectorId
|
||
),
|
||
idTagIdx: index('idx_transaction_id_tag').on(table.idTag),
|
||
startTimestampIdx: index('idx_transaction_start_timestamp').on(
|
||
table.startTimestamp
|
||
),
|
||
})
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 计量值 / Meter Value
|
||
// OCPP 1.6-J Section 4.7 MeterValues;也内嵌于 StartTransaction / StopTransaction
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/** SampledValue 类型定义(对应 OCPP 1.6-J Section 7.17) */
|
||
export type SampledValue = {
|
||
/** 测量值(字符串形式) */
|
||
value: string
|
||
/**
|
||
* 读数上下文 — ReadingContext
|
||
* Interruption.Begin / Interruption.End / Sample.Clock /
|
||
* Sample.Periodic / Transaction.Begin / Transaction.End / Trigger / Other
|
||
*/
|
||
context?:
|
||
| 'Interruption.Begin'
|
||
| 'Interruption.End'
|
||
| 'Sample.Clock'
|
||
| 'Sample.Periodic'
|
||
| 'Transaction.Begin'
|
||
| 'Transaction.End'
|
||
| 'Trigger'
|
||
| 'Other'
|
||
/** 数据格式:Raw(原始数值)/ SignedData(签名数据) */
|
||
format?: 'Raw' | 'SignedData'
|
||
/**
|
||
* 测量量 — Measurand(常用值)
|
||
* Energy.Active.Import.Register / Power.Active.Import /
|
||
* Current.Import / Voltage / SoC / Temperature 等
|
||
*/
|
||
measurand?: string
|
||
/** 相位信息(三相系统):L1 / L2 / L3 / N / L1-N / L2-N / L3-N / L1-L2 / L2-L3 / L3-L1 */
|
||
phase?: string
|
||
/** 测量位置:Cable / EV / Inlet / Outlet / Body */
|
||
location?: string
|
||
/** 单位:Wh / kWh / varh / kvarh / W / kW / VA / kVA / var / kvar / A / V / K / Celcius / Fahrenheit / Percent */
|
||
unit?: string
|
||
}
|
||
|
||
export const meterValue = pgTable(
|
||
'meter_value',
|
||
{
|
||
id: varchar('id').primaryKey(),
|
||
/**
|
||
* 关联的充电事务(可 null,如充电桩空闲时上报的周期性计量值)
|
||
* MeterValues.req.transactionId(optional)
|
||
*/
|
||
transactionId: integer('transaction_id').references(() => transaction.id, {
|
||
onDelete: 'set null',
|
||
}),
|
||
connectorId: varchar('connector_id')
|
||
.notNull()
|
||
.references(() => connector.id, { onDelete: 'cascade' }),
|
||
chargePointId: varchar('charge_point_id')
|
||
.notNull()
|
||
.references(() => chargePoint.id, { onDelete: 'cascade' }),
|
||
/** OCPP connectorId(冗余存储) */
|
||
connectorNumber: integer('connector_number').notNull(),
|
||
/**
|
||
* 充电桩上报的计量时间戳(UTC)
|
||
* MeterValues.req.meterValue[].timestamp
|
||
*/
|
||
timestamp: timestamp('timestamp', { withTimezone: true }).notNull(),
|
||
/**
|
||
* 采样值数组(JSONB)
|
||
* MeterValues.req.meterValue[].sampledValue[]
|
||
* 保持 JSONB 格式以支持 measurand 类型不固定的场景
|
||
*/
|
||
sampledValues: jsonb('sampled_values')
|
||
.notNull()
|
||
.$type<SampledValue[]>(),
|
||
/** CSMS 收到消息的时间 */
|
||
receivedAt: timestamp('received_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow(),
|
||
},
|
||
(table) => ({
|
||
transactionIdIdx: index('idx_meter_value_transaction_id').on(
|
||
table.transactionId
|
||
),
|
||
connectorIdIdx: index('idx_meter_value_connector_id').on(
|
||
table.connectorId
|
||
),
|
||
timestampIdx: index('idx_meter_value_timestamp').on(table.timestamp),
|
||
})
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 预约 / Reservation
|
||
// OCPP 1.6-J Section 3.11 ReserveNow / Section 3.2 CancelReservation
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const reservation = pgTable(
|
||
'reservation',
|
||
{
|
||
/**
|
||
* OCPP reservationId(整型,由 CSMS 分配)
|
||
* ReserveNow.req.reservationId
|
||
*/
|
||
id: integer('id').primaryKey(),
|
||
chargePointId: varchar('charge_point_id')
|
||
.notNull()
|
||
.references(() => chargePoint.id, { onDelete: 'cascade' }),
|
||
/**
|
||
* 关联的内部连接器记录(可 null:connectorId=0 表示任意可用插口)
|
||
*/
|
||
connectorId: varchar('connector_id').references(() => connector.id, {
|
||
onDelete: 'set null',
|
||
}),
|
||
/**
|
||
* OCPP connectorId(0 = 整桩任意插口,≥1 = 指定插口)
|
||
* ReserveNow.req.connectorId
|
||
*/
|
||
connectorNumber: integer('connector_number').notNull(),
|
||
/**
|
||
* 预约到期时间(UTC)
|
||
* ReserveNow.req.expiryDate
|
||
*/
|
||
expiryDate: timestamp('expiry_date', { withTimezone: true }).notNull(),
|
||
/**
|
||
* 预约使用的 idTag,CiString20Type
|
||
* ReserveNow.req.idTag
|
||
*/
|
||
idTag: varchar('id_tag', { length: 20 }).notNull(),
|
||
/**
|
||
* 可选的父标签(用于允许同组任意 idTag 使用本预约)
|
||
* ReserveNow.req.parentIdTag
|
||
*/
|
||
parentIdTag: varchar('parent_id_tag', { length: 20 }),
|
||
/**
|
||
* 预约状态(扩展字段,非 OCPP 原生)
|
||
* Active = 有效中,Cancelled = 已取消,Expired = 已过期,Used = 已使用
|
||
*/
|
||
status: varchar('status', {
|
||
enum: ['Active', 'Cancelled', 'Expired', 'Used'],
|
||
})
|
||
.notNull()
|
||
.default('Active'),
|
||
createdAt: timestamp('created_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow(),
|
||
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow()
|
||
.$onUpdate(() => new Date()),
|
||
},
|
||
(table) => ({
|
||
chargePointIdIdx: index('idx_reservation_charge_point_id').on(
|
||
table.chargePointId
|
||
),
|
||
statusIdx: index('idx_reservation_status').on(table.status),
|
||
expiryDateIdx: index('idx_reservation_expiry_date').on(table.expiryDate),
|
||
})
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 本地授权列表 / Local Auth List
|
||
// OCPP 1.6-J Section 3.9 SendLocalList / Section 3.8 GetLocalListVersion
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* 充电桩本地授权列表缓存
|
||
*
|
||
* 充电桩断网时依赖本地列表进行离线授权。CSMS 通过 SendLocalList 将列表
|
||
* 推送至充电桩,每次推送携带 listVersion(单调递增整数)。
|
||
* CSMS 侧同步维护此表,以便在需要时重新推送或审计。
|
||
*/
|
||
export const localAuthList = pgTable(
|
||
'local_auth_list',
|
||
{
|
||
id: varchar('id').primaryKey(),
|
||
chargePointId: varchar('charge_point_id')
|
||
.notNull()
|
||
.references(() => chargePoint.id, { onDelete: 'cascade' }),
|
||
/**
|
||
* 列表版本号(单调递增)
|
||
* SendLocalList.req.listVersion
|
||
*/
|
||
listVersion: integer('list_version').notNull(),
|
||
/** idTag,CiString20Type */
|
||
idTag: varchar('id_tag', { length: 20 }).notNull(),
|
||
/** 可选父标签 */
|
||
parentIdTag: varchar('parent_id_tag', { length: 20 }),
|
||
/** 本地授权状态 — AuthorizationStatus */
|
||
idTagStatus: varchar('id_tag_status', {
|
||
enum: ['Accepted', 'Blocked', 'Expired', 'Invalid', 'ConcurrentTx'],
|
||
}).notNull(),
|
||
/** 过期时间(UTC,可选) */
|
||
expiryDate: timestamp('expiry_date', { withTimezone: true }),
|
||
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow()
|
||
.$onUpdate(() => new Date()),
|
||
},
|
||
(table) => ({
|
||
/** 同一充电桩内同一 idTag 唯一 */
|
||
chargePointIdTagUniq: uniqueIndex(
|
||
'idx_local_auth_list_charge_point_id_tag'
|
||
).on(table.chargePointId, table.idTag),
|
||
})
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 充电配置文件 / Charging Profile
|
||
// OCPP 1.6-J Section 3.12 SetChargingProfile(Smart Charging Profile)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/**
|
||
* ChargingSchedulePeriod 类型(对应 OCPP 1.6-J Section 7.8)
|
||
* 描述某一时间段内的充电限制
|
||
*/
|
||
export type ChargingSchedulePeriod = {
|
||
/** 距 chargingSchedule.startSchedule 的偏移秒数 */
|
||
startPeriod: number
|
||
/** 最大充电限制(单位由 chargingRateUnit 决定:W 或 A) */
|
||
limit: number
|
||
/** 可选:允许使用的相数(1 或 3) */
|
||
numberPhases?: number
|
||
}
|
||
|
||
/**
|
||
* ChargingSchedule 类型(对应 OCPP 1.6-J Section 7.9)
|
||
*/
|
||
export type ChargingSchedule = {
|
||
/** 总时长(秒,可选)*/
|
||
duration?: number
|
||
/** 计划开始时间(UTC ISO 8601,可选,Absolute 类型时使用) */
|
||
startSchedule?: string
|
||
/** 限制单位:W(瓦特)或 A(安培) */
|
||
chargingRateUnit: 'W' | 'A'
|
||
/** 各时间段的限制列表(按 startPeriod 升序排列) */
|
||
chargingSchedulePeriod: ChargingSchedulePeriod[]
|
||
/** 最小充电速率(W / A,可选) */
|
||
minChargingRate?: number
|
||
}
|
||
|
||
export const chargingProfile = pgTable(
|
||
'charging_profile',
|
||
{
|
||
/**
|
||
* OCPP chargingProfileId(整型,由 CSMS 分配)
|
||
* SetChargingProfile.req.csChargingProfiles.chargingProfileId
|
||
*/
|
||
id: integer('id').primaryKey(),
|
||
chargePointId: varchar('charge_point_id')
|
||
.notNull()
|
||
.references(() => chargePoint.id, { onDelete: 'cascade' }),
|
||
/**
|
||
* 关联的内部连接器(0 表示整桩级别的配置文件,对应 connectorId=0)
|
||
*/
|
||
connectorId: varchar('connector_id').references(() => connector.id, {
|
||
onDelete: 'cascade',
|
||
}),
|
||
/** OCPP connectorId(0 = 充电桩级别,≥1 = 指定插口) */
|
||
connectorNumber: integer('connector_number').notNull(),
|
||
/**
|
||
* 关联的事务 ID(仅 TxProfile 类型时有效)
|
||
* SetChargingProfile.req.csChargingProfiles.transactionId
|
||
*/
|
||
transactionId: integer('transaction_id').references(() => transaction.id, {
|
||
onDelete: 'set null',
|
||
}),
|
||
/**
|
||
* 优先级堆叠层级(数值越高,优先级越高)
|
||
* SetChargingProfile.req.csChargingProfiles.stackLevel
|
||
*/
|
||
stackLevel: integer('stack_level').notNull(),
|
||
/**
|
||
* 配置文件用途 — ChargingProfilePurposeType
|
||
* ChargePointMaxProfile:限制整桩最大功率
|
||
* TxDefaultProfile:默认事务充电功率
|
||
* TxProfile:绑定到特定事务的充电功率
|
||
*/
|
||
chargingProfilePurpose: varchar('charging_profile_purpose', {
|
||
enum: ['ChargePointMaxProfile', 'TxDefaultProfile', 'TxProfile'],
|
||
}).notNull(),
|
||
/**
|
||
* 配置文件类型 — ChargingProfileKindType
|
||
* Absolute:从指定时间点开始
|
||
* Recurring:周期性重复(Daily / Weekly)
|
||
* Relative:相对于事务开始时间
|
||
*/
|
||
chargingProfileKind: varchar('charging_profile_kind', {
|
||
enum: ['Absolute', 'Recurring', 'Relative'],
|
||
}).notNull(),
|
||
/**
|
||
* 周期类型(仅 Recurring 类型时有效)
|
||
* Daily / Weekly
|
||
*/
|
||
recurrencyKind: varchar('recurrency_kind', {
|
||
enum: ['Daily', 'Weekly'],
|
||
}),
|
||
/** 配置文件生效时间(UTC,可选) */
|
||
validFrom: timestamp('valid_from', { withTimezone: true }),
|
||
/** 配置文件失效时间(UTC,可选) */
|
||
validTo: timestamp('valid_to', { withTimezone: true }),
|
||
/**
|
||
* 充电计划(JSONB)
|
||
* SetChargingProfile.req.csChargingProfiles.chargingSchedule
|
||
*/
|
||
chargingSchedule: jsonb('charging_schedule')
|
||
.notNull()
|
||
.$type<ChargingSchedule>(),
|
||
createdAt: timestamp('created_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow(),
|
||
updatedAt: timestamp('updated_at', { withTimezone: true })
|
||
.notNull()
|
||
.defaultNow()
|
||
.$onUpdate(() => new Date()),
|
||
},
|
||
(table) => ({
|
||
chargePointIdIdx: index('idx_charging_profile_charge_point_id').on(
|
||
table.chargePointId
|
||
),
|
||
connectorIdIdx: index('idx_charging_profile_connector_id').on(
|
||
table.connectorId
|
||
),
|
||
purposeStackIdx: index('idx_charging_profile_purpose_stack').on(
|
||
table.connectorNumber,
|
||
table.chargingProfilePurpose,
|
||
table.stackLevel
|
||
),
|
||
})
|
||
)
|