add groovy script

This commit is contained in:
Timi
2026-09-04 20:38:17 +08:00
parent df16ac3c8e
commit 41f692aad9
53 changed files with 1708 additions and 541 deletions
@@ -0,0 +1,30 @@
CREATE TABLE `groovy_script` (
`id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`biz_type` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务归属',
`code` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务内唯一代码',
`name` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称',
`description` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '说明',
`enabled` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'true 为允许执行已发布版本',
`published_version_id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '当前发布版本 ID',
`created_at` BIGINT(20),
`updated_at` BIGINT(20),
`deleted_at` BIGINT(20),
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_groovy_script_biz_code` (`biz_type`, `code`),
KEY `idx_groovy_script_published_version` (`published_version_id`),
KEY `idx_groovy_script_deleted_created` (`deleted_at`, `created_at`)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Groovy 脚本' ROW_FORMAT = Dynamic;
CREATE TABLE `groovy_script_version` (
`id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`script_id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '脚本 ID',
`version` INT NOT NULL COMMENT '版本号',
`content` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '脚本内容',
`checksum` CHAR(64) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL COMMENT '脚本内容 SHA-256',
`created_at` BIGINT(20),
`updated_at` BIGINT(20),
`deleted_at` BIGINT(20),
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_groovy_script_version` (`script_id`, `version`),
KEY `idx_groovy_script_version_script_created` (`script_id`, `created_at`)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Groovy 脚本版本' ROW_FORMAT = Dynamic;
@@ -0,0 +1,62 @@
CREATE TABLE `gao_customer_consumption_record` (
`id` VARCHAR(36) NOT NULL,
`store_id` VARCHAR(36) NOT NULL COMMENT '门店 ID',
`customer_id` VARCHAR(36) NOT NULL COMMENT '客户 ID',
`source_type` VARCHAR(32) NOT NULL COMMENT '来源类型',
`source_id` VARCHAR(36) NOT NULL COMMENT '来源业务对象 ID',
`sales_order_id` VARCHAR(36) NULL COMMENT '销售订单 ID',
`order_type` VARCHAR(32) NULL COMMENT '订单类型',
`amount` BIGINT NOT NULL COMMENT '有效消费金额,单位分',
`effective_at` BIGINT NOT NULL COMMENT '成为有效消费时间',
`status` VARCHAR(32) NOT NULL COMMENT '状态',
`revoked_at` BIGINT NULL COMMENT '失效时间',
`revoke_reason` TEXT NULL COMMENT '失效原因',
`source_payload` JSON NULL COMMENT '来源快照',
`created_at` BIGINT NOT NULL,
`updated_at` BIGINT NULL,
`deleted_at` BIGINT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gao_customer_consumption_source` (`source_type`, `source_id`),
KEY `idx_gao_customer_consumption_customer_time` (`store_id`, `customer_id`, `status`, `effective_at`),
KEY `idx_gao_customer_consumption_order` (`sales_order_id`)
) COMMENT='GAO 客户有效消费事实';
CREATE TABLE `gao_point_ledger_consumption_record` (
`point_ledger_id` VARCHAR(36) NOT NULL COMMENT '积分流水 ID',
`consumption_record_id` VARCHAR(36) NOT NULL COMMENT '消费事实 ID',
`created_at` BIGINT NOT NULL,
PRIMARY KEY (`point_ledger_id`, `consumption_record_id`),
KEY `idx_gao_point_ledger_consumption_record_consumption` (`consumption_record_id`)
) COMMENT='GAO 积分流水消费事实依赖';
ALTER TABLE `gao_sales_order`
ADD COLUMN `paid_at` BIGINT NULL COMMENT '首次全额实收时间' AFTER `paid_amount`;
ALTER TABLE `gao_point_rule`
ADD COLUMN `rule_mode` VARCHAR(16) NULL COMMENT '规则模式' AFTER `status`,
ADD COLUMN `trigger_source_type` VARCHAR(32) NULL COMMENT '触发来源类型' AFTER `rule_mode`,
ADD COLUMN `trigger_ref_type` VARCHAR(32) NULL COMMENT '触发对象类型' AFTER `trigger_source_type`,
ADD COLUMN `trigger_ref_id` VARCHAR(36) NULL COMMENT '触发对象 ID' AFTER `trigger_ref_type`,
ADD COLUMN `groovy_script_id` VARCHAR(36) NULL COMMENT 'Groovy 脚本 ID' AFTER `point_value`,
ADD KEY `idx_gao_point_rule_trigger` (`store_id`, `trigger_source_type`, `trigger_ref_id`, `status`, `deleted_at`, `priority`);
ALTER TABLE `gao_point_ledger`
ADD COLUMN `trigger_source_type` VARCHAR(32) NULL COMMENT '直接触发来源类型' AFTER `rule_id`,
ADD COLUMN `trigger_source_id` VARCHAR(36) NULL COMMENT '直接触发来源 ID' AFTER `trigger_source_type`,
ADD COLUMN `groovy_script_version_id` VARCHAR(36) NULL COMMENT '实际执行 Groovy 脚本版本 ID' AFTER `trigger_source_id`,
ADD COLUMN `rule_snapshot` JSON NULL COMMENT '规则快照' AFTER `groovy_script_version_id`,
ADD KEY `idx_gao_point_ledger_trigger_source` (`trigger_source_type`, `trigger_source_id`, `change_type`);
UPDATE `gao_point_rule`
SET `rule_mode` = 'SIMPLE',
`trigger_source_type` = 'EVENT_RECORD',
`trigger_ref_type` = 'GAO_EVENT',
`trigger_ref_id` = `event_id`
WHERE `trigger_type` = 'EVERY_TIME'
AND `point_mode` = 'FIXED'
AND `condition_json` IS NULL;
UPDATE `gao_point_ledger`
SET `trigger_source_type` = 'EVENT_RECORD',
`trigger_source_id` = `event_record_id`
WHERE `event_record_id` IS NOT NULL;
@@ -0,0 +1,23 @@
INSERT INTO `groovy_script` (`id`, `biz_type`, `code`, `name`, `description`, `enabled`, `published_version_id`, `created_at`)
VALUES
('f2511e0e-6e55-4a59-a221-700000000001', 'GAO_POINT_RULE', 'SIGN_IN_CONSUMPTION_TIER', '签到按消费分档积分', '签到时按过去一年有效消费分档积分', TRUE, 'f2511e0e-6e55-4a59-a221-700000000101', 1787248052654),
('f2511e0e-6e55-4a59-a221-700000000002', 'GAO_POINT_RULE', 'SIGN_IN_AFTER_THIRTY', '无消费累计签到积分', '第 30 次签到及以后每次积分', TRUE, 'f2511e0e-6e55-4a59-a221-700000000102', 1787041413269);
INSERT INTO `groovy_script_version` (`id`, `script_id`, `version`, `content`, `checksum`, `created_at`)
VALUES
('f2511e0e-6e55-4a59-a221-700000000101', 'f2511e0e-6e55-4a59-a221-700000000001', 1,
'long amount = context.rollingYearConsumptionAmount()\nif (amount < 1L) return null\nlong point = amount < 200000L ? 20L : amount < 2000000L ? 50L : amount < 5000000L ? 80L : 100L\nreturn context.decision(point, "签到消费分档积分", [amount: amount, window: "ROLLING_1_YEAR"])',
SHA2('long amount = context.rollingYearConsumptionAmount()\nif (amount < 1L) return null\nlong point = amount < 200000L ? 20L : amount < 2000000L ? 50L : amount < 5000000L ? 80L : 100L\nreturn context.decision(point, "签到消费分档积分", [amount: amount, window: "ROLLING_1_YEAR"])', 256), 1787248052654),
('f2511e0e-6e55-4a59-a221-700000000102', 'f2511e0e-6e55-4a59-a221-700000000002', 1,
'if (context.eventCount() < 30L) return null\nreturn context.decision(10L, "第 30 次及以后签到积分", [eventCount: context.eventCount()])',
SHA2('if (context.eventCount() < 30L) return null\nreturn context.decision(10L, "第 30 次及以后签到积分", [eventCount: context.eventCount()])', 256), 1787041413269);
UPDATE `gao_point_rule`
SET `rule_mode` = 'GROOVY', `trigger_source_type` = 'EVENT_RECORD', `trigger_ref_type` = 'EVENT',
`trigger_ref_id` = `event_id`, `groovy_script_id` = 'f2511e0e-6e55-4a59-a221-700000000001', `point_value` = NULL
WHERE `id` = '1746879e-3726-4927-95a6-a3b738cbc134';
UPDATE `gao_point_rule`
SET `rule_mode` = 'GROOVY', `trigger_source_type` = 'EVENT_RECORD', `trigger_ref_type` = 'EVENT',
`trigger_ref_id` = `event_id`, `groovy_script_id` = 'f2511e0e-6e55-4a59-a221-700000000002', `point_value` = NULL
WHERE `id` = '1b71dbb8-be2e-4783-9209-e6a94a8745b4';
@@ -0,0 +1,44 @@
ALTER TABLE `gao_point_ledger`
ADD COLUMN `sales_order_id` VARCHAR(36) NULL COMMENT '关联销售订单 ID' AFTER `trigger_source_id`,
ADD KEY `idx_gao_point_ledger_sales_order` (`sales_order_id`);
-- 将旧规则 sourceEventId 指向的登记事件标记为退役消费事件
UPDATE `gao_event` e
SET e.`biz_type` = 'LEGACY_CONSUMPTION'
WHERE e.`deleted_at` IS NULL
AND e.`biz_type` <> 'PACKAGE_PURCHASE'
AND e.`id` IN (
SELECT DISTINCT JSON_UNQUOTE(JSON_EXTRACT(r.`condition_json`, '$.sourceEventId'))
FROM `gao_point_rule` r
WHERE r.`condition_json` IS NOT NULL
AND JSON_EXTRACT(r.`condition_json`, '$.sourceEventId') IS NOT NULL
);
-- 历史消费事件迁移为 LEGACY_EVENT 消费事实,来源唯一约束保证可重跑
-- 不能使用 INSERT IGNORE,否则数据异常会被静默吞掉,导致消费累计缺失
INSERT INTO `gao_customer_consumption_record`
(`id`, `store_id`, `customer_id`, `source_type`, `source_id`, `sales_order_id`, `order_type`, `amount`, `effective_at`, `status`, `revoked_at`, `revoke_reason`, `source_payload`, `created_at`, `updated_at`, `deleted_at`)
SELECT
UUID(),
r.`store_id`,
r.`customer_id`,
'LEGACY_EVENT',
r.`id`,
NULL,
NULL,
r.`number_value`,
r.`registered_at`,
'ACTIVE',
NULL,
NULL,
JSON_OBJECT('migrationBatch', 'V66', 'eventId', r.`event_id`, 'eventName', e.`name`, 'eventRecordId', r.`id`, 'registeredAt', r.`registered_at`),
FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000),
NULL,
NULL
FROM `gao_event_record` r
INNER JOIN `gao_event` e ON e.`id` = r.`event_id` AND e.`deleted_at` IS NULL
WHERE e.`biz_type` = 'LEGACY_CONSUMPTION'
AND r.`number_value` IS NOT NULL
AND r.`number_value` > 0
AND r.`deleted_at` IS NULL
ON DUPLICATE KEY UPDATE `id` = `id`;
@@ -0,0 +1,4 @@
-- V64 曾把枚举值误写为 GAO_EVENT,MyBatis 读取时会无法映射到 TriggerRefType.EVENT
UPDATE `gao_point_rule`
SET `trigger_ref_type` = 'EVENT'
WHERE `trigger_ref_type` = 'GAO_EVENT';
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.gao.mapper.GaoCustomerConsumptionRecordMapper">
<select id="selectBySource" resultType="com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord">
SELECT * FROM `gao_customer_consumption_record`
WHERE `source_type` = #{sourceType} AND `source_id` = #{sourceId} AND `deleted_at` IS NULL
LIMIT 1
</select>
<select id="selectActiveByWindow" resultType="com.imyeyu.api.modules.gao.entity.GaoCustomerConsumptionRecord">
SELECT * FROM `gao_customer_consumption_record`
WHERE `store_id` = #{storeId} AND `customer_id` = #{customerId} AND `status` = 'ACTIVE'
AND `effective_at` &gt;= #{beginAt} AND `effective_at` &lt;= #{endAt} AND `deleted_at` IS NULL
ORDER BY `effective_at` ASC, `id` ASC
</select>
<update id="revokeBySource">
UPDATE `gao_customer_consumption_record`
SET `status` = 'REVOKED', `revoked_at` = #{revokedAt}, `revoke_reason` = #{revokeReason}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
WHERE `source_type` = #{sourceType} AND `source_id` = #{sourceId} AND `status` = 'ACTIVE' AND `deleted_at` IS NULL
</update>
</mapper>
@@ -26,6 +26,7 @@
WHERE
`store_id` = #{storeId}
AND `status` = '${@com.imyeyu.api.modules.gao.entity.GaoEvent$Status@ACTIVE.name()}'
AND (`biz_type` IS NULL OR `biz_type` != 'LEGACY_CONSUMPTION')
AND (`begin_at` IS NULL OR `begin_at` &lt;= #{now})
AND (`end_at` IS NULL OR #{now} &lt;= `end_at`)
AND `deleted_at` IS NULL
@@ -119,6 +119,12 @@
#{eventId}
</foreach>
</if>
<if test="eventIdList == null or eventIdList.size() == 0">
AND NOT EXISTS (
SELECT 1 FROM `gao_event` re
WHERE re.`id` = r.`event_id` AND re.`biz_type` = 'LEGACY_CONSUMPTION' AND re.`deleted_at` IS NULL
)
</if>
<if test="optionIdList != null and optionIdList.size() > 0">
<foreach collection="optionIdList" item="optionId">
AND EXISTS (
@@ -212,6 +218,12 @@
#{eventId}
</foreach>
</if>
<if test="eventIdList == null or eventIdList.size() == 0">
AND NOT EXISTS (
SELECT 1 FROM `gao_event` re
WHERE re.`id` = r.`event_id` AND re.`biz_type` = 'LEGACY_CONSUMPTION' AND re.`deleted_at` IS NULL
)
</if>
<if test="rankType != null and rankType.name() == 'TOTAL_NUMBER_VALUE'">
AND r.`number_value` IS NOT NULL
</if>
@@ -386,6 +398,12 @@
#{eventId}
</foreach>
</if>
<if test="eventIdList == null or eventIdList.size() == 0">
AND NOT EXISTS (
SELECT 1 FROM `gao_event` re
WHERE re.`id` = r.`event_id` AND re.`biz_type` = 'LEGACY_CONSUMPTION' AND re.`deleted_at` IS NULL
)
</if>
</sql>
<select id="stateDailyByRange" resultType="com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView">
SELECT
@@ -447,6 +465,12 @@
#{eventId}
</foreach>
</if>
<if test="eventIdList == null or eventIdList.size() == 0">
AND NOT EXISTS (
SELECT 1 FROM `gao_event` re
WHERE re.`id` = r.`event_id` AND re.`biz_type` = 'LEGACY_CONSUMPTION' AND re.`deleted_at` IS NULL
)
</if>
</sql>
<select id="stateCustomerDailyByRange" resultType="com.imyeyu.api.modules.gao.vo.GaoEventRecordDailyStatView">
SELECT
@@ -9,15 +9,26 @@
LIMIT 1
</select>
<select id="selectRuleChangeByEventRecordId" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
<select id="selectRuleChangeByTriggerSource" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
SELECT *
FROM `gao_point_ledger`
WHERE `event_record_id` = #{eventRecordId}
WHERE `trigger_source_type` = #{triggerSourceType}
AND `trigger_source_id` = #{triggerSourceId}
AND `change_type` IN ('EARN', 'CONSUME')
AND `deleted_at` IS NULL
ORDER BY `created_at` ASC, `id` ASC
</select>
<select id="selectRuleChangeByConsumptionRecordId" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
SELECT l.*
FROM `gao_point_ledger` l
INNER JOIN `gao_point_ledger_consumption_record` d ON d.`point_ledger_id` = l.`id`
WHERE d.`consumption_record_id` = #{consumptionRecordId}
AND l.`change_type` IN ('EARN', 'CONSUME')
AND l.`deleted_at` IS NULL
ORDER BY l.`created_at` ASC, l.`id` ASC
</select>
<select id="selectByIdempotencyKeyList" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
SELECT *
FROM `gao_point_ledger`
@@ -11,11 +11,16 @@
</foreach>
</select>
<select id="selectActiveByStoreIdAndEventId" resultType="com.imyeyu.api.modules.gao.entity.GaoPointRule">
<select id="selectActiveByTrigger" resultType="com.imyeyu.api.modules.gao.entity.GaoPointRule">
SELECT *
FROM `gao_point_rule`
WHERE `store_id` = #{storeId}
AND `event_id` = #{eventId}
AND `trigger_source_type` = #{triggerSourceType}
AND (`trigger_ref_id` IS NULL
<if test="triggerRefId != null and triggerRefId != ''">
OR `trigger_ref_id` = #{triggerRefId}
</if>
)
AND `status` = 'ACTIVE'
AND (`begin_at` IS NULL OR `begin_at` &lt;= #{now})
AND (`end_at` IS NULL OR #{now} &lt;= `end_at`)
@@ -33,7 +33,7 @@
</select>
<update id="updatePayment">
UPDATE `gao_sales_order`
SET `pending_paid_amount` = #{pendingPaidAmount}, `paid_amount` = #{paidAmount}, `refunded_amount` = #{refundedAmount},
SET `pending_paid_amount` = #{pendingPaidAmount}, `paid_amount` = #{paidAmount}, `paid_at` = #{paidAt}, `refunded_amount` = #{refundedAmount},
`payment_status` = #{paymentStatus}, `refund_status` = #{refundStatus}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
WHERE `id` = #{id} AND `deleted_at` IS NULL
</update>
@@ -43,11 +43,6 @@
`cancel_reason` = #{cancelReason}, `refund_status` = #{refundStatus}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
WHERE `id` = #{id} AND `status` = #{fromStatus} AND `deleted_at` IS NULL
</update>
<update id="updateSourceEventRecordId">
UPDATE `gao_sales_order`
SET `source_event_record_id` = #{sourceEventRecordId}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)
WHERE `id` = #{id} AND `source_event_record_id` IS NULL AND `deleted_at` IS NULL
</update>
<update id="updateStatus">
UPDATE `gao_sales_order`
SET `status` = #{status}, `updated_at` = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000)